Merge pull request #648 from odoo-dev/7.0-fix-searchbar-navigation-ged
[odoo/odoo.git] / openerp / modules / registry.py
index 9053eb8..853fab0 100644 (file)
 """ Models registries.
 
 """
+from contextlib import contextmanager
 import logging
 import threading
 
 import openerp.sql_db
 import openerp.osv.orm
-import openerp.cron
 import openerp.tools
 import openerp.modules.db
 import openerp.tools.config
+from openerp.tools import assertion_report
 
 _logger = logging.getLogger(__name__)
 
@@ -43,21 +44,30 @@ class Registry(object):
     """
 
     def __init__(self, db_name):
-        self.models = {} # model name/model instance mapping
+        self.models = {}    # model name/model instance mapping
         self._sql_error = {}
         self._store_function = {}
         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()
+
         self.db_name = db_name
         self.db = openerp.sql_db.db_connect(db_name)
 
+        # Indicates that the registry is 
+        self.ready = 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
+        self.base_registry_signaling_sequence = None
+        self.base_cache_signaling_sequence = None
 
         # Flag indicating if at least one model cache has been cleared.
         # Useful only in a multi-process context.
@@ -100,24 +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
-
-    def schedule_cron_jobs(self):
-        """ Make the cron thread care about this registry/database jobs.
-        This will initiate the cron thread to check for any pending jobs for
-        this registry/database as soon as possible. Then it will continuously
-        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)
+            # 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 clear_caches(self):
         """ Clear the caches
@@ -140,6 +142,44 @@ class Registry(object):
     def any_cache_cleared(self):
         return self._any_cache_cleared
 
+    @classmethod
+    def setup_multi_process_signaling(cls, cr):
+        if not openerp.multi_process:
+            return None, None
+
+        # 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')""")
+        
+        cr.execute("""
+                    SELECT base_registry_signaling.last_value,
+                           base_cache_signaling.last_value
+                    FROM base_registry_signaling, base_cache_signaling""")
+        r, c = cr.fetchone()
+        _logger.debug("Multiprocess load registry signaling: [Registry: # %s] "\
+                    "[Cache: # %s]",
+                    r, c)
+        return r, c
+
+    @contextmanager
+    def cursor(self, auto_commit=True):
+        cr = self.db.cursor()
+        try:
+            yield cr
+            if auto_commit:
+                cr.commit()
+        finally:
+            cr.close()
+
+
 class RegistryManager(object):
     """ Model registries manager.
 
@@ -153,18 +193,22 @@ class RegistryManager(object):
     registries_lock = threading.RLock()
 
     @classmethod
-    def get(cls, db_name, force_demo=False, status=None, update_module=False,
-            pooljobs=True):
+    def get(cls, db_name, force_demo=False, status=None, update_module=False):
         """ Return a registry for a given database name."""
-        try:
-            return cls.registries[db_name]
-        except KeyError:
-            return cls.new(db_name, force_demo, status,
-                           update_module, pooljobs)
+        with cls.registries_lock:
+            try:
+                return cls.registries[db_name]
+            except KeyError:
+                return cls.new(db_name, force_demo, status,
+                               update_module)
+            finally:
+                # set db tracker - cleaned up at the WSGI
+                # dispatching phase in openerp.service.wsgi_server.application
+                threading.current_thread().dbname = db_name
 
     @classmethod
     def new(cls, db_name, force_demo=False, status=None,
-            update_module=False, pooljobs=True):
+            update_module=False):
         """ Create and return a new registry for a given database name.
 
         The (possibly) previous registry for that database name is discarded.
@@ -181,12 +225,21 @@ class RegistryManager(object):
             cls.delete(db_name)
             cls.registries[db_name] = registry
             try:
+                with registry.cursor() as cr:
+                    seq_registry, seq_cache = Registry.setup_multi_process_signaling(cr)
+                    registry.base_registry_signaling_sequence = seq_registry
+                    registry.base_cache_signaling_sequence = seq_cache
                 # This should be a method on Registry
                 openerp.modules.load_modules(registry.db, force_demo, status, update_module)
             except Exception:
                 del cls.registries[db_name]
                 raise
 
+            # load_modules() above can replace the registry by calling
+            # indirectly new() again (when modules have to be uninstalled).
+            # Yeah, crazy.
+            registry = cls.registries[db_name]
+
             cr = registry.db.cursor()
             try:
                 registry.do_parent_store(cr)
@@ -195,28 +248,20 @@ class RegistryManager(object):
             finally:
                 cr.close()
 
-        if pooljobs:
-            registry.schedule_cron_jobs()
+        registry.ready = True
 
+        if update_module:
+            # only in case of update, otherwise we'll have an infinite reload loop!
+            cls.signal_registry_change(db_name)
         return registry
 
     @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):
@@ -243,35 +288,39 @@ class RegistryManager(object):
     @classmethod
     def check_registry_signaling(cls, db_name):
         if openerp.multi_process and db_name in cls.registries:
-            # Check if the model registry must be reloaded (e.g. after the
-            # database has been updated by another process).
-            registry = cls.get(db_name, pooljobs=False)
+            registry = cls.get(db_name)
             cr = registry.db.cursor()
-            registry_reloaded = False
             try:
-                cr.execute('SELECT last_value FROM base_registry_signaling')
-                r = cr.fetchone()[0]
-                if registry.base_registry_signaling_sequence != r:
+                cr.execute("""
+                    SELECT base_registry_signaling.last_value,
+                           base_cache_signaling.last_value
+                    FROM base_registry_signaling, base_cache_signaling""")
+                r, c = cr.fetchone()
+                _logger.debug("Multiprocess signaling check: [Registry - old# %s new# %s] "\
+                    "[Cache - old# %s new# %s]",
+                    registry.base_registry_signaling_sequence, r,
+                    registry.base_cache_signaling_sequence, c)
+                # 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 is not None and 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
-                    registry_reloaded = True
-            finally:
-                cr.close()
-
-            # 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.
-            cr = openerp.sql_db.db_connect(db_name).cursor()
-            try:
-                cr.execute('SELECT last_value FROM base_cache_signaling')
-                r = cr.fetchone()[0]
-                if registry.base_cache_signaling_sequence != r and not registry_reloaded:
+                    registry = cls.new(db_name)
+                # 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 is not None and registry.base_cache_signaling_sequence != c:
                     _logger.info("Invalidating all model caches after database signaling.")
-                    registry.base_cache_signaling_sequence = r
                     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)
+                registry.base_registry_signaling_sequence = r
+                registry.base_cache_signaling_sequence = c
             finally:
                 cr.close()
 
@@ -280,13 +329,12 @@ class RegistryManager(object):
         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)
+            registry = cls.get(db_name)
             if registry.any_cache_cleared():
-                _logger.info("At least one model cache has been cleare, signaling through the database.")
+                _logger.info("At least one model cache has been cleared, signaling through the database.")
                 cr = registry.db.cursor()
                 r = 1
                 try:
-                    pass
                     cr.execute("select nextval('base_cache_signaling')")
                     r = cr.fetchone()[0]
                 finally:
@@ -297,7 +345,8 @@ class RegistryManager(object):
     @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)
+            _logger.info("Registry changed, signaling through the database")
+            registry = cls.get(db_name)
             cr = registry.db.cursor()
             r = 1
             try: