200 lines
7.3 KiB
Rust
200 lines
7.3 KiB
Rust
//! Example: two `JsonlBackend`s side by side, demonstrating boot-from-config,
|
|
//! priority-ordered read routing, write fanout, and a health snapshot.
|
|
//!
|
|
//! Layout:
|
|
//! - `primary` → `read_priority = 0`, `failure_mode = required` (default)
|
|
//! - `mirror` → `read_priority = 10`, `failure_mode = best_effort`
|
|
//!
|
|
//! The primary is the default write target (lowest priority among
|
|
//! Required+write-active backends). `append_messages` fans out inline to the
|
|
//! mirror too. Reads walk the registrations in priority order, so the primary
|
|
//! answers first; if it is missing a session, the walk falls through to the
|
|
//! mirror.
|
|
//!
|
|
//! Run with:
|
|
//!
|
|
//! cargo run --package dirigent_archivist --example multi_backend
|
|
|
|
use std::sync::Arc;
|
|
|
|
use chrono::Utc;
|
|
use dirigent_archivist::coordinator::Archivist;
|
|
use dirigent_archivist::registry::{ArchivesConfig, BackendRegistry};
|
|
use dirigent_archivist::types::{
|
|
MessageRecord, RegisterConnectorRequest, RegisterSessionRequest,
|
|
};
|
|
use uuid::Uuid;
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
let dir_a = tempfile::tempdir()?;
|
|
let dir_b = tempfile::tempdir()?;
|
|
|
|
// Build a two-archive config entirely from TOML so the example doubles as
|
|
// a faithful demonstration of the config surface.
|
|
let cfg_src = format!(
|
|
r#"
|
|
[[archives]]
|
|
name = "primary"
|
|
type = "jsonl"
|
|
read_priority = 0
|
|
[archives.params]
|
|
path = "{a}"
|
|
|
|
[[archives]]
|
|
name = "mirror"
|
|
type = "jsonl"
|
|
failure_mode = "best_effort"
|
|
read_priority = 10
|
|
[archives.params]
|
|
path = "{b}"
|
|
"#,
|
|
a = dir_a.path().to_string_lossy().replace('\\', "/"),
|
|
b = dir_b.path().to_string_lossy().replace('\\', "/"),
|
|
);
|
|
let cfg: ArchivesConfig = toml::from_str(&cfg_src)?;
|
|
let registry = BackendRegistry::with_jsonl();
|
|
let archivist = Arc::new(Archivist::from_config(cfg, ®istry, None).await?);
|
|
|
|
println!("\n=== Multi-backend Archivist example ===\n");
|
|
println!("Boot complete. Archives (ordered by read_priority):");
|
|
for s in archivist.list_archives_with_health().await {
|
|
println!(
|
|
" - name={:<8} type={:<6} priority={:<3} enabled={} write_active={} failure_mode={:?} health={:?}",
|
|
s.name,
|
|
s.type_name,
|
|
s.read_priority,
|
|
s.enabled,
|
|
s.write_active,
|
|
s.failure_mode,
|
|
s.health,
|
|
);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Register a connector. The primary owns the canonical record; fanout
|
|
// mirrors it to the secondary.
|
|
// ------------------------------------------------------------------
|
|
let connector_resp = archivist
|
|
.register_connector(
|
|
RegisterConnectorRequest {
|
|
r#type: "Example".into(),
|
|
title: "multi-backend demo".into(),
|
|
client_native_id: "example://multi_backend".into(),
|
|
custom_uid: None,
|
|
metadata: serde_json::json!({ "demo": true }),
|
|
fingerprint: None,
|
|
},
|
|
None,
|
|
)
|
|
.await?;
|
|
let connector_uid = connector_resp.connector_uid;
|
|
println!(
|
|
"\nRegistered connector: uid={} status={:?}",
|
|
connector_uid, connector_resp.status
|
|
);
|
|
|
|
// ------------------------------------------------------------------
|
|
// Register a session under that connector. `register_session` writes
|
|
// the mapping and `session.json` on the primary first, then fans out
|
|
// to any enabled secondaries.
|
|
// ------------------------------------------------------------------
|
|
let session_resp = archivist
|
|
.register_session(
|
|
RegisterSessionRequest {
|
|
connector_uid,
|
|
native_session_id: "demo-session-1".into(),
|
|
title: Some("multi-backend demo session".into()),
|
|
custom_scroll_id: None,
|
|
metadata: serde_json::json!({ "model": "demo" }),
|
|
completeness: Default::default(),
|
|
parent_scroll_id: None,
|
|
is_subagent: false,
|
|
continuation: None,
|
|
agent_id: None,
|
|
subagent_type: None,
|
|
spawning_tool_use_id: None,
|
|
},
|
|
None,
|
|
)
|
|
.await?;
|
|
let scroll_id = session_resp.scroll_id;
|
|
println!(
|
|
"Registered session: scroll_id={} status={:?}",
|
|
scroll_id, session_resp.status
|
|
);
|
|
|
|
// ------------------------------------------------------------------
|
|
// Append a couple of messages. `append_messages` writes to the primary
|
|
// and then fans out inline to the mirror.
|
|
// ------------------------------------------------------------------
|
|
let user_msg = MessageRecord {
|
|
version: 1,
|
|
message_id: Uuid::now_v7(),
|
|
session: scroll_id,
|
|
parent_id: None,
|
|
ts: Utc::now(),
|
|
role: "user".into(),
|
|
author: Some("alice".into()),
|
|
content_md: "Hello from the multi-backend example!".into(),
|
|
content_parts: None,
|
|
attachments: vec![],
|
|
metadata: serde_json::json!({}),
|
|
};
|
|
let asst_msg = MessageRecord {
|
|
version: 1,
|
|
message_id: Uuid::now_v7(),
|
|
session: scroll_id,
|
|
parent_id: Some(user_msg.message_id),
|
|
ts: Utc::now(),
|
|
role: "assistant".into(),
|
|
author: Some("claude".into()),
|
|
content_md: "Greetings. I have been written to two archives.".into(),
|
|
content_parts: None,
|
|
attachments: vec![],
|
|
metadata: serde_json::json!({}),
|
|
};
|
|
archivist
|
|
.append_messages(scroll_id, vec![user_msg, asst_msg], None)
|
|
.await?;
|
|
println!("\nAppended 2 messages — fanned out to primary + mirror.");
|
|
|
|
// ------------------------------------------------------------------
|
|
// Read path: the priority walk tries the primary first (priority=0).
|
|
// It finds the session there and never consults the mirror.
|
|
// ------------------------------------------------------------------
|
|
let meta = archivist.get_session_metadata(scroll_id, None).await?;
|
|
println!(
|
|
"\nRead session via priority walk: title={:?} completeness={:?}",
|
|
meta.title, meta.completeness
|
|
);
|
|
println!(
|
|
"Read cache size after read: {}",
|
|
archivist.read_cache_size().await
|
|
);
|
|
|
|
let messages = archivist.get_messages(scroll_id, None).await?;
|
|
println!("Read {} message(s) from the archive:", messages.len());
|
|
for m in &messages {
|
|
println!(" - [{}] {}", m.role, m.content_md);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Final health snapshot. Both backends should still be Available and
|
|
// have no queued writes (both run Inline write policies by default).
|
|
// ------------------------------------------------------------------
|
|
println!("\nFinal health snapshot:");
|
|
for s in archivist.list_archives_with_health().await {
|
|
println!(
|
|
" - {:<8} health={:?} queue_depth={:?} last_error={:?}",
|
|
s.name, s.health, s.queue_depth, s.last_error
|
|
);
|
|
}
|
|
|
|
// Clean shutdown drains any queued writer tasks. Both backends here run
|
|
// Inline, so this is effectively a no-op but remains the correct API.
|
|
archivist.shutdown().await?;
|
|
println!("\nShutdown complete.");
|
|
Ok(())
|
|
}
|