Skip to content
This repository was archived by the owner on Mar 9, 2026. It is now read-only.

Commit 39a83d3

Browse files
authored
fix: scheduler errors when executor in shutdown (#399)
1 parent e29a2c0 commit 39a83d3

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

google/cloud/pubsub_v1/subscriber/scheduler.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import abc
2222
import concurrent.futures
2323
import queue
24+
import warnings
2425

2526

2627
class Scheduler(metaclass=abc.ABCMeta):
@@ -114,7 +115,14 @@ def schedule(self, callback, *args, **kwargs):
114115
Returns:
115116
None
116117
"""
117-
self._executor.submit(callback, *args, **kwargs)
118+
try:
119+
self._executor.submit(callback, *args, **kwargs)
120+
except RuntimeError:
121+
warnings.warn(
122+
"Scheduling a callback after executor shutdown.",
123+
category=RuntimeWarning,
124+
stacklevel=2,
125+
)
118126

119127
def shutdown(self, await_msg_callbacks=False):
120128
"""Shut down the scheduler and immediately end all pending callbacks.
@@ -142,6 +150,8 @@ def shutdown(self, await_msg_callbacks=False):
142150
try:
143151
while True:
144152
work_item = self._executor._work_queue.get(block=False)
153+
if work_item is None: # Exceutor in shutdown mode.
154+
continue
145155
dropped_messages.append(work_item.args[0])
146156
except queue.Empty:
147157
pass

tests/unit/pubsub_v1/subscriber/test_scheduler.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import queue
1717
import threading
1818
import time
19+
import warnings
1920

2021
import mock
2122

@@ -61,6 +62,24 @@ def callback(*args, **kwargs):
6162
assert sorted(called_with) == expected_calls
6263

6364

65+
def test_schedule_after_executor_shutdown_warning():
66+
def callback(*args, **kwargs):
67+
pass
68+
69+
scheduler_ = scheduler.ThreadScheduler()
70+
71+
scheduler_.schedule(callback, "arg1", kwarg1="meep")
72+
scheduler_._executor.shutdown()
73+
74+
with warnings.catch_warnings(record=True) as warned:
75+
scheduler_.schedule(callback, "arg2", kwarg2="boop")
76+
77+
assert len(warned) == 1
78+
assert issubclass(warned[0].category, RuntimeWarning)
79+
warning_msg = str(warned[0].message)
80+
assert "after executor shutdown" in warning_msg
81+
82+
6483
def test_shutdown_nonblocking_by_default():
6584
called_with = []
6685
at_least_one_called = threading.Event()
@@ -125,3 +144,30 @@ def callback(message):
125144

126145
err_msg = "Shutdown did not wait for the already running callbacks to complete."
127146
assert at_least_one_completed.is_set(), err_msg
147+
148+
149+
def test_shutdown_handles_executor_queue_sentinels():
150+
at_least_one_called = threading.Event()
151+
152+
def callback(_):
153+
at_least_one_called.set()
154+
time.sleep(1.0)
155+
156+
executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
157+
scheduler_ = scheduler.ThreadScheduler(executor=executor)
158+
159+
scheduler_.schedule(callback, "message_1")
160+
scheduler_.schedule(callback, "message_2")
161+
scheduler_.schedule(callback, "message_3")
162+
163+
# Simulate executor shutdown from another thread.
164+
executor._work_queue.put(None)
165+
executor._work_queue.put(None)
166+
167+
at_least_one_called.wait()
168+
dropped = scheduler_.shutdown(await_msg_callbacks=True)
169+
170+
assert len(set(dropped)) == 2 # Also test for item uniqueness.
171+
for msg in dropped:
172+
assert msg is not None
173+
assert msg.startswith("message_")

0 commit comments

Comments
 (0)