{"record":{"id":"2ba23b9016f318d1","repo":"benbjohnson/litestream","slug":"wait-for-replica-sync-w","errorCode":null,"errorMessage":"wait for replica sync: %w","messagePattern":"wait for replica sync: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"replica.go","lineNumber":243,"sourceCode":"\t\tr.SetPos(ltx.Pos{TXID: txID})\n\t\tresult.synced = true\n\t\tsyncedFileN++\n\t}\n\n\t// Record successful sync for heartbeat monitoring.\n\tr.db.RecordSuccessfulSync()\n\n\treturn result, nil\n}\n\nfunc (r *Replica) lockSync(ctx context.Context) error {\n\tif r.syncSem.TryAcquire(1) {\n\t\treturn nil\n\t}\n\tr.syncWaiters.Add(1)\n\tdefer r.syncWaiters.Add(-1)\n\tif err := r.syncSem.Acquire(ctx, 1); err != nil {\n\t\treturn fmt.Errorf(\"wait for replica sync: %w\", context.Cause(ctx))\n\t}\n\treturn nil\n}\n\nfunc (r *Replica) uploadLTXFile(ctx context.Context, level int, minTXID, maxTXID ltx.TXID) (err error) {\n\tfilename := r.db.LTXPath(level, minTXID, maxTXID)\n\tf, err := os.Open(filename)\n\tif err != nil {\n\t\treturn NewLTXError(\"open\", filename, level, uint64(minTXID), uint64(maxTXID), err)\n\t}\n\tdefer func() { _ = f.Close() }()\n\n\tinfo, err := r.Client.WriteLTXFile(ctx, level, minTXID, maxTXID, f)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"write ltx file: %w\", err)\n\t}\n\tr.Logger().Debug(\"ltx file uploaded\",\n\t\t\"level\", info.Level,","sourceCodeStart":225,"sourceCodeEnd":261,"githubUrl":"https://github.com/benbjohnson/litestream/blob/4ed7a308f6271ebfd2b0a6e4b70b03011a37e4a3/replica.go#L225-L261","documentation":"lockSync tries to acquire the replica's sync semaphore non-blockingly; if another sync is in progress it registers as a waiter and blocks on semaphore acquisition. If the passed context is cancelled (or its deadline expires) while waiting, the error wraps context.Cause(ctx) with this message. It signals that the caller gave up waiting for a concurrent sync to finish, not that syncing itself failed.","triggerScenarios":"Two overlapping sync attempts on the same Replica: one holds syncSem, a second calls lockSync (from syncOnce or an anonymous goroutine), and its ctx is cancelled/times out while blocked in syncSem.Acquire.","commonSituations":"Application shutdown cancels the context mid-sync; a caller uses a short-context deadline while a long LTX upload is in progress; repeated SyncAndWait calls from multiple goroutines.","solutions":["Inspect context.Cause(ctx) wrapped in the message — it names the real reason (deadline exceeded, canceled).","Use a longer-lived context (e.g. context.Background() or a longer timeout) for sync operations.","Avoid launching concurrent syncs on the same Replica; let the store's internal sync loop drive replication.","Ensure graceful shutdown waits for in-flight syncs before cancelling the context."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// acquire with an explicit, generous timeout instead of an unbounded or tiny one\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\ndefer cancel()","typeGuard":null,"tryCatchPattern":"if err := replica.Sync(ctx); err != nil && strings.Contains(err.Error(), \"wait for replica sync\") {\n    if errors.Is(err, context.DeadlineExceeded) {\n        // retry later with a longer deadline\n    }\n}","preventionTips":["Don't fire concurrent manual syncs; rely on the store's sync loop","Use contexts with realistic deadlines for sync operations","On shutdown, wait for in-flight syncs before cancelling contexts"],"tags":["replication","context-cancelled","concurrency"],"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"}