Skip to content

Commit 4259699

Browse files
authored
fix: serialize async runner run and close
Prevent close from publishing or stopping a loop during an active startup/run and close partially initialized loops on startup failure.
1 parent 9b444d8 commit 4259699

2 files changed

Lines changed: 47 additions & 30 deletions

File tree

‎posthog/_async_utils.py‎

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -57,41 +57,47 @@ def __init__(self) -> None:
5757
self._startup_error: BaseException | None = None
5858
self._close_requested = False
5959
self._lock = threading.Lock()
60+
self._operation_lock = threading.Lock()
6061

6162
def run(self, awaitable: Awaitable[Any]) -> Any:
62-
loop = self._ensure_loop()
63-
future = asyncio.run_coroutine_threadsafe(self._await_result(awaitable), loop)
64-
return future.result()
63+
with self._operation_lock:
64+
loop = self._ensure_loop()
65+
future = asyncio.run_coroutine_threadsafe(
66+
self._await_result(awaitable), loop
67+
)
68+
return future.result()
6569

6670
def close(self) -> None:
71+
current = threading.current_thread()
6772
with self._lock:
68-
loop = self._loop
69-
thread = self._thread
70-
if thread is None:
73+
if current is self._thread and self._loop is not None:
74+
self._loop.call_soon(self._loop.stop)
7175
return
72-
if loop is None:
73-
self._close_requested = True
74-
else:
75-
self._loop = None
76-
self._thread = None
77-
self._closing_threads.add(thread)
78-
79-
if loop is None:
80-
if thread is not threading.current_thread():
81-
thread.join()
82-
return
8376

84-
if loop.is_closed():
77+
with self._operation_lock:
8578
with self._lock:
86-
self._closing_threads.discard(thread)
87-
return
79+
loop = self._loop
80+
thread = self._thread
81+
if thread is None:
82+
return
83+
if loop is None:
84+
self._close_requested = True
85+
else:
86+
self._loop = None
87+
self._thread = None
88+
self._closing_threads.add(thread)
8889

89-
if thread is threading.current_thread():
90-
loop.call_soon(loop.stop)
91-
return
90+
if loop is None:
91+
thread.join()
92+
return
93+
94+
if loop.is_closed():
95+
with self._lock:
96+
self._closing_threads.discard(thread)
97+
return
9298

93-
loop.call_soon_threadsafe(loop.stop)
94-
thread.join()
99+
loop.call_soon_threadsafe(loop.stop)
100+
thread.join()
95101

96102
def owns_thread(self, thread: threading.Thread) -> bool:
97103
with self._lock:
@@ -130,10 +136,13 @@ def _ensure_loop(self) -> asyncio.AbstractEventLoop:
130136
return self._loop
131137

132138
def _run_loop(self) -> None:
139+
loop = None
133140
try:
134141
loop = _ContextEventLoop()
135142
asyncio.set_event_loop(loop)
136143
except BaseException as error:
144+
if loop is not None and not loop.is_closed():
145+
loop.close()
137146
with self._lock:
138147
self._startup_error = error
139148
self._thread = None

‎posthog/test/test_async_utils.py‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,38 @@ def test_startup_error_is_reported(self):
2020

2121
awaitable.close()
2222

23-
def test_close_during_startup_does_not_orphan_thread(self):
23+
def test_close_waits_for_run_during_startup(self):
2424
runner = _BackgroundEventLoopRunner()
2525
construction_started = threading.Event()
2626
release_construction = threading.Event()
27+
run_errors = []
2728

2829
def create_loop():
2930
construction_started.set()
3031
self.assertTrue(release_construction.wait(2))
3132
return _ContextEventLoop()
3233

34+
def run():
35+
try:
36+
runner.run(asyncio.sleep(0))
37+
except BaseException as error:
38+
run_errors.append(error)
39+
3340
with mock.patch(
3441
"posthog._async_utils._ContextEventLoop", side_effect=create_loop
3542
):
36-
ensure_thread = threading.Thread(target=runner._ensure_loop)
37-
ensure_thread.start()
43+
run_thread = threading.Thread(target=run)
44+
run_thread.start()
3845
self.assertTrue(construction_started.wait(1))
3946

4047
close_thread = threading.Thread(target=runner.close)
4148
close_thread.start()
4249
release_construction.set()
43-
ensure_thread.join(2)
50+
run_thread.join(2)
4451
close_thread.join(2)
4552

46-
self.assertFalse(ensure_thread.is_alive())
53+
self.assertFalse(run_thread.is_alive())
4754
self.assertFalse(close_thread.is_alive())
55+
self.assertEqual(run_errors, [])
4856
self.assertIsNone(runner._thread)
4957
self.assertIsNone(runner._loop)

0 commit comments

Comments
 (0)