summaryrefslogtreecommitdiffstats
path: root/src/remake/coordinator.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remake/coordinator.rs')
-rw-r--r--src/remake/coordinator.rs41
1 files changed, 26 insertions, 15 deletions
diff --git a/src/remake/coordinator.rs b/src/remake/coordinator.rs
index 9bcf8ba..20de712 100644
--- a/src/remake/coordinator.rs
+++ b/src/remake/coordinator.rs
@@ -12,7 +12,7 @@
//! The decision is written in the same step that checks the records once more, so a
//! commit never overrides an abort request or a competitor's commit that arrived while
//! the snapshot was being built. Nor is a commit written once the remake has expired:
-//! from then on, any node may abort it (`api`), and the two mustn't race.
+//! a grace period later, any node may abort it (`api`), and the two mustn't race.
//!
//! Committing builds the snapshot at the base heads and records the new epoch, so
//! every node can verify its own build against it.
@@ -106,7 +106,7 @@ async fn step<H: Host>(host: &H, id: &str) -> Result<bool, StoreError> {
let tally = vote::tally(id, &record, &all, &register);
// The deadline first: a majority counted after it is too late.
- if records::unix_millis() >= record.proposal.settle_until {
+ if host.now_millis() >= record.proposal.settle_until {
decide_abort(
host,
id,
@@ -141,7 +141,23 @@ fn obstacle(id: &str, record: &Record, all: &[(String, Record)]) -> Option<Strin
}
async fn commit<H: Host>(host: &H, id: &str, record: &Record) -> Result<(), StoreError> {
- let Some(mut request) = record.snapshot_request(id) else {
+ let content = match &record.proposal.content {
+ Some(reference) => match content::ensure(host, reference).await {
+ Ok(bytes) => Some(bytes),
+ Err(reason) => return decide_abort(host, id, reason).await,
+ },
+ None => None,
+ };
+
+ // Clone under the read lock, build without any lock.
+ let mut copy = host.read_doc(|doc| doc.clone()).await;
+ // A base left to the commit is what this node has now. Every node that sees the
+ // decision has it too: the decision change depends on it.
+ let (base, fixed_now) = match &record.proposal.base_heads {
+ None => (Some(copy.get_heads()), true),
+ Some(heads) => (records::decode_heads(heads), false),
+ };
+ let Some(mut request) = base.and_then(|base| record.snapshot_request_at(id, base)) else {
return decide_abort(
host,
id,
@@ -149,15 +165,8 @@ async fn commit<H: Host>(host: &H, id: &str, record: &Record) -> Result<(), Stor
)
.await;
};
- if let Some(reference) = &record.proposal.content {
- match content::ensure(host, reference).await {
- Ok(bytes) => request.content = Some(bytes),
- Err(reason) => return decide_abort(host, id, reason).await,
- }
- }
-
- // Clone under the read lock, build without any lock.
- let copy = host.read_doc(|doc| doc.clone()).await;
+ let base_heads = fixed_now.then(|| records::encode_heads(&request.base_heads));
+ request.content = content;
let mut snapshot = match snapshot::build_detached(copy, request).await {
Ok((snapshot, _copy)) => snapshot,
Err(error) => {
@@ -170,8 +179,9 @@ async fn commit<H: Host>(host: &H, id: &str, record: &Record) -> Result<(), Stor
let decision = Decision::Committed {
to_epoch: to_epoch.into(),
+ base_heads,
snapshot_bytes,
- decided_at: records::unix_millis(),
+ decided_at: host.now_millis(),
};
match decide(host, id, decision).await? {
Some(Decision::Committed { .. }) => {
@@ -190,7 +200,7 @@ async fn commit<H: Host>(host: &H, id: &str, record: &Record) -> Result<(), Stor
async fn decide_abort<H: Host>(host: &H, id: &str, reason: String) -> Result<(), StoreError> {
let decision = Decision::Aborted {
reason: reason.clone(),
- decided_at: records::unix_millis(),
+ decided_at: host.now_millis(),
};
if decide(host, id, decision).await?.is_some() {
warn!(remake = %id, reason = %reason, "Remake aborted");
@@ -210,6 +220,7 @@ async fn decide<H: Host>(
) -> Result<Option<Decision>, StoreError> {
let id = id.to_owned();
let grace = host.dormant_after().as_millis() as u64;
+ let clock = host.clock();
host.commit_doc(move |doc| -> Result<Option<Decision>, StoreError> {
let all = records::load_all(doc);
let Some((_, record)) = all.iter().find(|(other, _)| *other == id) else {
@@ -222,7 +233,7 @@ async fn decide<H: Host>(
Decision::Committed { decided_at, .. } => {
let register = records::load_register(doc);
let tally = vote::tally(&id, record, &all, &register);
- let expired = record.phase(records::unix_millis(), grace) == Phase::Expired;
+ let expired = record.phase(clock.now_millis(), grace) == Phase::Expired;
match obstacle(&id, record, &all) {
Some(reason) => Decision::Aborted { reason, decided_at },
// Others may abort it by now (`api`): too late to commit.