Skip to content

feat(gateway): add rejected-record listing and node removal recovery APIs - #1046

Merged
kvinwang merged 3 commits into
nextfrom
feat/gateway-admin-recovery-apis
Aug 12, 2026
Merged

feat(gateway): add rejected-record listing and node removal recovery APIs#1046
kvinwang merged 3 commits into
nextfrom
feat/gateway-admin-recovery-apis

Conversation

@kvinwang

Copy link
Copy Markdown
Collaborator

Summary

Three operator recovery additions in the same family as Admin.RemoveCvm (#1044).

Behavior

Admin.ListRejectedInstances

Reports the instance records this node currently refuses to import, with the rejection reason (unusable / lost_conflict) and whether the instance still holds local data-plane state (so the operator knows a removal would also drop live routing).

Rejections were previously logged only on transitions (by design, see #1035), so finding what to remove meant grepping old logs or restarting the gateway to re-log them. The listing is recomputed from the store on every call, so it is current even right after a restart.

Admin.RemoveNode

Removes a decommissioned gateway node:

  • writes tombstones for node/info/, node/status/, and __peer_addr/;
  • drops this node's own ephemeral last_seen observation of it;
  • drops the node from this gateway's sync peer set immediately.

The __peer_addr tombstone doubles as the cluster-wide removal signal: every gateway watches the prefix and prunes its own peer set when the deletion replicates, so no restart is needed anywhere. An address that was never written does not count as removed, because bootstrap can add a peer before its address record has synced in.

A node removed by mistake rejoins when it restarts (startup re-registers its records), or via SetNodeUrl from any live gateway. A node cannot remove itself.

Admin.DeleteZtDomain on corrupt records

Deletion used to be gated on get_zt_domain_config, which cannot tell a missing record from an unreadable one, so a corrupt cert/{domain}/config was permanently stuck ("ZT-Domain config not found") — the same trap RemoveCvm was added to fix for instance records. Deletion now checks for the record itself and works regardless of decodability.

Both removal RPCs are idempotent and report record_existed, so a mistyped ID is visible to the operator instead of silently succeeding.

Verification

  • cargo fmt --all --check
  • cargo test -p dstack-gateway --all-features (156 tests, 5 new)
  • cargo clippy -p dstack-gateway --all-features -- -D warnings -D clippy::expect_used -D clippy::unwrap_used --allow unused_variables

…APIs

Three operator recovery additions in the same family as Admin.RemoveCvm:

- Admin.ListRejectedInstances reports the instance records this node
  currently refuses to import, with the reason and whether the instance
  still holds local data-plane state. Rejections were previously logged
  only on transitions, so finding what to remove meant grepping old logs
  or restarting the gateway to re-log them.

- Admin.RemoveNode tombstones a decommissioned gateway node's replicated
  records (info, status, sync address) and drops it from the sync peer
  set immediately. The __peer_addr tombstone doubles as the cluster-wide
  removal signal: every gateway watches the prefix and prunes its own
  peer set when the deletion replicates, so no restart is needed. An
  address that was never written does not count as removed, because
  bootstrap can add a peer before its address record has synced in.

- Admin.DeleteZtDomain now works on a corrupt config record. It used to
  gate deletion on get_zt_domain_config, which cannot tell missing from
  unreadable, so a corrupt record was permanently stuck — the same trap
  RemoveCvm was added to fix for instance records.

Both removal RPCs are idempotent and report record_existed so a mistyped
ID is visible instead of silently succeeding.
Copilot AI lite review requested due to automatic review settings August 12, 2026 10:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds operator-focused recovery APIs to dstack-gateway’s Admin RPC surface, extending the existing “explicit recovery instead of raw KV deletion” approach. It improves observability into rejected instance records, enables safe removal of decommissioned gateway nodes, and makes ZT-domain deletion resilient to corrupt config records.

Changes:

  • Add Admin.ListRejectedInstances to report currently refused instance records, including reason, rejection class, and whether the instance is still active in the local data plane.
  • Add Admin.RemoveNode plus KV/watch plumbing so node removals replicate via a __peer_addr tombstone and peers prune without restart.
  • Allow Admin.DeleteZtDomain to delete a domain even when its cert/{domain}/config record exists but is unreadable.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
dstack/gateway/src/main_service/tests.rs Adds tests covering rejected-instance listing visibility, node removal idempotency/self-protection, peer pruning on replicated tombstones, and deletion of corrupt ZT-domain configs.
dstack/gateway/src/main_service.rs Implements Proxy::rejected_instances() and Proxy::remove_node(), and adds a WaveKV watch to prune peers when __peer_addr deletions replicate.
dstack/gateway/src/kv/mod.rs Adds node-removal tombstoning (sync_remove_node), peer removal/pruning helpers, a __peer_addr prefix watch, and a “config exists even if corrupt” check for ZT domains.
dstack/gateway/src/admin_service.rs Exposes the new Admin RPC methods, maps internal rejection types to RPC payloads, and updates ZT-domain deletion gating to use existence checks instead of decode.
dstack/gateway/rpc/proto/gateway_rpc.proto Extends the Admin service with ListRejectedInstances and RemoveNode plus new request/response message types.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread dstack/gateway/src/kv/mod.rs
Comment thread dstack/gateway/src/main_service.rs
- RemoveNode now reports record_existed when any of the node's
  persistent records (info, status, or sync address) was live, so a
  node registered via SetNodeUrl but never booted is still visible to
  the operator. The three deletes also run under one write handle.
- Loading instances no longer logs each undecodable record at error!
  level. That log line fired on every reload and every rejected-record
  listing, bypassing the transition-only reporting the reload path
  already has. The decode error now travels in LoadedInstances instead,
  which also gives ListRejectedInstances the actual error to show
  rather than a generic "record does not decode".
…stone

RemoveNode wrote the __peer_addr tombstone first and asked the peer set
second. The tombstone wakes the peer-address watcher, so on a
multi-threaded runtime prune_removed_peers() could drop the peer before
the RPC path's own remove_peer() ran, and removed_from_peer_set would
report false for a peer that was present when the request began.

Drop the peer before publishing the tombstone, so the reported
membership no longer depends on scheduling. Add a regression test that
races remove_node against a live watcher on a multi-threaded runtime.
@kvinwang
kvinwang merged commit d0386bd into next Aug 12, 2026
16 checks passed
@kvinwang
kvinwang deleted the feat/gateway-admin-recovery-apis branch August 12, 2026 11:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants