1919#
2020##############################################################################
2121
22+ import hashlib
2223import logging
24+ import struct
25+
2326from contextlib import contextmanager
2427from openerp import models , fields
2528
26- from .deprecate import log_deprecate , DeprecatedClass
29+ from .exception import RetryableJobError
2730
2831_logger = logging .getLogger (__name__ )
2932
@@ -47,33 +50,14 @@ def _get_openerp_module_name(module_path):
4750 return module_name
4851
4952
50- def install_in_connector ():
51- log_deprecate ("This call to 'install_in_connector()' has no effect and is "
52- "not required." )
53-
54-
5553def is_module_installed (env , module_name ):
5654 """ Check if an Odoo addon is installed.
5755
58- The function might be called before `connector` is even installed;
59- in such case, `ir_module_module.is_module_installed()` is not available yet
60- and this is why we first check the installation of `connector` by looking
61- up for a model in the registry.
62-
63- :param module_name: name of the addon to check being 'connector' or
64- an addon depending on it
65-
56+ :param module_name: name of the addon
6657 """
67- if env .registry .get ('connector.backend' ):
68- if module_name == 'connector' :
69- # fast-path: connector is necessarily installed because
70- # the model is in the registry
71- return True
72- # for another addon, check in ir.module.module
73- return env ['ir.module.module' ].is_module_installed (module_name )
74-
75- # connector module is not installed neither any sub-addons
76- return False
58+ # the registry maintains a set of fully loaded modules so we can
59+ # lookup for our module there
60+ return module_name in env .registry ._init_modules
7761
7862
7963def get_openerp_module (cls_or_func ):
@@ -94,11 +78,6 @@ class MetaConnectorUnit(type):
9478 the state of the module (installed or not).
9579 """
9680
97- @property
98- def model_name (cls ):
99- log_deprecate ('renamed to for_model_names' )
100- return cls .for_model_names
101-
10281 @property
10382 def for_model_names (cls ):
10483 """ Returns the list of models on which a
@@ -149,11 +128,6 @@ def __init__(self, connector_env):
149128 self .backend_record = self .connector_env .backend_record
150129 self .session = self .connector_env .session
151130
152- @property
153- def environment (self ):
154- log_deprecate ('renamed to connector_env' )
155- return self .connector_env
156-
157131 @classmethod
158132 def match (cls , session , model ):
159133 """ Returns True if the current class correspond to the
@@ -222,24 +196,45 @@ def unit_for(self, connector_unit_class, model=None):
222196
223197 return env .get_connector_unit (connector_unit_class )
224198
225- def get_connector_unit_for_model (self , connector_unit_class , model = None ):
226- """ Deprecated in favor of :meth:`~unit_for` """
227- log_deprecate ('renamed to unit_for()' )
228- return self .unit_for (connector_unit_class , model = model )
229-
230199 def binder_for (self , model = None ):
231200 """ Returns an new instance of the correct ``Binder`` for
232201 a model """
233202 return self .unit_for (Binder , model )
234203
235- def get_binder_for_model (self , model = None ):
236- """ Returns an new instance of the correct ``Binder`` for
237- a model
204+ def advisory_lock_or_retry (self , lock , retry_seconds = 1 ):
205+ """ Acquire a Postgres transactional advisory lock or retry job
206+
207+ When the lock cannot be acquired, it raises a
208+ ``RetryableJobError`` so the job is retried after n
209+ ``retry_seconds``.
210+
211+ Usage example:
238212
239- Deprecated, use ``binder_for`` now.
213+ ::
214+
215+ lock_name = 'import_record({}, {}, {}, {})'.format(
216+ self.backend_record._name,
217+ self.backend_record.id,
218+ self.model._name,
219+ self.external_id,
220+ )
221+ self.advisory_lock_or_retry(lock_name, retry_seconds=2)
222+
223+ See :func:``openerp.addons.connector.connector.pg_try_advisory_lock``
224+ for details.
225+
226+ :param lock: The lock name. Can be anything convertible to a
227+ string. It needs to represent what should not be synchronized
228+ concurrently, usually the string will contain at least: the
229+ action, the backend type, the backend id, the model name, the
230+ external id
231+ :param retry_seconds: number of seconds after which a job should
232+ be retried when the lock cannot be acquired.
240233 """
241- log_deprecate ('renamed to binder_for()' )
242- return self .binder_for (model = model )
234+ if not pg_try_advisory_lock (self .env , lock ):
235+ raise RetryableJobError ('Could not acquire advisory lock' ,
236+ seconds = retry_seconds ,
237+ ignore_retry = True )
243238
244239
245240class ConnectorEnvironment (object ):
@@ -304,18 +299,6 @@ def pool(self):
304299 def env (self ):
305300 return self .session .env
306301
307- @contextmanager
308- def set_lang (self , code ):
309- """ Change the working language in the environment.
310-
311- It changes the ``lang`` key in the session's context.
312-
313-
314- """
315- raise DeprecationWarning ('ConnectorEnvironment.set_lang has been '
316- 'deprecated. session.change_context should '
317- 'be used instead.' )
318-
319302 def get_connector_unit (self , base_class ):
320303 """ Searches and returns an instance of the
321304 :py:class:`~connector.connector.ConnectorUnit` for the current
@@ -354,9 +337,6 @@ def create_environment(cls, backend_record, session, model,
354337 else :
355338 return cls (backend_record , session , model )
356339
357- Environment = DeprecatedClass ('Environment' ,
358- ConnectorEnvironment )
359-
360340
361341class Binder (ConnectorUnit ):
362342 """ For one record of a model, capable to find an external or
@@ -480,3 +460,74 @@ def unwrap_model(self):
480460 'Cannot unwrap model %s, because it has no %s fields'
481461 % (self .model ._name , self ._openerp_field ))
482462 return column .comodel_name
463+
464+
465+ def pg_try_advisory_lock (env , lock ):
466+ """ Try to acquire a Postgres transactional advisory lock.
467+
468+ The function tries to acquire a lock, returns a boolean indicating
469+ if it could be obtained or not. An acquired lock is released at the
470+ end of the transaction.
471+
472+ A typical use is to acquire a lock at the beginning of an importer
473+ to prevent 2 jobs to do the same import at the same time. Since the
474+ record doesn't exist yet, we can't put a lock on a record, so we put
475+ an advisory lock.
476+
477+ Example:
478+ - Job 1 imports Partner A
479+ - Job 2 imports Partner B
480+ - Partner A has a category X which happens not to exist yet
481+ - Partner B has a category X which happens not to exist yet
482+ - Job 1 import category X as a dependency
483+ - Job 2 import category X as a dependency
484+
485+ Since both jobs are executed concurrently, they both create a record
486+ for category X so we have duplicated records. With this lock:
487+
488+ - Job 1 imports Partner A, it acquires a lock for this partner
489+ - Job 2 imports Partner B, it acquires a lock for this partner
490+ - Partner A has a category X which happens not to exist yet
491+ - Partner B has a category X which happens not to exist yet
492+ - Job 1 import category X as a dependency, it acquires a lock for
493+ this category
494+ - Job 2 import category X as a dependency, try to acquire a lock
495+ but can't, Job 2 is retried later, and when it is retried, it
496+ sees the category X created by Job 1.
497+
498+ The lock is acquired until the end of the transaction.
499+
500+ Usage example:
501+
502+ ::
503+
504+ lock_name = 'import_record({}, {}, {}, {})'.format(
505+ self.backend_record._name,
506+ self.backend_record.id,
507+ self.model._name,
508+ self.external_id,
509+ )
510+ if pg_try_advisory_lock(lock_name):
511+ # do sync
512+ else:
513+ raise RetryableJobError('Could not acquire advisory lock',
514+ seconds=2,
515+ ignore_retry=True)
516+
517+ :param env: the Odoo Environment
518+ :param lock: The lock name. Can be anything convertible to a
519+ string. It needs to represents what should not be synchronized
520+ concurrently so usually the string will contain at least: the
521+ action, the backend type, the backend id, the model name, the
522+ external id
523+ :return True/False whether lock was acquired.
524+ """
525+ hasher = hashlib .sha1 ()
526+ hasher .update ('{}' .format (lock ))
527+ # pg_lock accepts an int8 so we build an hash composed with
528+ # contextual information and we throw away some bits
529+ int_lock = struct .unpack ('q' , hasher .digest ()[:8 ])
530+
531+ env .cr .execute ('SELECT pg_try_advisory_xact_lock(%s);' , (int_lock ,))
532+ acquired = env .cr .fetchone ()[0 ]
533+ return acquired
0 commit comments