Skip to content

Commit 0a2f36e

Browse files
authored
Always release persistence lock after 1 second (#36)
1 parent 0eda823 commit 0a2f36e

4 files changed

Lines changed: 31 additions & 8 deletions

File tree

docker-compose.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ services:
2424
- AWS_DEFAULT_REGION=us-east-1
2525
- AWS_ACCESS_KEY_ID=dummy
2626
- AWS_SECRET_ACCESS_KEY=dummy
27+
volumes:
28+
- "./test:/usr/test"
2729
depends_on:
2830
localstack-persist:
2931
condition: service_healthy

src/localstack_persist/state.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from localstack.services.plugins import SERVICE_PLUGINS
1111
from localstack.aws.api import RequestContext
1212
from collections import defaultdict
13-
from threading import Thread, Condition
13+
from threading import Thread, Condition, Timer
1414
from readerwriterlock.rwlock import RWLockWrite, Lockable
1515
from .visitors import LoadStateVisitor, SaveStateVisitor
1616
from .config import BASE_DIR, is_persistence_enabled, PERSIST_FREQUENCY
@@ -71,10 +71,16 @@ def on_request(self, chain, context: RequestContext, response):
7171
if service_name not in self.loaded_services:
7272
self._load_service_state(service_name)
7373

74-
# Prevent persistence from running for this service while handling this request
74+
# Prevent persistence from running for this service while handling this request...
7575
rlock = self.rwlocks[service_name].gen_rlock()
7676
setattr(context, "localstack-persist_rlock", rlock)
7777
rlock.acquire()
78+
# ...unless the request takes over 1 second, in which case we force release the lock to
79+
# prevent long-running requests from blocking persistence which would in turn block other
80+
# requests
81+
timer = Timer(1, try_release, [rlock])
82+
setattr(context, "localstack-persist_rlock_timer", timer)
83+
timer.start()
7884

7985
def on_response(self, chain, context: RequestContext, response):
8086
if not context.service or not context.request or not context.operation:
@@ -94,8 +100,15 @@ def on_response(self, chain, context: RequestContext, response):
94100
self.add_affected_service(service_name)
95101

96102
def on_finalize(self, chain, context: RequestContext, response):
97-
if rlock := getattr(context, "localstack-persist_rlock", None):
98-
cast(Lockable, rlock).release()
103+
if rlock := cast(
104+
Lockable | None, getattr(context, "localstack-persist_rlock", None)
105+
):
106+
try_release(rlock)
107+
108+
if timer := cast(
109+
Timer | None, getattr(context, "localstack-persist_rlock_timer", None)
110+
):
111+
timer.cancel()
99112

100113
def load_all_services_state(self):
101114
LOG.info("Loading persisted state of all services...")
@@ -182,3 +195,11 @@ def _save_service_state(self, service_name: str):
182195

183196

184197
STATE_TRACKER = StateTracker()
198+
199+
200+
def try_release(lock: Lockable):
201+
if lock and lock.locked():
202+
try:
203+
lock.release()
204+
except:
205+
pass

test/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM python
22

3-
RUN pip3 install boto3==1.28.80 botocore==1.31.80
3+
WORKDIR /usr/test
44

5-
COPY . .
5+
RUN pip3 install boto3==1.28.80 botocore==1.31.80
66

77
ENTRYPOINT [ "python", "./main.py" ]

test/main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,8 +107,8 @@ def wait_until_es_ready(domain_name: str):
107107
assert_equal(queue.attributes["ApproximateNumberOfMessages"], "0")
108108

109109
table = dynamodb.Table("test-table")
110-
item = table.get_item(Key={"id": 123})["Item"]
111-
assert_equal(item["foo"], "bar")
110+
item = table.get_item(Key={"id": 123}).get("Item", {})
111+
assert_equal(item.get("foo"), "bar")
112112

113113
bucket = s3.Bucket("test-bucket")
114114
obj = bucket.Object("test-object")

0 commit comments

Comments
 (0)