|
1 """Hooks management |
|
2 |
|
3 This module defined the `Hook` class and registry and a set of abstract classes |
|
4 for operations. |
|
5 |
|
6 |
|
7 Hooks are called before / after any individual update of entities / relations |
|
8 in the repository and on special events such as server startup or shutdown. |
|
9 |
|
10 |
|
11 Operations may be registered by hooks during a transaction, which will be |
|
12 fired when the pool is commited or rollbacked. |
|
13 |
|
14 |
|
15 Entity hooks (eg before_add_entity, after_add_entity, before_update_entity, |
|
16 after_update_entity, before_delete_entity, after_delete_entity) all have an |
|
17 `entity` attribute |
|
18 |
|
19 Relation (eg before_add_relation, after_add_relation, before_delete_relation, |
|
20 after_delete_relation) all have `eidfrom`, `rtype`, `eidto` attributes. |
|
21 |
|
22 Server start/stop hooks (eg server_startup, server_shutdown) have a `repo` |
|
23 attribute, but *their `cw_req` attribute is None*. |
|
24 |
|
25 Backup/restore hooks (eg server_backup, server_restore) have a `repo` and a |
|
26 `timestamp` attributes, but *their `cw_req` attribute is None*. |
|
27 |
|
28 Session hooks (eg session_open, session_close) have no special attribute. |
|
29 |
|
30 |
|
31 :organization: Logilab |
|
32 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. |
|
33 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
34 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses |
|
35 """ |
|
36 __docformat__ = "restructuredtext en" |
|
37 |
|
38 from warnings import warn |
|
39 from logging import getLogger |
|
40 |
|
41 from logilab.common.decorators import classproperty |
|
42 from logilab.common.logging_ext import set_log_methods |
|
43 |
|
44 from cubicweb.cwvreg import CWRegistry, VRegistry |
|
45 from cubicweb.selectors import (objectify_selector, lltrace, match_search_state, |
|
46 entity_implements) |
|
47 from cubicweb.appobject import AppObject |
|
48 |
|
49 |
|
50 ENTITIES_HOOKS = set(('before_add_entity', 'after_add_entity', |
|
51 'before_update_entity', 'after_update_entity', |
|
52 'before_delete_entity', 'after_delete_entity')) |
|
53 RELATIONS_HOOKS = set(('before_add_relation', 'after_add_relation' , |
|
54 'before_delete_relation','after_delete_relation')) |
|
55 SYSTEM_HOOKS = set(('server_backup', 'server_restore', |
|
56 'server_startup', 'server_shutdown', |
|
57 'session_open', 'session_close')) |
|
58 ALL_HOOKS = ENTITIES_HOOKS | RELATIONS_HOOKS | SYSTEM_HOOKS |
|
59 |
|
60 |
|
61 class HooksRegistry(CWRegistry): |
|
62 |
|
63 def register(self, obj, **kwargs): |
|
64 for event in obj.events: |
|
65 if event not in ALL_HOOKS: |
|
66 raise Exception('bad event %s on %s' % (event, obj)) |
|
67 super(HooksRegistry, self).register(obj, **kwargs) |
|
68 |
|
69 def call_hooks(self, event, req=None, **kwargs): |
|
70 kwargs['event'] = event |
|
71 # XXX remove .enabled |
|
72 for hook in sorted([x for x in self.possible_objects(req, **kwargs) |
|
73 if x.enabled], key=lambda x: x.order): |
|
74 hook() |
|
75 |
|
76 VRegistry.REGISTRY_FACTORY['hooks'] = HooksRegistry |
|
77 |
|
78 |
|
79 # some hook specific selectors ################################################# |
|
80 |
|
81 @objectify_selector |
|
82 @lltrace |
|
83 def match_event(cls, req, **kwargs): |
|
84 if kwargs.get('event') in cls.events: |
|
85 return 1 |
|
86 return 0 |
|
87 |
|
88 @objectify_selector |
|
89 @lltrace |
|
90 def enabled_category(cls, req, **kwargs): |
|
91 if req is None: |
|
92 # server startup / shutdown event |
|
93 config = kwargs['repo'].config |
|
94 else: |
|
95 config = req.vreg.config |
|
96 if enabled_category in config.disabled_hooks_categories: |
|
97 return 0 |
|
98 return 1 |
|
99 |
|
100 @objectify_selector |
|
101 @lltrace |
|
102 def regular_session(cls, req, **kwargs): |
|
103 if req is None or req.is_super_session: |
|
104 return 0 |
|
105 return 1 |
|
106 |
|
107 class match_rtype(match_search_state): |
|
108 """accept if parameters specified as initializer arguments are specified |
|
109 in named arguments given to the selector |
|
110 |
|
111 :param *expected: parameters (eg `basestring`) which are expected to be |
|
112 found in named arguments (kwargs) |
|
113 """ |
|
114 |
|
115 @lltrace |
|
116 def __call__(self, cls, req, *args, **kwargs): |
|
117 return kwargs.get('rtype') in self.expected |
|
118 |
|
119 |
|
120 # base class for hook ########################################################## |
|
121 |
|
122 class Hook(AppObject): |
|
123 __registry__ = 'hooks' |
|
124 __select__ = match_event() & enabled_category() |
|
125 # set this in derivated classes |
|
126 events = None |
|
127 category = None |
|
128 order = 0 |
|
129 # XXX deprecates |
|
130 enabled = True |
|
131 |
|
132 @classproperty |
|
133 def __id__(cls): |
|
134 warn('[3.5] %s: please specify an id for your hook' % cls) |
|
135 return str(id(cls)) |
|
136 |
|
137 @classmethod |
|
138 def __registered__(cls, vreg): |
|
139 super(Hook, cls).__registered__(vreg) |
|
140 if getattr(cls, 'accepts', None): |
|
141 warn('[3.5] %s: accepts is deprecated, define proper __select__' % cls) |
|
142 rtypes = [] |
|
143 for ertype in cls.accepts: |
|
144 if ertype.islower(): |
|
145 rtypes.append(ertype) |
|
146 else: |
|
147 cls.__select__ = cls.__select__ & entity_implements(ertype) |
|
148 if rtypes: |
|
149 cls.__select__ = cls.__select__ & match_rtype(*rtypes) |
|
150 return cls |
|
151 |
|
152 known_args = set(('entity', 'rtype', 'eidfrom', 'eidto', 'repo', 'timestamp')) |
|
153 def __init__(self, req, event, **kwargs): |
|
154 for arg in self.known_args: |
|
155 if arg in kwargs: |
|
156 setattr(self, arg, kwargs.pop(arg)) |
|
157 super(Hook, self).__init__(req, **kwargs) |
|
158 self.event = event |
|
159 |
|
160 def __call__(self): |
|
161 if hasattr(self, 'call'): |
|
162 warn('[3.5] %s: call is deprecated, implements __call__' % self.__class__) |
|
163 if self.event.endswith('_relation'): |
|
164 self.call(self.cw_req, self.eidfrom, self.rtype, self.eidto) |
|
165 elif 'delete' in self.event: |
|
166 self.call(self.cw_req, self.entity.eid) |
|
167 elif self.event.startswith('server_'): |
|
168 self.call(self.repo) |
|
169 elif self.event.startswith('session_'): |
|
170 self.call(self.cw_req) |
|
171 else: |
|
172 self.call(self.cw_req, self.entity) |
|
173 |
|
174 set_log_methods(Hook, getLogger('cubicweb.hook')) |
|
175 |
|
176 |
|
177 # abstract classes for operation ############################################### |
|
178 |
|
179 class Operation(object): |
|
180 """an operation is triggered on connections pool events related to |
|
181 commit / rollback transations. Possible events are: |
|
182 |
|
183 precommit: |
|
184 the pool is preparing to commit. You shouldn't do anything things which |
|
185 has to be reverted if the commit fail at this point, but you can freely |
|
186 do any heavy computation or raise an exception if the commit can't go. |
|
187 You can add some new operation during this phase but their precommit |
|
188 event won't be triggered |
|
189 |
|
190 commit: |
|
191 the pool is preparing to commit. You should avoid to do to expensive |
|
192 stuff or something that may cause an exception in this event |
|
193 |
|
194 revertcommit: |
|
195 if an operation failed while commited, this event is triggered for |
|
196 all operations which had their commit event already to let them |
|
197 revert things (including the operation which made fail the commit) |
|
198 |
|
199 rollback: |
|
200 the transaction has been either rollbacked either |
|
201 * intentionaly |
|
202 * a precommit event failed, all operations are rollbacked |
|
203 * a commit event failed, all operations which are not been triggered for |
|
204 commit are rollbacked |
|
205 |
|
206 order of operations may be important, and is controlled according to: |
|
207 * operation's class |
|
208 """ |
|
209 |
|
210 def __init__(self, session, **kwargs): |
|
211 self.session = session |
|
212 self.user = session.user |
|
213 self.repo = session.repo |
|
214 self.schema = session.repo.schema |
|
215 self.config = session.repo.config |
|
216 self.__dict__.update(kwargs) |
|
217 self.register(session) |
|
218 # execution information |
|
219 self.processed = None # 'precommit', 'commit' |
|
220 self.failed = False |
|
221 |
|
222 def register(self, session): |
|
223 session.add_operation(self, self.insert_index()) |
|
224 |
|
225 def insert_index(self): |
|
226 """return the index of the lastest instance which is not a |
|
227 LateOperation instance |
|
228 """ |
|
229 for i, op in enumerate(self.session.pending_operations): |
|
230 if isinstance(op, (LateOperation, SingleLastOperation)): |
|
231 return i |
|
232 return None |
|
233 |
|
234 def handle_event(self, event): |
|
235 """delegate event handling to the opertaion""" |
|
236 getattr(self, event)() |
|
237 |
|
238 def precommit_event(self): |
|
239 """the observed connections pool is preparing a commit""" |
|
240 |
|
241 def revertprecommit_event(self): |
|
242 """an error went when pre-commiting this operation or a later one |
|
243 |
|
244 should revert pre-commit's changes but take care, they may have not |
|
245 been all considered if it's this operation which failed |
|
246 """ |
|
247 |
|
248 def commit_event(self): |
|
249 """the observed connections pool is commiting""" |
|
250 |
|
251 def revertcommit_event(self): |
|
252 """an error went when commiting this operation or a later one |
|
253 |
|
254 should revert commit's changes but take care, they may have not |
|
255 been all considered if it's this operation which failed |
|
256 """ |
|
257 |
|
258 def rollback_event(self): |
|
259 """the observed connections pool has been rollbacked |
|
260 |
|
261 do nothing by default, the operation will just be removed from the pool |
|
262 operation list |
|
263 """ |
|
264 |
|
265 set_log_methods(Operation, getLogger('cubicweb.session')) |
|
266 |
|
267 |
|
268 class LateOperation(Operation): |
|
269 """special operation which should be called after all possible (ie non late) |
|
270 operations |
|
271 """ |
|
272 def insert_index(self): |
|
273 """return the index of the lastest instance which is not a |
|
274 SingleLastOperation instance |
|
275 """ |
|
276 for i, op in enumerate(self.session.pending_operations): |
|
277 if isinstance(op, SingleLastOperation): |
|
278 return i |
|
279 return None |
|
280 |
|
281 |
|
282 class SingleOperation(Operation): |
|
283 """special operation which should be called once""" |
|
284 def register(self, session): |
|
285 """override register to handle cases where this operation has already |
|
286 been added |
|
287 """ |
|
288 operations = session.pending_operations |
|
289 index = self.equivalent_index(operations) |
|
290 if index is not None: |
|
291 equivalent = operations.pop(index) |
|
292 else: |
|
293 equivalent = None |
|
294 session.add_operation(self, self.insert_index()) |
|
295 return equivalent |
|
296 |
|
297 def equivalent_index(self, operations): |
|
298 """return the index of the equivalent operation if any""" |
|
299 equivalents = [i for i, op in enumerate(operations) |
|
300 if op.__class__ is self.__class__] |
|
301 if equivalents: |
|
302 return equivalents[0] |
|
303 return None |
|
304 |
|
305 |
|
306 class SingleLastOperation(SingleOperation): |
|
307 """special operation which should be called once and after all other |
|
308 operations |
|
309 """ |
|
310 def insert_index(self): |
|
311 return None |