{"record":{"id":"ea508df863d69f1c","repo":"unslothai/unsloth","slug":"chatgpt-credential-update-is-busy-please-retry","errorCode":null,"errorMessage":"ChatGPT credential update is busy. Please retry.","messagePattern":"ChatGPT credential update is busy\\. Please retry\\.","errorType":"exception","errorClass":"CodexAuthError","httpStatus":400,"severity":"warning","filePath":"studio/backend/core/inference/openai_codex_auth.py","lineNumber":116,"sourceCode":"\n\ndef _provider_file_lock(provider_id: str) -> FileLock:\n    lock_name = hashlib.sha256(provider_id.encode()).hexdigest()[:24]\n    return FileLock(\n        str(studio_db_path().parent / f\".openai-codex-refresh-{lock_name}.lock\"),\n        timeout = 30,\n        thread_local = False,\n    )\n\n\n@asynccontextmanager\nasync def provider_oauth_write_guard(provider_id: str):\n    \"\"\"Serialize refresh and deletion across Studio workers without blocking the event loop.\"\"\"\n    file_lock = _provider_file_lock(provider_id)\n    try:\n        await asyncio.to_thread(file_lock.acquire)\n    except FileLockTimeout as exc:\n        raise CodexAuthError(\"ChatGPT credential update is busy. Please retry.\") from exc\n    try:\n        yield\n    finally:\n        await asyncio.to_thread(file_lock.release)\n\n\ndef _flow_is_stale(flow: OAuthFlow, now: float) -> bool:\n    if flow.status == \"pending\":\n        return now >= flow.expires_at\n    return now >= min(flow.expires_at, flow.created_at + _FLOW_TERMINAL_RETENTION_SECONDS)\n\n\nasync def _prune_flows() -> None:\n    now = time.time()\n    for flow_id, flow in list(_flows.items()):\n        if _flow_is_stale(flow, now):\n            await cancel_flow(flow_id)\n","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/unslothai/unsloth/blob/203007d19051dcd2ae33876786d117c99f6b0368/studio/backend/core/inference/openai_codex_auth.py#L98-L134","documentation":"CodexAuthError raised by provider_oauth_write_guard when acquiring the cross-worker file lock for a provider's OAuth credentials times out (default 30s, acquired off the event loop via asyncio.to_thread). The guard serializes ChatGPT credential refresh and deletion across Studio workers; a concurrent long refresh holds the lock, and instead of blocking indefinitely the waiter reports 'busy, please retry'.","triggerScenarios":"Two or more workers/requests simultaneously refresh or delete the same provider's ChatGPT credentials; the first holds the lock longer than the 30s timeout (slow token endpoint), and the second's file_lock.acquire raises FileLockTimeout.","commonSituations":"Burst of requests right after access-token expiry, all triggering refresh; a hung/slow OpenAI auth endpoint during an outage; a previous worker crashed mid-refresh leaving contention behind (the lock eventually times out).","solutions":["Retry the operation after a short backoff — once the in-flight refresh finishes, the lock frees and the cached token is likely already fresh.","Debounce refresh at the call site: check token expiry with margin and only one caller refreshes (single-flight), so workers do not pile on the lock.","If it recurs constantly, investigate why the lock holder exceeds 30s (network to the token endpoint) and raise the timeout or fix connectivity."],"exampleFix":"# before\nasync with provider_oauth_write_guard(provider_id):\n    await refresh_credentials(provider_id)  # raises immediately under contention\n\n# after\nfor attempt in range(3):\n    try:\n        async with provider_oauth_write_guard(provider_id):\n            await refresh_credentials(provider_id)\n        break\n    except CodexAuthError:\n        await asyncio.sleep(2 * (attempt + 1))","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"for attempt in range(3):\n    try:\n        async with provider_oauth_write_guard(provider_id):\n            return await refresh(provider_id)\n    except CodexAuthError as e:\n        if 'busy' in str(e) and attempt < 2:\n            await asyncio.sleep(1.5 * (attempt + 1))\n            continue\n        raise","preventionTips":["Single-flight refreshes: only one caller refreshes per provider; others await the result, so the lock is rarely contended.","Refresh tokens proactively with a margin (e.g. 60s before expiry) instead of on the request path.","Treat 'busy' as transient — surface a retryable error to clients, not a hard failure."],"tags":["oauth","file-lock","concurrency","retry","codex"],"backgroundTag":null,"analyzedSha":"203007d19051dcd2ae33876786d117c99f6b0368","analyzedAt":"2026-08-15T02:48:39.846Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}