Skip to content

Commit 4569a68

Browse files
committed
feat: added update and delete to flow export
1 parent 8049584 commit 4569a68

3 files changed

Lines changed: 117 additions & 22 deletions

File tree

src/sagittarius/flow_service_client_impl/dev_export.rs

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,73 @@
22
//! JSON file, so a developer can inspect what Sagittarius last synced
33
//! without needing NATS/KV tooling. Only called when running with
44
//! `environment == DEVELOPMENT`.
5+
//!
6+
//! The destination is `config.static_config.flow_path` — the same path
7+
//! static mode loads from at startup — so a development export can be fed
8+
//! straight back in as a static-mode fallback.
59
610
use std::path::Path;
711

812
use tokio::fs;
9-
use tucana::shared::Flows;
13+
use tucana::shared::{Flows, ValidationFlow};
1014

11-
const EXPORT_PATH: &str = "flowExport.json";
12-
const EXPORT_TMP_PATH: &str = "flowExport.json.tmp";
15+
/// Reads the current export at `path`, treating a missing or unparsable
16+
/// file as an empty set so callers can always fall back to a fresh export.
17+
async fn read_flows(path: &str) -> Flows {
18+
match fs::read(path).await {
19+
Ok(bytes) => match serde_json::from_slice(&bytes) {
20+
Ok(flows) => flows,
21+
Err(error) => {
22+
log::warn!(
23+
"Failed to parse existing development flow export path={} error={:?}; starting from an empty export",
24+
path,
25+
error
26+
);
27+
Flows::default()
28+
}
29+
},
30+
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Flows::default(),
31+
Err(error) => {
32+
log::warn!(
33+
"Failed to read existing development flow export path={} error={}; starting from an empty export",
34+
path,
35+
error
36+
);
37+
Flows::default()
38+
}
39+
}
40+
}
1341

14-
/// Writes `flows` to `flowExport.json`, replacing any previous export.
42+
/// Writes `flows` to `path`, replacing any previous export.
1543
///
16-
/// Goes through a temp file plus rename so a reader never observes a
17-
/// partially written export. If the rename fails because the destination
18-
/// already exists, the destination is removed and the rename retried once
19-
/// before giving up.
20-
pub(super) async fn overwrite(flows: Flows) {
21-
let json = match serde_json::to_vec_pretty(&flows) {
44+
/// Goes through a `path`-adjacent temp file plus rename so a reader never
45+
/// observes a partially written export. If the rename fails because the
46+
/// destination already exists, the destination is removed and the rename
47+
/// retried once before giving up. Returns whether the write succeeded.
48+
async fn write(path: &str, flows: &Flows) -> bool {
49+
let json = match serde_json::to_vec_pretty(flows) {
2250
Ok(bytes) => bytes,
2351
Err(error) => {
2452
log::error!(
2553
"Failed to serialize development flow export flow_count={} error={:?}",
2654
flows.flows.len(),
2755
error
2856
);
29-
return;
57+
return false;
3058
}
3159
};
3260

33-
let final_path = Path::new(EXPORT_PATH);
34-
let tmp_path = Path::new(EXPORT_TMP_PATH);
61+
let final_path = Path::new(path);
62+
let tmp_path_buf = format!("{path}.tmp");
63+
let tmp_path = Path::new(&tmp_path_buf);
3564

3665
if let Err(error) = fs::write(tmp_path, &json).await {
3766
log::error!(
3867
"Failed to write development flow export path={} error={}",
3968
tmp_path.display(),
4069
error
4170
);
42-
return;
71+
return false;
4372
}
4473

4574
if let Err(error) = fs::rename(tmp_path, final_path).await {
@@ -75,13 +104,61 @@ pub(super) async fn overwrite(flows: Flows) {
75104
cleanup_error
76105
);
77106
}
78-
return;
107+
return false;
79108
}
80109
}
81110

82-
log::info!(
83-
"Exported {} flows to {}",
84-
flows.flows.len(),
85-
final_path.display()
86-
);
111+
true
112+
}
113+
114+
/// Overwrites `path` with `flows` wholesale.
115+
pub(super) async fn overwrite(path: &str, flows: Flows) {
116+
let flow_count = flows.flows.len();
117+
if write(path, &flows).await {
118+
log::info!("Exported {} flows to {}", flow_count, path);
119+
}
120+
}
121+
122+
/// Adds or replaces a single flow in the export at `path`, read-modify-write.
123+
pub(super) async fn upsert_flow(path: &str, flow: ValidationFlow) {
124+
let mut flows = read_flows(path).await;
125+
let flow_id = flow.flow_id;
126+
127+
match flows.flows.iter_mut().find(|f| f.flow_id == flow_id) {
128+
Some(existing) => *existing = flow,
129+
None => flows.flows.push(flow),
130+
}
131+
132+
if write(path, &flows).await {
133+
log::info!(
134+
"Upserted flow_id={} into development flow export path={}",
135+
flow_id,
136+
path
137+
);
138+
}
139+
}
140+
141+
/// Removes a single flow from the export at `path` by id, read-modify-write.
142+
/// Logs a warning (without failing) if no matching flow was found.
143+
pub(super) async fn remove_flow(path: &str, flow_id: i64) {
144+
let mut flows = read_flows(path).await;
145+
let original_count = flows.flows.len();
146+
flows.flows.retain(|f| f.flow_id != flow_id);
147+
148+
if flows.flows.len() == original_count {
149+
log::warn!(
150+
"Development flow export had no flow matching flow_id={} to remove path={}",
151+
flow_id,
152+
path
153+
);
154+
return;
155+
}
156+
157+
if write(path, &flows).await {
158+
log::info!(
159+
"Removed flow_id={} from development flow export path={}",
160+
flow_id,
161+
path
162+
);
163+
}
87164
}

src/sagittarius/flow_service_client_impl/mod.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
//! along the way.
55
//!
66
//! - [`flow_store`] applies the sync operations (delete/replace/update) to the KV store.
7-
//! - [`dev_export`] mirrors the synced flows to a local JSON file, development only.
7+
//! - [`dev_export`] mirrors the synced flows to a local JSON file, development only,
8+
//! updating it incrementally for single-flow updates/deletes and wholesale on replace.
89
910
mod dev_export;
1011
mod flow_store;
@@ -38,6 +39,9 @@ pub struct SagittariusFlowClient {
3839
client: FlowServiceClient<Channel>,
3940
env: String,
4041
token: String,
42+
/// Path to mirror synced flows to when running in development, same
43+
/// path static mode loads its fallback export from.
44+
flow_export_path: String,
4145
/// Flipped to `true` once the sync stream is established, so other
4246
/// components can hold off on work that depends on Sagittarius state
4347
/// actually being loaded.
@@ -50,6 +54,7 @@ impl SagittariusFlowClient {
5054
store: Arc<async_nats::jetstream::kv::Store>,
5155
env: String,
5256
token: String,
57+
flow_export_path: String,
5358
channel: Channel,
5459
sagittarius_ready: Arc<AtomicBool>,
5560
action_config_tx: broadcast::Sender<tucana::shared::ModuleConfigurations>,
@@ -61,6 +66,7 @@ impl SagittariusFlowClient {
6166
client,
6267
env,
6368
token,
69+
flow_export_path,
6470
sagittarius_ready,
6571
action_config_tx,
6672
}
@@ -83,6 +89,11 @@ impl SagittariusFlowClient {
8389
match data {
8490
Data::DeletedFlowId(id) => {
8591
log::debug!("Applying flow deletion flow_id={}", id);
92+
93+
if self.is_development() {
94+
dev_export::remove_flow(&self.flow_export_path, id).await;
95+
}
96+
8697
let deleted_count = flow_store::delete_flow(&self.store, id).await;
8798

8899
if deleted_count == 0 {
@@ -99,6 +110,11 @@ impl SagittariusFlowClient {
99110
}
100111
Data::UpdatedFlow(flow) => {
101112
let flow_id = flow.flow_id;
113+
114+
if self.is_development() {
115+
dev_export::upsert_flow(&self.flow_export_path, flow.clone()).await;
116+
}
117+
102118
let (key, result) = flow_store::store_flow(&self.store, &flow).await;
103119
match result {
104120
Ok(()) => {
@@ -124,7 +140,7 @@ impl SagittariusFlowClient {
124140
);
125141

126142
if self.is_development() {
127-
dev_export::overwrite(flows.clone()).await;
143+
dev_export::overwrite(&self.flow_export_path, flows.clone()).await;
128144
}
129145

130146
let (purged_count, stored_count) = flow_store::replace_all(&self.store, flows).await;

src/startup/dynamic_mode.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub async fn run(
8080

8181
let backend_url_for_flow = config.dynamic_config.backend_url.clone();
8282
let runtime_token_for_flow = config.dynamic_config.backend_token.clone();
83+
let flow_export_path_for_flow = config.static_config.flow_path.clone();
8384
let sagittarius_ready_for_flow = app_readiness.sagittarius_ready.clone();
8485

8586
let env = match config.environment {
@@ -146,6 +147,7 @@ pub async fn run(
146147
kv_for_flow.clone(),
147148
env.clone(),
148149
runtime_token_for_flow.clone(),
150+
flow_export_path_for_flow.clone(),
149151
ch,
150152
sagittarius_ready_for_flow.clone(),
151153
action_config_tx.clone(),

0 commit comments

Comments
 (0)