7070# refresh token is rotating, so redeeming it twice in quick succession from
7171# concurrent `ucode opencode` sessions can revoke the token family (#190).
7272TOKEN_REFRESH_COALESCE_SECONDS = 60
73- _LOCK_ACQUIRE_TIMEOUT_SECONDS = 15
73+ # Must exceed the `databricks auth token` subprocess timeout (`timeout=15` at
74+ # the `run(...)` call in the `_fetch` closure below), so a live peer that is
75+ # still inside its own (up to 15s) --force-refresh subprocess is guaranteed
76+ # to finish and touch the sentinel before any waiter gives up on the lock and
77+ # proceeds unlocked (#190: an unlocked waiter racing a still-refreshing peer
78+ # can redeem the rotating refresh token twice).
79+ _LOCK_ACQUIRE_TIMEOUT_SECONDS = 30
7480_LOCK_POLL_INTERVAL_SECONDS = 0.2
7581
7682
@@ -859,6 +865,8 @@ def _refresh_lock(lock_path: Path):
859865 break
860866 except OSError :
861867 time .sleep (_LOCK_POLL_INTERVAL_SECONDS )
868+ if not acquired :
869+ _debug ("refresh_lock" , "acquire timeout, proceeding without lock" )
862870 _debug ("refresh_lock" , f"acquired={ acquired } path={ lock_path } " )
863871 yield acquired
864872 finally :
@@ -883,7 +891,23 @@ def _perform_force_refresh(
883891 refresh token concurrently. If a peer already force-refreshed within
884892 `TOKEN_REFRESH_COALESCE_SECONDS`, drops `--force-refresh` from `cmd` (in
885893 place, so retries via the same `fetch` closure stay coalesced too) and
886- just fetches the now-current cached token instead."""
894+ just fetches the now-current cached token instead.
895+
896+ The sentinel is touched on ATTEMPT, not on success: Databricks rotates
897+ the OAuth refresh token when the `--force-refresh` redemption reaches the
898+ server, not when we successfully parse a token out of our subprocess's
899+ stdout. If the redemption subprocess actually rotated the token
900+ server-side but returned a non-zero exit code or unparseable stdout
901+ (both of which `_fetch` swallows into `""`), failing to touch the
902+ sentinel would let a concurrent peer redeem the same (now-stale)
903+ refresh token again and revoke the whole token family (#190). Marking on
904+ attempt instead means we only risk occasionally serving a slightly-stale
905+ cached token to a coalesced peer — strictly safer than a double
906+ redemption — and any transient failure that gets coalesced away here
907+ will simply be retried after the `TOKEN_REFRESH_COALESCE_SECONDS` window
908+ elapses. Only touch it here, in the branch that actually sent
909+ `--force-refresh`; the coalesced branch above reads the cached token and
910+ performs no redemption, so it must never touch the sentinel."""
887911 lock_path , sentinel_path = _refresh_lock_paths (workspace , profile )
888912 with _refresh_lock (lock_path ) as locked :
889913 _debug (
@@ -902,8 +926,7 @@ def _perform_force_refresh(
902926
903927 _debug ("get_databricks_token" , "performing --force-refresh" )
904928 token = fetch ()
905- if token :
906- _touch_sentinel (sentinel_path )
929+ _touch_sentinel (sentinel_path )
907930 return token
908931
909932
0 commit comments