1use std::cmp::Reverse;
7
8use chrono::{DateTime, Utc};
9use serde_json::Value;
10use uuid::Uuid;
11
12use crate::entities::{
13 NewSignal, Page, RunStatus, Signal, SignalFilter, SignalInsert, SignalStepResolution, Step,
14 StepKind, StepStatus,
15};
16use crate::error::StoreError;
17use crate::memory::InMemoryStore;
18use crate::signal_store::SignalStore;
19use crate::store::StoreFuture;
20
21const SIGNALS_FOR_KEY_LIMIT: usize = 100;
23
24fn is_waiting_signal_step(step: &Step) -> bool {
26 step.kind == StepKind::Signal && step.status.state == StepStatus::Running
27}
28
29fn step_waits_for(step: &Step, name: &str, key: &str) -> bool {
31 let Some(input) = step.input.as_ref() else {
32 return false;
33 };
34 input.get("name").and_then(Value::as_str) == Some(name)
35 && input.get("key").and_then(Value::as_str) == Some(key)
36}
37
38impl SignalStore for InMemoryStore {
39 fn insert_signal(&self, signal: NewSignal) -> StoreFuture<'_, SignalInsert> {
40 Box::pin(async move {
41 let mut state = self.state.write().await;
42
43 if let Some(ref idempotency_id) = signal.idempotency_id
44 && let Some(existing) = state
45 .signal_idempotency
46 .get(idempotency_id)
47 .and_then(|id| state.signals.iter().find(|s| s.id == *id))
48 {
49 return Ok(SignalInsert::Duplicate(existing.clone()));
50 }
51
52 let stored = Signal {
53 id: Uuid::now_v7(),
54 name: signal.name,
55 key: signal.key,
56 payload: signal.payload,
57 idempotency_id: signal.idempotency_id,
58 received_at: Utc::now(),
59 };
60 if let Some(ref idempotency_id) = stored.idempotency_id {
61 state
62 .signal_idempotency
63 .insert(idempotency_id.clone(), stored.id);
64 }
65 state.signals.push(stored.clone());
66 Ok(SignalInsert::Created(stored))
67 })
68 }
69
70 fn list_signals(
71 &self,
72 filter: SignalFilter,
73 page: u32,
74 per_page: u32,
75 ) -> StoreFuture<'_, Page<Signal>> {
76 Box::pin(async move {
77 let state = self.state.read().await;
78 let mut items: Vec<Signal> = state
79 .signals
80 .iter()
81 .filter(|s| filter.name.as_ref().is_none_or(|name| &s.name == name))
82 .filter(|s| filter.key.as_ref().is_none_or(|key| &s.key == key))
83 .cloned()
84 .collect();
85 items.sort_by_key(|s| Reverse((s.received_at, s.id)));
86
87 let total = items.len() as u64;
88 let page = page.max(1);
89 let per_page = per_page.clamp(1, 100);
90 let offset = ((page - 1) * per_page) as usize;
91 let items = items
92 .into_iter()
93 .skip(offset)
94 .take(per_page as usize)
95 .collect();
96
97 Ok(Page {
98 items,
99 total,
100 page,
101 per_page,
102 })
103 })
104 }
105
106 fn list_signals_for_key(
107 &self,
108 name: &str,
109 key: &str,
110 since: DateTime<Utc>,
111 ) -> StoreFuture<'_, Vec<Signal>> {
112 let name = name.to_string();
113 let key = key.to_string();
114 Box::pin(async move {
115 let state = self.state.read().await;
116 let mut items: Vec<Signal> = state
117 .signals
118 .iter()
119 .filter(|s| s.name == name && s.key == key && s.received_at >= since)
120 .cloned()
121 .collect();
122 items.sort_by_key(|s| (s.received_at, s.id));
123 items.truncate(SIGNALS_FOR_KEY_LIMIT);
124 Ok(items)
125 })
126 }
127
128 fn list_signal_waiters(&self, name: &str, key: &str) -> StoreFuture<'_, Vec<Step>> {
129 let name = name.to_string();
130 let key = key.to_string();
131 Box::pin(async move {
132 let state = self.state.read().await;
133 let mut steps: Vec<Step> = state
134 .steps
135 .values()
136 .filter(|s| is_waiting_signal_step(s) && step_waits_for(s, &name, &key))
137 .filter(|s| {
138 state.runs.get(&s.run_id).is_some_and(|run| {
139 matches!(
140 run.status.state,
141 RunStatus::Sleeping | RunStatus::Running | RunStatus::Pending
142 )
143 })
144 })
145 .cloned()
146 .collect();
147 steps.sort_by_key(|s| (s.created_at, s.id));
148 Ok(steps)
149 })
150 }
151
152 fn resolve_signal_step(
153 &self,
154 step_id: Uuid,
155 output: Value,
156 ) -> StoreFuture<'_, SignalStepResolution> {
157 Box::pin(async move {
158 let mut state = self.state.write().await;
159 let now = Utc::now();
160
161 let step = state
162 .steps
163 .get_mut(&step_id)
164 .ok_or(StoreError::StepNotFound(step_id))?;
165 if !is_waiting_signal_step(step) {
166 return Ok(SignalStepResolution::NotWaiting {
167 output: step.output.clone(),
168 });
169 }
170 step.status.state = StepStatus::Completed;
171 step.output = Some(output);
172 step.completed_at = Some(now);
173 step.duration_ms = step
174 .started_at
175 .map(|started| (now - started).num_milliseconds().max(0) as u64)
176 .unwrap_or(0);
177 step.updated_at = now;
178 let run_id = step.run_id;
179
180 let run = state
181 .runs
182 .get_mut(&run_id)
183 .ok_or(StoreError::RunNotFound(run_id))?;
184 let run_resumed = run.status.state == RunStatus::Sleeping;
185 if run_resumed {
186 run.status.state = RunStatus::Pending;
187 run.scheduled_at = None;
188 run.updated_at = now;
189 }
190
191 Ok(SignalStepResolution::Resolved {
192 run_id,
193 run_resumed,
194 })
195 })
196 }
197
198 fn suspend_run_on_signal(
199 &self,
200 run_id: Uuid,
201 step_id: Uuid,
202 deadline_at: DateTime<Utc>,
203 ) -> StoreFuture<'_, bool> {
204 Box::pin(async move {
205 let mut state = self.state.write().await;
206 let now = Utc::now();
207
208 let waiting = state
209 .steps
210 .get(&step_id)
211 .map(is_waiting_signal_step)
212 .ok_or(StoreError::StepNotFound(step_id))?;
213
214 let run = state
215 .runs
216 .get_mut(&run_id)
217 .ok_or(StoreError::RunNotFound(run_id))?;
218 if run.status.state != RunStatus::Running {
219 return Err(StoreError::InvalidTransition {
220 from: run.status.state,
221 to: RunStatus::Sleeping,
222 });
223 }
224 run.status.state = RunStatus::Sleeping;
225 run.worker_id = None;
226 run.lease_expires_at = None;
227 run.scheduled_at = Some(if waiting { deadline_at } else { now });
230 run.updated_at = now;
231
232 Ok(waiting)
233 })
234 }
235
236 fn purge_signals(&self, before: DateTime<Utc>) -> StoreFuture<'_, u64> {
237 Box::pin(async move {
238 let mut state = self.state.write().await;
239 let initial = state.signals.len();
240 state.signals.retain(|s| s.received_at >= before);
241 let removed = (initial - state.signals.len()) as u64;
242
243 let kept: Vec<Uuid> = state.signals.iter().map(|s| s.id).collect();
244 state
245 .signal_idempotency
246 .retain(|_, signal_id| kept.contains(signal_id));
247
248 Ok(removed)
249 })
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use chrono::TimeDelta;
256 use serde_json::json;
257
258 use super::*;
259 use crate::entities::{NewStep, StepUpdate, step_trace_id};
260 use crate::memory::tests::new_run_req;
261 use crate::store::RunStore;
262
263 fn new_signal(name: &str, key: &str, idempotency_id: Option<&str>) -> NewSignal {
264 NewSignal {
265 name: name.to_string(),
266 key: key.to_string(),
267 payload: json!({"status": "success"}),
268 idempotency_id: idempotency_id.map(str::to_string),
269 }
270 }
271
272 async fn waiting_step(store: &InMemoryStore, name: &str, key: &str) -> Step {
274 let run = store
275 .create_run(new_run_req("wait"))
276 .await
277 .unwrap()
278 .into_run();
279 store
280 .update_run_status(run.id, RunStatus::Running)
281 .await
282 .unwrap();
283 let step = store
284 .create_step(NewStep {
285 run_id: run.id,
286 trace_id: step_trace_id(run.id, "wait-ci", 0),
287 name: "wait-ci".to_string(),
288 kind: StepKind::Signal,
289 position: 0,
290 input: Some(json!({"name": name, "key": key, "schema": {}})),
291 is_error_handler: false,
292 })
293 .await
294 .unwrap();
295 store
296 .update_step(
297 step.id,
298 StepUpdate {
299 status: Some(StepStatus::Running),
300 started_at: Some(Utc::now()),
301 ..StepUpdate::default()
302 },
303 )
304 .await
305 .unwrap();
306 store.get_step(step.id).await.unwrap().unwrap()
307 }
308
309 #[tokio::test]
310 async fn insert_signal_is_idempotent() {
311 let store = InMemoryStore::new();
312 let first = store
313 .insert_signal(new_signal("demo.done", "k1", Some("d-1")))
314 .await
315 .unwrap();
316 assert!(!first.is_duplicate());
317
318 let second = store
319 .insert_signal(new_signal("demo.done", "k2", Some("d-1")))
320 .await
321 .unwrap();
322 assert!(second.is_duplicate());
323 assert_eq!(second.signal(), first.signal());
324
325 let page = store
326 .list_signals(SignalFilter::default(), 1, 20)
327 .await
328 .unwrap();
329 assert_eq!(page.total, 1);
330 }
331
332 #[tokio::test]
333 async fn insert_signal_without_idempotency_id_always_stores() {
334 let store = InMemoryStore::new();
335 for _ in 0..2 {
336 let insert = store
337 .insert_signal(new_signal("demo.done", "k1", None))
338 .await
339 .unwrap();
340 assert!(!insert.is_duplicate());
341 }
342 let page = store
343 .list_signals(SignalFilter::default(), 1, 20)
344 .await
345 .unwrap();
346 assert_eq!(page.total, 2);
347 }
348
349 #[tokio::test]
350 async fn list_signals_filters_and_orders_newest_first() {
351 let store = InMemoryStore::new();
352 store
353 .insert_signal(new_signal("demo.done", "k1", None))
354 .await
355 .unwrap();
356 let newest = store
357 .insert_signal(new_signal("demo.done", "k2", None))
358 .await
359 .unwrap();
360 store
361 .insert_signal(new_signal("other", "k1", None))
362 .await
363 .unwrap();
364
365 let page = store
366 .list_signals(
367 SignalFilter {
368 name: Some("demo.done".to_string()),
369 key: None,
370 },
371 1,
372 20,
373 )
374 .await
375 .unwrap();
376 assert_eq!(page.total, 2);
377 assert_eq!(page.items[0].id, newest.signal().id);
378
379 let page = store
380 .list_signals(
381 SignalFilter {
382 name: Some("demo.done".to_string()),
383 key: Some("k1".to_string()),
384 },
385 1,
386 20,
387 )
388 .await
389 .unwrap();
390 assert_eq!(page.total, 1);
391 assert_eq!(page.items[0].key, "k1");
392 }
393
394 #[tokio::test]
395 async fn list_signals_for_key_respects_since_and_orders_oldest_first() {
396 let store = InMemoryStore::new();
397 let first = store
398 .insert_signal(new_signal("demo.done", "k1", None))
399 .await
400 .unwrap();
401 let second = store
402 .insert_signal(new_signal("demo.done", "k1", None))
403 .await
404 .unwrap();
405 store
406 .insert_signal(new_signal("demo.done", "k2", None))
407 .await
408 .unwrap();
409
410 let found = store
411 .list_signals_for_key("demo.done", "k1", first.signal().received_at)
412 .await
413 .unwrap();
414 let ids: Vec<Uuid> = found.iter().map(|s| s.id).collect();
415 assert_eq!(ids, vec![first.signal().id, second.signal().id]);
416
417 let later = store
418 .list_signals_for_key("demo.done", "k1", Utc::now() + TimeDelta::seconds(5))
419 .await
420 .unwrap();
421 assert!(later.is_empty());
422 }
423
424 #[tokio::test]
425 async fn list_signal_waiters_matches_name_and_key() {
426 let store = InMemoryStore::new();
427 let step = waiting_step(&store, "demo.done", "k1").await;
428 waiting_step(&store, "demo.done", "k2").await;
429
430 let waiters = store.list_signal_waiters("demo.done", "k1").await.unwrap();
431 assert_eq!(waiters.len(), 1);
432 assert_eq!(waiters[0].id, step.id);
433 }
434
435 #[tokio::test]
436 async fn list_signal_waiters_excludes_cancelled_runs() {
437 let store = InMemoryStore::new();
438 let step = waiting_step(&store, "demo.done", "k1").await;
439 store
440 .update_run_status(step.run_id, RunStatus::Cancelled)
441 .await
442 .unwrap();
443
444 let waiters = store.list_signal_waiters("demo.done", "k1").await.unwrap();
445 assert!(waiters.is_empty());
446 }
447
448 #[tokio::test]
449 async fn resolve_signal_step_resumes_sleeping_run() {
450 let store = InMemoryStore::new();
451 let step = waiting_step(&store, "demo.done", "k1").await;
452 let deadline = Utc::now() + TimeDelta::hours(1);
453 assert!(
454 store
455 .suspend_run_on_signal(step.run_id, step.id, deadline)
456 .await
457 .unwrap()
458 );
459
460 let resolution = store
461 .resolve_signal_step(step.id, json!({"timed_out": false}))
462 .await
463 .unwrap();
464 assert_eq!(
465 resolution,
466 SignalStepResolution::Resolved {
467 run_id: step.run_id,
468 run_resumed: true,
469 }
470 );
471
472 let run = store.get_run(step.run_id).await.unwrap().unwrap();
473 assert_eq!(run.status.state, RunStatus::Pending);
474 assert!(run.scheduled_at.is_none());
475 let step = store.get_step(step.id).await.unwrap().unwrap();
476 assert_eq!(step.status.state, StepStatus::Completed);
477 assert_eq!(step.output, Some(json!({"timed_out": false})));
478 }
479
480 #[tokio::test]
481 async fn resolve_signal_step_leaves_running_run_alone() {
482 let store = InMemoryStore::new();
483 let step = waiting_step(&store, "demo.done", "k1").await;
484
485 let resolution = store
486 .resolve_signal_step(step.id, json!({"timed_out": false}))
487 .await
488 .unwrap();
489 assert_eq!(
490 resolution,
491 SignalStepResolution::Resolved {
492 run_id: step.run_id,
493 run_resumed: false,
494 }
495 );
496 let run = store.get_run(step.run_id).await.unwrap().unwrap();
497 assert_eq!(run.status.state, RunStatus::Running);
498 }
499
500 #[tokio::test]
501 async fn resolve_signal_step_twice_returns_first_output() {
502 let store = InMemoryStore::new();
503 let step = waiting_step(&store, "demo.done", "k1").await;
504 store
505 .resolve_signal_step(step.id, json!({"first": true}))
506 .await
507 .unwrap();
508
509 let second = store
510 .resolve_signal_step(step.id, json!({"first": false}))
511 .await
512 .unwrap();
513 assert_eq!(
514 second,
515 SignalStepResolution::NotWaiting {
516 output: Some(json!({"first": true})),
517 }
518 );
519 }
520
521 #[tokio::test]
522 async fn resolve_signal_step_unknown_step_errors() {
523 let store = InMemoryStore::new();
524 let err = store
525 .resolve_signal_step(Uuid::now_v7(), json!({}))
526 .await
527 .unwrap_err();
528 assert!(matches!(err, StoreError::StepNotFound(_)));
529 }
530
531 #[tokio::test]
532 async fn suspend_run_on_signal_after_resolution_schedules_now() {
533 let store = InMemoryStore::new();
534 let step = waiting_step(&store, "demo.done", "k1").await;
535 store
536 .resolve_signal_step(step.id, json!({"timed_out": false}))
537 .await
538 .unwrap();
539
540 let deadline = Utc::now() + TimeDelta::hours(1);
541 let waiting = store
542 .suspend_run_on_signal(step.run_id, step.id, deadline)
543 .await
544 .unwrap();
545 assert!(!waiting);
546
547 let run = store.get_run(step.run_id).await.unwrap().unwrap();
548 assert_eq!(run.status.state, RunStatus::Sleeping);
549 assert!(run.scheduled_at.is_some_and(|at| at < deadline));
550 }
551
552 #[tokio::test]
553 async fn suspend_run_on_signal_rejects_non_running_run() {
554 let store = InMemoryStore::new();
555 let step = waiting_step(&store, "demo.done", "k1").await;
556 store
557 .update_run_status(step.run_id, RunStatus::Cancelled)
558 .await
559 .unwrap();
560
561 let err = store
562 .suspend_run_on_signal(step.run_id, step.id, Utc::now())
563 .await
564 .unwrap_err();
565 assert!(matches!(err, StoreError::InvalidTransition { .. }));
566 }
567
568 #[tokio::test]
569 async fn purge_signals_removes_old_signals_and_frees_idempotency_ids() {
570 let store = InMemoryStore::new();
571 store
572 .insert_signal(new_signal("demo.done", "k1", Some("d-1")))
573 .await
574 .unwrap();
575
576 let purged = store
577 .purge_signals(Utc::now() + TimeDelta::seconds(1))
578 .await
579 .unwrap();
580 assert_eq!(purged, 1);
581
582 let again = store
583 .insert_signal(new_signal("demo.done", "k1", Some("d-1")))
584 .await
585 .unwrap();
586 assert!(!again.is_duplicate());
587 }
588}