summaryrefslogtreecommitdiffstats
path: root/src/remake/vote.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remake/vote.rs')
-rw-r--r--src/remake/vote.rs31
1 files changed, 27 insertions, 4 deletions
diff --git a/src/remake/vote.rs b/src/remake/vote.rs
index cc805e3..669cab8 100644
--- a/src/remake/vote.rs
+++ b/src/remake/vote.rs
@@ -16,6 +16,10 @@
//! and only once it holds the remake's content (`content`), so a committed remake has
//! its content on a majority.
//!
+//! Every vote is a change that reaches every node, and only a majority is needed. So a
+//! node waits a random part of half a keepalive interval before voting, and doesn't
+//! vote once the remake has its majority: of a large electorate, about half votes.
+//!
//! A vote can be lost: a node whose vote reached only the coordinator restarts and
//! resyncs from nodes that never saw it. Should it vote again for a competitor, its
//! entry ends up with two concurrent values, and it counts for no remake. To avoid
@@ -23,6 +27,7 @@
//! for is older than its start, and with one, votes only for that one.
use std::collections::HashSet;
+use std::time::Duration;
use tracing::{info, warn};
@@ -242,10 +247,12 @@ pub(super) async fn cast<H: Host>(host: &H) {
let all = records::load_all(doc);
let register = records::load_register(doc);
let id = choice(&voter, &wanted, &all, &register, started, now, grace)?;
- let content = all
- .into_iter()
- .find(|(other, _)| *other == id)
- .and_then(|(_, record)| record.proposal.content);
+ let (_, record) = all.iter().find(|(other, _)| *other == id)?;
+ // Enough voted already: ours would only be one more change for everyone.
+ if tally(&id, record, &all, &register).reached() {
+ return None;
+ }
+ let content = record.proposal.content.clone();
Some((id, content))
})
.await;
@@ -258,6 +265,12 @@ pub(super) async fn cast<H: Host>(host: &H) {
return;
}
}
+ // Spread over half a keepalive interval, so later voters see the earlier votes
+ // (the worker checks every second).
+ let spread = host.dormant_after() / 8;
+ if now < host.remake().vote_due(&chosen, now, || random_part(spread)) {
+ return;
+ }
// Decided again under the write lock, so two passes can't both vote.
let voter = me.clone();
@@ -287,6 +300,16 @@ pub(super) async fn cast<H: Host>(host: &H) {
}
}
+/// A random duration below `limit`.
+fn random_part(limit: Duration) -> Duration {
+ let mut bytes = [0u8; 8];
+ if getrandom::fill(&mut bytes).is_err() {
+ return Duration::ZERO;
+ }
+ let limit_ms = limit.as_millis().max(1) as u64;
+ Duration::from_millis(u64::from_le_bytes(bytes) % limit_ms)
+}
+
#[cfg(test)]
mod tests {
use super::*;