Skip to content

Commit 2b3316c

Browse files
committed
fix(core): make KeyAuth.close() wait for a running refresh
1 parent 132d8a1 commit 2b3316c

2 files changed

Lines changed: 41 additions & 4 deletions

File tree

‎core/src/stackit/core/auth_methods/key_auth.py‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,15 +101,18 @@ def close(self) -> None:
101101
102102
Call this when the instance is no longer needed, for example after
103103
replacing it during key rotation in a long-running process. Without it
104-
the refresh thread keeps running and keeps requesting tokens. Safe to
105-
call more than once.
104+
the refresh thread keeps running and keeps requesting tokens. When it
105+
returns, no refresh is running and none will start. Safe to call more
106+
than once.
106107
"""
107-
self._stop.set()
108-
self.executor.shutdown(wait=False, cancel_futures=True)
108+
# Set under the lock so the refresh loop cannot submit after shutdown.
109+
with self.lock:
110+
self._stop.set()
109111
atexit.unregister(self.__shutdown)
110112
thread = self._refresh_thread
111113
if thread is not None and thread is not threading.current_thread():
112114
thread.join()
115+
self.executor.shutdown(wait=True, cancel_futures=True)
113116

114117
def __create_initial_token(self) -> None:
115118
payload = {
@@ -147,6 +150,8 @@ def __start_token_refresh_task(self):
147150
def token_refresh_task():
148151
while not self._stop.wait(self.TOKEN_EXPIRY_CHECK_INTERVAL.total_seconds()):
149152
with self.lock:
153+
if self._stop.is_set():
154+
return
150155
if self.refresh_future is not None and self.refresh_future.done():
151156
self.refresh_future.result()
152157
if self.__is_token_expired(self.access_token) and (
@@ -175,6 +180,8 @@ def __refresh_token(self):
175180

176181
last_exception = None
177182
for _ in range(self.MAX_REFRESH_RETRIES):
183+
if self._stop.is_set():
184+
return
178185
try:
179186
response = requests.post(self.token_endpoint, data=body, timeout=self.timeout)
180187
response.raise_for_status()

‎core/tests/core/test_auth.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ def test_call_propagates_token_refresh_failure(self, refresh_already_failed):
311311
auth.refresh_token = jwt.encode({"exp": 4_000_000_000}, "x" * 32, algorithm="HS256")
312312
auth.token_endpoint = KeyAuth.DEFAULT_TOKEN_ENDPOINT
313313
auth.refresh_future = None
314+
auth._stop = Event()
314315
original_token = auth.access_token
315316
request = Request("GET", "https://example.com")
316317
refresh_error = requests.RequestException("refresh failed")
@@ -425,6 +426,35 @@ def set_initial_token(auth):
425426
time.sleep(0.1) # ten refresh intervals
426427
assert mock_post.call_count == requests_after_close
427428

429+
def test_close_waits_for_a_refresh_in_progress(self, service_account_key_file_json):
430+
def set_initial_token(auth):
431+
auth.initial_token = "test-initial-token"
432+
433+
expired = jwt.encode({"exp": 1}, "secret", algorithm="HS256")
434+
refresh_started = Event()
435+
with (
436+
patch.object(KeyAuth, "_KeyAuth__create_initial_token", new=set_initial_token),
437+
patch.object(KeyAuth, "TOKEN_EXPIRY_CHECK_INTERVAL", timedelta(milliseconds=10)),
438+
patch("requests.post") as mock_post,
439+
):
440+
auth = self._make_auth(service_account_key_file_json, mock_post, expired)
441+
initial_response = mock_post.return_value
442+
443+
def slow_refresh(*args, **kwargs):
444+
refresh_started.set()
445+
time.sleep(0.2)
446+
return initial_response
447+
448+
mock_post.side_effect = slow_refresh
449+
assert refresh_started.wait(timeout=5)
450+
451+
auth.close()
452+
453+
assert auth.refresh_future is not None and auth.refresh_future.done()
454+
requests_after_close = mock_post.call_count
455+
time.sleep(0.1)
456+
assert mock_post.call_count == requests_after_close
457+
428458
def test_close_is_idempotent(self, service_account_key_file_json):
429459
def set_initial_token(auth):
430460
auth.initial_token = "test-initial-token"

0 commit comments

Comments
 (0)