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