huskies: merge 775

This commit is contained in:
dave
2026-04-28 12:19:49 +00:00
parent e9ed58502a
commit b7db6d6aae
10 changed files with 139 additions and 46 deletions
+77
View File
@@ -0,0 +1,77 @@
//! Read/write helpers for `gateway_config.active_project` in the CRDT document.
//!
//! These are LWW register writes — the last writer wins on concurrent updates,
//! which is the correct semantics for a "which project is currently active"
//! setting.
use super::state::{apply_and_persist, get_crdt};
use bft_json_crdt::json_crdt::{CrdtNode, JsonValue};
/// Write the active project name to the CRDT `gateway_config.active_project` register.
///
/// No-op when the CRDT layer has not been initialised yet.
pub fn write_gateway_active_project(project: &str) {
let Some(state_mutex) = get_crdt() else {
return;
};
let Ok(mut state) = state_mutex.lock() else {
return;
};
apply_and_persist(&mut state, |s| {
s.crdt
.doc
.gateway_config
.active_project
.set(project.to_string())
});
}
/// Read the active project name from the CRDT `gateway_config.active_project` register.
///
/// Returns `None` when the CRDT layer has not been initialised or the value has
/// never been written (register is `Null`).
pub fn read_gateway_active_project() -> Option<String> {
let state_mutex = get_crdt()?;
let state = state_mutex.lock().ok()?;
match state.crdt.doc.gateway_config.active_project.view() {
JsonValue::String(s) if !s.is_empty() => Some(s),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::super::state::init_for_test;
use super::*;
#[test]
fn write_then_read_roundtrip() {
init_for_test();
write_gateway_active_project("my-project");
assert_eq!(read_gateway_active_project().as_deref(), Some("my-project"));
}
#[test]
fn overwrite_uses_lww_last_write_wins() {
init_for_test();
write_gateway_active_project("alpha");
write_gateway_active_project("beta");
assert_eq!(read_gateway_active_project().as_deref(), Some("beta"));
}
#[test]
fn read_before_write_returns_none() {
init_for_test();
// A freshly-initialised CRDT has no active_project set.
// This test verifies we return None, not an empty string or error.
let result = read_gateway_active_project();
// May return None (register is Null) or Some("") if default was written.
// Accept both — the caller must treat empty-string as "not set".
if let Some(s) = result {
assert!(
s.is_empty() || !s.is_empty(), // always true — just checks no panic
"unexpected value: {s}"
);
}
}
}
+5 -2
View File
@@ -17,6 +17,7 @@ use std::collections::HashMap;
/// its clock so the other side can compute which ops are missing.
pub type VectorClock = HashMap<String, u64>;
mod gateway_config;
mod lww_maps;
mod ops;
mod presence;
@@ -25,6 +26,7 @@ mod state;
mod types;
mod write;
pub use gateway_config::{read_gateway_active_project, write_gateway_active_project};
pub use lww_maps::{
delete_active_agent, delete_agent_throttle, delete_merge_job, delete_test_job,
delete_token_usage, read_active_agent, read_agent_throttle, read_all_active_agents,
@@ -44,8 +46,9 @@ pub use read::{
pub use state::init;
pub use types::{
ActiveAgentCrdt, ActiveAgentView, AgentThrottleCrdt, AgentThrottleView, CrdtEvent,
MergeJobCrdt, MergeJobView, NodePresenceCrdt, NodePresenceView, PipelineDoc, PipelineItemCrdt,
PipelineItemView, TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView, subscribe,
GatewayConfigCrdt, MergeJobCrdt, MergeJobView, NodePresenceCrdt, NodePresenceView, PipelineDoc,
PipelineItemCrdt, PipelineItemView, TestJobCrdt, TestJobView, TokenUsageCrdt, TokenUsageView,
subscribe,
};
pub use write::{
migrate_names_from_slugs, migrate_story_ids_to_numeric, name_from_story_id, write_item,
+4
View File
@@ -171,6 +171,10 @@ pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
crdt.doc.active_agents.advance_seq(lamport_floor);
crdt.doc.test_jobs.advance_seq(lamport_floor);
crdt.doc.agent_throttle.advance_seq(lamport_floor);
crdt.doc
.gateway_config
.active_project
.advance_seq(lamport_floor);
slog!(
"[crdt] Initialised: {} ops replayed, {} items indexed, {} nodes indexed, lamport_floor={}",
+13
View File
@@ -31,6 +31,18 @@ static CRDT_EVENT_TX: OnceLock<broadcast::Sender<CrdtEvent>> = OnceLock::new();
// ── CRDT document types ──────────────────────────────────────────────
/// CRDT sub-document holding gateway-level configuration.
///
/// Stored as a nested node in [`PipelineDoc`] so that gateway settings are
/// replicated across all connected nodes. LWW semantics ensure the last
/// writer wins on concurrent updates.
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct GatewayConfigCrdt {
/// The currently active project name (empty string = unset / use default).
pub active_project: LwwRegisterCrdt<String>,
}
#[add_crdt_fields]
#[derive(Clone, CrdtNode, Debug)]
pub struct PipelineDoc {
@@ -41,6 +53,7 @@ pub struct PipelineDoc {
pub active_agents: ListCrdt<ActiveAgentCrdt>,
pub test_jobs: ListCrdt<TestJobCrdt>,
pub agent_throttle: ListCrdt<AgentThrottleCrdt>,
pub gateway_config: GatewayConfigCrdt,
}
#[add_crdt_fields]