100 lines
3.8 KiB
Rust
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()));
|
|
}
|
|
}
|