{"record":{"id":"47ae56131c0bdd01","repo":"plandex-ai/plandex","slug":"context-canceled-while-waiting-to-retry-w","errorCode":null,"errorMessage":"context canceled while waiting to retry: %w","messagePattern":"context canceled while waiting to retry: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"app/server/db/locks.go","lineNumber":592,"sourceCode":"\t}\n\n\t// Exponential delay: initialRetryDelay * 2^(attempt)\n\tbackoff := time.Duration(float64(initialLockRetryDelay) * math.Pow(backoffFactor, float64(attempt)))\n\t// Add jitter: ± jitterFraction\n\tjitterRange := time.Duration(float64(backoff) * jitterFraction)\n\tjitter := time.Duration(rand.Int63n(int64(jitterRange)*2)) - jitterRange\n\n\twait := backoff + jitter\n\tif wait < 0 {\n\t\twait = 0\n\t}\n\n\tlog.Printf(\"[Lock][Retry][%d] Lock/transaction conflict (attempt #%d). Retrying in %s... (cause: %v)\", getGoroutineID(), attempt, wait, cause)\n\n\tselect {\n\tcase <-ctx.Done():\n\t\tlog.Printf(\"[Lock][Retry][%d] Context canceled while waiting to retry: %v\", getGoroutineID(), ctx.Err())\n\t\treturn \"\", fmt.Errorf(\"context canceled while waiting to retry: %w\", ctx.Err())\n\tcase <-time.After(wait):\n\t\t// Proceed with the next attempt.\n\t}\n\n\treturn nextCall(attempt + 1)\n}\n\nfunc retryDeleteLock(ctx context.Context, cause error, attempt int, nextCall func(int) error) error {\n\tif attempt >= maxDeleteRetries {\n\t\treturn fmt.Errorf(\"delete lock failed after 10 attempts: %w\", cause)\n\t}\n\t// retry 10 times, no backoff or maybe a tiny 50ms\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-time.After(deleteRetryDelay):\n\t}\n\treturn nextCall(attempt + 1)","sourceCodeStart":574,"sourceCodeEnd":610,"githubUrl":"https://github.com/plandex-ai/plandex/blob/e2d772072efadbe41d2946d97d79be55532dbab5/app/server/db/locks.go#L574-L610","documentation":"While waiting between lock retries, retryWithExponentialBackoff selects on ctx.Done(); if the caller's context is canceled or times out during the backoff sleep it returns 'context canceled while waiting to retry' wrapping ctx.Err(). This means lock acquisition was abandoned by the caller, not that the lock was unavailable forever.","triggerScenarios":"Caller's context deadline exceeded during an exponential backoff wait; HTTP request canceled by the client; server shutdown propagating a cancel through the context tree; parent context canceled after an upstream timeout.","commonSituations":"Request timeout set shorter than total backoff time (initialLockRetryDelay * 2^attempts); user cancels a long-running plan operation; graceful shutdown cancels in-flight lock acquisition.","solutions":["Increase the caller's context deadline to cover the worst-case backoff total (sum of initialLockRetryDelay * 2^attempt up to maxLockRetries)","Treat this as an expected cancellation: unwrap with errors.Is(err, context.DeadlineExceeded/Canceled) and report as 'operation canceled' not a lock bug","Check whether a proxy/load balancer cancels requests prematurely","Reduce backoff delays if they routinely exceed the request budget"],"exampleFix":"// before\nctx := context.Background()\nid, err := lockRepoDB(ctx, orgId, planId, reason)\n// after\nctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)\ndefer cancel()\nid, err := lockRepoDB(ctx, orgId, planId, reason)\nif err != nil && errors.Is(err, context.DeadlineExceeded) {\n    return fmt.Errorf(\"timed out waiting for plan lock (canceled while retrying)\")\n}","handlingStrategy":"try-catch","validationCode":"totalBackoff := time.Duration(0)\nfor i := 0; i < maxLockRetries; i++ {\n    totalBackoff += time.Duration(float64(initialLockRetryDelay) * math.Pow(backoffFactor, float64(i)))\n}\nif deadline, ok := ctx.Deadline(); !ok || time.Until(deadline) < totalBackoff {\n    return errors.New(\"context deadline too short for lock retry budget\")\n}","typeGuard":"func isCtxCanceledDuringRetry(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"context canceled while waiting to retry\")\n}","tryCatchPattern":"id, err := lockRepoDB(ctx, orgId, planId, reason)\nif err != nil {\n    if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n        return fmt.Errorf(\"canceled while waiting for plan lock: %w\", err)\n    }\n    return err\n}","preventionTips":["Set the acquisition context deadline above the total possible backoff","Expect cancellation during retries as a normal outcome, not a lock bug","Propagate shutdown contexts so cancellation is intentional and logged","Keep request-level timeouts larger than lock retry budgets"],"tags":["context","timeout","retry","locking"],"backgroundTag":"context-canceled","analyzedSha":"e2d772072efadbe41d2946d97d79be55532dbab5","analyzedAt":"2026-09-05T20:56:53.631Z","contentChangedAt":"2026-09-05T20:56:53.631Z","schemaVersion":2},"datasetVersion":"2026-09-12T22:17:10.623Z"}