aboutsummaryrefslogtreecommitdiffstats
path: root/src/remake/vote.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-01 13:32:35 +0100
committertv <tv@krebsco.de>2026-11-01 13:32:35 +0100
commit484efbe2d62dbad445b0b9b771a3532c8566f024 (patch)
treecf75a2eafb747ca2848496d913ab1e081a191033 /src/remake/vote.rs
parentf68e417555a714bec27fcb877c07bc83cc13fe20 (diff)
remake: log expired and missed votes
Diffstat (limited to 'src/remake/vote.rs')
-rw-r--r--src/remake/vote.rs61
1 files changed, 52 insertions, 9 deletions
diff --git a/src/remake/vote.rs b/src/remake/vote.rs
index e5f8dbe..6604ff6 100644
--- a/src/remake/vote.rs
+++ b/src/remake/vote.rs
@@ -240,6 +240,33 @@ pub(crate) fn choice(
.map(|(id, _)| id.clone())
}
+/// Undecided remakes of `epoch` with `me` in their electorate that expired (by `now`)
+/// before `me` voted for them.
+fn missed(
+ me: &str,
+ epoch: &EpochRecord,
+ all: &[(String, Record)],
+ register: &Register,
+ now: UnixMillis,
+ grace: u64,
+) -> Vec<String> {
+ let voted = |id: &str| {
+ register
+ .get(me)
+ .is_some_and(|votes| votes.iter().any(|vote| vote.remake == id))
+ };
+ all.iter()
+ .filter(|(id, record)| {
+ &record.proposal.from_epoch == epoch
+ && record.decision.is_none()
+ && record.phase(now, grace) == Phase::Expired
+ && record.proposal.electorate.iter().any(|member| member == me)
+ && !voted(id)
+ })
+ .map(|(id, _)| id.clone())
+ .collect()
+}
+
/// Votes for a remake of the local epoch if this node should. Called by the remake
/// worker whenever the records change (and on its timer).
pub(super) async fn cast<H: Host>(host: &H) {
@@ -254,20 +281,36 @@ pub(super) async fn cast<H: Host>(host: &H) {
// Usually there is nothing to vote on: look first, without writing.
let (voter, wanted) = (me.clone(), epoch.clone());
let now = host.now_millis();
- let due = host
+ let (due, missed) = host
.read_doc(move |doc| {
let all = records::load_all(doc);
let register = records::load_register(doc);
- let id = choice(&voter, &wanted, &all, &register, started, now, grace)?;
- 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))
+ let missed = missed(&voter, &wanted, &all, &register, now, grace);
+ let due =
+ choice(&voter, &wanted, &all, &register, started, now, grace).and_then(|id| {
+ 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))
+ });
+ (due, missed)
})
.await;
+ for id in missed {
+ if host.remake().missed(&id) {
+ // By this node's clock: one ahead sees remakes expire that others still vote
+ // on, and on a small swarm, its vote missing keeps them from a majority.
+ let skew = host.clock_skew().await;
+ if skew.off {
+ warn!(remake = %id, offset_ms = skew.offset_ms, "Didn't vote for a remake: it expired by this node's clock, which is off from its peers'");
+ } else {
+ info!(remake = %id, "Didn't vote for a remake: it expired before this node voted");
+ }
+ }
+ }
let Some((chosen, content)) = due else {
return;
};