19
19
#
20
20
##############################################################################
21
21
22
+ import hashlib
22
23
import logging
24
+ import struct
25
+
23
26
from contextlib import contextmanager
24
27
from openerp import models , fields
25
28
26
- from .deprecate import log_deprecate , DeprecatedClass
29
+ from .exception import RetryableJobError
27
30
28
31
_logger = logging .getLogger (__name__ )
29
32
@@ -47,33 +50,14 @@ def _get_openerp_module_name(module_path):
47
50
return module_name
48
51
49
52
50
- def install_in_connector ():
51
- log_deprecate ("This call to 'install_in_connector()' has no effect and is "
52
- "not required." )
53
-
54
-
55
53
def is_module_installed (env , module_name ):
56
54
""" Check if an Odoo addon is installed.
57
55
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
66
57
"""
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
77
61
78
62
79
63
def get_openerp_module (cls_or_func ):
@@ -94,11 +78,6 @@ class MetaConnectorUnit(type):
94
78
the state of the module (installed or not).
95
79
"""
96
80
97
- @property
98
- def model_name (cls ):
99
- log_deprecate ('renamed to for_model_names' )
100
- return cls .for_model_names
101
-
102
81
@property
103
82
def for_model_names (cls ):
104
83
""" Returns the list of models on which a
@@ -149,11 +128,6 @@ def __init__(self, connector_env):
149
128
self .backend_record = self .connector_env .backend_record
150
129
self .session = self .connector_env .session
151
130
152
- @property
153
- def environment (self ):
154
- log_deprecate ('renamed to connector_env' )
155
- return self .connector_env
156
-
157
131
@classmethod
158
132
def match (cls , session , model ):
159
133
""" Returns True if the current class correspond to the
@@ -222,24 +196,45 @@ def unit_for(self, connector_unit_class, model=None):
222
196
223
197
return env .get_connector_unit (connector_unit_class )
224
198
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
-
230
199
def binder_for (self , model = None ):
231
200
""" Returns an new instance of the correct ``Binder`` for
232
201
a model """
233
202
return self .unit_for (Binder , model )
234
203
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:
238
212
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.
240
233
"""
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 )
243
238
244
239
245
240
class ConnectorEnvironment (object ):
@@ -304,18 +299,6 @@ def pool(self):
304
299
def env (self ):
305
300
return self .session .env
306
301
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
-
319
302
def get_connector_unit (self , base_class ):
320
303
""" Searches and returns an instance of the
321
304
:py:class:`~connector.connector.ConnectorUnit` for the current
@@ -354,9 +337,6 @@ def create_environment(cls, backend_record, session, model,
354
337
else :
355
338
return cls (backend_record , session , model )
356
339
357
- Environment = DeprecatedClass ('Environment' ,
358
- ConnectorEnvironment )
359
-
360
340
361
341
class Binder (ConnectorUnit ):
362
342
""" For one record of a model, capable to find an external or
@@ -480,3 +460,74 @@ def unwrap_model(self):
480
460
'Cannot unwrap model %s, because it has no %s fields'
481
461
% (self .model ._name , self ._openerp_field ))
482
462
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