Add focused cfgsync examples

This commit is contained in:
andrussal 2026-03-12 10:03:02 +01:00
parent 6218d4070c
commit 96dc957881
4 changed files with 174 additions and 19 deletions

View File

@ -145,21 +145,19 @@ Those belong in the adapter or in the consuming application.
## Start here
If you want the shortest path into the library, start with the end-to-end
runtime example:
Start with the examples in `cfgsync/runtime/examples/`.
- `cfgsync/runtime/examples/minimal_cfgsync.rs`
- `minimal_cfgsync.rs` shows the smallest complete flow: serve cfgsync, register
one node, fetch artifacts, and write them locally.
- `precomputed_registration_cfgsync.rs` shows how precomputed artifacts still
use the same registration flow, including a later node that joins after the
server is already running.
- `wait_for_registrations_cfgsync.rs` shows the normal `NotReady` path: one node
waits until the materializer sees enough registrations, then both nodes
receive config.
It shows the full loop:
- define a snapshot materializer
- serve cfgsync
- register a node
- fetch artifacts
- write them locally
After that, the only concepts you usually need to learn are the ones in the
next section.
Those three examples cover the full public model. The rest of this README just
names the pieces and explains where application-specific logic belongs.
## Minimal integration path
@ -169,11 +167,12 @@ For a new application, the shortest sensible path is:
2. implement `RegistrationSnapshotMaterializer`
3. return node-local and optional shared artifacts
4. serve them with `serve(...)`
5. use `CfgsyncClient` or the runtime helpers on the node side
5. use `Client` on the node side
That gives you the main value of the library without forcing extra application logic into cfgsync itself.
That gives you the main value of the library without pushing application logic
into cfgsync itself.
## Code sketch
## API sketch
Typed registration payload:

View File

@ -44,15 +44,17 @@ async fn main() -> anyhow::Result<()> {
sleep(Duration::from_millis(100)).await;
let tempdir = tempdir()?;
let config_path = tempdir.path().join("config.yaml");
let outputs = OutputMap::new().route("/config.yaml", &config_path);
let outputs = OutputMap::under(tempdir.path());
let registration = NodeRegistration::new("node-1", "127.0.0.1".parse()?);
Client::new("http://127.0.0.1:4400")
.fetch_and_write(&registration, &outputs)
.await?;
println!("{}", std::fs::read_to_string(&config_path)?);
println!(
"{}",
std::fs::read_to_string(tempdir.path().join("config.yaml"))?
);
server.abort();
Ok(())

View File

@ -0,0 +1,73 @@
use cfgsync_adapter::MaterializedArtifacts;
use cfgsync_artifacts::{ArtifactFile, ArtifactSet};
use cfgsync_core::NodeRegistration;
use cfgsync_runtime::{Client, OutputMap, serve};
use tempfile::tempdir;
use tokio::time::{Duration, sleep};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let port = 4401;
let artifacts = MaterializedArtifacts::from_nodes([
(
"node-1".to_owned(),
ArtifactSet::new(vec![ArtifactFile::new("/config.yaml", "id: node-1\n")]),
),
(
"node-2".to_owned(),
ArtifactSet::new(vec![ArtifactFile::new("/config.yaml", "id: node-2\n")]),
),
])
.with_shared(ArtifactSet::new(vec![ArtifactFile::new(
"/shared/cluster.yaml",
"cluster: demo\n",
)]));
let server = tokio::spawn(async move { serve(port, artifacts).await });
// Give the server a moment to bind before clients register.
sleep(Duration::from_millis(100)).await;
let node_1_dir = tempdir()?;
let node_1_outputs = OutputMap::config_and_shared(
node_1_dir.path().join("config.yaml"),
node_1_dir.path().join("shared"),
);
let node_1 = NodeRegistration::new("node-1", "127.0.0.1".parse()?);
Client::new("http://127.0.0.1:4401")
.fetch_and_write(&node_1, &node_1_outputs)
.await?;
println!(
"node-1 config:\n{}",
std::fs::read_to_string(node_1_dir.path().join("config.yaml"))?
);
// A later node still uses the same registration/fetch flow. The artifacts
// were already known; registration only gates delivery.
sleep(Duration::from_millis(250)).await;
let node_2_dir = tempdir()?;
let node_2_outputs = OutputMap::config_and_shared(
node_2_dir.path().join("config.yaml"),
node_2_dir.path().join("shared"),
);
let node_2 = NodeRegistration::new("node-2", "127.0.0.2".parse()?);
Client::new("http://127.0.0.1:4401")
.fetch_and_write(&node_2, &node_2_outputs)
.await?;
println!(
"node-2 config:\n{}",
std::fs::read_to_string(node_2_dir.path().join("config.yaml"))?
);
println!(
"shared artifact:\n{}",
std::fs::read_to_string(node_2_dir.path().join("shared/shared/cluster.yaml"))?
);
server.abort();
Ok(())
}

View File

@ -0,0 +1,81 @@
use cfgsync_adapter::{
DynCfgsyncError, MaterializationResult, MaterializedArtifacts, RegistrationSnapshot,
RegistrationSnapshotMaterializer,
};
use cfgsync_artifacts::{ArtifactFile, ArtifactSet};
use cfgsync_core::NodeRegistration;
use cfgsync_runtime::{Client, OutputMap, serve};
use tempfile::tempdir;
use tokio::time::{Duration, sleep};
struct ThresholdMaterializer;
impl RegistrationSnapshotMaterializer for ThresholdMaterializer {
fn materialize_snapshot(
&self,
registrations: &RegistrationSnapshot,
) -> Result<MaterializationResult, DynCfgsyncError> {
if registrations.len() < 2 {
return Ok(MaterializationResult::NotReady);
}
let nodes = registrations.iter().map(|registration| {
(
registration.identifier.clone(),
ArtifactSet::new(vec![ArtifactFile::new(
"/config.yaml",
format!("id: {}\ncluster_ready: true\n", registration.identifier),
)]),
)
});
Ok(MaterializationResult::ready(
MaterializedArtifacts::from_nodes(nodes),
))
}
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let port = 4402;
let server = tokio::spawn(async move { serve(port, ThresholdMaterializer).await });
sleep(Duration::from_millis(100)).await;
let waiting_dir = tempdir()?;
let waiting_outputs = OutputMap::under(waiting_dir.path());
let waiting_node = NodeRegistration::new("node-1", "127.0.0.1".parse()?);
let waiting_client = Client::new("http://127.0.0.1:4402");
let waiting_task = tokio::spawn(async move {
waiting_client
.fetch_and_write(&waiting_node, &waiting_outputs)
.await
});
// node-1 is now polling. The materializer will keep returning NotReady
// until node-2 registers.
sleep(Duration::from_millis(400)).await;
let second_dir = tempdir()?;
let second_outputs = OutputMap::under(second_dir.path());
let second_node = NodeRegistration::new("node-2", "127.0.0.2".parse()?);
Client::new("http://127.0.0.1:4402")
.fetch_and_write(&second_node, &second_outputs)
.await?;
waiting_task.await??;
println!(
"node-1 config after threshold reached:\n{}",
std::fs::read_to_string(waiting_dir.path().join("config.yaml"))?
);
println!(
"node-2 config:\n{}",
std::fs::read_to_string(second_dir.path().join("config.yaml"))?
);
server.abort();
Ok(())
}