cron cleanup, back to the Kernighan KISS roots 1min poll time, rely only on database...
[odoo/odoo.git] / openerp / modules / registry.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 """ Models registries.
23
24 """
25 from contextlib import contextmanager
26 import logging
27 import threading
28
29 import openerp.sql_db
30 import openerp.osv.orm
31 import openerp.tools
32 import openerp.modules.db
33 import openerp.tools.config
34 from openerp.tools import assertion_report
35
36 _logger = logging.getLogger(__name__)
37
38 class Registry(object):
39     """ Model registry for a particular database.
40
41     The registry is essentially a mapping between model names and model
42     instances. There is one registry instance per database.
43
44     """
45
46     def __init__(self, db_name):
47         self.models = {}    # model name/model instance mapping
48         self._sql_error = {}
49         self._store_function = {}
50         self._init = True
51         self._init_parent = {}
52         self._assertion_report = assertion_report.assertion_report()
53
54         # modules fully loaded (maintained during init phase by `loading` module)
55         self._init_modules = set()
56
57         self.db_name = db_name
58         self.db = openerp.sql_db.db_connect(db_name)
59
60         # In monoprocess cron jobs flag (pooljobs)
61         self.cron = False
62
63         # Inter-process signaling (used only when openerp.multi_process is True):
64         # The `base_registry_signaling` sequence indicates the whole registry
65         # must be reloaded.
66         # The `base_cache_signaling sequence` indicates all caches must be
67         # invalidated (i.e. cleared).
68         self.base_registry_signaling_sequence = 1
69         self.base_cache_signaling_sequence = 1
70
71         # Flag indicating if at least one model cache has been cleared.
72         # Useful only in a multi-process context.
73         self._any_cache_cleared = False
74
75         cr = self.db.cursor()
76         has_unaccent = openerp.modules.db.has_unaccent(cr)
77         if openerp.tools.config['unaccent'] and not has_unaccent:
78             _logger.warning("The option --unaccent was given but no unaccent() function was found in database.")
79         self.has_unaccent = openerp.tools.config['unaccent'] and has_unaccent
80         cr.close()
81
82     def do_parent_store(self, cr):
83         for o in self._init_parent:
84             self.get(o)._parent_store_compute(cr)
85         self._init = False
86
87     def obj_list(self):
88         """ Return the list of model names in this registry."""
89         return self.models.keys()
90
91     def add(self, model_name, model):
92         """ Add or replace a model in the registry."""
93         self.models[model_name] = model
94
95     def get(self, model_name):
96         """ Return a model for a given name or None if it doesn't exist."""
97         return self.models.get(model_name)
98
99     def __getitem__(self, model_name):
100         """ Return a model for a given name or raise KeyError if it doesn't exist."""
101         return self.models[model_name]
102
103     def load(self, cr, module):
104         """ Load a given module in the registry.
105
106         At the Python level, the modules are already loaded, but not yet on a
107         per-registry level. This method populates a registry with the given
108         modules, i.e. it instanciates all the classes of a the given module
109         and registers them in the registry.
110
111         """
112
113         res = []
114
115         # Instantiate registered classes (via the MetaModel automatic discovery
116         # or via explicit constructor call), and add them to the pool.
117         for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
118             res.append(cls.create_instance(self, cr))
119
120         return res
121
122     def schedule_cron_jobs(self):
123         """ Make the cron thread care about this registry/database jobs.
124         This will initiate the cron thread to check for any pending jobs for
125         this registry/database as soon as possible. Then it will continuously
126         monitor the ir.cron model for future jobs. See openerp.cron for
127         details.
128         """
129         self.cron = True
130
131     def clear_caches(self):
132         """ Clear the caches
133         This clears the caches associated to methods decorated with
134         ``tools.ormcache`` or ``tools.ormcache_multi`` for all the models.
135         """
136         for model in self.models.itervalues():
137             model.clear_caches()
138         # Special case for ir_ui_menu which does not use openerp.tools.ormcache.
139         ir_ui_menu = self.models.get('ir.ui.menu')
140         if ir_ui_menu:
141             ir_ui_menu.clear_cache()
142
143
144     # Useful only in a multi-process context.
145     def reset_any_cache_cleared(self):
146         self._any_cache_cleared = False
147
148     # Useful only in a multi-process context.
149     def any_cache_cleared(self):
150         return self._any_cache_cleared
151
152     @classmethod
153     def setup_multi_process_signaling(cls, cr):
154         if not openerp.multi_process:
155             return
156
157         # Inter-process signaling:
158         # The `base_registry_signaling` sequence indicates the whole registry
159         # must be reloaded.
160         # The `base_cache_signaling sequence` indicates all caches must be
161         # invalidated (i.e. cleared).
162         cr.execute("""SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'""")
163         if not cr.fetchall():
164             cr.execute("""CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1""")
165             cr.execute("""SELECT nextval('base_registry_signaling')""")
166             cr.execute("""CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1""")
167             cr.execute("""SELECT nextval('base_cache_signaling')""")
168
169     @contextmanager
170     def cursor(self, auto_commit=True):
171         cr = self.db.cursor()
172         try:
173             yield cr
174             if auto_commit:
175                 cr.commit()
176         finally:
177             cr.close()
178
179
180 class RegistryManager(object):
181     """ Model registries manager.
182
183         The manager is responsible for creation and deletion of model
184         registries (essentially database connection/model registry pairs).
185
186     """
187     # Mapping between db name and model registry.
188     # Accessed through the methods below.
189     registries = {}
190     registries_lock = threading.RLock()
191
192     @classmethod
193     def get(cls, db_name, force_demo=False, status=None, update_module=False,
194             pooljobs=True):
195         """ Return a registry for a given database name."""
196         try:
197             return cls.registries[db_name]
198         except KeyError:
199             return cls.new(db_name, force_demo, status,
200                            update_module, pooljobs)
201
202     @classmethod
203     def new(cls, db_name, force_demo=False, status=None,
204             update_module=False, pooljobs=True):
205         """ Create and return a new registry for a given database name.
206
207         The (possibly) previous registry for that database name is discarded.
208
209         """
210         import openerp.modules
211         with cls.registries_lock:
212             registry = Registry(db_name)
213
214             # Initializing a registry will call general code which will in turn
215             # call registries.get (this object) to obtain the registry being
216             # initialized. Make it available in the registries dictionary then
217             # remove it if an exception is raised.
218             cls.delete(db_name)
219             cls.registries[db_name] = registry
220             try:
221                 # This should be a method on Registry
222                 openerp.modules.load_modules(registry.db, force_demo, status, update_module)
223             except Exception:
224                 del cls.registries[db_name]
225                 raise
226
227             cr = registry.db.cursor()
228             try:
229                 Registry.setup_multi_process_signaling(cr)
230                 registry.do_parent_store(cr)
231                 registry.get('ir.actions.report.xml').register_all(cr)
232                 cr.commit()
233             finally:
234                 cr.close()
235
236         if pooljobs:
237             registry.schedule_cron_jobs()
238
239         return registry
240
241     @classmethod
242     def delete(cls, db_name):
243         """Delete the registry linked to a given database.
244
245         This also cleans the associated caches. For good measure this also
246         cancels the associated cron job. But please note that the cron job can
247         be running and take some time before ending, and that you should not
248         remove a registry if it can still be used by some thread. So it might
249         be necessary to call yourself openerp.cron.Agent.cancel(db_name) and
250         and join (i.e. wait for) the thread.
251         """
252         with cls.registries_lock:
253             if db_name in cls.registries:
254                 cls.registries[db_name].clear_caches()
255                 del cls.registries[db_name]
256                 openerp.cron.cancel(db_name)
257
258     @classmethod
259     def delete_all(cls):
260         """Delete all the registries. """
261         with cls.registries_lock:
262             for db_name in cls.registries.keys():
263                 cls.delete(db_name)
264
265     @classmethod
266     def clear_caches(cls, db_name):
267         """Clear caches
268
269         This clears the caches associated to methods decorated with
270         ``tools.ormcache`` or ``tools.ormcache_multi`` for all the models
271         of the given database name.
272
273         This method is given to spare you a ``RegistryManager.get(db_name)``
274         that would loads the given database if it was not already loaded.
275         """
276         with cls.registries_lock:
277             if db_name in cls.registries:
278                 cls.registries[db_name].clear_caches()
279
280     @classmethod
281     def check_registry_signaling(cls, db_name):
282         if openerp.multi_process and db_name in cls.registries:
283             registry = cls.get(db_name, pooljobs=False)
284             cr = registry.db.cursor()
285             try:
286                 cr.execute("""
287                     SELECT base_registry_signaling.last_value,
288                            base_cache_signaling.last_value
289                     FROM base_registry_signaling, base_cache_signaling""")
290                 r, c = cr.fetchone()
291                 # Check if the model registry must be reloaded (e.g. after the
292                 # database has been updated by another process).
293                 if registry.base_registry_signaling_sequence != r:
294                     _logger.info("Reloading the model registry after database signaling.")
295                     # Don't run the cron in the Gunicorn worker.
296                     registry = cls.new(db_name, pooljobs=False)
297                     registry.base_registry_signaling_sequence = r
298                 # Check if the model caches must be invalidated (e.g. after a write
299                 # occured on another process). Don't clear right after a registry
300                 # has been reload.
301                 elif registry.base_cache_signaling_sequence != c:
302                     _logger.info("Invalidating all model caches after database signaling.")
303                     registry.base_cache_signaling_sequence = c
304                     registry.clear_caches()
305                     registry.reset_any_cache_cleared()
306                     # One possible reason caches have been invalidated is the
307                     # use of decimal_precision.write(), in which case we need
308                     # to refresh fields.float columns.
309                     for model in registry.models.values():
310                         for column in model._columns.values():
311                             if hasattr(column, 'digits_change'):
312                                 column.digits_change(cr)
313             finally:
314                 cr.close()
315
316     @classmethod
317     def signal_caches_change(cls, db_name):
318         if openerp.multi_process and db_name in cls.registries:
319             # Check the registries if any cache has been cleared and signal it
320             # through the database to other processes.
321             registry = cls.get(db_name, pooljobs=False)
322             if registry.any_cache_cleared():
323                 _logger.info("At least one model cache has been cleared, signaling through the database.")
324                 cr = registry.db.cursor()
325                 r = 1
326                 try:
327                     cr.execute("select nextval('base_cache_signaling')")
328                     r = cr.fetchone()[0]
329                 finally:
330                     cr.close()
331                 registry.base_cache_signaling_sequence = r
332                 registry.reset_any_cache_cleared()
333
334     @classmethod
335     def signal_registry_change(cls, db_name):
336         if openerp.multi_process and db_name in cls.registries:
337             registry = cls.get(db_name, pooljobs=False)
338             cr = registry.db.cursor()
339             r = 1
340             try:
341                 cr.execute("select nextval('base_registry_signaling')")
342                 r = cr.fetchone()[0]
343             finally:
344                 cr.close()
345             registry.base_registry_signaling_sequence = r
346
347 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: