({
callback,
context = {},
logger,
}: {
callback: TransactionCallback<T>
context: Record<string, unknown>
logger: Logger
})
| 174 | * @returns The result of the transaction |
| 175 | */ |
| 176 | export async function withSerializableTransaction<T>({ |
| 177 | callback, |
| 178 | context = {}, |
| 179 | logger, |
| 180 | }: { |
| 181 | callback: TransactionCallback<T> |
| 182 | context: Record<string, unknown> |
| 183 | logger: Logger |
| 184 | }): Promise<T> { |
| 185 | return withRetry( |
| 186 | async () => { |
| 187 | return await db.transaction(callback, { isolationLevel: 'serializable' }) |
| 188 | }, |
| 189 | { |
| 190 | maxRetries: 5, // Allow more retries for connection errors to recover |
| 191 | retryDelayMs: INITIAL_RETRY_DELAY, // 1s, 2s, 4s, 8s, 16s exponential backoff |
| 192 | retryIf: (error) => { |
| 193 | // Only determine if error is retryable; logging happens in onRetry |
| 194 | return getRetryableErrorDescription(error) !== null |
| 195 | }, |
| 196 | onRetry: (error, attempt) => { |
| 197 | const errorCode = getPostgresErrorCode(error) ?? 'unknown' |
| 198 | const errorDescription = |
| 199 | getRetryableErrorDescription(error) ?? 'unknown' |
| 200 | // Calculate cumulative retry delay: 1s + 2s + 4s + ... (geometric series) |
| 201 | const cumulativeDelayMs = INITIAL_RETRY_DELAY * (Math.pow(2, attempt) - 1) |
| 202 | |
| 203 | // Only log at WARN level after significant cumulative delay to avoid excessive logging |
| 204 | // First few quick retries are expected behavior; extended retries indicate real issues |
| 205 | if (cumulativeDelayMs >= SIGNIFICANT_RETRY_DELAY_MS) { |
| 206 | logger.warn( |
| 207 | { |
| 208 | ...context, |
| 209 | attempt, |
| 210 | pgErrorCode: errorCode, |
| 211 | pgErrorDescription: errorDescription, |
| 212 | cumulativeDelayMs, |
| 213 | }, |
| 214 | `Serializable transaction retry ${attempt}: ${errorDescription} (${errorCode}), cumulative delay ${(cumulativeDelayMs / 1000).toFixed(1)}s`, |
| 215 | ) |
| 216 | |
| 217 | // Track in PostHog for analytics |
| 218 | trackEvent({ |
| 219 | event: AnalyticsEvent.TRANSACTION_RETRY_THRESHOLD_EXCEEDED, |
| 220 | userId: getUserIdForAnalytics(context), |
| 221 | properties: { |
| 222 | ...context, |
| 223 | transactionType: 'serializable', |
| 224 | attempt, |
| 225 | pgErrorCode: errorCode, |
| 226 | pgErrorDescription: errorDescription, |
| 227 | cumulativeDelayMs, |
| 228 | }, |
| 229 | logger, |
| 230 | }) |
| 231 | } |
| 232 | }, |
| 233 | }, |
no test coverage detected