-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
232 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import os | ||
import uuid | ||
import json | ||
import glob | ||
import time | ||
import datetime | ||
from contextlib import contextmanager | ||
|
||
|
||
LOCK_TTL_SECONDS = 3600 # 1 hour | ||
WAIT_TTL_SECONDS = 60 * 10 # 10 minutes | ||
BASE_LOCK_PATH = os.getenv('BASE_LOCK_PATH', '/var/ckan_dgp_locks') | ||
|
||
|
||
def is_my_lock_active(my_lock_id, lock_path): | ||
locks_to_remove = [] | ||
valid_locks = {} | ||
for lock_file in glob.glob(os.path.join(lock_path, f'*.json')): | ||
with open(lock_file) as f: | ||
lock = json.load(f) | ||
lock_id = lock['id'] | ||
lock_time = datetime.datetime.strptime(lock['time'], '%Y-%m-%d %H:%M:%S:%f') | ||
if lock_time < datetime.datetime.now() - datetime.timedelta(seconds=LOCK_TTL_SECONDS): | ||
locks_to_remove.append(lock_file) | ||
else: | ||
valid_locks[f'{lock_time.strftime("%Y-%m-%d %H:%M:%S:%f")}_{lock_id}'] = lock_id | ||
active_lock_key = list(sorted(valid_locks.keys()))[0] if len(valid_locks) > 0 else None | ||
active_lock_id = valid_locks[active_lock_key] if active_lock_key else None | ||
if active_lock_id == my_lock_id: | ||
for lock_file in locks_to_remove: | ||
os.remove(lock_file) | ||
return True | ||
else: | ||
return False | ||
|
||
|
||
@contextmanager | ||
def instance_package_lock(instance_name, package_id, with_lock=True): | ||
if with_lock: | ||
lock_id = str(uuid.uuid4()) | ||
lock_time = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S:%f') | ||
lock_file = os.path.join(BASE_LOCK_PATH, instance_name, package_id, f'{lock_id}.json') | ||
os.makedirs(os.path.dirname(lock_file), exist_ok=True) | ||
with open(lock_file, 'w') as f: | ||
json.dump({ | ||
'id': lock_id, | ||
'time': lock_time, | ||
}, f) | ||
start_wait_time = datetime.datetime.now() | ||
while True: | ||
if is_my_lock_active(lock_id, os.path.dirname(lock_file)): | ||
break | ||
if datetime.datetime.now() - start_wait_time > datetime.timedelta(seconds=WAIT_TTL_SECONDS): | ||
os.remove(lock_file) | ||
raise Exception(f'Failed to acquire lock for {instance_name}/{package_id} after {WAIT_TTL_SECONDS} seconds') | ||
time.sleep(1) | ||
try: | ||
yield | ||
finally: | ||
os.remove(lock_file) | ||
else: | ||
yield |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,3 @@ | ||
pytest==6.2.1 | ||
pytest==8.3.2 | ||
pytest-mock==3.14.0 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import os | ||
import uuid | ||
import json | ||
import glob | ||
import tempfile | ||
import datetime | ||
|
||
import pytest | ||
|
||
from datacity_ckan_dgp.utils.locking import is_my_lock_active, LOCK_TTL_SECONDS, instance_package_lock | ||
|
||
|
||
def create_lock_file(lockdir, my_lock_time=None): | ||
my_lock_id = str(uuid.uuid4()) | ||
if my_lock_time is None: | ||
my_lock_time = datetime.datetime.now() | ||
with open(f'{lockdir}/{my_lock_id}.json', 'w') as f: | ||
json.dump({ | ||
'id': my_lock_id, | ||
'time': my_lock_time.strftime('%Y-%m-%d %H:%M:%S:%f') | ||
}, f) | ||
return my_lock_id | ||
|
||
|
||
def test_is_my_lock_active_no_locks(): | ||
with tempfile.TemporaryDirectory() as tmpdir: | ||
my_lock_id = create_lock_file(tmpdir) | ||
assert is_my_lock_active(my_lock_id, tmpdir) | ||
|
||
|
||
def test_is_my_lock_active_older_lock_exists(): | ||
with tempfile.TemporaryDirectory() as tmpdir: | ||
older_lock_id = create_lock_file(tmpdir, datetime.datetime.now() - datetime.timedelta(seconds=1)) | ||
my_lock_id = create_lock_file(tmpdir) | ||
assert not is_my_lock_active(my_lock_id, tmpdir) | ||
os.remove(f'{tmpdir}/{older_lock_id}.json') | ||
assert is_my_lock_active(my_lock_id, tmpdir) | ||
|
||
|
||
def test_is_my_lock_active_delete_older_expired_lock(): | ||
with tempfile.TemporaryDirectory() as tmpdir: | ||
expired_lock_id = create_lock_file(tmpdir, datetime.datetime.now() - datetime.timedelta(seconds=LOCK_TTL_SECONDS+1)) | ||
my_lock_id = create_lock_file(tmpdir) | ||
assert is_my_lock_active(my_lock_id, tmpdir) | ||
assert not os.path.exists(f'{tmpdir}/{expired_lock_id}.json') | ||
|
||
|
||
def test_is_my_lock_active_ignore_newer_lock(): | ||
with tempfile.TemporaryDirectory() as tmpdir: | ||
create_lock_file(tmpdir, datetime.datetime.now() + datetime.timedelta(seconds=5)) | ||
my_lock_id = create_lock_file(tmpdir) | ||
assert is_my_lock_active(my_lock_id, tmpdir) | ||
|
||
|
||
def test_is_my_lock_active_same_time(): | ||
with tempfile.TemporaryDirectory() as tmpdir: | ||
my_lock_time = datetime.datetime.now() | ||
my_lock_id = create_lock_file(tmpdir, my_lock_time) | ||
other_lock_id = create_lock_file(tmpdir, my_lock_time) | ||
assert is_my_lock_active(my_lock_id, tmpdir) == (my_lock_id < other_lock_id) | ||
|
||
|
||
def test_instance_package_lock(): | ||
from datacity_ckan_dgp.utils import locking | ||
with tempfile.TemporaryDirectory() as tmpdir: | ||
locking.BASE_LOCK_PATH = tmpdir | ||
locking.WAIT_TTL_SECONDS = 2 | ||
with instance_package_lock('test_instance', 'test_package'): | ||
assert len(glob.glob(f'{tmpdir}/test_instance/test_package/*.json')) == 1 | ||
with pytest.raises(Exception, match='Failed to acquire lock for test_instance/test_package after 2 seconds'): | ||
with instance_package_lock('test_instance', 'test_package'): | ||
pass | ||
assert len(glob.glob(f'{tmpdir}/test_instance/test_package/*.json')) == 0 |