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
142 | RunStatus::Running
143 | RunStatus::Pending
144 | RunStatus::Paused
145 )
146 })
147 })
148 .cloned()
149 .collect();
150 steps.sort_by_key(|s| (s.created_at, s.id));
151 Ok(steps)
152 })
153 }
154
155 fn resolve_signal_step(
156 &self,
157 step_id: Uuid,
158 output: Value,
159 ) -> StoreFuture<'_, SignalStepResolution> {
160 Box::pin(async move {
161 let mut state = self.state.write().await;
162 let now = Utc::now();
163
164 let step = state
165 .steps
166 .get_mut(&step_id)
167 .ok_or(StoreError::StepNotFound(step_id))?;
168 if !is_waiting_signal_step(step) {
169 return Ok(SignalStepResolution::NotWaiting {
170 output: step.output.clone(),
171 });
172 }
173 step.status.state = StepStatus::Completed;
174 step.output = Some(output);
175 step.completed_at = Some(now);
176 step.duration_ms = step
177 .started_at
178 .map(|started| (now - started).num_milliseconds().max(0) as u64)
179 .unwrap_or(0);
180 step.updated_at = now;
181 let run_id = step.run_id;
182
183 let run = state
184 .runs
185 .get_mut(&run_id)
186 .ok_or(StoreError::RunNotFound(run_id))?;
187 let run_resumed = run.status.state == RunStatus::Sleeping;
188 if run_resumed {
189 run.status.state = RunStatus::Pending;
190 run.scheduled_at = None;
191 run.capacity_wait_kind = None;
192 run.updated_at = now;
193 } else if run.status.state == RunStatus::Paused
194 && run.resume_status == Some(RunStatus::Sleeping)
195 {
196 run.resume_status = Some(RunStatus::Pending);
199 run.scheduled_at = None;
200 run.capacity_wait_kind = None;
201 run.updated_at = now;
202 }
203
204 Ok(SignalStepResolution::Resolved {
205 run_id,
206 run_resumed,
207 })
208 })
209 }
210
211 fn suspend_run_on_signal(
212 &self,
213 run_id: Uuid,
214 step_id: Uuid,
215 deadline_at: DateTime<Utc>,
216 ) -> StoreFuture<'_, bool> {
217 Box::pin(async move {
218 let mut state = self.state.write().await;
219 let now = Utc::now();
220
221 let waiting = state
222 .steps
223 .get(&step_id)
224 .map(is_waiting_signal_step)
225 .ok_or(StoreError::StepNotFound(step_id))?;
226
227 let run = state
228 .runs
229 .get_mut(&run_id)
230 .ok_or(StoreError::RunNotFound(run_id))?;
231 if run.status.state != RunStatus::Running {
232 return Err(StoreError::InvalidTransition {
233 from: run.status.state,
234 to: RunStatus::Sleeping,
235 });
236 }
237 run.status.state = RunStatus::Sleeping;
238 run.worker_id = None;
239 run.capacity_wait_kind = None;
240 run.lease_expires_at = None;
241 run.scheduled_at = Some(if waiting { deadline_at } else { now });
244 run.updated_at = now;
245
246 Ok(waiting)
247 })
248 }
249
250 fn purge_signals(&self, before: DateTime<Utc>) -> StoreFuture<'_, u64> {
251 Box::pin(async move {
252 let mut state = self.state.write().await;
253 let initial = state.signals.len();
254 state.signals.retain(|s| s.received_at >= before);
255 let removed = (initial - state.signals.len()) as u64;
256
257 let kept: Vec<Uuid> = state.signals.iter().map(|s| s.id).collect();
258 state
259 .signal_idempotency
260 .retain(|_, signal_id| kept.contains(signal_id));
261
262 Ok(removed)
263 })
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use chrono::TimeDelta;
270 use serde_json::json;
271
272 use super::*;
273 use crate::entities::{NewStep, StepUpdate, step_trace_id};
274 use crate::memory::tests::new_run_req;
275 use crate::store::RunStore;
276
277 fn new_signal(name: &str, key: &str, idempotency_id: Option<&str>) -> NewSignal {
278 NewSignal {
279 name: name.to_string(),
280 key: key.to_string(),
281 payload: json!({"status": "success"}),
282 idempotency_id: idempotency_id.map(str::to_string),
283 }
284 }
285
286 async fn waiting_step(store: &InMemoryStore, name: &str, key: &str) -> Step {
288 let run = store
289 .create_run(new_run_req("wait"))
290 .await
291 .unwrap()
292 .into_run();
293 store
294 .update_run_status(run.id, RunStatus::Running)
295 .await
296 .unwrap();
297 let step = store
298 .create_step(NewStep {
299 run_id: run.id,
300 trace_id: step_trace_id(run.id, "wait-ci", 0),
301 name: "wait-ci".to_string(),
302 kind: StepKind::Signal,
303 position: 0,
304 input: Some(json!({"name": name, "key": key, "schema": {}})),
305 is_error_handler: false,
306 })
307 .await
308 .unwrap();
309 store
310 .update_step(
311 step.id,
312 StepUpdate {
313 status: Some(StepStatus::Running),
314 started_at: Some(Utc::now()),
315 ..StepUpdate::default()
316 },
317 )
318 .await
319 .unwrap();
320 store.get_step(step.id).await.unwrap().unwrap()
321 }
322
323 #[tokio::test]
324 async fn insert_signal_is_idempotent() {
325 let store = InMemoryStore::new();
326 let first = store
327 .insert_signal(new_signal("demo.done", "k1", Some("d-1")))
328 .await
329 .unwrap();
330 assert!(!first.is_duplicate());
331
332 let second = store
333 .insert_signal(new_signal("demo.done", "k2", Some("d-1")))
334 .await
335 .unwrap();
336 assert!(second.is_duplicate());
337 assert_eq!(second.signal(), first.signal());
338
339 let page = store
340 .list_signals(SignalFilter::default(), 1, 20)
341 .await
342 .unwrap();
343 assert_eq!(page.total, 1);
344 }
345
346 #[tokio::test]
347 async fn insert_signal_without_idempotency_id_always_stores() {
348 let store = InMemoryStore::new();
349 for _ in 0..2 {
350 let insert = store
351 .insert_signal(new_signal("demo.done", "k1", None))
352 .await
353 .unwrap();
354 assert!(!insert.is_duplicate());
355 }
356 let page = store
357 .list_signals(SignalFilter::default(), 1, 20)
358 .await
359 .unwrap();
360 assert_eq!(page.total, 2);
361 }
362
363 #[tokio::test]
364 async fn list_signals_filters_and_orders_newest_first() {
365 let store = InMemoryStore::new();
366 store
367 .insert_signal(new_signal("demo.done", "k1", None))
368 .await
369 .unwrap();
370 let newest = store
371 .insert_signal(new_signal("demo.done", "k2", None))
372 .await
373 .unwrap();
374 store
375 .insert_signal(new_signal("other", "k1", None))
376 .await
377 .unwrap();
378
379 let page = store
380 .list_signals(
381 SignalFilter {
382 name: Some("demo.done".to_string()),
383 key: None,
384 },
385 1,
386 20,
387 )
388 .await
389 .unwrap();
390 assert_eq!(page.total, 2);
391 assert_eq!(page.items[0].id, newest.signal().id);
392
393 let page = store
394 .list_signals(
395 SignalFilter {
396 name: Some("demo.done".to_string()),
397 key: Some("k1".to_string()),
398 },
399 1,
400 20,
401 )
402 .await
403 .unwrap();
404 assert_eq!(page.total, 1);
405 assert_eq!(page.items[0].key, "k1");
406 }
407
408 #[tokio::test]
409 async fn list_signals_for_key_respects_since_and_orders_oldest_first() {
410 let store = InMemoryStore::new();
411 let first = store
412 .insert_signal(new_signal("demo.done", "k1", None))
413 .await
414 .unwrap();
415 let second = store
416 .insert_signal(new_signal("demo.done", "k1", None))
417 .await
418 .unwrap();
419 store
420 .insert_signal(new_signal("demo.done", "k2", None))
421 .await
422 .unwrap();
423
424 let found = store
425 .list_signals_for_key("demo.done", "k1", first.signal().received_at)
426 .await
427 .unwrap();
428 let ids: Vec<Uuid> = found.iter().map(|s| s.id).collect();
429 assert_eq!(ids, vec![first.signal().id, second.signal().id]);
430
431 let later = store
432 .list_signals_for_key("demo.done", "k1", Utc::now() + TimeDelta::seconds(5))
433 .await
434 .unwrap();
435 assert!(later.is_empty());
436 }
437
438 #[tokio::test]
439 async fn list_signal_waiters_matches_name_and_key() {
440 let store = InMemoryStore::new();
441 let step = waiting_step(&store, "demo.done", "k1").await;
442 waiting_step(&store, "demo.done", "k2").await;
443
444 let waiters = store.list_signal_waiters("demo.done", "k1").await.unwrap();
445 assert_eq!(waiters.len(), 1);
446 assert_eq!(waiters[0].id, step.id);
447 }
448
449 #[tokio::test]
450 async fn list_signal_waiters_excludes_cancelled_runs() {
451 let store = InMemoryStore::new();
452 let step = waiting_step(&store, "demo.done", "k1").await;
453 store
454 .update_run_status(step.run_id, RunStatus::Cancelled)
455 .await
456 .unwrap();
457
458 let waiters = store.list_signal_waiters("demo.done", "k1").await.unwrap();
459 assert!(waiters.is_empty());
460 }
461
462 #[tokio::test]
463 async fn resolve_signal_step_resumes_sleeping_run() {
464 let store = InMemoryStore::new();
465 let step = waiting_step(&store, "demo.done", "k1").await;
466 let deadline = Utc::now() + TimeDelta::hours(1);
467 assert!(
468 store
469 .suspend_run_on_signal(step.run_id, step.id, deadline)
470 .await
471 .unwrap()
472 );
473
474 let resolution = store
475 .resolve_signal_step(step.id, json!({"timed_out": false}))
476 .await
477 .unwrap();
478 assert_eq!(
479 resolution,
480 SignalStepResolution::Resolved {
481 run_id: step.run_id,
482 run_resumed: true,
483 }
484 );
485
486 let run = store.get_run(step.run_id).await.unwrap().unwrap();
487 assert_eq!(run.status.state, RunStatus::Pending);
488 assert!(run.scheduled_at.is_none());
489 let step = store.get_step(step.id).await.unwrap().unwrap();
490 assert_eq!(step.status.state, StepStatus::Completed);
491 assert_eq!(step.output, Some(json!({"timed_out": false})));
492 }
493
494 #[tokio::test]
495 async fn resolve_signal_step_leaves_running_run_alone() {
496 let store = InMemoryStore::new();
497 let step = waiting_step(&store, "demo.done", "k1").await;
498
499 let resolution = store
500 .resolve_signal_step(step.id, json!({"timed_out": false}))
501 .await
502 .unwrap();
503 assert_eq!(
504 resolution,
505 SignalStepResolution::Resolved {
506 run_id: step.run_id,
507 run_resumed: false,
508 }
509 );
510 let run = store.get_run(step.run_id).await.unwrap().unwrap();
511 assert_eq!(run.status.state, RunStatus::Running);
512 }
513
514 #[tokio::test]
515 async fn resolve_signal_step_twice_returns_first_output() {
516 let store = InMemoryStore::new();
517 let step = waiting_step(&store, "demo.done", "k1").await;
518 store
519 .resolve_signal_step(step.id, json!({"first": true}))
520 .await
521 .unwrap();
522
523 let second = store
524 .resolve_signal_step(step.id, json!({"first": false}))
525 .await
526 .unwrap();
527 assert_eq!(
528 second,
529 SignalStepResolution::NotWaiting {
530 output: Some(json!({"first": true})),
531 }
532 );
533 }
534
535 #[tokio::test]
536 async fn resolve_signal_step_unknown_step_errors() {
537 let store = InMemoryStore::new();
538 let err = store
539 .resolve_signal_step(Uuid::now_v7(), json!({}))
540 .await
541 .unwrap_err();
542 assert!(matches!(err, StoreError::StepNotFound(_)));
543 }
544
545 #[tokio::test]
546 async fn suspend_run_on_signal_after_resolution_schedules_now() {
547 let store = InMemoryStore::new();
548 let step = waiting_step(&store, "demo.done", "k1").await;
549 store
550 .resolve_signal_step(step.id, json!({"timed_out": false}))
551 .await
552 .unwrap();
553
554 let deadline = Utc::now() + TimeDelta::hours(1);
555 let waiting = store
556 .suspend_run_on_signal(step.run_id, step.id, deadline)
557 .await
558 .unwrap();
559 assert!(!waiting);
560
561 let run = store.get_run(step.run_id).await.unwrap().unwrap();
562 assert_eq!(run.status.state, RunStatus::Sleeping);
563 assert!(run.scheduled_at.is_some_and(|at| at < deadline));
564 }
565
566 #[tokio::test]
567 async fn suspend_run_on_signal_rejects_non_running_run() {
568 let store = InMemoryStore::new();
569 let step = waiting_step(&store, "demo.done", "k1").await;
570 store
571 .update_run_status(step.run_id, RunStatus::Cancelled)
572 .await
573 .unwrap();
574
575 let err = store
576 .suspend_run_on_signal(step.run_id, step.id, Utc::now())
577 .await
578 .unwrap_err();
579 assert!(matches!(err, StoreError::InvalidTransition { .. }));
580 }
581
582 #[tokio::test]
583 async fn purge_signals_removes_old_signals_and_frees_idempotency_ids() {
584 let store = InMemoryStore::new();
585 store
586 .insert_signal(new_signal("demo.done", "k1", Some("d-1")))
587 .await
588 .unwrap();
589
590 let purged = store
591 .purge_signals(Utc::now() + TimeDelta::seconds(1))
592 .await
593 .unwrap();
594 assert_eq!(purged, 1);
595
596 let again = store
597 .insert_signal(new_signal("demo.done", "k1", Some("d-1")))
598 .await
599 .unwrap();
600 assert!(!again.is_duplicate());
601 }
602}