{"record":{"id":"41421e933ed06868","repo":"benbjohnson/litestream","slug":"context-cancelled-while-waiting-for-transaction","errorCode":null,"errorMessage":"context cancelled while waiting for transaction: %w","messagePattern":"context cancelled while waiting for transaction: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"vfs.go","lineNumber":1853,"sourceCode":"\t\t\t\tf.cond.Broadcast() // Wake cond.Wait()\n\t\t\tcase <-timeoutCh:\n\t\t\t\tf.cond.Broadcast() // Wake cond.Wait() on timeout\n\t\t\tcase <-waitDone:\n\t\t\t\t// Normal completion, nothing to do\n\t\t\t}\n\t\t}()\n\n\t\t// Wait for active transaction to complete\n\t\tdeadline := time.Now().Add(timeout)\n\t\tfor f.inTransaction {\n\t\t\t// Check context before waiting\n\t\t\tselect {\n\t\t\tcase <-f.ctx.Done():\n\t\t\t\tclose(waitDone)\n\t\t\t\tf.disabling = false\n\t\t\t\tf.cond.Broadcast() // Wake any waiting Lock() calls\n\t\t\t\tf.mu.Unlock()\n\t\t\t\treturn fmt.Errorf(\"context cancelled while waiting for transaction: %w\", f.ctx.Err())\n\t\t\tdefault:\n\t\t\t}\n\t\t\t// Check timeout if specified\n\t\t\tif timeout > 0 && time.Now().After(deadline) {\n\t\t\t\tclose(waitDone)\n\t\t\t\tf.disabling = false\n\t\t\t\tf.cond.Broadcast() // Wake any waiting Lock() calls\n\t\t\t\tf.mu.Unlock()\n\t\t\t\treturn fmt.Errorf(\"timeout waiting for transaction to complete (waited %v)\", timeout)\n\t\t\t}\n\t\t\tf.cond.Wait() // Unlocks mu, waits for signal, relocks mu\n\t\t}\n\t\tclose(waitDone) // Stop the watcher goroutine\n\n\t\t// Sync dirty pages if any exist\n\t\tif len(f.dirty) > 0 {\n\t\t\tif err := f.syncToRemoteWithLock(); err != nil {\n\t\t\t\tf.disabling = false","sourceCodeStart":1835,"sourceCodeEnd":1871,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/vfs.go#L1835-L1871","documentation":"SetWriteEnabledWithTimeout(false) was waiting for an active SQLite transaction to finish before disabling write support, but the file's context (f.ctx) was cancelled first. The disabling attempt is abandoned and the disabling flag is rolled back so pending Lock() callers are woken.","triggerScenarios":"Calling SetWriteEnabled(false)/SetWriteEnabledWithTimeout(false, t) while f.inTransaction is true and the VFS/file context is cancelled (process shutdown, parent context cancelled, Close) before the transaction completes.","commonSituations":"Graceful shutdown that cancels the context while a long SQLite write transaction is still open; test harness tearing down contexts; connection drop causing ctx cancellation during a large batch write; calling Close() concurrently with an in-flight transaction.","solutions":["Wait for in-flight transactions to complete (or roll them back) before disabling writes.","Increase the shutdown grace period so the transaction can finish before context cancellation.","Retry SetWriteEnabled(false) with a live context after the transaction ends.","Use SetWriteEnabledWithTimeout(false, timeout) with an explicit timeout and handle both timeout and cancellation paths.","Drain writers before shutdown (checkpoint/close connections) instead of cancelling mid-transaction."],"exampleFix":"// before\nctxCancel() // cancels f.ctx while tx in flight\nfile.SetWriteEnabled(false) // -> context cancelled error\n// after\nif err := file.SetWriteEnabledWithTimeout(false, 30*time.Second); err != nil {\n    log.Warn(\"disable deferred\", \"err\", err) // retry after tx completes\n}","handlingStrategy":"try-catch","validationCode":"// Check no transaction is active before disabling:\n// serialize with your writer layer so inTransaction == false at disable time.\nselect {\ncase <-ctx.Done():\n    return fmt.Errorf(\"context already cancelled\")\ndefault:\n}","typeGuard":null,"tryCatchPattern":"if err := file.SetWriteEnabledWithTimeout(false, 30*time.Second); err != nil {\n    if errors.Is(err, context.Canceled) {\n        // retry with a fresh context after shutdown drain\n    }\n}","preventionTips":["Commit or roll back all transactions before cancelling the context on shutdown.","Use a generous shutdown grace period longer than your longest transaction.","Retry disable with a live context after cancellation.","Drain SQLite writers before calling Close()."],"tags":["go","context-cancellation","sqlite-vfs","shutdown"],"backgroundTag":"request-timeout","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}