1use std::sync::Arc;
2
3use async_trait::async_trait;
4use tokio::sync::OnceCell;
5
6use camel_api::{
7 CamelError, MetricsCollector, RuntimeCommand, RuntimeCommandBus, RuntimeCommandResult,
8 RuntimeQuery, RuntimeQueryBus, RuntimeQueryResult,
9};
10
11use crate::lifecycle::application::commands::{
12 CommandDeps, execute_command, handle_register_internal,
13};
14use crate::lifecycle::application::ports::RouteRegistrationPort;
15use crate::lifecycle::application::ports::{
16 CommandDedupPort, EventPublisherPort, InFlightCountResult, ProjectionStorePort,
17 RouteRepositoryPort, RuntimeExecutionPort, RuntimeUnitOfWorkPort,
18};
19use crate::lifecycle::application::queries::{QueryDeps, execute_query};
20use crate::lifecycle::application::route_definition::RouteDefinition;
21use crate::lifecycle::domain::DomainError;
22use camel_component_api::HealthCheckRegistry as HealthCheckRegistryTrait;
23
24impl From<InFlightCountResult> for RuntimeQueryResult {
25 fn from(r: InFlightCountResult) -> Self {
26 match r {
27 InFlightCountResult::InFlightCount { route_id, count } => {
28 RuntimeQueryResult::InFlightCount { route_id, count }
29 }
30 InFlightCountResult::RouteNotFound { route_id } => {
31 RuntimeQueryResult::RouteNotFound { route_id }
32 }
33 }
34 }
35}
36
37pub struct RuntimeBus {
38 repo: Arc<dyn RouteRepositoryPort>,
39 projections: Arc<dyn ProjectionStorePort>,
40 events: Arc<dyn EventPublisherPort>,
41 dedup: Arc<dyn CommandDedupPort>,
42 uow: Option<Arc<dyn RuntimeUnitOfWorkPort>>,
43 execution: Option<Arc<dyn RuntimeExecutionPort>>,
44 health_registry: Option<Arc<dyn HealthCheckRegistryTrait>>,
45 metrics: Option<Arc<dyn MetricsCollector>>,
46 journal_recovered_once: OnceCell<()>,
47}
48
49impl RuntimeBus {
50 pub fn new(
51 repo: Arc<dyn RouteRepositoryPort>,
52 projections: Arc<dyn ProjectionStorePort>,
53 events: Arc<dyn EventPublisherPort>,
54 dedup: Arc<dyn CommandDedupPort>,
55 ) -> Self {
56 Self {
57 repo,
58 projections,
59 events,
60 dedup,
61 uow: None,
62 execution: None,
63 health_registry: None,
64 metrics: None,
65 journal_recovered_once: OnceCell::new(),
66 }
67 }
68
69 pub fn with_uow(mut self, uow: Arc<dyn RuntimeUnitOfWorkPort>) -> Self {
70 self.uow = Some(uow);
71 self
72 }
73
74 pub fn with_execution(mut self, execution: Arc<dyn RuntimeExecutionPort>) -> Self {
75 self.execution = Some(execution);
76 self
77 }
78
79 pub fn with_health_registry(
80 mut self,
81 health_registry: Arc<crate::health_registry::HealthCheckRegistry>,
82 ) -> Self {
83 self.health_registry = Some(health_registry);
84 self
85 }
86
87 pub fn with_metrics(mut self, metrics: Arc<dyn MetricsCollector>) -> Self {
91 self.metrics = Some(metrics);
92 self
93 }
94
95 pub fn repo(&self) -> &Arc<dyn RouteRepositoryPort> {
96 &self.repo
97 }
98
99 pub async fn reconcile_transient_states(&self) -> Result<(), CamelError> {
103 self.ensure_journal_recovered().await?;
104 let deps = self.deps();
105 crate::lifecycle::application::commands::reconcile_transient_states(&deps).await
106 }
107
108 pub(crate) async fn register_aggregate_only(&self, route_id: String) -> Result<(), CamelError> {
109 self.ensure_journal_recovered().await?;
110 let deps = self.deps();
111 if deps.repo.load(&route_id).await?.is_some() {
112 return Err(CamelError::RouteError(format!(
113 "route '{route_id}' already registered"
114 )));
115 }
116 let (aggregate, events) =
117 crate::lifecycle::domain::RouteRuntimeAggregate::register(route_id.clone());
118 if let Some(uow) = &deps.uow {
119 uow.persist_upsert(
120 aggregate.clone(),
121 None,
122 crate::lifecycle::application::commands::project_from_aggregate(&aggregate),
123 &events,
124 )
125 .await?;
126 } else {
127 deps.repo.save(aggregate.clone()).await?;
128 if let Some(primary_error) =
129 crate::lifecycle::application::commands::upsert_projection_with_reconciliation(
130 &*deps.projections,
131 crate::lifecycle::application::commands::project_from_aggregate(&aggregate),
132 )
133 .await?
134 {
135 deps.events.publish(&events).await?;
136 return Err(CamelError::RouteError(format!(
137 "post-effect reconciliation recovered after runtime persistence error: {primary_error}"
138 )));
139 }
140 deps.events.publish(&events).await?;
141 }
142 Ok(())
143 }
144
145 fn deps(&self) -> CommandDeps {
146 CommandDeps {
147 repo: Arc::clone(&self.repo),
148 projections: Arc::clone(&self.projections),
149 events: Arc::clone(&self.events),
150 uow: self.uow.clone(),
151 execution: self.execution.clone(),
152 health_registry: self.health_registry.clone(),
153 }
154 }
155
156 fn query_deps(&self) -> QueryDeps {
157 QueryDeps {
158 projections: Arc::clone(&self.projections),
159 }
160 }
161
162 async fn ensure_journal_recovered(&self) -> Result<(), CamelError> {
163 let Some(uow) = &self.uow else {
164 return Ok(());
165 };
166
167 self.journal_recovered_once
168 .get_or_try_init(|| async {
169 uow.recover_from_journal().await?;
170 Ok::<(), CamelError>(())
171 })
172 .await?;
173 Ok(())
174 }
175}
176
177#[async_trait]
178impl RuntimeCommandBus for RuntimeBus {
179 async fn execute(&self, cmd: RuntimeCommand) -> Result<RuntimeCommandResult, CamelError> {
180 if let RuntimeCommand::ReloadTlsCerts {
184 scheme, host, port, ..
185 } = &cmd
186 {
187 let registry = camel_component_api::tls_source::TlsReloadRegistry::global();
188 match registry.find(scheme, host, *port) {
189 Some(handler) => {
190 handler.reload().await?;
191 if let Some(metrics) = &self.metrics {
193 metrics.record_counter(
194 "tls_reloads_total",
195 1.0,
196 &[("scheme", scheme), ("host", host)],
197 );
198 }
199 return Ok(RuntimeCommandResult::TlsCertsReloaded {
200 scheme: scheme.clone(),
201 host: host.clone(),
202 port: *port,
203 });
204 }
205 None => {
206 return Err(CamelError::Config(format!(
207 "no TLS server found for {scheme}://{host}:{port}"
208 )));
209 }
210 }
211 }
212 if let RuntimeCommand::ReloadTemplates { route_id, .. } = &cmd {
221 let route_id = route_id.clone();
222 camel_component_api::template_reload::TemplateReloadRegistry::global()
223 .reload_route(&route_id)
224 .await?;
225 if let Some(metrics) = &self.metrics {
227 metrics.record_counter(
228 "template_reloads_total",
229 1.0,
230 &[("route_id", route_id.as_str())],
231 );
232 }
233 return Ok(RuntimeCommandResult::TemplatesReloaded { route_id });
234 }
235 self.ensure_journal_recovered().await?;
238 let command_id = cmd.command_id().to_string();
239 if !self.dedup.first_seen(&command_id).await? {
240 return Ok(RuntimeCommandResult::Duplicate { command_id });
241 }
242 let deps = self.deps();
243 match execute_command(&deps, cmd).await {
244 Ok(result) => Ok(result),
245 Err(err) => {
246 let _ = self.dedup.forget_seen(&command_id).await;
247 Err(err)
248 }
249 }
250 }
251}
252
253#[async_trait]
254impl RuntimeQueryBus for RuntimeBus {
255 async fn ask(&self, query: RuntimeQuery) -> Result<RuntimeQueryResult, CamelError> {
256 self.ensure_journal_recovered().await?;
257
258 match query {
259 RuntimeQuery::InFlightCount { route_id } => {
260 if let Some(execution) = &self.execution {
261 execution
262 .in_flight_count(&route_id)
263 .await
264 .map(|r| r.into())
265 .map_err(Into::into)
266 } else {
267 Ok(RuntimeQueryResult::RouteNotFound { route_id })
268 }
269 }
270 other => {
271 let deps = self.query_deps();
272 execute_query(&deps, other).await
273 }
274 }
275 }
276}
277
278#[async_trait]
279impl RouteRegistrationPort for RuntimeBus {
280 async fn register_route(&self, def: RouteDefinition) -> Result<(), DomainError> {
281 self.ensure_journal_recovered()
282 .await
283 .map_err(|e| DomainError::InvalidState(e.to_string()))?;
284 let deps = self.deps();
285 handle_register_internal(&deps, def)
286 .await
287 .map(|_| ())
288 .map_err(|e| match e {
289 CamelError::RouteError(msg) => DomainError::InvalidState(msg),
290 other => DomainError::InvalidState(other.to_string()),
291 })
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use crate::lifecycle::domain::DomainError;
298
299 use super::*;
300 use std::collections::{HashMap, HashSet};
301 use std::sync::Mutex;
302
303 use crate::lifecycle::application::ports::RouteRegistrationPort as InternalRuntimeCommandBus;
304 use crate::lifecycle::application::ports::RouteStatusProjection;
305 use crate::lifecycle::application::route_definition::RouteDefinition;
306 use crate::lifecycle::domain::{RouteRuntimeAggregate, RuntimeEvent};
307
308 #[derive(Clone, Default)]
309 struct InMemoryTestRepo {
310 routes: Arc<Mutex<HashMap<String, RouteRuntimeAggregate>>>,
311 }
312
313 #[async_trait]
314 impl RouteRepositoryPort for InMemoryTestRepo {
315 async fn load(&self, route_id: &str) -> Result<Option<RouteRuntimeAggregate>, DomainError> {
316 Ok(self
317 .routes
318 .lock()
319 .expect("lock test routes")
320 .get(route_id)
321 .cloned())
322 }
323
324 async fn save(&self, aggregate: RouteRuntimeAggregate) -> Result<(), DomainError> {
325 self.routes
326 .lock()
327 .expect("lock test routes")
328 .insert(aggregate.route_id().to_string(), aggregate);
329 Ok(())
330 }
331
332 async fn save_if_version(
333 &self,
334 aggregate: RouteRuntimeAggregate,
335 expected_version: u64,
336 ) -> Result<(), DomainError> {
337 let route_id = aggregate.route_id().to_string();
338 let mut routes = self.routes.lock().expect("lock test routes");
339 let current = routes.get(&route_id).ok_or_else(|| {
340 DomainError::InvalidState(format!(
341 "optimistic lock conflict for route '{route_id}': route not found"
342 ))
343 })?;
344
345 if current.version() != expected_version {
346 return Err(DomainError::InvalidState(format!(
347 "optimistic lock conflict for route '{route_id}': expected version {expected_version}, actual {}",
348 current.version()
349 )));
350 }
351
352 routes.insert(route_id, aggregate);
353 Ok(())
354 }
355
356 async fn delete(&self, route_id: &str) -> Result<(), DomainError> {
357 self.routes
358 .lock()
359 .expect("lock test routes")
360 .remove(route_id);
361 Ok(())
362 }
363 }
364
365 #[derive(Clone, Default)]
366 struct InMemoryTestProjectionStore {
367 statuses: Arc<Mutex<HashMap<String, RouteStatusProjection>>>,
368 }
369
370 #[async_trait]
371 impl ProjectionStorePort for InMemoryTestProjectionStore {
372 async fn upsert_status(&self, status: RouteStatusProjection) -> Result<(), DomainError> {
373 self.statuses
374 .lock()
375 .expect("lock test statuses")
376 .insert(status.route_id.clone(), status);
377 Ok(())
378 }
379
380 async fn get_status(
381 &self,
382 route_id: &str,
383 ) -> Result<Option<RouteStatusProjection>, DomainError> {
384 Ok(self
385 .statuses
386 .lock()
387 .expect("lock test statuses")
388 .get(route_id)
389 .cloned())
390 }
391
392 async fn list_statuses(&self) -> Result<Vec<RouteStatusProjection>, DomainError> {
393 Ok(self
394 .statuses
395 .lock()
396 .expect("lock test statuses")
397 .values()
398 .cloned()
399 .collect())
400 }
401
402 async fn remove_status(&self, route_id: &str) -> Result<(), DomainError> {
403 self.statuses
404 .lock()
405 .expect("lock test statuses")
406 .remove(route_id);
407 Ok(())
408 }
409 }
410
411 #[derive(Clone, Default)]
412 struct InMemoryTestEventPublisher;
413
414 #[async_trait]
415 impl EventPublisherPort for InMemoryTestEventPublisher {
416 async fn publish(&self, _events: &[RuntimeEvent]) -> Result<(), DomainError> {
417 Ok(())
418 }
419 }
420
421 #[derive(Clone, Default)]
422 struct InMemoryTestDedup {
423 seen: Arc<Mutex<HashSet<String>>>,
424 }
425
426 #[derive(Clone, Default)]
427 struct InspectableDedup {
428 seen: Arc<Mutex<HashSet<String>>>,
429 forget_calls: Arc<Mutex<u32>>,
430 }
431
432 #[async_trait]
433 impl CommandDedupPort for InMemoryTestDedup {
434 async fn first_seen(&self, command_id: &str) -> Result<bool, DomainError> {
435 let mut seen = self.seen.lock().expect("lock dedup set");
436 Ok(seen.insert(command_id.to_string()))
437 }
438
439 async fn forget_seen(&self, command_id: &str) -> Result<(), DomainError> {
440 self.seen.lock().expect("lock dedup set").remove(command_id);
441 Ok(())
442 }
443 }
444
445 #[async_trait]
446 impl CommandDedupPort for InspectableDedup {
447 async fn first_seen(&self, command_id: &str) -> Result<bool, DomainError> {
448 let mut seen = self.seen.lock().expect("lock dedup set");
449 Ok(seen.insert(command_id.to_string()))
450 }
451
452 async fn forget_seen(&self, command_id: &str) -> Result<(), DomainError> {
453 self.seen.lock().expect("lock dedup set").remove(command_id);
454 let mut calls = self.forget_calls.lock().expect("forget calls");
455 *calls += 1;
456 Ok(())
457 }
458 }
459
460 fn build_test_runtime_bus() -> RuntimeBus {
461 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
462 let projections: Arc<dyn ProjectionStorePort> =
463 Arc::new(InMemoryTestProjectionStore::default());
464 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
465 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
466 RuntimeBus::new(repo, projections, events, dedup)
467 }
468
469 #[derive(Default)]
470 struct CountingUow {
471 recover_calls: Arc<Mutex<u32>>,
472 }
473
474 #[derive(Default)]
475 struct FailingRecoverUow;
476
477 #[async_trait]
478 impl RuntimeUnitOfWorkPort for CountingUow {
479 async fn persist_upsert(
480 &self,
481 _aggregate: RouteRuntimeAggregate,
482 _expected_version: Option<u64>,
483 _projection: RouteStatusProjection,
484 _events: &[RuntimeEvent],
485 ) -> Result<(), DomainError> {
486 Ok(())
487 }
488
489 async fn persist_delete(
490 &self,
491 _route_id: &str,
492 _events: &[RuntimeEvent],
493 ) -> Result<(), DomainError> {
494 Ok(())
495 }
496
497 async fn recover_from_journal(&self) -> Result<(), DomainError> {
498 let mut calls = self.recover_calls.lock().expect("recover_calls");
499 *calls += 1;
500 Ok(())
501 }
502 }
503
504 #[async_trait]
505 impl RuntimeUnitOfWorkPort for FailingRecoverUow {
506 async fn persist_upsert(
507 &self,
508 _aggregate: RouteRuntimeAggregate,
509 _expected_version: Option<u64>,
510 _projection: RouteStatusProjection,
511 _events: &[RuntimeEvent],
512 ) -> Result<(), DomainError> {
513 Ok(())
514 }
515
516 async fn persist_delete(
517 &self,
518 _route_id: &str,
519 _events: &[RuntimeEvent],
520 ) -> Result<(), DomainError> {
521 Ok(())
522 }
523
524 async fn recover_from_journal(&self) -> Result<(), DomainError> {
525 Err(DomainError::InvalidState("recover failed".into()))
526 }
527 }
528
529 #[derive(Default)]
530 struct InFlightExecutionPort;
531
532 #[async_trait]
533 impl RuntimeExecutionPort for InFlightExecutionPort {
534 async fn register_route(&self, _definition: RouteDefinition) -> Result<(), DomainError> {
535 Ok(())
536 }
537 async fn start_route(&self, _route_id: &str) -> Result<(), DomainError> {
538 Ok(())
539 }
540 async fn stop_route(&self, _route_id: &str) -> Result<(), DomainError> {
541 Ok(())
542 }
543 async fn suspend_route(&self, _route_id: &str) -> Result<(), DomainError> {
544 Ok(())
545 }
546 async fn resume_route(&self, _route_id: &str) -> Result<(), DomainError> {
547 Ok(())
548 }
549 async fn reload_route(&self, _route_id: &str) -> Result<(), DomainError> {
550 Ok(())
551 }
552 async fn remove_route(&self, _route_id: &str) -> Result<(), DomainError> {
553 Ok(())
554 }
555 async fn in_flight_count(
556 &self,
557 route_id: &str,
558 ) -> Result<InFlightCountResult, DomainError> {
559 if route_id == "known" {
560 Ok(InFlightCountResult::InFlightCount {
561 route_id: route_id.to_string(),
562 count: 3,
563 })
564 } else {
565 Ok(InFlightCountResult::RouteNotFound {
566 route_id: route_id.to_string(),
567 })
568 }
569 }
570 }
571
572 #[tokio::test]
573 async fn runtime_bus_implements_internal_command_bus() {
574 let bus = build_test_runtime_bus();
575 let def = RouteDefinition::new("timer:test", vec![]).with_route_id("internal-route");
576 let result = InternalRuntimeCommandBus::register_route(&bus, def).await;
577 assert!(
578 result.is_ok(),
579 "internal bus registration failed: {:?}",
580 result
581 );
582
583 let status = bus
584 .ask(RuntimeQuery::GetRouteStatus {
585 route_id: "internal-route".to_string(),
586 })
587 .await
588 .unwrap();
589 match status {
590 RuntimeQueryResult::RouteStatus { status, .. } => {
591 assert_eq!(status, "Registered");
592 }
593 _ => panic!("unexpected query result"),
594 }
595 }
596
597 #[tokio::test]
598 async fn execute_returns_duplicate_for_replayed_command_id() {
599 use camel_api::runtime::{CanonicalRouteSpec, CanonicalStepSpec, RuntimeCommand};
600
601 let bus = build_test_runtime_bus();
602
603 let mut spec = CanonicalRouteSpec::new("dup-route", "timer:tick");
604 spec.steps = vec![CanonicalStepSpec::Stop];
605
606 let cmd = RuntimeCommand::RegisterRoute {
607 spec: spec.clone(),
608 command_id: "dup-cmd".into(),
609 causation_id: None,
610 };
611 let first = bus.execute(cmd).await.unwrap();
612 assert!(matches!(
613 first,
614 RuntimeCommandResult::RouteRegistered { route_id } if route_id == "dup-route"
615 ));
616
617 let second = bus
618 .execute(RuntimeCommand::RegisterRoute {
619 spec,
620 command_id: "dup-cmd".into(),
621 causation_id: None,
622 })
623 .await
624 .unwrap();
625 assert!(matches!(
626 second,
627 RuntimeCommandResult::Duplicate { command_id } if command_id == "dup-cmd"
628 ));
629 }
630
631 #[tokio::test]
632 async fn ask_in_flight_count_without_execution_returns_route_not_found() {
633 let bus = build_test_runtime_bus();
634 let res = bus
635 .ask(RuntimeQuery::InFlightCount {
636 route_id: "missing".into(),
637 })
638 .await
639 .unwrap();
640 assert!(matches!(
641 res,
642 RuntimeQueryResult::RouteNotFound { route_id } if route_id == "missing"
643 ));
644 }
645
646 #[tokio::test]
647 async fn ask_in_flight_count_with_execution_delegates_to_adapter() {
648 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
649 let projections: Arc<dyn ProjectionStorePort> =
650 Arc::new(InMemoryTestProjectionStore::default());
651 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
652 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
653 let execution: Arc<dyn RuntimeExecutionPort> = Arc::new(InFlightExecutionPort);
654 let bus = RuntimeBus::new(repo, projections, events, dedup).with_execution(execution);
655
656 let known = bus
657 .ask(RuntimeQuery::InFlightCount {
658 route_id: "known".into(),
659 })
660 .await
661 .unwrap();
662 assert!(matches!(
663 known,
664 RuntimeQueryResult::InFlightCount { route_id, count }
665 if route_id == "known" && count == 3
666 ));
667 }
668
669 #[tokio::test]
670 async fn journal_recovery_runs_once_even_with_multiple_commands() {
671 use camel_api::runtime::{CanonicalRouteSpec, CanonicalStepSpec, RuntimeCommand};
672
673 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
674 let projections: Arc<dyn ProjectionStorePort> =
675 Arc::new(InMemoryTestProjectionStore::default());
676 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
677 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
678 let uow = Arc::new(CountingUow::default());
679 let bus = RuntimeBus::new(repo, projections, events, dedup).with_uow(uow.clone());
680
681 let mut spec_a = CanonicalRouteSpec::new("a", "timer:a");
682 spec_a.steps = vec![CanonicalStepSpec::Stop];
683 let mut spec_b = CanonicalRouteSpec::new("b", "timer:b");
684 spec_b.steps = vec![CanonicalStepSpec::Stop];
685
686 bus.execute(RuntimeCommand::RegisterRoute {
687 spec: spec_a,
688 command_id: "c-a".into(),
689 causation_id: None,
690 })
691 .await
692 .unwrap();
693
694 bus.execute(RuntimeCommand::RegisterRoute {
695 spec: spec_b,
696 command_id: "c-b".into(),
697 causation_id: None,
698 })
699 .await
700 .unwrap();
701
702 let calls = *uow.recover_calls.lock().expect("recover calls");
703 assert_eq!(calls, 1, "journal recovery should run once");
704 }
705
706 #[tokio::test]
707 async fn execute_on_command_error_forgets_dedup_marker() {
708 use camel_api::runtime::{CanonicalRouteSpec, RuntimeCommand};
709
710 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
711 let projections: Arc<dyn ProjectionStorePort> =
712 Arc::new(InMemoryTestProjectionStore::default());
713 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
714 let dedup = Arc::new(InspectableDedup::default());
715 let dedup_port: Arc<dyn CommandDedupPort> = dedup.clone();
716
717 let bus = RuntimeBus::new(repo, projections, events, dedup_port);
718
719 let cmd = RuntimeCommand::RegisterRoute {
721 spec: CanonicalRouteSpec::new("", "timer:tick"),
722 command_id: "err-cmd".into(),
723 causation_id: None,
724 };
725
726 let err = bus.execute(cmd).await.expect_err("must fail");
727 assert!(err.to_string().contains("route_id cannot be empty"));
728
729 assert_eq!(*dedup.forget_calls.lock().expect("forget calls"), 1);
730 assert!(!dedup.seen.lock().expect("seen").contains("err-cmd"));
731 }
732
733 #[tokio::test]
734 async fn execute_propagates_recover_error_from_uow() {
735 use camel_api::runtime::{CanonicalRouteSpec, CanonicalStepSpec, RuntimeCommand};
736
737 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
738 let projections: Arc<dyn ProjectionStorePort> =
739 Arc::new(InMemoryTestProjectionStore::default());
740 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
741 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
742 let uow: Arc<dyn RuntimeUnitOfWorkPort> = Arc::new(FailingRecoverUow);
743
744 let bus = RuntimeBus::new(repo, projections, events, dedup).with_uow(uow);
745
746 let mut spec = CanonicalRouteSpec::new("x", "timer:x");
747 spec.steps = vec![CanonicalStepSpec::Stop];
748 let err = bus
749 .execute(RuntimeCommand::RegisterRoute {
750 spec,
751 command_id: "recover-err".into(),
752 causation_id: None,
753 })
754 .await
755 .expect_err("recover should fail");
756
757 assert!(err.to_string().contains("recover failed"));
758 }
759
760 #[tokio::test]
761 async fn ask_in_flight_count_with_execution_handles_unknown_route() {
762 let repo: Arc<dyn RouteRepositoryPort> = Arc::new(InMemoryTestRepo::default());
763 let projections: Arc<dyn ProjectionStorePort> =
764 Arc::new(InMemoryTestProjectionStore::default());
765 let events: Arc<dyn EventPublisherPort> = Arc::new(InMemoryTestEventPublisher);
766 let dedup: Arc<dyn CommandDedupPort> = Arc::new(InMemoryTestDedup::default());
767 let execution: Arc<dyn RuntimeExecutionPort> = Arc::new(InFlightExecutionPort);
768 let bus = RuntimeBus::new(repo, projections, events, dedup).with_execution(execution);
769
770 let unknown = bus
771 .ask(RuntimeQuery::InFlightCount {
772 route_id: "unknown".into(),
773 })
774 .await
775 .unwrap();
776 assert!(matches!(
777 unknown,
778 RuntimeQueryResult::RouteNotFound { route_id } if route_id == "unknown"
779 ));
780 }
781}