From c598d9c25bc2d1f07044040aae6e1264ef7ada36 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 26 Oct 2026 20:41:14 +0100 Subject: allow aborting remake with unreachable coordiantor --- src/remake/api.rs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) (limited to 'src/remake') diff --git a/src/remake/api.rs b/src/remake/api.rs index f019696..0c2ab10 100644 --- a/src/remake/api.rs +++ b/src/remake/api.rs @@ -11,7 +11,8 @@ //! * `GET /admin/epochs` shows the local epoch, the archived previous ones and whether //! the node is catching up with a newer epoch from a peer. //! * `DELETE /admin/remakes/{id}` asks the coordinator to abort. Only possible while -//! undecided; `409` once committed (a switch can't be undone) or aborted. An expired +//! undecided; `409` once committed (a switch can't be undone) or aborted, or while +//! this node doesn't reach the coordinator (unless `coordinatorInactive`). An expired //! remake is aborted right away by the node asked: its coordinator is gone, and //! can't commit it any more anyway (`coordinator`). `DELETE /admin/remakes` does the //! same for the undecided remake of the local epoch, without having to know its ID. @@ -30,7 +31,10 @@ //! * `switchDelay`: accept one below a keepalive interval (for tests and debugging, //! not for real swarms); //! * `supersede`: abort expired remakes of this epoch and propose in their place. -//! Without it, an expired remake still holding votes answers `409`. +//! Without it, an expired remake still holding votes answers `409`; +//! * for `DELETE`: `coordinatorActive` (abort an expired remake whose coordinator this +//! node sees as active) and `coordinatorInactive` (ask for an abort of an undecided +//! one whose coordinator this node doesn't reach). //! //! `kind` is `compact` (no spec), `replace`, whose spec is //! `{ "document": {…store root…}, "rules": [{"from": "a", "to": "b"}, {"from": "c", "drop": true}] }` @@ -200,7 +204,7 @@ impl From for Retention { /// What `?force=` may override on a proposal (`force`). const FORCE: &[&str] = &["switchDelay", "supersede", "coordinatorActive"]; /// What `?force=` may override on an abort. -const ABORT_FORCE: &[&str] = &["coordinatorActive"]; +const ABORT_FORCE: &[&str] = &["coordinatorActive", "coordinatorInactive"]; /// When a node other than the coordinator may abort an expired remake: a whole grace /// period after it expired. Its coordinator stops committing at expiry @@ -878,7 +882,25 @@ async fn abort_by_id(host: H, id: String, query: AbortQuery) -> Respons } // The coordinator applies the request on its next check (within about a second). - // If it is gone, the proposal expires, and can then be aborted right away. + // If it is gone, the proposal expires, and can then be aborted right away. One this + // node doesn't reach (another partition) may commit before the request reaches it: + // `202` would promise what the coordinator may not keep. + let coordinator = ¤t.record.proposal.coordinator; + if *coordinator != host.node_id() + && !active_peers(&host).await.contains(coordinator) + && !force.allows("coordinatorInactive") + { + return error_response( + StatusCode::CONFLICT, + "this node doesn't reach the coordinator, which may commit before the request \ + reaches it; ask a node that reaches it, or abort once the remake expired", + Some(json!({ + "remake": current, + "abortableAt": abortable_at(¤t.record, grace(&host)), + "force": "coordinatorInactive", + })), + ); + } if current.record.abort_requested.is_none() { let request = AbortRequest { by: host.node_id(), -- cgit v1.3.1