Skip to content

Commit ba40ebc

Browse files
dependabot[bot]AlexSCorey
authored andcommitted
Bump pandas from 2.2.3 to 2.3.3 (ansible#252)
Bumps [pandas](https://github.com/pandas-dev/pandas) from 2.2.3 to 2.3.3. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Commits](pandas-dev/pandas@v2.2.3...v2.3.3) --- updated-dependencies: - dependency-name: pandas dependency-version: 2.3.3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent 489b1ce commit ba40ebc

File tree

4 files changed

+146
-34
lines changed

4 files changed

+146
-34
lines changed

metrics_utility/automation_controller_billing/collector.py

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,20 @@
1-
import contextlib
21
import json
32
import os
43

4+
from contextlib import contextmanager
5+
56
from django.conf import settings
67
from django.core.serializers.json import DjangoJSONEncoder
78
from django.db import connection
89

910
import metrics_utility.base as base
1011

12+
from metrics_utility.automation_controller_billing.db_lock_helpers import advisory_lock
1113
from metrics_utility.automation_controller_billing.helpers import get_last_entries_from_db
1214
from metrics_utility.automation_controller_billing.package.factory import Factory as PackageFactory
1315
from metrics_utility.logger import logger
1416

1517

16-
# work around https://github.com/ansible/awx/pull/15676
17-
try:
18-
# 2.4, early 2.5
19-
from awx.main.utils.pglock import advisory_lock
20-
except ImportError:
21-
# later 2.5, 2.6
22-
from ansible_base.lib.utils.db import advisory_lock
23-
24-
2518
class Collector(base.Collector):
2619
def __init__(self, collection_type=base.Collector.SCHEDULED_COLLECTION, collector_module=None, ship_target=None, billing_provider_params=None):
2720
if collector_module is None:
@@ -106,7 +99,7 @@ def registered_collectors(cls, module=None):
10699

107100
return base.Collector.registered_collectors(collectors)
108101

109-
@contextlib.contextmanager
102+
@contextmanager
110103
def _pg_advisory_lock(self, key, wait=False):
111104
"""Use awx specific implementation to pass tests with sqlite3"""
112105
with advisory_lock(key, wait=wait) as lock:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from contextlib import contextmanager
2+
from zlib import crc32
3+
4+
from django.db import DEFAULT_DB_ALIAS, connection, connections
5+
6+
from metrics_utility.logger import logger
7+
8+
9+
@contextmanager
10+
def django_pglocks_advisory_lock(key, shared=False, wait=False, db_alias=DEFAULT_DB_ALIAS):
11+
function_name = 'pg_'
12+
release_function_name = 'pg_advisory_unlock'
13+
if not wait:
14+
function_name += 'try_'
15+
function_name += 'advisory_lock'
16+
if shared:
17+
function_name += '_shared'
18+
release_function_name += '_shared'
19+
tuple_format = False
20+
if isinstance(key, (list, tuple)):
21+
if len(key) != 2:
22+
raise ValueError('Tuples and lists as lock IDs must have exactly two entries.')
23+
if not isinstance(key[0], int) or not isinstance(key[1], int):
24+
raise ValueError('Both members of a tuple/list lock ID must be integers')
25+
tuple_format = True
26+
elif isinstance(key, str):
27+
pos = crc32(key.encode('utf-8'))
28+
key = (2**31 - 1) & pos
29+
if pos & 2**31:
30+
key -= 2**31
31+
elif not isinstance(key, int):
32+
raise ValueError('Cannot use %s as a lock id' % key)
33+
if tuple_format:
34+
base = 'SELECT %s(%d, %d)'
35+
params = (key[0], key[1])
36+
else:
37+
base = 'SELECT %s(%d)'
38+
params = (key,)
39+
acquire_params = (function_name,) + params
40+
command = base % acquire_params
41+
42+
with connections[db_alias].cursor() as cursor:
43+
cursor.execute(command)
44+
45+
if not wait:
46+
acquired = cursor.fetchone()[0]
47+
else:
48+
acquired = True
49+
try:
50+
yield acquired
51+
except Exception as e:
52+
if not acquired and not wait:
53+
raise e
54+
finally:
55+
if acquired:
56+
release_params = (release_function_name,) + params
57+
58+
command = base % release_params
59+
cursor.execute(command)
60+
61+
62+
@contextmanager
63+
def advisory_lock(*args, lock_session_timeout_milliseconds=0, **kwargs):
64+
if connection.vendor == 'postgresql':
65+
cur = None
66+
idle_in_transaction_session_timeout = None
67+
idle_session_timeout = None
68+
try:
69+
if lock_session_timeout_milliseconds > 0:
70+
"""Set the idle session timeout for the current transaction"""
71+
with connection.cursor() as cur:
72+
idle_in_transaction_session_timeout = cur.execute('SHOW idle_in_transaction_session_timeout').fetchone()[0]
73+
idle_session_timeout = cur.execute('SHOW idle_session_timeout').fetchone()[0]
74+
cur.execute('SET idle_in_transaction_session_timeout = %s', (lock_session_timeout_milliseconds,))
75+
cur.execute('SET idle_session_timeout = %s', (lock_session_timeout_milliseconds,))
76+
with django_pglocks_advisory_lock(*args, **kwargs) as internal_lock:
77+
yield internal_lock
78+
if lock_session_timeout_milliseconds > 0:
79+
with connection.cursor() as cur:
80+
cur.execute('SET idle_in_transaction_session_timeout = %s', (idle_in_transaction_session_timeout,))
81+
cur.execute('SET idle_session_timeout = %s', (idle_session_timeout,))
82+
except Exception as e:
83+
logger.error(f'Error setting idle session timeout: {e}')
84+
elif connection.vendor == 'sqlite':
85+
yield True
86+
else:
87+
raise RuntimeError(f'Advisory lock not implemented for database type {connection.vendor}')

metrics_utility/base/collector.py

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import contextlib
2-
import hashlib
32
import inspect
43
import logging
54
import os
@@ -9,8 +8,10 @@
98

109
from abc import abstractmethod
1110

11+
from django.db import DEFAULT_DB_ALIAS
1212
from django.utils.timezone import now, timedelta
1313

14+
from metrics_utility.automation_controller_billing.db_lock_helpers import advisory_lock
1415
from metrics_utility.logger import logger
1516

1617
from .collection import Collection
@@ -332,29 +333,51 @@ def _add_collection_to_package(self, collection):
332333
if collection.ship_immediately():
333334
self._process_package(package)
334335

336+
@abstractmethod
335337
@contextlib.contextmanager
336-
def _pg_advisory_lock(self, key, wait=False):
337-
"""Postgres db lock"""
338-
connection = self.db_connection()
339-
340-
if connection is None:
341-
yield True
342-
else:
343-
# Build 64-bit integer out of the resource id
344-
resource_key = int(hashlib.sha512(key.encode()).hexdigest(), 16) % 2**63
345-
346-
cursor = connection.cursor()
347-
348-
try:
349-
if wait:
350-
cursor.execute('SELECT pg_advisory_lock(%s);', (resource_key,))
351-
else:
352-
cursor.execute('SELECT pg_try_advisory_lock(%s);', (resource_key,))
353-
acquired = cursor.fetchall()[0][0]
354-
yield acquired
355-
finally:
356-
cursor.execute('SELECT pg_advisory_unlock(%s);', (resource_key,))
357-
cursor.close()
338+
def _pg_advisory_lock(self, key, shared=False, wait=False, db_alias=DEFAULT_DB_ALIAS):
339+
"""PostgreSQL advisory lock context manager.
340+
341+
Subclasses must implement this method to provide database advisory locking.
342+
The implementation should:
343+
344+
Args:
345+
key (str): Lock identifier. Can be a string which will be converted to an integer
346+
using a hash function (CRC32, SHA512, etc.), or an integer, or a tuple
347+
of two integers for PostgreSQL's two-parameter lock functions.
348+
shared (bool): If True, acquire a shared lock (pg_advisory_lock_shared).
349+
If False, acquire an exclusive lock (pg_advisory_lock).
350+
Default is False (exclusive lock).
351+
wait (bool): If True, block until lock is acquired (pg_advisory_lock).
352+
If False, return immediately (pg_try_advisory_lock).
353+
When wait=False and lock cannot be acquired, should raise an exception.
354+
Default is False (non-blocking).
355+
db_alias (str): Django database alias to use for the connection.
356+
Default is DEFAULT_DB_ALIAS (usually 'default').
357+
358+
Yields:
359+
bool: True if lock was acquired, False if not acquired (only when wait=True).
360+
When wait=False, should either yield True or raise an exception.
361+
362+
The implementation should:
363+
- Support PostgreSQL advisory locks using pg_advisory_lock/pg_try_advisory_lock
364+
- Handle lock release automatically in finally block using pg_advisory_unlock
365+
- Support both shared and exclusive locks if needed
366+
- Handle different database vendors (PostgreSQL, SQLite, etc.)
367+
- Convert string keys to appropriate integer format for PostgreSQL
368+
- Raise exceptions when wait=False and lock cannot be acquired
369+
370+
Example PostgreSQL lock functions used:
371+
- pg_advisory_lock(key) - blocking exclusive lock
372+
- pg_try_advisory_lock(key) - non-blocking exclusive lock
373+
- pg_advisory_lock_shared(key) - blocking shared lock
374+
- pg_try_advisory_lock_shared(key) - non-blocking shared lock
375+
- pg_advisory_unlock(key) - release exclusive lock
376+
- pg_advisory_unlock_shared(key) - release shared lock
377+
"""
378+
with advisory_lock(key, shared=shared, wait=wait, db_alias=db_alias) as lock:
379+
yield lock
380+
pass
358381

359382
def _process_packages(self):
360383
for group, packages in self.packages.items():

metrics_utility/test/base/classes/analytics_collector.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from contextlib import contextmanager
2+
13
from base.classes.package import Package
4+
from django.db import DEFAULT_DB_ALIAS
25

6+
from metrics_utility.automation_controller_billing.db_lock_helpers import advisory_lock
37
from metrics_utility.base import Collector
48

59

@@ -12,6 +16,11 @@ def db_connection():
1216
def _package_class():
1317
return Package
1418

19+
@contextmanager
20+
def _pg_advisory_lock(self, key, shared=False, wait=False, db_alias=DEFAULT_DB_ALIAS):
21+
with advisory_lock(key, shared=shared, wait=wait, db_alias=db_alias) as lock:
22+
yield lock
23+
1524
def _is_shipping_configured(self):
1625
return False
1726

0 commit comments

Comments
 (0)