summaryrefslogtreecommitdiffstats
path: root/src/remake/api.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remake/api.rs')
-rw-r--r--src/remake/api.rs69
1 files changed, 27 insertions, 42 deletions
diff --git a/src/remake/api.rs b/src/remake/api.rs
index 8467429..de8166c 100644
--- a/src/remake/api.rs
+++ b/src/remake/api.rs
@@ -3,8 +3,9 @@
//! * `POST /admin/remakes` proposes a remake of the current epoch (`POST
//! /admin/compaction` is a shortcut, kept from before). The receiving
//! node becomes its coordinator. `202` with the report, or with `?wait=true` the final
-//! report once decided. A dropped request does not abort anything. `?force=true`
-//! accepts a `settleSecs` below the dormancy threshold (tests, single-site swarms).
+//! report once decided. A dropped request does not abort anything. The remake
+//! commits as soon as a majority of its electorate voted for it (`vote`), and aborts
+//! once that is out of reach or `settleSecs` passed without it.
//! * `GET /admin/remakes` lists all remakes.
//! * `GET /admin/remakes/{id}` reports one; `?wait=true` long-polls until decided
//! (bounded by the proposal's own expiry).
@@ -21,8 +22,8 @@
//! "archiveRetention": {"epochs": 2, "seconds": 2592000},
//! "settleSecs": 480, "switchDelaySecs": 60 }`
//!
-//! `?force=true` accepts `settleSecs` below the dormancy threshold and `switchDelaySecs`
-//! below one keepalive interval: for tests and debugging, not for real swarms.
+//! `?force=true` accepts a `switchDelaySecs` below one keepalive interval: for tests
+//! and debugging, not for real swarms.
//!
//! `kind` is `compact` (no spec), `replace`, whose spec is
//! `{ "document": {…store root…}, "rules": [{"from": "a", "to": "b"}, {"from": "c", "drop": true}] }`
@@ -60,9 +61,9 @@ use uuid::Uuid;
use super::coordinator;
use super::local::LocalStatus;
use super::records::{
- self, AbortRequest, EpochRecord, Phase, Policy, Proposal, Record, Retention, unix_millis,
+ self, AbortRequest, EpochRecord, Phase, Policy, Proposal, Record, Retention, Vote, unix_millis,
};
-use super::{Host, kind};
+use super::{Host, kind, vote};
use crate::document::store::StoreError;
#[derive(Debug, Default, Deserialize)]
@@ -76,7 +77,7 @@ pub(crate) struct WaitQuery {
pub(crate) struct StartQuery {
#[serde(default)]
wait: bool,
- /// Allows a settle period shorter than the dormancy threshold.
+ /// Allows a switch delay shorter than one keepalive interval.
#[serde(default)]
force: bool,
// Only for an import uploaded as `application/octet-stream`, which has no JSON
@@ -145,6 +146,7 @@ struct StartRequest {
base_heads: Option<Vec<String>>,
replay_window: Option<Retention>,
archive_retention: Option<Retention>,
+ /// How long votes are collected; twice the dormancy threshold if absent.
settle_secs: Option<u64>,
/// How long nodes wait after seeing the commit before switching; one keepalive
/// interval if absent. Shorter only with `?force=true`.
@@ -354,27 +356,13 @@ async fn start_kind<H: Host>(
}
};
+ // The voting period. No lower bound: the majority protects against competitors,
+ // a short period only risks an abort for lack of votes.
let dormant_after = host.dormant_after();
let settle = request
.settle_secs
.map(Duration::from_secs)
.unwrap_or(dormant_after * 2);
- if settle < dormant_after && query.force {
- warn!(
- settle_secs = settle.as_secs_f64(),
- dormant_after_secs = dormant_after.as_secs(),
- "Forced settle period shorter than the dormancy threshold; a partition may go unnoticed"
- );
- } else if settle < dormant_after {
- return error_response(
- StatusCode::BAD_REQUEST,
- format!(
- "settleSecs must be at least {} (the dormancy threshold), so a partition is detected before deciding; add ?force=true to override",
- dormant_after.as_secs()
- ),
- None,
- );
- }
// The delay lets the commit reach every node through ordinary sync before any of
// them stops syncing the old epoch.
@@ -401,24 +389,6 @@ async fn start_kind<H: Host>(
_ => {}
}
- // Fail fast instead of proposing something that will abort anyway.
- let peers = host.peer_summaries().await;
- let dormant: Vec<String> = peers
- .iter()
- .filter(|peer| !peer.active)
- .map(|peer| peer.id.clone())
- .collect();
- if !dormant.is_empty() {
- return error_response(
- StatusCode::CONFLICT,
- "dormant peers block remake; forget them first (DELETE /peers/{pk})",
- Some(json!({ "dormantPeers": dormant })),
- );
- }
-
- let mut peers_considered: Vec<String> = peers.into_iter().map(|peer| peer.id).collect();
- peers_considered.sort();
-
let now = unix_millis();
let settle_ms = settle.as_millis() as u64;
let id = Uuid::new_v4().to_string();
@@ -440,7 +410,9 @@ async fn start_kind<H: Host>(
proposed_at: now,
settle_until: now + settle_ms,
switch_delay_ms: switch_delay.map(|delay| delay.as_millis() as u64),
- peers_considered,
+ // Filled in when the proposal is written, from the document at that moment.
+ electorate: Vec::new(),
+ quorum: 0,
kind: kind.name().to_owned(),
content: prepared.content,
translation: prepared.translation,
@@ -524,6 +496,8 @@ async fn propose<H: Host>(
grace: u64,
) -> Result<Record, StartError> {
let record_id = id.clone();
+ let electorate_host = host.clone();
+ let me = host.node_id();
let record = host
.commit_doc(move |doc| -> Result<Record, StartError> {
if let Some((open, _)) = records::load_all(doc).into_iter().find(|(_, record)| {
@@ -551,11 +525,22 @@ async fn propose<H: Host>(
heads.sort();
proposal.base_heads = heads;
+ // We vote for our own remake, so we are always in the electorate.
+ let mut electorate = electorate_host.electorate(doc, now);
+ if let Err(position) = electorate.binary_search(&me) {
+ electorate.insert(position, me.clone());
+ }
+ proposal.quorum = vote::quorum(electorate.len());
+ proposal.electorate = electorate;
+
records::write_proposal(doc, &record_id, &proposal)?;
+ let own_vote = Vote { at: now };
+ records::write_vote(doc, &record_id, &me, &own_vote)?;
Ok(Record {
proposal,
decision: None,
abort_requested: None,
+ votes: [(me, own_vote)].into(),
})
})
.await?;