80 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			80 lines
		
	
	
		
			2.9 KiB
		
	
	
	
		
			Python
		
	
	
| # -*- coding: utf-8 -*-
 | |
| # Copyright 2013-2017 Camptocamp SA
 | |
| # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html)
 | |
| 
 | |
| import hashlib
 | |
| import logging
 | |
| import struct
 | |
| 
 | |
| _logger = logging.getLogger(__name__)
 | |
| 
 | |
| 
 | |
| def pg_try_advisory_lock(env, lock):
 | |
|     """ Try to acquire a Postgres transactional advisory lock.
 | |
| 
 | |
|     The function tries to acquire a lock, returns a boolean indicating
 | |
|     if it could be obtained or not. An acquired lock is released at the
 | |
|     end of the transaction.
 | |
| 
 | |
|     A typical use is to acquire a lock at the beginning of an importer
 | |
|     to prevent 2 jobs to do the same import at the same time. Since the
 | |
|     record doesn't exist yet, we can't put a lock on a record, so we put
 | |
|     an advisory lock.
 | |
| 
 | |
|     Example:
 | |
|      - Job 1 imports Partner A
 | |
|      - Job 2 imports Partner B
 | |
|      - Partner A has a category X which happens not to exist yet
 | |
|      - Partner B has a category X which happens not to exist yet
 | |
|      - Job 1 import category X as a dependency
 | |
|      - Job 2 import category X as a dependency
 | |
| 
 | |
|     Since both jobs are executed concurrently, they both create a record
 | |
|     for category X so we have duplicated records.  With this lock:
 | |
| 
 | |
|      - Job 1 imports Partner A, it acquires a lock for this partner
 | |
|      - Job 2 imports Partner B, it acquires a lock for this partner
 | |
|      - Partner A has a category X which happens not to exist yet
 | |
|      - Partner B has a category X which happens not to exist yet
 | |
|      - Job 1 import category X as a dependency, it acquires a lock for
 | |
|        this category
 | |
|      - Job 2 import category X as a dependency, try to acquire a lock
 | |
|        but can't, Job 2 is retried later, and when it is retried, it
 | |
|        sees the category X created by Job 1.
 | |
| 
 | |
|     The lock is acquired until the end of the transaction.
 | |
| 
 | |
|     Usage example:
 | |
| 
 | |
|     ::
 | |
| 
 | |
|         lock_name = 'import_record({}, {}, {}, {})'.format(
 | |
|             self.backend_record._name,
 | |
|             self.backend_record.id,
 | |
|             self.model._name,
 | |
|             self.external_id,
 | |
|         )
 | |
|         if pg_try_advisory_lock(lock_name):
 | |
|             # do sync
 | |
|         else:
 | |
|             raise RetryableJobError('Could not acquire advisory lock',
 | |
|                                     seconds=2,
 | |
|                                     ignore_retry=True)
 | |
| 
 | |
|     :param env: the Odoo Environment
 | |
|     :param lock: The lock name. Can be anything convertible to a
 | |
|        string.  It needs to represents what should not be synchronized
 | |
|        concurrently so usually the string will contain at least: the
 | |
|        action, the backend type, the backend id, the model name, the
 | |
|        external id
 | |
|     :return True/False whether lock was acquired.
 | |
|     """
 | |
|     hasher = hashlib.sha1(str(lock).encode())
 | |
|     # pg_lock accepts an int8 so we build an hash composed with
 | |
|     # contextual information and we throw away some bits
 | |
|     int_lock = struct.unpack('q', hasher.digest()[:8])
 | |
| 
 | |
|     env.cr.execute('SELECT pg_try_advisory_xact_lock(%s);', (int_lock,))
 | |
|     acquired = env.cr.fetchone()[0]
 | |
|     return acquired
 |