Skip to content

Commit f0a36b5

Browse files
committed
fix(tasks): raise sandbox credential lock TTL to cover the full write
The 30s lease could expire mid-write: each managed write is a chain of in-sandbox execs bounded by a 30s timeout, and the per-message gate can run two chains back-to-back (apply the new token, then log out), so a slow sandbox can hold the lock ~2 min. Raise the TTL to 5 min so the lease cannot expire mid-write and let a concurrent refresh acquire and interleave. Document the derivation and add a test covering a writer past the old 30s lease.
1 parent 5c8d4eb commit f0a36b5

2 files changed

Lines changed: 40 additions & 3 deletions

File tree

products/tasks/backend/temporal/process_task/sandbox_credentials.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,9 +166,15 @@ def _actor_rebound_away_from_owner(run_id: str, state: dict | None, owner_id: in
166166
return bound_actor if bound_actor is not None and bound_actor != owner_id else None
167167

168168

169-
# Only the fast in-sandbox writes run under this lock (token resolution stays outside), so a short
170-
# TTL comfortably covers them; the wait stays well under the refresh activity's 2 min timeout.
171-
_CREDENTIAL_LOCK_TTL_SECONDS = 30
169+
# TTL derivation: the lock must outlive the whole critical section, or the lease can expire mid-write
170+
# and let a concurrent refresh acquire and interleave (a regression we hit before). Each managed
171+
# write is a chain of in-sandbox execs, each bounded by a 30s timeout — set_git_remote_token (1 exec)
172+
# and _write_sandbox_credential_file (a file write + a chmod exec). The per-message gate can run two
173+
# such chains back-to-back in one lock hold (apply the new token, then, if that fails, log out), so
174+
# the worst case is ~4 × 30s ≈ 2 min. A 5 min TTL clears that with margin; recompute if those exec
175+
# timeouts change. The wait stays under the refresh activity's 2 min timeout, so a contender that
176+
# can't acquire skips (fail-safe) rather than blocking the activity.
177+
_CREDENTIAL_LOCK_TTL_SECONDS = 5 * 60
172178
_CREDENTIAL_LOCK_WAIT_SECONDS = 15
173179

174180

products/tasks/backend/temporal/process_task/tests/test_sandbox_credentials.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,37 @@ def test_fails_closed_without_applying_when_the_lock_is_contended(self):
396396
apply.assert_not_called()
397397
lock.release.assert_not_called()
398398

399+
def test_lock_is_leased_long_enough_to_outlive_a_writer_past_the_old_30s_lease(self):
400+
# A credential write does a git-remote rewrite + env-file write + chmod, each an in-sandbox
401+
# exec bounded by a 30s timeout, so it can run well past the old 30s lease. The redis lock
402+
# must be leased for that whole worst case, or the lease expires mid-write and a concurrent
403+
# refresh could acquire and interleave.
404+
import contextlib
405+
406+
from products.tasks.backend.temporal.process_task.sandbox_credentials import (
407+
_CREDENTIAL_LOCK_TTL_SECONDS,
408+
_apply_owner_token_locked,
409+
)
410+
411+
assert _CREDENTIAL_LOCK_TTL_SECONDS == 5 * 60
412+
assert _CREDENTIAL_LOCK_TTL_SECONDS > 2 * 30 # clears a 30s git-remote + 30s chmod worst case
413+
414+
with contextlib.ExitStack() as stack:
415+
get_client = stack.enter_context(patch(f"{MODULE}.get_client"))
416+
lock = MagicMock()
417+
lock.acquire.return_value = True
418+
get_client.return_value.lock.return_value = lock
419+
stack.enter_context(patch(f"{MODULE}.get_sandbox_github_identity_user", return_value=None))
420+
stack.enter_context(patch(f"{MODULE}.apply_github_credentials_to_sandbox", return_value=True))
421+
sandbox = MagicMock()
422+
sandbox.id = "sb-1"
423+
424+
_apply_owner_token_locked(sandbox, "org/repo", "ghu_x", "run-1", {}, 7)
425+
426+
# The lock is leased for the full worst-case write, not the old 30s.
427+
get_client.return_value.lock.assert_called_once()
428+
assert get_client.return_value.lock.call_args.kwargs["timeout"] == _CREDENTIAL_LOCK_TTL_SECONDS
429+
399430

400431
class TestLoopOwnerRefreshGate:
401432
def _as_user_integration_run(self, stack):

0 commit comments

Comments
 (0)