[server/sources] make sure entity._cw is a Connection before calling Storages

The storage callbacks don't get an explicit Connection objects, so they go through entity._cw to access the repo and schedule operations. Since that entity object comes out of the entity cache, its _cw may be a web request instead of a Connection. So fix it up around the storage callbacks.

Closes #5753543

authorJulien Cristau <julien.cristau@logilab.fr>
changesetd4bd28d5fca8
branchdefault
phasepublic
hiddenno
parent revision#5fc21bf2684f [hooks] base64-encode extid before inserting it into moved_entities
child revision#1182f5f16a3d [datafeed] fix typo in DataFeedXMLParser.is_deleted (closes #5729755)
files modified by this revision
server/sources/native.py
server/test/unittest_storage.py
# HG changeset patch
# User Julien Cristau <julien.cristau@logilab.fr>
# Date 1437987089 -7200
# Mon Jul 27 10:51:29 2015 +0200
# Node ID d4bd28d5fca860591b918b06ddbccebf265f64ca
# Parent 5fc21bf2684f494e7ade9bcc5e653a411dc53b6c
[server/sources] make sure entity._cw is a Connection before calling Storages

The storage callbacks don't get an explicit Connection objects, so they
go through entity._cw to access the repo and schedule operations. Since
that entity object comes out of the entity cache, its _cw may be a web
request instead of a Connection. So fix it up around the storage
callbacks.

Closes #5753543

diff --git a/server/sources/native.py b/server/sources/native.py
@@ -561,11 +561,20 @@
1          results = self.process_result(cursor, cnx, cbs)
2          assert dbg_results(results)
3          return results
4 
5      @contextmanager
6 -    def _storage_handler(self, entity, event):
7 +    def _fixup_cw(self, cnx, entity):
8 +        _cw = entity._cw
9 +        entity._cw = cnx
10 +        try:
11 +            yield
12 +        finally:
13 +            entity._cw = _cw
14 +
15 +    @contextmanager
16 +    def _storage_handler(self, cnx, entity, event):
17          # 1/ memorize values as they are before the storage is called.
18          #    For instance, the BFSStorage will replace the `data`
19          #    binary value with a Binary containing the destination path
20          #    on the filesystem. To make the entity.data usage absolutely
21          #    transparent, we'll have to reset entity.data to its binary
@@ -576,38 +585,39 @@
22          else:
23              entities = [entity]
24          etype = entities[0].__regid__
25          for attr, storage in self._storages.get(etype, {}).items():
26              for entity in entities:
27 -                if event == 'deleted':
28 -                    storage.entity_deleted(entity, attr)
29 -                else:
30 -                    edited = entity.cw_edited
31 -                    if attr in edited:
32 -                        handler = getattr(storage, 'entity_%s' % event)
33 -                        to_restore = handler(entity, attr)
34 -                        restore_values.append((entity, attr, to_restore))
35 +                with self._fixup_cw(cnx, entity):
36 +                    if event == 'deleted':
37 +                        storage.entity_deleted(entity, attr)
38 +                    else:
39 +                        edited = entity.cw_edited
40 +                        if attr in edited:
41 +                            handler = getattr(storage, 'entity_%s' % event)
42 +                            to_restore = handler(entity, attr)
43 +                            restore_values.append((entity, attr, to_restore))
44          try:
45              yield # 2/ execute the source's instructions
46          finally:
47              # 3/ restore original values
48              for entity, attr, value in restore_values:
49                  entity.cw_edited.edited_attribute(attr, value)
50 
51      def add_entity(self, cnx, entity):
52          """add a new entity to the source"""
53 -        with self._storage_handler(entity, 'added'):
54 +        with self._storage_handler(cnx, entity, 'added'):
55              attrs = self.preprocess_entity(entity)
56              sql = self.sqlgen.insert(SQL_PREFIX + entity.cw_etype, attrs)
57              self.doexec(cnx, sql, attrs)
58              if cnx.ertype_supports_undo(entity.cw_etype):
59                  self._record_tx_action(cnx, 'tx_entity_actions', u'C',
60                                         etype=unicode(entity.cw_etype), eid=entity.eid)
61 
62      def update_entity(self, cnx, entity):
63          """replace an entity in the source"""
64 -        with self._storage_handler(entity, 'updated'):
65 +        with self._storage_handler(cnx, entity, 'updated'):
66              attrs = self.preprocess_entity(entity)
67              if cnx.ertype_supports_undo(entity.cw_etype):
68                  changes = self._save_attrs(cnx, entity, attrs)
69                  self._record_tx_action(cnx, 'tx_entity_actions', u'U',
70                                         etype=unicode(entity.cw_etype), eid=entity.eid,
@@ -616,11 +626,11 @@
71                                       ['cw_eid'])
72              self.doexec(cnx, sql, attrs)
73 
74      def delete_entity(self, cnx, entity):
75          """delete an entity from the source"""
76 -        with self._storage_handler(entity, 'deleted'):
77 +        with self._storage_handler(cnx, entity, 'deleted'):
78              if cnx.ertype_supports_undo(entity.cw_etype):
79                  attrs = [SQL_PREFIX + r.type
80                           for r in entity.e_schema.subject_relations()
81                           if (r.final or r.inlined) and not r in VIRTUAL_RTYPES]
82                  changes = self._save_attrs(cnx, entity, attrs)
diff --git a/server/test/unittest_storage.py b/server/test/unittest_storage.py
@@ -85,31 +85,35 @@
83              self.assertEqual(str(cm.exception),
84                               'This callback is only available for BytesFileSystemStorage '
85                               'managed attribute. Is FSPATH() argument BFSS managed?')
86 
87      def test_bfss_storage(self):
88 -        with self.admin_access.repo_cnx() as cnx:
89 -            f1 = self.create_file(cnx)
90 +        with self.admin_access.web_request() as req:
91 +            cnx = req.cnx
92 +            f1 = self.create_file(req)
93              filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
94              self.assertEqual(len(filepaths), 1, filepaths)
95              expected_filepath = filepaths[0]
96              # file should be read only
97              self.assertFalse(os.access(expected_filepath, os.W_OK))
98 -            self.assertEqual(file(expected_filepath).read(), 'the-data')
99 +            self.assertEqual(open(expected_filepath).read(), 'the-data')
100              cnx.rollback()
101              self.assertFalse(osp.isfile(expected_filepath))
102              filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
103              self.assertEqual(len(filepaths), 0, filepaths)
104 -            f1 = self.create_file(cnx)
105 +            f1 = self.create_file(req)
106              cnx.commit()
107              filepaths = glob(osp.join(self.tempdir, '%s_data_*' % f1.eid))
108              self.assertEqual(len(filepaths), 1, filepaths)
109              expected_filepath = filepaths[0]
110 -            self.assertEqual(file(expected_filepath).read(), 'the-data')
111 +            self.assertEqual(open(expected_filepath).read(), 'the-data')
112 +
113 +            # add f1 back to the entity cache with req as _cw
114 +            f1 = req.entity_from_eid(f1.eid)
115              f1.cw_set(data=Binary('the new data'))
116              cnx.rollback()
117 -            self.assertEqual(file(expected_filepath).read(), 'the-data')
118 +            self.assertEqual(open(expected_filepath).read(), 'the-data')
119              f1.cw_delete()
120              self.assertTrue(osp.isfile(expected_filepath))
121              cnx.rollback()
122              self.assertTrue(osp.isfile(expected_filepath))
123              f1.cw_delete()