aboutsummaryrefslogtreecommitdiffstats
path: root/src/remake/records.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remake/records.rs')
-rw-r--r--src/remake/records.rs166
1 files changed, 87 insertions, 79 deletions
diff --git a/src/remake/records.rs b/src/remake/records.rs
index 720aea1..0c4c444 100644
--- a/src/remake/records.rs
+++ b/src/remake/records.rs
@@ -28,7 +28,6 @@
use std::collections::BTreeMap;
-use automerge::{AutoCommit, ChangeHash};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use tracing::warn;
@@ -39,28 +38,14 @@ use crate::clock::UnixMillis;
use crate::document::meta::{self, REMAKES, VOTES};
use crate::document::replica::Epoch;
use crate::document::store::{self, StoreError};
+use crate::document::{ChangeHash, ReadDoc, Transactable};
/// One day, for retention defaults.
const DAY_MS: u64 = 24 * 60 * 60 * 1000;
// --- record types ------------------------------------------------------------
-/// An epoch in a record: generation plus hex-encoded snapshot hash.
-#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub(crate) struct EpochRecord {
- pub(crate) generation: u64,
- pub(crate) id: String,
-}
-
-impl From<Epoch> for EpochRecord {
- fn from(epoch: Epoch) -> Self {
- Self {
- generation: epoch.generation,
- id: epoch.id_hex(),
- }
- }
-}
+pub(crate) use crate::document::replica::EpochRecord;
impl EpochRecord {
pub(crate) fn to_epoch(&self) -> Option<Epoch> {
@@ -137,6 +122,11 @@ pub(crate) struct Proposal {
/// interval if absent.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) switch_delay_ms: Option<u64>,
+ /// The coordinator's grace period (ms, its `dormant_after`): every node judges this
+ /// remake's expiry, and when others may abort it, by it, whatever its own keepalive
+ /// (`Record::grace`). Absent in records from before: each node's own.
+ #[serde(default, skip_serializing_if = "Option::is_none")]
+ pub(crate) grace_ms: Option<u64>,
/// Members whose votes count (sorted), fixed at proposal time (`vote`).
pub(crate) electorate: Vec<String>,
/// Votes needed to commit: a majority of the electorate.
@@ -326,14 +316,23 @@ impl Record {
})
}
- pub(crate) fn phase(&self, now: UnixMillis, grace_ms: u64) -> Phase {
+ /// The phase at `now`, expiry judged by the record's grace period (`grace`; `own`,
+ /// this node's, for a record without one).
+ pub(crate) fn phase(&self, now: UnixMillis, own: u64) -> Phase {
match &self.decision {
Some(Decision::Committed { .. }) => Phase::Committed,
Some(Decision::Aborted { .. }) => Phase::Aborted,
- None if now > self.proposal.settle_until.plus_ms(grace_ms) => Phase::Expired,
+ None if now > self.proposal.settle_until.plus_ms(self.grace(own)) => Phase::Expired,
None => Phase::Proposed,
}
}
+
+ /// The grace period this remake is judged by (ms): its coordinator's, the same on
+ /// every node, so nodes with different keepalives agree on when it expired; `own`
+ /// for a record from before it was written down.
+ pub(crate) fn grace(&self, own: u64) -> u64 {
+ self.proposal.grace_ms.unwrap_or(own)
+ }
}
// --- document access ---------------------------------------------------------
@@ -352,13 +351,13 @@ pub(crate) fn record_path(id: &str, part: &str) -> Vec<String> {
}
/// The record with this ID, if it exists and is readable, with its votes.
-pub(crate) fn load(doc: &AutoCommit, id: &str) -> Option<Record> {
+pub(crate) fn load(doc: &impl ReadDoc, id: &str) -> Option<Record> {
let record = load_record(doc, id)?;
Some(with_votes(id, record, &load_register(doc)))
}
/// All readable records, with their votes.
-pub(crate) fn load_all(doc: &AutoCommit) -> Vec<(String, Record)> {
+pub(crate) fn load_all(doc: &impl ReadDoc) -> Vec<(String, Record)> {
let register = load_register(doc);
store::keys_at(doc, &records_dir())
.unwrap_or_default()
@@ -381,7 +380,7 @@ fn with_votes(id: &str, mut record: Record, register: &Register) -> Record {
record
}
-fn load_record(doc: &AutoCommit, id: &str) -> Option<Record> {
+fn load_record(doc: &impl ReadDoc, id: &str) -> Option<Record> {
let value = match store::read_path(doc, &record_path(id, "")) {
Ok(Some(value)) => value,
Ok(None) => return None,
@@ -405,7 +404,7 @@ fn load_record(doc: &AutoCommit, id: &str) -> Option<Record> {
}
/// A commit among concurrent decisions of remake `id` that disagree, if they do.
-fn contested_decision(doc: &AutoCommit, id: &str) -> Option<Decision> {
+fn contested_decision(doc: &impl ReadDoc, id: &str) -> Option<Decision> {
let decisions: Vec<Decision> = store::read_candidates(doc, &record_path(id, "decision"))
.ok()?
.into_iter()
@@ -433,7 +432,7 @@ fn register_path(voter: &str) -> Vec<String> {
}
/// The votes register; unreadable entries are skipped with a warning.
-pub(crate) fn load_register(doc: &AutoCommit) -> Register {
+pub(crate) fn load_register(doc: &impl ReadDoc) -> Register {
store::keys_at(doc, &meta::path(&[VOTES]))
.unwrap_or_default()
.into_iter()
@@ -461,7 +460,7 @@ pub(crate) fn load_register(doc: &AutoCommit) -> Register {
}
fn write_part<T: Serialize>(
- doc: &mut AutoCommit,
+ doc: &mut impl Transactable,
id: &str,
part: &str,
value: &T,
@@ -471,7 +470,7 @@ fn write_part<T: Serialize>(
}
pub(crate) fn write_proposal(
- doc: &mut AutoCommit,
+ doc: &mut impl Transactable,
id: &str,
proposal: &Proposal,
) -> Result<(), StoreError> {
@@ -479,7 +478,7 @@ pub(crate) fn write_proposal(
}
pub(crate) fn write_decision(
- doc: &mut AutoCommit,
+ doc: &mut impl Transactable,
id: &str,
decision: &Decision,
) -> Result<(), StoreError> {
@@ -487,7 +486,7 @@ pub(crate) fn write_decision(
}
pub(crate) fn write_abort_request(
- doc: &mut AutoCommit,
+ doc: &mut impl Transactable,
id: &str,
request: &AbortRequest,
) -> Result<(), StoreError> {
@@ -495,7 +494,11 @@ pub(crate) fn write_abort_request(
}
/// Writes `voter`'s entry in the votes register (replacing its earlier one).
-pub(crate) fn write_vote(doc: &mut AutoCommit, voter: &str, vote: &Vote) -> Result<(), StoreError> {
+pub(crate) fn write_vote(
+ doc: &mut impl Transactable,
+ voter: &str,
+ vote: &Vote,
+) -> Result<(), StoreError> {
let json = serde_json::to_value(vote).map_err(anyhow::Error::from)?;
store::write_path(doc, &register_path(voter), &json)
}
@@ -514,6 +517,8 @@ pub(crate) fn decode_hex32(hex: &str) -> Option<[u8; 32]> {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::document::Document;
+ use crate::document::testing::change;
#[test]
fn retention_covers_until_every_limit_passed() {
@@ -573,6 +578,7 @@ mod tests {
proposed_at: UnixMillis(settle_until.saturating_sub(1_000)),
settle_until: UnixMillis(settle_until),
switch_delay_ms: None,
+ grace_ms: None,
electorate: vec!["node-a".into(), "node-b".into()],
quorum: 2,
kind: DEFAULT_KIND.to_owned(),
@@ -583,18 +589,8 @@ mod tests {
#[test]
fn round_trip_through_the_document() {
- let mut doc = AutoCommit::new();
+ let mut doc = Document::new();
let p = proposal(10_000);
- write_proposal(&mut doc, "c-1", &p).unwrap();
- write_abort_request(
- &mut doc,
- "c-1",
- &AbortRequest {
- by: "x".into(),
- at: UnixMillis(5),
- },
- )
- .unwrap();
let decision = Decision::Committed {
to_epoch: EpochRecord {
generation: 1,
@@ -604,14 +600,25 @@ mod tests {
snapshot_bytes: 123,
decided_at: UnixMillis(11_000),
};
- write_decision(&mut doc, "c-1", &decision).unwrap();
let vote = |remake: &str| Vote {
remake: remake.into(),
at: UnixMillis(7),
};
- write_vote(&mut doc, "node-b", &vote("c-1")).unwrap();
- write_vote(&mut doc, "node-c", &vote("other")).unwrap();
- doc.commit();
+ change(&mut doc, |doc| {
+ write_proposal(doc, "c-1", &p).unwrap();
+ write_abort_request(
+ doc,
+ "c-1",
+ &AbortRequest {
+ by: "x".into(),
+ at: UnixMillis(5),
+ },
+ )
+ .unwrap();
+ write_decision(doc, "c-1", &decision).unwrap();
+ write_vote(doc, "node-b", &vote("c-1")).unwrap();
+ write_vote(doc, "node-c", &vote("other")).unwrap();
+ });
let all = load_all(&doc);
assert_eq!(all.len(), 1);
@@ -627,19 +634,16 @@ mod tests {
#[test]
fn concurrent_votes_are_kept_apart() {
- let mut a = AutoCommit::new();
+ let mut a = Document::new();
let vote = |remake: &str, at: u64| Vote {
remake: remake.into(),
at: UnixMillis(at),
};
- write_vote(&mut a, "node-a", &vote("x", 1)).unwrap();
- a.commit();
+ change(&mut a, |a| write_vote(a, "node-a", &vote("x", 1)).unwrap());
let mut b = a.fork();
// node-b votes in both, never seeing its other vote (a lost vote).
- write_vote(&mut a, "node-b", &vote("x", 2)).unwrap();
- write_vote(&mut b, "node-b", &vote("y", 3)).unwrap();
- a.commit();
- b.commit();
+ change(&mut a, |a| write_vote(a, "node-b", &vote("x", 2)).unwrap());
+ change(&mut b, |b| write_vote(b, "node-b", &vote("y", 3)).unwrap());
a.merge(&mut b).unwrap();
let register = load_register(&a);
@@ -649,8 +653,7 @@ mod tests {
assert_eq!(twice, [vote("x", 2), vote("y", 3)]);
// Seen, a vote is replaced.
- write_vote(&mut a, "node-b", &vote("z", 4)).unwrap();
- a.commit();
+ change(&mut a, |a| write_vote(a, "node-b", &vote("z", 4)).unwrap());
assert_eq!(load_register(&a)["node-b"], [vote("z", 4)]);
}
@@ -666,6 +669,10 @@ mod tests {
assert_eq!(record.phase(UnixMillis(10_000), 500), Phase::Proposed);
assert_eq!(record.phase(UnixMillis(10_500), 500), Phase::Proposed);
assert_eq!(record.phase(UnixMillis(10_501), 500), Phase::Expired);
+ // The coordinator's grace period, written down, wins over this node's.
+ record.proposal.grace_ms = Some(2_000);
+ assert_eq!(record.phase(UnixMillis(10_501), 500), Phase::Proposed);
+ assert_eq!(record.phase(UnixMillis(12_001), 500), Phase::Expired);
record.decision = Some(Decision::Aborted {
reason: "test".into(),
decided_at: UnixMillis(1),
@@ -700,9 +707,10 @@ mod tests {
#[test]
fn disagreeing_decisions_are_contested() {
- let mut base = AutoCommit::new();
- write_proposal(&mut base, "c-1", &proposal(10_000)).unwrap();
- base.commit();
+ let mut base = Document::new();
+ change(&mut base, |base| {
+ write_proposal(base, "c-1", &proposal(10_000)).unwrap()
+ });
let (mut a, mut b, mut c) = (base.fork(), base.fork(), base.fork());
let committed = Decision::Committed {
to_epoch: EpochRecord {
@@ -713,21 +721,20 @@ mod tests {
snapshot_bytes: 1,
decided_at: UnixMillis(2),
};
- write_decision(&mut a, "c-1", &committed).unwrap();
- write_decision(
- &mut b,
- "c-1",
- &Decision::Aborted {
- reason: "test".into(),
- decided_at: UnixMillis(3),
- },
- )
- .unwrap();
+ change(&mut a, |a| write_decision(a, "c-1", &committed).unwrap());
+ change(&mut b, |b| {
+ write_decision(
+ b,
+ "c-1",
+ &Decision::Aborted {
+ reason: "test".into(),
+ decided_at: UnixMillis(3),
+ },
+ )
+ .unwrap()
+ });
// The same decision, concurrently: they agree.
- write_decision(&mut c, "c-1", &committed).unwrap();
- a.commit();
- b.commit();
- c.commit();
+ change(&mut c, |c| write_decision(c, "c-1", &committed).unwrap());
let mut agreeing = a.fork();
agreeing.merge(&mut c).unwrap();
assert!(!load(&agreeing, "c-1").unwrap().contested);
@@ -740,15 +747,16 @@ mod tests {
#[test]
fn unreadable_records_are_skipped() {
- let mut doc = AutoCommit::new();
- store::write_path(
- &mut doc,
- &record_path("broken", ""),
- &serde_json::json!({ "proposal": "not an object" }),
- )
- .unwrap();
- write_proposal(&mut doc, "fine", &proposal(1)).unwrap();
- doc.commit();
+ let mut doc = Document::new();
+ change(&mut doc, |doc| {
+ store::write_path(
+ doc,
+ &record_path("broken", ""),
+ &serde_json::json!({ "proposal": "not an object" }),
+ )
+ .unwrap();
+ write_proposal(doc, "fine", &proposal(1)).unwrap();
+ });
let all = load_all(&doc);
assert_eq!(all.len(), 1);
assert_eq!(all[0].0, "fine");