huskies: merge 775
This commit is contained in:
@@ -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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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={}",
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user