aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/dashboard.md4
-rw-r--r--docs/openapi.yaml4
-rw-r--r--src/api/dashboard/dashboard.html44
-rw-r--r--src/api/dashboard/mod.rs1
-rw-r--r--src/remake/local.rs6
-rw-r--r--src/remake/mod.rs34
6 files changed, 83 insertions, 10 deletions
diff --git a/docs/dashboard.md b/docs/dashboard.md
index 7cdc7b7..4067da5 100644
--- a/docs/dashboard.md
+++ b/docs/dashboard.md
@@ -80,7 +80,9 @@ Every member of the swarm, by alias (or a shortened node ID):
around it), or voted for one, while that remake is open: voting, or committed until
the member switched. A small amber breath as a remake is proposed (its coordinator)
and as each vote comes; a short pop once it's decided, green if committed, red if
- aborted (or expired), and the nodes turn back. Of the remakes in this node's document:
+ aborted (or expired), and the nodes turn back. Committed, the coordinator's ring
+ counts down this node's wait to switch (`switchDelay`): from the top, it goes
+ counterclockwise, and is gone as this node switches. Of the remakes in this node's document:
every member's vote shows, as it reaches this node.
* **A golden glow**, growing from it and fading, once: it just entered a new epoch (a
remake, [`remakes.md`](remakes.md)). For every member that reports its links (they
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index bfad909..d9d6c04 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -758,7 +758,9 @@ paths:
answered a LINKS; ours always, the others' while fresh) and `live` (whether the
members' links are subscribed to, `/admin/dashboard/links`), and `remakes`
(those of this node's epoch: `id`, `coordinator`, `phase` as in
- `/admin/remakes`, `voters` (node IDs) and `fromGeneration`, the epoch's). One producer
+ `/admin/remakes`, `voters` (node IDs), `fromGeneration`, the epoch's, and
+ once committed `switchDelayMs` and `switchInMs`, this node's wait to switch
+ and what is left of it). One producer
per node makes it every 2 s while anyone looks (and for a minute after this
request), subscribed to the members' links (each tells of a change as it
happens; renewed every 30 s); every client gets the same. Only with the `dashboard` cargo feature.
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index 5180038..ede191d 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -1236,8 +1236,13 @@ function renderGraph() {
// In a remake (`inRemakes`): amber; its coordinator also ringed.
const voting = amber.has(id);
if (coordinating.has(id)) {
+ // Counting down once committed (`countdowns`): from the top, as a clock's hand.
+ const dash = countdownDash(id, radius + 4);
children.push(drawing("coordinator " + id, "circle", { cx: p.x, cy: p.y, r: radius + 4,
- fill: "none", stroke: AMBER, "stroke-width": 1, "pointer-events": "none" }));
+ fill: "none", stroke: AMBER, "stroke-width": 1, "pointer-events": "none",
+ ...(dash ? { "stroke-dasharray": dash,
+ transform: "rotate(-90 " + p.x + " " + p.y + ")" } : {}) }));
+ if (dash && countdownFrame === null) countdownFrame = requestAnimationFrame(tickCountdowns);
}
const circle = drawing("node " + id, "circle", { cx: p.x, cy: p.y, r: radius,
fill: voting ? AMBER : member.self ? "var(--self)" : reported ? "var(--fg)" : "var(--panel)",
@@ -2096,8 +2101,15 @@ function remakesArrived() {
breathe(id, { fill, rim: fill, to: 26, ms: 600, opacity: .6 });
}
};
+ countdowns.clear();
for (const remake of remakes) {
seen.add(remake.id);
+ // Committed: this node's wait to switch, as of now (re-anchored with every update).
+ if (remake.phase === "committed" && typeof remake.switchInMs === "number" &&
+ remake.switchDelayMs > 0) {
+ countdowns.set(remake.coordinator, { until: performance.now() + remake.switchInMs,
+ total: remake.switchDelayMs });
+ }
const was = knownRemakes.get(remake.id);
if (remakesKnown) {
const breath = id => breathe(id, { fill: AMBER, rim: AMBER, to: 20, ms: 900, opacity: .45 });
@@ -2113,6 +2125,9 @@ function remakesArrived() {
}
knownRemakes.set(remake.id, remake);
}
+ if (countdowns.size > 0 && countdownFrame === null) {
+ countdownFrame = requestAnimationFrame(tickCountdowns);
+ }
// Gone from the snapshot: this node switched to the next epoch (the records stay in
// the old one's document). Committed then, if it was still open.
for (const [id, remake] of knownRemakes) {
@@ -2125,6 +2140,33 @@ function remakesArrived() {
}
remakesKnown = true;
}
+// Committed remakes' coordinators, by node ID: when this node switches (by
+// `performance.now()`) and the whole wait (ms). Their ring counts down: a full circle
+// from the top, going counterclockwise as the wait runs out, gone as the epoch switches.
+const countdowns = new Map();
+let countdownFrame = null;
+// The ring's dashes for `id` (radius `r`), if it counts down: the part left.
+function countdownDash(id, r) {
+ const countdown = countdowns.get(id);
+ if (!countdown) return null;
+ const left = Math.min(1, Math.max(0, (countdown.until - performance.now()) / countdown.total));
+ const around = 2 * Math.PI * r;
+ return (around * left).toFixed(1) + " " + around.toFixed(1);
+}
+// Every frame while a ring counts down: its dashes, and nothing else drawn anew.
+function tickCountdowns() {
+ countdownFrame = null;
+ const now = performance.now();
+ if (document.hidden || ![...countdowns.values()].some(c => c.until > now - 100)) return;
+ for (const id of countdowns.keys()) {
+ const ring = drawn.get("coordinator " + id);
+ if (ring) {
+ const dash = countdownDash(id, Number(ring.getAttribute("r")));
+ if (dash) ring.setAttribute("stroke-dasharray", dash);
+ }
+ }
+ countdownFrame = requestAnimationFrame(tickCountdowns);
+}
// Who is amber now (`remakesArrived`), and who of them coordinates: of the open
// remakes, those not past the epoch yet (`knownEpochs`).
function inRemakes() {
diff --git a/src/api/dashboard/mod.rs b/src/api/dashboard/mod.rs
index e83f485..831df6f 100644
--- a/src/api/dashboard/mod.rs
+++ b/src/api/dashboard/mod.rs
@@ -323,6 +323,7 @@ async fn build(state: &AppState, graph: &Graph, live: bool) -> JsonValue {
let replica = state.doc.read().await;
// The remakes of our epoch, with who voted: the map shows them.
let remakes = crate::remake::current(
+ &state.remake,
&replica.doc,
replica.epoch,
state.clock.now_millis(),
diff --git a/src/remake/local.rs b/src/remake/local.rs
index c80a5a5..e13c6fd 100644
--- a/src/remake/local.rs
+++ b/src/remake/local.rs
@@ -253,6 +253,12 @@ impl RemakeState {
.or_insert_with(Instant::now)
}
+ /// How long ago this node first saw remake `id` committed (`first_seen`), if it did.
+ #[cfg_attr(not(feature = "dashboard"), allow(dead_code))]
+ pub(crate) fn seen_for(&self, id: &str) -> Option<std::time::Duration> {
+ self.lock().first_seen.get(id).map(|at| at.elapsed())
+ }
+
pub(crate) fn status(&self, id: &str) -> Option<LocalStatus> {
self.lock().status.get(id).cloned()
}
diff --git a/src/remake/mod.rs b/src/remake/mod.rs
index 2273456..b16b8a3 100644
--- a/src/remake/mod.rs
+++ b/src/remake/mod.rs
@@ -88,12 +88,21 @@ pub(crate) struct Current {
pub(crate) voters: Vec<String>,
/// The epoch's generation it remakes: a member past it switched.
pub(crate) from_generation: u64,
+ /// Committed: the wait between seeing the commit and switching
+ /// (`Proposal::switch_delay_ms`), and how much of it is left on this node (ms).
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) switch_delay_ms: Option<u64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub(crate) switch_in_ms: Option<u64>,
}
/// The remakes of `epoch` in `doc`, with their phase at `now` (expiry by `grace`, this
-/// node's for records without their own: `Record::phase`).
+/// node's `dormant_after`, for records without their own: `Record::phase`), and for one
+/// committed, this node's wait to switch (`remake`: when it saw the commit; as
+/// `switch::check` waits: the proposal's delay, or a quarter of `grace`).
#[cfg_attr(not(feature = "dashboard"), allow(dead_code))]
pub(crate) fn current(
+ remake: &RemakeState,
doc: &Document,
epoch: Epoch,
now: crate::clock::UnixMillis,
@@ -103,12 +112,23 @@ pub(crate) fn current(
records::load_all(doc)
.into_iter()
.filter(|(_, record)| record.proposal.from_epoch == local)
- .map(|(id, record)| Current {
- phase: record.phase(now, grace),
- coordinator: record.proposal.coordinator.clone(),
- voters: record.votes.keys().cloned().collect(),
- from_generation: local.generation,
- id,
+ .map(|(id, record)| {
+ let phase = record.phase(now, grace);
+ let delay = (phase == records::Phase::Committed)
+ .then(|| record.proposal.switch_delay_ms.unwrap_or(grace / 4));
+ let left = delay.and_then(|delay| {
+ let seen = remake.seen_for(&id)?;
+ Some(delay.saturating_sub(seen.as_millis() as u64))
+ });
+ Current {
+ phase,
+ coordinator: record.proposal.coordinator.clone(),
+ voters: record.votes.keys().cloned().collect(),
+ from_generation: local.generation,
+ switch_delay_ms: delay,
+ switch_in_ms: left,
+ id,
+ }
})
.collect()
}