internal/rest/resources/control: Use independent context for join faiure cleanup#777
internal/rest/resources/control: Use independent context for join faiure cleanup#777claudiubelu wants to merge 1 commit into
Conversation
…lure cleanup The controlPost reverter used r.Context() for all cleanup operations (PreRemove hook, certificate fetch, client creation). When a join fails due to context deadline exceeded, r.Context() may already be expired, causing the entire cleanup to silently fail and leaving a ghost node in the dqlite cluster. In particular, shared.GetRemoteCertificate was silently returning due an error returned (due to r.Context() being expired), which means that internalClient.DeleteClusterMember was never called. Use an independent context with a 2-minute timeout for cleanup operations so they can complete regardless of the original request context state. Also add error logging to GetRemoteCertificate and client creation failures which previously returned silently.
There was a problem hiding this comment.
Pull request overview
This PR adjusts controlPost’s failure cleanup logic so that cleanup work isn’t blocked by an already-expired request context (e.g., join timeouts), and adds missing error logs for previously silent cleanup failures.
Changes:
- Introduces a dedicated cleanup context with a 2-minute timeout for failure reverter operations.
- Uses that cleanup context for pre-remove hook execution, certificate retrieval, and client creation during join cleanup.
- Adds error logging when remote certificate fetch or client creation fails during cleanup.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Use an independent context for cleanup so that it is not tied | ||
| // to the request context, which may already be expired (e.g. the | ||
| // join timed out). | ||
| cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 2*time.Minute) | ||
| defer cleanupCancel() |
| if joinInfo == nil || req.JoinToken == "" { | ||
| reExec, err := resetClusterMember(r.Context(), state, true) | ||
| reExec, err := resetClusterMember(cleanupCtx, state, true) | ||
| if err != nil { | ||
| logger.Error("Failed to reset cluster member on bootstrap error", slog.String("error", err.Error())) | ||
| return |
roosterfish
left a comment
There was a problem hiding this comment.
Thanks, could you please also add a test that triggers this?
| // Use an independent context for cleanup so that it is not tied | ||
| // to the request context, which may already be expired (e.g. the | ||
| // join timed out). | ||
| cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 2*time.Minute) |
There was a problem hiding this comment.
Instead of deriving a new context, should we rather use the daemon's shutdownCtx here to be independent of the join request but still account for the health of the daemon when running the hooks?
|
Marking as draft until you get a chance to get back to it @claudiubelu. |
The
controlPostreverter usedr.Context()for all cleanup operations (PreRemovehook, certificate fetch, client creation).When a join fails due to context deadline exceeded,
r.Context()may already be expired, causing the entire cleanup to silently fail and leaving a ghost node in the dqlite cluster. In particular,shared.GetRemoteCertificatewas silently returning due an error returned (due tor.Context()being expired), which means thatinternalClient.DeleteClusterMemberwas never called.Use an independent context with a 2-minute timeout for cleanup operations so they can complete regardless of the original request context state.
Also add error logging to
GetRemoteCertificateand client creation failures which previously returned silently.