Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/swarm-controller/src/meta_fanout.rs

100 lines
3.8 KiB
Rust

//! 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 or skipped by its build gate. 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 the gate skipped counts as
//! sent: `jobq::queue_meta_update` declares it again when its build gave no
//! verdict, and a red build waits for the next config merge. 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()));
}
}