feat(github): mirror remote control-request rejections back to the accepting plane - #967
feat(github): mirror remote control-request rejections back to the accepting plane#967aparajon wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds end-to-end propagation of terminal control-request outcomes (especially remote/data-plane rejections) back to the accepting plane, so operators can see when a queued command ultimately did not take effect, and so the PR comment/summary can surface those rejections safely.
Changes:
- Extend the Tern
ProgressResponseto includesettled_control_requests, allowing the accepting plane to learn terminal control-request outcomes from the serving plane. - Persist and mirror remote control-request rejections into control-plane storage, emit apply-log entries, and record an observability counter for attribution.
- Render a “Command not applied” warning section in PR comments/summaries based on stored failed control requests, with sanitization to prevent endpoint leakage and markdown escape.
Reviewed changes
Copilot reviewed 15 out of 16 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/webhook/templates/control_rejection.go | Adds a template helper to render rejected control commands into a safe, quoted warning block. |
| pkg/webhook/templates/control_rejection_test.go | Tests rendering behavior and sanitization/blockquote containment for rejection messages. |
| pkg/webhook/control_rejection_notice.go | Loads failed settled control requests and appends the warning section when within GitHub comment limits. |
| pkg/webhook/control_rejection_notice_test.go | Ensures status comments include the rejection notice only for failed requests. |
| pkg/webhook/comment_observer.go | Appends the rejection notice to status and summary comment bodies. |
| pkg/webhook/comment_observer_test.go | Extends stub storage to support settled-control-request loading in comment rendering tests. |
| pkg/tern/local_client.go | Populates SettledControlRequests in Progress responses by reading terminal control requests from storage. |
| pkg/tern/local_client_test.go | Updates test storage to implement ListSettled and ensure ControlRequests store availability. |
| pkg/tern/grpc_control_rejection_integration_test.go | Adds integration coverage for Progress reporting settled requests and control-plane mirroring of remote failures. |
| pkg/tern/grpc_client.go | Mirrors remote control-request failures into control-plane storage, logs, and metrics during polling. |
| pkg/storage/types.go | Adds validation for control-operation strings crossing plane boundaries. |
| pkg/storage/storage.go | Extends ControlRequestStore interface with ListSettled and RecordRemoteFailure. |
| pkg/storage/internal/sqlstore/control_requests.go | Implements ListSettled and RecordRemoteFailure in the SQL store. |
| pkg/proto/tern.proto | Adds SettledControlRequest and settled_control_requests on ProgressResponse. |
| pkg/proto/ternv1/tern.pb.go | Regenerates protobuf bindings for the new message/field. |
| pkg/metrics/metrics.go | Adds a counter for remote control requests rejected by operation/engine/deployment/env attribution. |
Files not reviewed (1)
- pkg/proto/ternv1/tern.pb.go: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
61f0bc0 to
4452386
Compare
… the accepting plane Accepting a control RPC means the request was queued, not that it took effect. A control operation that the remote engine later rejects settles in the data plane's apply_control_requests row and never reaches the plane that accepted it, so the operator sees an acknowledgement and no outcome. ProgressResponse now carries the terminal fate of every control request the serving plane consumed. The control plane records a rejection durably (creating the row when it never held one, as with volume, which is a pure proxy), warns once, writes an apply-log entry, and counts it by operation and engine so a spike is attributable to a data plane. Recording is idempotent: the data plane re-reports the same settled request every poll, and only a state change writes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…pplied A control command that fails after it was queued left the operator with an acknowledgement and no outcome: the failure settled in apply_control_requests and the PR said nothing. The apply's progress and summary comments now carry a warning naming each rejected command, who issued it, and the sanitized reason. It is rebuilt from storage on every render, so re-issuing a command clears the notice once it succeeds, and it can never double-post. Engine text goes through the comment sanitizer, so a dial failure cannot leak an endpoint or break out of the blockquote. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ction mirrors A pending control request is a live command this plane has not handed to the serving plane yet, so it is necessarily newer than anything the serving plane has already settled. Overwriting it with a mirrored rejection dropped the operator's command outright: nothing forwards a request once it stops being pending, so the drive found no pending work and the operator waited for an effect that never came. The mirror now refuses to settle a pending row. A mirrored rejection also had no way to retire. Volume is a pure proxy — the control plane holds no request lifecycle of its own to reset the row — so the "Command not applied" notice survived a later successful re-issue forever. Rows the mirror creates are now marked in metadata, and a settled request the data plane reports as completed clears the mirror's own failed row (and only that row), with an apply-log entry recording that the earlier rejection no longer applies. Progress serves the settled-request list as advisory display data, so a failed load now degrades to a logged error rather than failing the RPC that carries the state and task progress the caller drives the apply from. The same rows are reported on every poll until the operator retries, so the notice self-heals on the next tick. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d it A repeated rejection was treated as already-recorded on status and error message alone, and the update path never refreshed the requester. A second operator whose re-issued command was rejected for the same reason was reported under the first operator's name — and volume is the case that reaches it, since a pure proxy queues no local request whose own lifecycle would reset the row. The requester is now part of the rejection's identity. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The control RPCs carry no operator identity, so a control request the data plane records is attributed to the forwarding path rather than the person who issued the command. Mirroring that report back must not trade a known operator for it: the notice exists to tell an operator which of their commands did not take effect. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6806888 to
905c36e
Compare
|
🤖 Review findings - created by Kiran's code review agent - for pull/967, 905c36e. Verdict: 8 findings — 6 non-blocking, 2 suggestions, none blocking; CI is green (31/31 checks pass at head 905c36e: Build, Unit, Integration, Lint x4, all E2E suites including gRPC and gRPC multi-deployment, LocalScale x3) and the PR is still open with head unchanged. Non-blocking
General suggestions
The one thing that could have broken, verifiedThe riskiest mechanism is Verified correct
This review was generated by Claude Code (claude-fable-5). |
Why this matters
Running
stop,cutover, orvolumeon a schema change that a remote data plane is driving gets you an acknowledgement right away, but that only means the command was queued. If the engine then refuses it, the failure stays on the far side and never travels back, so the command looks like it worked when it did not.What changed
The data plane now reports what happened to every control command it finished with. The accepting plane records that, counts it by operation and engine, and puts a warning on the apply's PR comment. The notice clears once the command is re-issued and succeeds.
The new field is additive, so neither plane has to deploy first, but the warning only appears once both are on a version that carries it.
🤖 Generated with Claude Code