{"record":{"id":"bd4560fa51f32202","repo":"paperclipai/paperclip","slug":"externalchatwaitauthorizationcontentionerror","errorCode":null,"errorMessage":"ExternalChatWaitAuthorizationContentionError","messagePattern":"ExternalChatWaitAuthorizationContentionError","errorType":"exception","errorClass":"ExternalChatWaitAuthorizationContentionError","httpStatus":null,"severity":"warning","filePath":"server/src/services/native-runtime/chat-attachment-reuse.ts","lineNumber":338,"sourceCode":"  });\n}\n\nasync function principalAuthorized(\n  tx: Db,\n  endpoint: typeof chatEndpoints.$inferSelect,\n  principalId: string,\n  lockMode: AuthorizationLockMode = \"blocking\",\n): Promise<boolean> {\n  if (lockMode === \"blocking\") {\n    await tx.execute(\n      sql`select pg_advisory_xact_lock(hashtextextended(${`chat-identity:${endpoint.companyId}:${principalId}`}, 0))`,\n    );\n  } else if (lockMode === \"nonblocking\") {\n    const [lock] = (await tx.execute(\n      sql`select pg_try_advisory_xact_lock(hashtextextended(${`chat-identity:${endpoint.companyId}:${principalId}`}, 0)) as acquired`,\n    )) as unknown as Array<{ acquired: boolean }>;\n    if (!lock?.acquired) {\n      throw new ExternalChatWaitAuthorizationContentionError();\n    }\n  }\n  const principalQuery = tx\n    .select({ id: chatExternalPrincipals.id })\n    .from(chatExternalPrincipals)\n    .where(\n      and(\n        eq(chatExternalPrincipals.id, principalId),\n        eq(chatExternalPrincipals.companyId, endpoint.companyId),\n        eq(chatExternalPrincipals.provider, endpoint.provider),\n        eq(\n          chatExternalPrincipals.providerAccountId,\n          endpoint.providerAccountId ?? \"\",\n        ),\n      ),\n    );\n  const [principal] = await (lockMode === \"read\"\n    ? principalQuery.limit(1)","sourceCodeStart":320,"sourceCodeEnd":356,"githubUrl":"https://github.com/paperclipai/paperclip/blob/3f1d897a7c018d76563a21c6e39c3c9b03933622/server/src/services/native-runtime/chat-attachment-reuse.ts#L320-L356","documentation":"Thrown by principalAuthorized when running in 'nonblocking' lock mode and the pg_try_advisory_xact_lock for the chat-identity key (companyId:principalId) cannot be acquired, meaning another transaction is concurrently authorizing or mutating the same external chat principal. The error (message 'paperclip_external_chat_wait_authorization_contended') signals lock contention rather than authorization failure; use isExternalChatWaitAuthorizationContention() to detect it (it also matches PG 55P03 lock_not_available).","triggerScenarios":"authorizeChatConversationForBoundRun invoked with lockMode 'nonblocking' while a concurrent transaction already holds the advisory xact lock or row locks (FOR UPDATE NOWAIT) for the same endpoint company/principal — e.g. simultaneous webhook deliveries or a wait-authorization check racing a delivery-processing transaction.","commonSituations":"Duplicate/rapid-fire chat platform webhooks for the same user; a long-running delivery-processing transaction holding the lock; retry storms after a slow provider response; multiple worker instances processing the same principal concurrently.","solutions":["Detect contention with isExternalChatWaitAuthorizationContention(err) and retry the authorization after a short backoff — contention is transient.","Fall back to lockMode 'blocking' (pg_advisory_xact_lock) when the caller can afford to wait for the concurrent transaction to finish.","Reduce lock hold time: keep transactions using this lock short and avoid slow I/O inside them.","Serialize per-principal work (per-principal queue or deduplication) so concurrent requests for the same chat principal are rare."],"exampleFix":"// before\nconst ok = await authorizeChatConversationForBoundRun(tx, endpoint, principalId, \"nonblocking\");\n\n// after\nlet ok: boolean;\ntry {\n  ok = await authorizeChatConversationForBoundRun(tx, endpoint, principalId, \"nonblocking\");\n} catch (err) {\n  if (!isExternalChatWaitAuthorizationContention(err)) throw err;\n  ok = await authorizeChatConversationForBoundRun(tx, endpoint, principalId, \"blocking\"); // wait out the contention\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isLockContention(err: unknown): boolean {\n  return isExternalChatWaitAuthorizationContention(err);\n}","tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    return await authorizeChatConversationForBoundRun(tx, endpoint, principalId, \"nonblocking\");\n  } catch (err) {\n    if (!isExternalChatWaitAuthorizationContention(err) || attempt === 2) throw err;\n    await new Promise(r => setTimeout(r, 50 * 2 ** attempt));\n  }\n}","preventionTips":["Keep transactions that take the chat-identity advisory lock short — no external I/O inside them.","Deduplicate webhooks per principal before processing to avoid self-contention.","Use blocking lock mode wherever waiting is acceptable; reserve nonblocking for latency-sensitive paths with retry.","Alert on repeated contention for the same principal — it usually indicates a stuck long transaction or retry storm."],"tags":["postgres","advisory-lock","concurrency","contention"],"backgroundTag":"lock-contention","analyzedSha":"3f1d897a7c018d76563a21c6e39c3c9b03933622","analyzedAt":"2026-09-18T08:03:59.046Z","contentChangedAt":"2026-09-18T08:03:59.046Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}