fan out the meta pin by a recorded pin, not by the move
This commit is contained in:
parent
81fd4466a5
commit
2b9927fda2
1 changed files with 489 additions and 75 deletions
98
swarm-controller/src/meta_fanout.rs
Normal file
98
swarm-controller/src/meta_fanout.rs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
//! The hyperhive pin the controller last sent the hives to build from.
|
||||
//!
|
||||
//! One key ([`KEY`]) in [`BUCKET`], which only this daemon reads or writes.
|
||||
//! The `UpdateMeta` node fans out whenever the pin `internal/meta` holds is
|
||||
//! not the one recorded here, and the `RecordFanout` node writes it once every
|
||||
//! `TriggerDeploy` of that fan-out is done. A pass that dies, or a deploy that
|
||||
//! fails, before then leaves the record stale, so the next pass sends the
|
||||
//! deploys again. A deploy is idempotent per pin, so sending twice is safe and
|
||||
//! sending never is not.
|
||||
//!
|
||||
//! With no record, as on the first pass of a controller that has never written
|
||||
//! one, the pin is not the recorded one and the agents are sent a deploy.
|
||||
|
||||
use std::future::Future;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_nats::jetstream::kv::Store;
|
||||
|
||||
/// The bucket holding the record. Agents hold no grant on it.
|
||||
pub(crate) const BUCKET: &str = "meta-fanout";
|
||||
|
||||
/// The one key in [`BUCKET`]: the pin, as a rev string.
|
||||
const KEY: &str = "pin";
|
||||
|
||||
/// Where the record lives: the bucket in production, a fake in the tests.
|
||||
pub(crate) trait PinStore {
|
||||
/// The recorded pin; `None` when none was ever written.
|
||||
fn recorded(&self) -> impl Future<Output = Result<Option<String>>> + Send;
|
||||
fn record(&self, pin: &str) -> impl Future<Output = Result<()>> + Send;
|
||||
}
|
||||
|
||||
impl PinStore for Store {
|
||||
async fn recorded(&self) -> Result<Option<String>> {
|
||||
let entry = self
|
||||
.get(KEY)
|
||||
.await
|
||||
.with_context(|| format!("read {BUCKET}/{KEY}"))?;
|
||||
Ok(entry.map(|b| String::from_utf8_lossy(&b).into_owned()))
|
||||
}
|
||||
|
||||
async fn record(&self, pin: &str) -> Result<()> {
|
||||
self.put(KEY, pin.to_owned().into())
|
||||
.await
|
||||
.map(|_| ())
|
||||
.with_context(|| format!("write {BUCKET}/{KEY}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Open [`BUCKET`], creating it if nothing has yet.
|
||||
async fn open(client: &async_nats::Client) -> Result<Store> {
|
||||
let js = async_nats::jetstream::new(client.clone());
|
||||
if let Ok(store) = js.get_key_value(BUCKET).await {
|
||||
return Ok(store);
|
||||
}
|
||||
js.create_key_value(async_nats::jetstream::kv::Config {
|
||||
bucket: BUCKET.to_owned(),
|
||||
description: "The hyperhive pin the controller last sent the hives to build from"
|
||||
.to_owned(),
|
||||
history: 1,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("creating the {BUCKET} bucket"))
|
||||
}
|
||||
|
||||
/// The pin recorded in the bucket on `client`, opening or creating it.
|
||||
///
|
||||
/// # Errors
|
||||
/// The bucket cannot be opened or created, or its key cannot be read.
|
||||
pub(crate) async fn recorded(client: &async_nats::Client) -> Result<Option<String>> {
|
||||
open(client).await?.recorded().await
|
||||
}
|
||||
|
||||
/// Record `pin` as sent, in the bucket on `client`.
|
||||
///
|
||||
/// # Errors
|
||||
/// The bucket cannot be opened or created, or the key cannot be written.
|
||||
pub(crate) async fn record(client: &async_nats::Client, pin: &str) -> Result<()> {
|
||||
open(client).await?.record(pin).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The bucket on a real server: absent until written, then the pin
|
||||
/// written, and a second write replaces it.
|
||||
#[tokio::test]
|
||||
async fn the_pin_round_trips_through_the_bucket() {
|
||||
let nats = crate::test_nats::TestNats::start();
|
||||
let client = nats.client().await;
|
||||
assert_eq!(recorded(&client).await.expect("opens"), None);
|
||||
record(&client, "aaaa").await.expect("written");
|
||||
assert_eq!(recorded(&client).await.expect("read"), Some("aaaa".into()));
|
||||
record(&client, "bbbb").await.expect("replaced");
|
||||
assert_eq!(recorded(&client).await.expect("read"), Some("bbbb".into()));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue