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