dynamodb-persistent-lock 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: dynamodb-persistent-lock
3
+ Version: 0.0.2
4
+ Summary: DynamoDB Persistent Lock
5
+ Author-email: Tim Heiko <175552092+timheiko@users.noreply.github.com>
6
+ License: Apache-2.0 AND MIT
7
+ Requires-Python: >=3.14
8
+ Description-Content-Type: text/x-rst
9
+
10
+ ========================
11
+ DynamoDB Persistent Lock
12
+ ========================
13
+
14
+ Getting Started
15
+ ---------------
16
+
17
+ 1. Install the library
18
+
19
+ .. code-block:: bash
20
+
21
+ % python3 -m pip install dynamodb-persistent-lock
22
+
23
+ 2. Usage
24
+
25
+ Non-async DynamoDB local download:
26
+
27
+ .. code-block:: python
28
+
29
+ from dynamodb_persistent_lock.dynamodb_persistent_lock import (
30
+ DynamoDBPersistentLockFactory,
31
+ DynamoDBPersistentLockClient,
32
+ )
33
+ from datetime import timedelta
34
+ ...
35
+ # Create a lock factory
36
+ lock_factory = DynamoDBPersistentLockFactory(
37
+ table_name="locks",
38
+ region_name="eu-central-1",
39
+ partition_key_name="lock_key",
40
+ sort_key_name="sort_key",
41
+ record_version_number_name="rvn",
42
+ heartbeat_period=timedelta(seconds=10),
43
+ ttl_attribute_name="expire_at",
44
+ ttl_heartbeat_multiplier=3,
45
+ )
46
+ lock_factory.ensure_table()
47
+ ...
48
+ # Open a lock client
49
+ lock_client = lock_factory.open_lock_client()
50
+ try:
51
+ lock = lock_client.try_acquire_lock("my_lock_key")
52
+ ...
53
+ # Lock-guarded code here.
54
+ finally:
55
+ lock_client.close()
56
+
57
+
58
+ Features
59
+ --------
60
+ * Creates a new lock, if there is none, in DynamoDB table.
61
+ * Reacquires stale existing lock based on the ttl column value.
62
+ * Keeps extending the ttl of an acquired lock with a given heartbeat rate.
63
+
64
+ Credits
65
+ -------
66
+ * `Building Distributed Locks with the DynamoDB Lock Client <https://aws.amazon.com/blogs/database/building-distributed-locks-with-the-dynamodb-lock-client/>`_.
@@ -0,0 +1,57 @@
1
+ ========================
2
+ DynamoDB Persistent Lock
3
+ ========================
4
+
5
+ Getting Started
6
+ ---------------
7
+
8
+ 1. Install the library
9
+
10
+ .. code-block:: bash
11
+
12
+ % python3 -m pip install dynamodb-persistent-lock
13
+
14
+ 2. Usage
15
+
16
+ Non-async DynamoDB local download:
17
+
18
+ .. code-block:: python
19
+
20
+ from dynamodb_persistent_lock.dynamodb_persistent_lock import (
21
+ DynamoDBPersistentLockFactory,
22
+ DynamoDBPersistentLockClient,
23
+ )
24
+ from datetime import timedelta
25
+ ...
26
+ # Create a lock factory
27
+ lock_factory = DynamoDBPersistentLockFactory(
28
+ table_name="locks",
29
+ region_name="eu-central-1",
30
+ partition_key_name="lock_key",
31
+ sort_key_name="sort_key",
32
+ record_version_number_name="rvn",
33
+ heartbeat_period=timedelta(seconds=10),
34
+ ttl_attribute_name="expire_at",
35
+ ttl_heartbeat_multiplier=3,
36
+ )
37
+ lock_factory.ensure_table()
38
+ ...
39
+ # Open a lock client
40
+ lock_client = lock_factory.open_lock_client()
41
+ try:
42
+ lock = lock_client.try_acquire_lock("my_lock_key")
43
+ ...
44
+ # Lock-guarded code here.
45
+ finally:
46
+ lock_client.close()
47
+
48
+
49
+ Features
50
+ --------
51
+ * Creates a new lock, if there is none, in DynamoDB table.
52
+ * Reacquires stale existing lock based on the ttl column value.
53
+ * Keeps extending the ttl of an acquired lock with a given heartbeat rate.
54
+
55
+ Credits
56
+ -------
57
+ * `Building Distributed Locks with the DynamoDB Lock Client <https://aws.amazon.com/blogs/database/building-distributed-locks-with-the-dynamodb-lock-client/>`_.
@@ -0,0 +1,15 @@
1
+ [project]
2
+ description = "DynamoDB Persistent Lock"
3
+ name = "dynamodb-persistent-lock"
4
+ license = {text = "Apache-2.0 AND MIT"}
5
+ authors = [
6
+ {name = "Tim Heiko", email = "175552092+timheiko@users.noreply.github.com"},
7
+ ]
8
+ requires-python = ">= 3.14"
9
+ readme = "README.rst"
10
+ dynamic = [
11
+ "version",
12
+ ]
13
+
14
+ [tool.setuptools.dynamic]
15
+ version = {attr = "dynamodb_persistent_lock.__version__"}
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.0.2"
@@ -0,0 +1,314 @@
1
+ from dataclasses import dataclass, field
2
+ import boto3
3
+ from botocore.exceptions import ClientError
4
+ from logging import getLogger
5
+ from datetime import timedelta
6
+ import os
7
+ import uuid
8
+ import time
9
+ from decimal import Decimal
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from threading import Event, Thread
12
+ from typing import Mapping
13
+
14
+ logger = getLogger(__name__)
15
+
16
+ DYNAMODB = "dynamodb"
17
+ DEFAULT_PARTITION_KEY_NAME = "lock_key"
18
+ DEFAULT_SORT_KEY_NAME = "sort_key"
19
+ DEFAULT_RECORD_VERSION_NUMBER_KEY_NAME = "rvn"
20
+ DEFAULT_HEARTBEAT_PERIOD = timedelta(seconds=5)
21
+ DEFAULT_TTL_ATTRIBUTE_NAME = "expire_at"
22
+ DEFAULT_TTL_HEARTBEAT_MULTIPLIER = 2
23
+
24
+ DEFAULT_READ_CAPACITY = 3
25
+ DEFAULT_WRITE_CAPACITY = 3
26
+
27
+
28
+ @dataclass(repr=True)
29
+ class DynamoDBLock:
30
+ lock_key: str
31
+ sort_key: str
32
+ heartbeat_period: timedelta
33
+ ttl_heartbeat_multiplier: int = DEFAULT_TTL_HEARTBEAT_MULTIPLIER
34
+ record_version_number: str = field(default_factory=lambda: str(uuid.uuid7()))
35
+ now: Decimal | None = None
36
+ ttl: Decimal | None = None
37
+
38
+ def __post_init__(self):
39
+ if self.ttl is None:
40
+ self.now = Decimal(int(time.time()))
41
+ self.ttl = self.now + Decimal(
42
+ int(
43
+ self.heartbeat_period.total_seconds()
44
+ * self.ttl_heartbeat_multiplier
45
+ )
46
+ )
47
+
48
+ def to_key(self):
49
+ return f"<{self.lock_key}|{self.sort_key}>"
50
+
51
+
52
+ @dataclass
53
+ class DynamoDBPersistentLockFactory:
54
+ table_name: str
55
+ region_name: str
56
+ endpoint_url: str | None = None
57
+ partition_key_name: str = DEFAULT_PARTITION_KEY_NAME
58
+ sort_key_name: str = DEFAULT_SORT_KEY_NAME
59
+ ttl_attribute_name: str = DEFAULT_TTL_ATTRIBUTE_NAME
60
+ record_version_number_name: str = DEFAULT_RECORD_VERSION_NUMBER_KEY_NAME
61
+ heartbeat_period: timedelta = DEFAULT_HEARTBEAT_PERIOD
62
+ ttl_heartbeat_multiplier: int = DEFAULT_TTL_HEARTBEAT_MULTIPLIER
63
+
64
+ read_capacity: int = DEFAULT_READ_CAPACITY
65
+ write_capacity: int = DEFAULT_WRITE_CAPACITY
66
+ dynamodb_resource: "boto3.resources.factory.dynamodb.ServiceResource" = None
67
+
68
+ def __post_init__(self):
69
+ if self.dynamodb_resource is None:
70
+ self.dynamodb_resource = boto3.resource(
71
+ service_name=DYNAMODB,
72
+ region_name=self.region_name,
73
+ endpoint_url=self.endpoint_url,
74
+ )
75
+
76
+ def open_lock_client(self):
77
+ return DynamoDBPersistentLockClient(
78
+ heartbeat_period=self.heartbeat_period,
79
+ table=self.dynamodb_resource.Table(self.table_name),
80
+ partition_key_name=self.partition_key_name,
81
+ sort_key_name=self.sort_key_name,
82
+ record_version_number_name=self.record_version_number_name,
83
+ ttl_heartbeat_multiplier=self.ttl_heartbeat_multiplier,
84
+ )
85
+
86
+ def ensure_table(self):
87
+ try:
88
+ logger.info(f"❓ Check if DynamoDB table <{self.table_name}> exists")
89
+ self.dynamodb_resource.meta.client.describe_table(TableName=self.table_name)
90
+ logger.info(f"✅ DynamoDB table <{self.table_name}> exists")
91
+ except ClientError as e:
92
+ logger.info(f"❌ DynamoDB table <{self.table_name}> does not exist")
93
+ logger.info(f"Create DynamoDB table <{self.table_name}>")
94
+ table = self.dynamodb_resource.create_table(
95
+ TableName=self.table_name,
96
+ KeySchema=[
97
+ {"AttributeName": self.partition_key_name, "KeyType": "HASH"},
98
+ {"AttributeName": self.sort_key_name, "KeyType": "RANGE"},
99
+ ],
100
+ AttributeDefinitions=[
101
+ {"AttributeName": self.partition_key_name, "AttributeType": "S"},
102
+ {"AttributeName": self.sort_key_name, "AttributeType": "S"},
103
+ ],
104
+ BillingMode="PAY_PER_REQUEST",
105
+ )
106
+
107
+ logger.info(f"⏳ Creating DynamoDB table <{self.table_name}>")
108
+ table.wait_until_exists()
109
+ logger.info(f"✅ Created DynamoDB table <{self.table_name}>")
110
+
111
+ logger.info(f"Enabling TTL on <{self.ttl_attribute_name}>")
112
+ response = self.dynamodb_resource.meta.client.update_time_to_live(
113
+ TableName=self.table_name,
114
+ TimeToLiveSpecification={
115
+ "Enabled": True,
116
+ "AttributeName": self.ttl_attribute_name,
117
+ },
118
+ )
119
+ if response["ResponseMetadata"]["HTTPStatusCode"] == 200:
120
+ logger.info("✅ TTL has been successfully enabled.")
121
+ else:
122
+ logger.error(
123
+ f"❌ Failed to enable TTL, status code {response['ResponseMetadata']['HTTPStatusCode']}"
124
+ )
125
+
126
+
127
+ @dataclass
128
+ class DynamoDBPersistentLockClient:
129
+ heartbeat_period: timedelta
130
+ table: "boto3.resources.factory.dynamodb.Table"
131
+ owner_name: str | None = None
132
+ partition_key_name: str = DEFAULT_PARTITION_KEY_NAME
133
+ sort_key_name: str = DEFAULT_SORT_KEY_NAME
134
+ ttl_attribute_name: str = DEFAULT_TTL_ATTRIBUTE_NAME
135
+ ttl_heartbeat_multiplier: int = DEFAULT_TTL_HEARTBEAT_MULTIPLIER
136
+ record_version_number_name: str = DEFAULT_RECORD_VERSION_NUMBER_KEY_NAME
137
+
138
+ locks: Mapping[str, Event] = field(default_factory=dict)
139
+
140
+ def __post_init__(self):
141
+ logger.info("table %s", type(self.table))
142
+ self.owner_name = self.owner_name or f"{os.uname().nodename}-{uuid.uuid7()}"
143
+
144
+ def try_acquire_lock(self, lock_key: str, sort_key: str = "-"):
145
+ lock = self._try_acquire_lock(lock_key=lock_key, sort_key=sort_key)
146
+ if lock is None:
147
+ return None
148
+
149
+ self._start_heartbeat(lock)
150
+ return lock
151
+
152
+ def close(self) -> None:
153
+ for lock_key, (event, _) in self.locks.items():
154
+ logger.info(f"Stopping lock heartbeat for {lock_key}")
155
+ event.set()
156
+
157
+ for lock_key, (_, thread) in self.locks.items():
158
+ logger.info(f"Waiting for lock {lock_key} heartbeat to exit")
159
+ thread.join()
160
+
161
+ def _try_acquire_lock(
162
+ self, lock_key: str, sort_key: str = "-"
163
+ ) -> DynamoDBLock | None:
164
+ lock = DynamoDBLock(
165
+ lock_key=lock_key,
166
+ sort_key=sort_key,
167
+ heartbeat_period=self.heartbeat_period,
168
+ ttl_heartbeat_multiplier=self.ttl_heartbeat_multiplier,
169
+ )
170
+
171
+ try:
172
+ return self._try_create_lock(lock)
173
+ except self.table.meta.client.exceptions.ConditionalCheckFailedException as e:
174
+ return self._try_reacquire_existing_lock(lock)
175
+ except Exception as e:
176
+ logger.error(e)
177
+ return None
178
+
179
+ return lock
180
+
181
+ def _try_create_lock(self, lock: DynamoDBLock) -> DynamoDBLock:
182
+ logger.info(f"❓ Trying to acquire the lock {lock.to_key()}.")
183
+ item = {
184
+ "Item": {
185
+ self.partition_key_name: lock.lock_key,
186
+ self.sort_key_name: lock.sort_key,
187
+ "owner_name": self.owner_name,
188
+ self.record_version_number_name: lock.record_version_number,
189
+ self.ttl_attribute_name: lock.ttl,
190
+ },
191
+ "ConditionExpression": "NOT(attribute_exists(#pk) AND attribute_exists(#sk))",
192
+ "ExpressionAttributeNames": {
193
+ "#pk": self.partition_key_name,
194
+ "#sk": self.sort_key_name,
195
+ },
196
+ }
197
+ self.table.put_item(**item)
198
+ logger.info(f"✅ The lock {lock.to_key()} has been acquired: {lock}")
199
+ return lock
200
+
201
+ def _read_existing_lock(self, lock: DynamoDBLock) -> DynamoDBLock:
202
+ logger.info(f"❓ Reading existing lock {lock.to_key()}: {lock}")
203
+ query = {
204
+ "Key": {
205
+ self.partition_key_name: lock.lock_key,
206
+ self.sort_key_name: lock.sort_key,
207
+ },
208
+ "ConsistentRead": True,
209
+ }
210
+ existing_item = self.table.get_item(**query)["Item"]
211
+ return DynamoDBLock(
212
+ lock_key=lock.lock_key,
213
+ sort_key=lock.sort_key,
214
+ heartbeat_period=lock.heartbeat_period,
215
+ ttl=Decimal(existing_item.get(self.ttl_attribute_name)),
216
+ record_version_number=existing_item.get(self.record_version_number_name),
217
+ )
218
+
219
+ def _try_reacquire_existing_lock(self, new_lock: DynamoDBLock) -> DynamoDBLock:
220
+ logger.warning(f"❌ The lock {new_lock.to_key()} already exists.")
221
+
222
+ existing_lock = self._read_existing_lock(new_lock)
223
+ logger.warning(f"❌ The lock {new_lock.to_key()} already exists.")
224
+ existing_lock = self._read_existing_lock(new_lock)
225
+
226
+ logger.info(
227
+ f"❓ Checking the expiration of the existing lock {new_lock.to_key()}."
228
+ )
229
+ if existing_lock.ttl > new_lock.now:
230
+ logger.error(f"❌ The existing lock {new_lock.to_key()} has not expired.")
231
+ return None
232
+
233
+ logger.info(
234
+ f"❓ Trying to re-acquired the existing expired lock {new_lock.to_key()}."
235
+ )
236
+ self._update_lock(existing_lock=existing_lock, new_lock=new_lock)
237
+ logger.info(f"✅ Re-acquired the existing expired lock {new_lock.to_key()}.")
238
+ return new_lock
239
+
240
+ def _update_lock(
241
+ self,
242
+ existing_lock: DynamoDBLock,
243
+ new_lock: DynamoDBLock,
244
+ ) -> DynamoDBLock:
245
+ update_query = {
246
+ "Key": {
247
+ self.partition_key_name: new_lock.lock_key,
248
+ self.sort_key_name: new_lock.sort_key,
249
+ },
250
+ "UpdateExpression": "SET #rvn = :new_rvn, #owner = :owner, #ttl = :ttl",
251
+ "ConditionExpression": "#rvn = :old_rvn",
252
+ "ExpressionAttributeNames": {
253
+ "#ttl": self.ttl_attribute_name,
254
+ "#rvn": self.record_version_number_name,
255
+ "#owner": "owner_name",
256
+ },
257
+ "ExpressionAttributeValues": {
258
+ ":ttl": new_lock.ttl,
259
+ ":owner": self.owner_name,
260
+ ":old_rvn": existing_lock.record_version_number,
261
+ ":new_rvn": new_lock.record_version_number,
262
+ },
263
+ "ReturnValues": "NONE",
264
+ }
265
+
266
+ try:
267
+ self.table.update_item(**update_query)
268
+ return new_lock
269
+ except self.table.meta.client.exceptions.ConditionalCheckFailedException as e:
270
+ logger.error(e)
271
+
272
+ def _delete_lock(self, existing_lock: DynamoDBLock) -> None:
273
+ delete_query = {
274
+ "Key": {
275
+ self.partition_key_name: existing_lock.lock_key,
276
+ self.sort_key_name: existing_lock.sort_key,
277
+ },
278
+ "ConditionExpression": "#rvn = :rvn",
279
+ "ExpressionAttributeNames": {
280
+ "#rvn": self.record_version_number_name,
281
+ },
282
+ "ExpressionAttributeValues": {
283
+ ":rvn": existing_lock.record_version_number,
284
+ },
285
+ }
286
+ try:
287
+ logger.info(f"Deleting the lock of {existing_lock.to_key()}")
288
+ self.table.delete_item(**delete_query)
289
+ except self.table.meta.client.exceptions.ConditionalCheckFailedException as e:
290
+ logger.error(e)
291
+
292
+ def _send_heartbeat(self, existing_lock: DynamoDBLock, event: Event) -> None:
293
+ while not event.wait(existing_lock.heartbeat_period.total_seconds()):
294
+ logger.info(f"⏳ Extending an existing lock: {existing_lock.to_key()}")
295
+ new_lock = DynamoDBLock(
296
+ lock_key=existing_lock.lock_key,
297
+ sort_key=existing_lock.sort_key,
298
+ heartbeat_period=existing_lock.heartbeat_period,
299
+ ttl_heartbeat_multiplier=existing_lock.ttl_heartbeat_multiplier,
300
+ )
301
+ self._update_lock(existing_lock=existing_lock, new_lock=new_lock)
302
+ existing_lock = new_lock
303
+
304
+ self._delete_lock(existing_lock=existing_lock)
305
+
306
+ def _start_heartbeat(self, existing_lock: DynamoDBLock) -> None:
307
+ event = Event()
308
+ thread = Thread(
309
+ name=f"DynamoDb-Persistent-Lock-on-<{existing_lock.to_key()}>",
310
+ target=self._send_heartbeat,
311
+ args=(existing_lock, event),
312
+ )
313
+ self.locks[existing_lock.to_key()] = (event, thread)
314
+ thread.start()
@@ -0,0 +1,5 @@
1
+ import os
2
+
3
+ os.environ["AWS_ACCESS_KEY_ID"] = "fakeMyKeyId"
4
+ os.environ["AWS_SECRET_ACCESS_KEY"] = "fakeSecretAccessKey"
5
+ os.environ["REGION"] = "eu-central-1"
@@ -0,0 +1,73 @@
1
+ import pytest
2
+ from datetime import timedelta
3
+
4
+ from dynamodb_persistent_lock.dynamodb_persistent_lock import (
5
+ DynamoDBPersistentLockFactory,
6
+ DynamoDBPersistentLockClient,
7
+ )
8
+
9
+ from dynamodb_local import download_dynamodb, start_dynamodb_local, DynamoDBLocalServer
10
+
11
+
12
+ @pytest.fixture
13
+ def factory(endpoint_url: str) -> DynamoDBPersistentLockFactory:
14
+ lock_factory = DynamoDBPersistentLockFactory(
15
+ table_name="locks",
16
+ region_name="eu-central-1",
17
+ endpoint_url=endpoint_url,
18
+ partition_key_name="lock_key",
19
+ sort_key_name="sort_key",
20
+ record_version_number_name="rvn",
21
+ heartbeat_period=timedelta(seconds=10),
22
+ ttl_attribute_name="expire_at",
23
+ ttl_heartbeat_multiplier=3,
24
+ )
25
+ lock_factory.ensure_table()
26
+ return lock_factory
27
+
28
+
29
+ @pytest.fixture
30
+ def endpoint_url() -> str:
31
+ dynamodb_local_server = start_dynamodb_local(parent_dir="tmp/DynamoDBLocal")
32
+
33
+ yield dynamodb_local_server.endpoint
34
+
35
+ dynamodb_local_server.shutdown()
36
+
37
+
38
+ @pytest.fixture
39
+ def lock_client(factory: DynamoDBPersistentLockFactory) -> DynamoDBPersistentLockClient:
40
+ client = factory.open_lock_client()
41
+ yield client
42
+ client.close()
43
+
44
+
45
+ def test_create_lock_client(factory: DynamoDBPersistentLockFactory):
46
+ client = factory.open_lock_client()
47
+
48
+ assert client is not None
49
+
50
+ client.close()
51
+
52
+
53
+ @pytest.mark.slow
54
+ def test_acquire_lock_once(lock_client: DynamoDBPersistentLockClient):
55
+ lock_client.heartbeat_period = timedelta(seconds=1)
56
+ lock_client.owner_name = "test_acquire_lock_once"
57
+
58
+ lock = lock_client.try_acquire_lock("my_lock_key")
59
+
60
+ assert lock is not None
61
+
62
+
63
+ def test_acquire_lock_twice(lock_client: DynamoDBPersistentLockClient):
64
+ lock_client.heartbeat_period = timedelta(seconds=5)
65
+ lock_client.owner_name = "test_acquire_lock_twice"
66
+
67
+ lock1 = lock_client.try_acquire_lock("my_lock_key")
68
+
69
+ assert lock1 is not None
70
+
71
+ lock2 = lock_client.try_acquire_lock("my_lock_key")
72
+
73
+ assert lock2 is None
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: dynamodb-persistent-lock
3
+ Version: 0.0.2
4
+ Summary: DynamoDB Persistent Lock
5
+ Author-email: Tim Heiko <175552092+timheiko@users.noreply.github.com>
6
+ License: Apache-2.0 AND MIT
7
+ Requires-Python: >=3.14
8
+ Description-Content-Type: text/x-rst
9
+
10
+ ========================
11
+ DynamoDB Persistent Lock
12
+ ========================
13
+
14
+ Getting Started
15
+ ---------------
16
+
17
+ 1. Install the library
18
+
19
+ .. code-block:: bash
20
+
21
+ % python3 -m pip install dynamodb-persistent-lock
22
+
23
+ 2. Usage
24
+
25
+ Non-async DynamoDB local download:
26
+
27
+ .. code-block:: python
28
+
29
+ from dynamodb_persistent_lock.dynamodb_persistent_lock import (
30
+ DynamoDBPersistentLockFactory,
31
+ DynamoDBPersistentLockClient,
32
+ )
33
+ from datetime import timedelta
34
+ ...
35
+ # Create a lock factory
36
+ lock_factory = DynamoDBPersistentLockFactory(
37
+ table_name="locks",
38
+ region_name="eu-central-1",
39
+ partition_key_name="lock_key",
40
+ sort_key_name="sort_key",
41
+ record_version_number_name="rvn",
42
+ heartbeat_period=timedelta(seconds=10),
43
+ ttl_attribute_name="expire_at",
44
+ ttl_heartbeat_multiplier=3,
45
+ )
46
+ lock_factory.ensure_table()
47
+ ...
48
+ # Open a lock client
49
+ lock_client = lock_factory.open_lock_client()
50
+ try:
51
+ lock = lock_client.try_acquire_lock("my_lock_key")
52
+ ...
53
+ # Lock-guarded code here.
54
+ finally:
55
+ lock_client.close()
56
+
57
+
58
+ Features
59
+ --------
60
+ * Creates a new lock, if there is none, in DynamoDB table.
61
+ * Reacquires stale existing lock based on the ttl column value.
62
+ * Keeps extending the ttl of an acquired lock with a given heartbeat rate.
63
+
64
+ Credits
65
+ -------
66
+ * `Building Distributed Locks with the DynamoDB Lock Client <https://aws.amazon.com/blogs/database/building-distributed-locks-with-the-dynamodb-lock-client/>`_.
@@ -0,0 +1,10 @@
1
+ README.rst
2
+ pyproject.toml
3
+ src/dynamodb_persistent_lock/__init__.py
4
+ src/dynamodb_persistent_lock/dynamodb_persistent_lock.py
5
+ src/dynamodb_persistent_lock.egg-info/PKG-INFO
6
+ src/dynamodb_persistent_lock.egg-info/SOURCES.txt
7
+ src/dynamodb_persistent_lock.egg-info/dependency_links.txt
8
+ src/dynamodb_persistent_lock.egg-info/top_level.txt
9
+ src/dynamodb_persistent_lock/tests/__init__.py
10
+ src/dynamodb_persistent_lock/tests/test_dynamodb_persistent_lock.py