summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-26 20:41:14 +0100
committertv <tv@krebsco.de>2026-10-26 20:41:14 +0100
commitc598d9c25bc2d1f07044040aae6e1264ef7ada36 (patch)
tree8e5da80870b2bcd5b321c96be6720afa45da5312
parent5247296250355b7f75a993856d3675f73114d253 (diff)
allow aborting remake with unreachable coordiantor
-rw-r--r--src/remake/api.rs30
1 files changed, 26 insertions, 4 deletions
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<RetentionRequest> 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<H: Host>(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 = &current.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(&current.record, grace(&host)),
+ "force": "coordinatorInactive",
+ })),
+ );
+ }
if current.record.abort_requested.is_none() {
let request = AbortRequest {
by: host.node_id(),