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