summaryrefslogtreecommitdiffstats
path: root/src/remake/api.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-24 23:06:03 +0200
committertv <tv@krebsco.de>2026-10-24 23:06:03 +0200
commit3ecc0fe0bb083e351d769550851d26a9edcb7cf6 (patch)
treed9716bfc6153e6094d979cc5e4ccca2377c59496 /src/remake/api.rs
parent0cba61b6beb4b1da685e21991c8b95a6e39d2287 (diff)
compact remake fixes base at the commit
Instead of fixing base heads at when proposed, compact fixes them when committing. This shrinks the window where writes can be lost if their author restarts to the switch delay; during replace and import remakes writes can still be lost during the voting period.
Diffstat (limited to 'src/remake/api.rs')
-rw-r--r--src/remake/api.rs46
1 files changed, 29 insertions, 17 deletions
diff --git a/src/remake/api.rs b/src/remake/api.rs
index 826c81e..3530423 100644
--- a/src/remake/api.rs
+++ b/src/remake/api.rs
@@ -17,7 +17,8 @@
//! 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.
//!
-//! Request body for POST (all optional):
+//! Request body for POST (all optional; `baseHeads` not for `compact`, whose base is
+//! fixed when it commits):
//! `{ "kind": "compact", "spec": null, "baseHeads": ["<hex>", …],
//! "replayWindow": {"epochs": 1, "seconds": 604800},
//! "archiveRetention": {"epochs": 2, "seconds": 2592000},
@@ -395,6 +396,15 @@ async fn start_kind<H: Host>(
}
};
+ if kind.base_at_commit() && request.base_heads.is_some() {
+ return error_response(
+ StatusCode::BAD_REQUEST,
+ format!(
+ "a {kind_name} remake fixes its base when it commits; baseHeads is not accepted"
+ ),
+ None,
+ );
+ }
let base = match &request.base_heads {
None => None,
Some(heads) => {
@@ -454,7 +464,8 @@ async fn start_kind<H: Host>(
let mut proposal = Proposal {
coordinator: host.node_id(),
from_epoch: host.current_epoch().into(),
- base_heads: Vec::new(),
+ // Filled in when the proposal is written, unless the kind fixes it at the commit.
+ base_heads: None,
policy: Policy {
replay_window: request
.replay_window
@@ -598,6 +609,7 @@ async fn propose<H: Host>(
let electorate_host = host.clone();
let me = host.node_id();
let started = host.remake().started_at() + grace;
+ let base_at_commit = kind::by_name(&proposal.kind).is_some_and(|kind| kind.base_at_commit());
let record = host
.commit_doc(move |doc| -> Result<Record, StartError> {
let mut all = records::load_all(doc);
@@ -656,22 +668,22 @@ async fn propose<H: Host>(
}
// The base is the requested heads, or everything before the proposal (the
- // proposal itself is never in it).
- let base = match base {
- Some(base) => {
- if let Some(unknown) = base
- .iter()
- .find(|head| doc.get_change_by_hash(head).is_none())
- {
- return Err(StartError::UnknownBase(unknown.to_string()));
+ // proposal itself is never in it); or, for a compaction, left to the commit.
+ if !base_at_commit {
+ let base = match base {
+ Some(base) => {
+ if let Some(unknown) = base
+ .iter()
+ .find(|head| doc.get_change_by_hash(head).is_none())
+ {
+ return Err(StartError::UnknownBase(unknown.to_string()));
+ }
+ base
}
- base
- }
- None => doc.get_heads(),
- };
- let mut heads: Vec<String> = base.iter().map(|h| h.to_string()).collect();
- heads.sort();
- proposal.base_heads = heads;
+ None => doc.get_heads(),
+ };
+ proposal.base_heads = Some(records::encode_heads(&base));
+ }
// We vote for our own remake, so we are always in the electorate.
let mut electorate = electorate_host.electorate(doc, now);