{"record":{"id":"52ec7d03fd9c7779","repo":"benbjohnson/litestream","slug":"sync-before-disable-w","errorCode":null,"errorMessage":"sync before disable: %w","messagePattern":"sync before disable: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"vfs.go","lineNumber":1874,"sourceCode":"\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\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(\"sync before disable: %w\", err)\n\t\t\t}\n\t\t}\n\n\t\t// Stop sync loop and ticker if running\n\t\tif f.syncStop != nil {\n\t\t\tclose(f.syncStop)\n\t\t\tf.syncStop = nil\n\t\t}\n\t\tif f.syncTicker != nil {\n\t\t\tf.syncTicker.Stop()\n\t\t\tf.syncTicker = nil\n\t\t}\n\n\t\tf.writeEnabled = false\n\t\tf.disabling = false\n\t\tf.cond.Broadcast() // Wake any Lock() calls waiting for disable to complete\n\t\tf.logger.Info(\"write support disabled\")\n\t\tf.mu.Unlock()","sourceCodeStart":1856,"sourceCodeEnd":1892,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/vfs.go#L1856-L1892","documentation":"While disabling write support, SetWriteEnabledWithTimeout(false) tried to flush remaining dirty pages to the replica (syncToRemoteWithLock) and that sync failed. The disable is aborted — writeEnabled stays true — because silently losing dirty pages would lose committed SQLite data. The wrapped error is the underlying sync failure (conflict check, LTX upload, etc.).","triggerScenarios":"Calling SetWriteEnabled(false) when len(f.dirty) > 0 and the final sync fails: replica unreachable, network error, remote has newer transactions (conflict), or storage write error during WriteLTXFile.","commonSituations":"Shutting down writes during a network partition or S3 outage; another writer advanced the remote TXID causing a conflict in checkForConflict; expired cloud credentials at disable time; disk issues reading the write buffer during LTX creation.","solutions":["Read the wrapped cause; fix connectivity/credentials to the replica storage and retry SetWriteEnabled(false).","Resolve TXID conflicts: if another writer took over, coordinate (lease) or reset local state with litestream reset before disabling.","Keep writes enabled (the API does this automatically) and ensure the periodic sync loop succeeds first, then disable with no dirty pages.","Verify replica health (litestream ltx listing) to confirm the remote is writable and consistent.","Retry after the transient outage; dirty pages were preserved."],"exampleFix":"// before\nif err := file.SetWriteEnabled(false); err != nil { os.Exit(1) } // fails on S3 outage\n// after\nif err := file.SetWriteEnabledWithTimeout(false, 0); err != nil {\n    log.Warn(\"write-disable failed, writes still enabled; will retry\", \"err\", err)\n}","handlingStrategy":"retry","validationCode":"// Pre-check replica reachability before disabling writes:\nitr, err := client.LTXFiles(ctx, 0, 0, false)\nif err != nil { return fmt.Errorf(\"replica unreachable: %w\", err) }\nitr.Close()","typeGuard":null,"tryCatchPattern":"if err := file.SetWriteEnabled(false); err != nil {\n    var syncErr error\n    if strings.HasPrefix(err.Error(), \"sync before disable:\") {\n        // writes still enabled; backoff and retry disable\n        time.Sleep(backoff); retry()\n    }\n}","preventionTips":["Confirm periodic syncs are succeeding before attempting disable.","Use credential rotation that does not expire mid-operation.","Ensure single-writer via leasing to avoid TXID conflicts.","Retry disable with exponential backoff during storage outages."],"tags":["go","sqlite-vfs","sync","replication","network"],"backgroundTag":"http-request-failed","analyzedSha":"4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3","analyzedAt":"2026-09-06T18:29:25.564Z","contentChangedAt":"2026-09-06T18:29:25.564Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}