[IMP] register_hook: improve a bit the code
[odoo/odoo.git] / openerp / modules / registry.py
index 5aa2eca..23dbc28 100644 (file)
@@ -28,7 +28,6 @@ import threading
 
 import openerp.sql_db
 import openerp.osv.orm
-import openerp.cron
 import openerp.tools
 import openerp.modules.db
 import openerp.tools.config
@@ -51,6 +50,7 @@ class Registry(object):
         self._init = True
         self._init_parent = {}
         self._assertion_report = assertion_report.assertion_report()
+        self.fields_by_model = None
 
         # modules fully loaded (maintained during init phase by `loading` module)
         self._init_modules = set()
@@ -58,6 +58,21 @@ class Registry(object):
         self.db_name = db_name
         self.db = openerp.sql_db.db_connect(db_name)
 
+        # In monoprocess cron jobs flag (pooljobs)
+        self.cron = False
+
+        # Inter-process signaling (used only when openerp.multi_process is True):
+        # The `base_registry_signaling` sequence indicates the whole registry
+        # must be reloaded.
+        # The `base_cache_signaling sequence` indicates all caches must be
+        # invalidated (i.e. cleared).
+        self.base_registry_signaling_sequence = 1
+        self.base_cache_signaling_sequence = 1
+
+        # Flag indicating if at least one model cache has been cleared.
+        # Useful only in a multi-process context.
+        self._any_cache_cleared = False
+
         cr = self.db.cursor()
         has_unaccent = openerp.modules.db.has_unaccent(cr)
         if openerp.tools.config['unaccent'] and not has_unaccent:
@@ -95,15 +110,16 @@ class Registry(object):
         and registers them in the registry.
 
         """
-
-        res = []
-
+        models_to_load = [] # need to preserve loading order
         # Instantiate registered classes (via the MetaModel automatic discovery
         # or via explicit constructor call), and add them to the pool.
         for cls in openerp.osv.orm.MetaModel.module_to_models.get(module.name, []):
-            res.append(cls.create_instance(self, cr))
-
-        return res
+            # models register themselves in self.models
+            model = cls.create_instance(self, cr)
+            if model._name not in models_to_load:
+                # avoid double-loading models whose declaration is split
+                models_to_load.append(model._name)
+        return [self.models[m] for m in models_to_load]
 
     def schedule_cron_jobs(self):
         """ Make the cron thread care about this registry/database jobs.
@@ -112,7 +128,7 @@ class Registry(object):
         monitor the ir.cron model for future jobs. See openerp.cron for
         details.
         """
-        openerp.cron.schedule_wakeup(openerp.cron.WAKE_UP_NOW, self.db.dbname)
+        self.cron = True
 
     def clear_caches(self):
         """ Clear the caches
@@ -121,6 +137,36 @@ class Registry(object):
         """
         for model in self.models.itervalues():
             model.clear_caches()
+        # Special case for ir_ui_menu which does not use openerp.tools.ormcache.
+        ir_ui_menu = self.models.get('ir.ui.menu')
+        if ir_ui_menu:
+            ir_ui_menu.clear_cache()
+
+
+    # Useful only in a multi-process context.
+    def reset_any_cache_cleared(self):
+        self._any_cache_cleared = False
+
+    # Useful only in a multi-process context.
+    def any_cache_cleared(self):
+        return self._any_cache_cleared
+
+    @classmethod
+    def setup_multi_process_signaling(cls, cr):
+        if not openerp.multi_process:
+            return
+
+        # Inter-process signaling:
+        # The `base_registry_signaling` sequence indicates the whole registry
+        # must be reloaded.
+        # The `base_cache_signaling sequence` indicates all caches must be
+        # invalidated (i.e. cleared).
+        cr.execute("""SELECT sequence_name FROM information_schema.sequences WHERE sequence_name='base_registry_signaling'""")
+        if not cr.fetchall():
+            cr.execute("""CREATE SEQUENCE base_registry_signaling INCREMENT BY 1 START WITH 1""")
+            cr.execute("""SELECT nextval('base_registry_signaling')""")
+            cr.execute("""CREATE SEQUENCE base_cache_signaling INCREMENT BY 1 START WITH 1""")
+            cr.execute("""SELECT nextval('base_cache_signaling')""")
 
     @contextmanager
     def cursor(self, auto_commit=True):
@@ -182,6 +228,7 @@ class RegistryManager(object):
 
             cr = registry.db.cursor()
             try:
+                Registry.setup_multi_process_signaling(cr)
                 registry.do_parent_store(cr)
                 registry.get('ir.actions.report.xml').register_all(cr)
                 cr.commit()
@@ -195,20 +242,11 @@ class RegistryManager(object):
 
     @classmethod
     def delete(cls, db_name):
-        """Delete the registry linked to a given database.
-
-        This also cleans the associated caches. For good measure this also
-        cancels the associated cron job. But please note that the cron job can
-        be running and take some time before ending, and that you should not
-        remove a registry if it can still be used by some thread. So it might
-        be necessary to call yourself openerp.cron.Agent.cancel(db_name) and
-        and join (i.e. wait for) the thread.
-        """
+        """Delete the registry linked to a given database.  """
         with cls.registries_lock:
             if db_name in cls.registries:
                 cls.registries[db_name].clear_caches()
                 del cls.registries[db_name]
-                openerp.cron.cancel(db_name)
 
     @classmethod
     def delete_all(cls):
@@ -232,5 +270,71 @@ class RegistryManager(object):
             if db_name in cls.registries:
                 cls.registries[db_name].clear_caches()
 
+    @classmethod
+    def check_registry_signaling(cls, db_name):
+        if openerp.multi_process and db_name in cls.registries:
+            registry = cls.get(db_name, pooljobs=False)
+            cr = registry.db.cursor()
+            try:
+                cr.execute("""
+                    SELECT base_registry_signaling.last_value,
+                           base_cache_signaling.last_value
+                    FROM base_registry_signaling, base_cache_signaling""")
+                r, c = cr.fetchone()
+                # Check if the model registry must be reloaded (e.g. after the
+                # database has been updated by another process).
+                if registry.base_registry_signaling_sequence != r:
+                    _logger.info("Reloading the model registry after database signaling.")
+                    # Don't run the cron in the Gunicorn worker.
+                    registry = cls.new(db_name, pooljobs=False)
+                    registry.base_registry_signaling_sequence = r
+                # Check if the model caches must be invalidated (e.g. after a write
+                # occured on another process). Don't clear right after a registry
+                # has been reload.
+                elif registry.base_cache_signaling_sequence != c:
+                    _logger.info("Invalidating all model caches after database signaling.")
+                    registry.base_cache_signaling_sequence = c
+                    registry.clear_caches()
+                    registry.reset_any_cache_cleared()
+                    # One possible reason caches have been invalidated is the
+                    # use of decimal_precision.write(), in which case we need
+                    # to refresh fields.float columns.
+                    for model in registry.models.values():
+                        for column in model._columns.values():
+                            if hasattr(column, 'digits_change'):
+                                column.digits_change(cr)
+            finally:
+                cr.close()
+
+    @classmethod
+    def signal_caches_change(cls, db_name):
+        if openerp.multi_process and db_name in cls.registries:
+            # Check the registries if any cache has been cleared and signal it
+            # through the database to other processes.
+            registry = cls.get(db_name, pooljobs=False)
+            if registry.any_cache_cleared():
+                _logger.info("At least one model cache has been cleared, signaling through the database.")
+                cr = registry.db.cursor()
+                r = 1
+                try:
+                    cr.execute("select nextval('base_cache_signaling')")
+                    r = cr.fetchone()[0]
+                finally:
+                    cr.close()
+                registry.base_cache_signaling_sequence = r
+                registry.reset_any_cache_cleared()
+
+    @classmethod
+    def signal_registry_change(cls, db_name):
+        if openerp.multi_process and db_name in cls.registries:
+            registry = cls.get(db_name, pooljobs=False)
+            cr = registry.db.cursor()
+            r = 1
+            try:
+                cr.execute("select nextval('base_registry_signaling')")
+                r = cr.fetchone()[0]
+            finally:
+                cr.close()
+            registry.base_registry_signaling_sequence = r
 
 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: