1use std::collections::HashSet;
7use std::sync::{Arc, Mutex};
8use std::time::Instant;
9
10use camel_api::{CamelError, MetricsCollector, RouteController, SupervisionConfig};
11use tokio::sync::mpsc;
12use tokio::task::JoinHandle;
13use tracing::{debug, error, info, warn};
14
15pub(crate) use super::controller_actor_commands::RouteControllerCommand;
16pub use super::controller_actor_commands::RouteControllerHandle;
17use super::route_controller::DefaultRouteController;
18use super::route_helpers::CrashNotification;
19
20pub fn spawn_controller_actor(
21 controller: DefaultRouteController,
22) -> (RouteControllerHandle, tokio::task::JoinHandle<()>) {
23 let (tx, mut rx) = mpsc::channel::<RouteControllerCommand>(256);
24 let tx_for_spawn = tx.clone();
28 let handle = tokio::spawn(async move {
29 let mut controller = controller;
30 let restarting: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
35 while let Some(cmd) = rx.recv().await {
36 match cmd {
37 RouteControllerCommand::StartRoute { route_id, reply } => {
38 if restarting
40 .lock()
41 .expect("restarting mutex poisoned") .contains(&route_id)
43 {
44 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
45 "route {} is restarting",
46 route_id
47 ))));
48 continue;
49 }
50 let _ = reply.send(controller.start_route(&route_id).await);
51 }
52 RouteControllerCommand::StopRoute { route_id, reply } => {
53 if restarting
55 .lock()
56 .expect("restarting mutex poisoned") .contains(&route_id)
58 {
59 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
60 "route {} is restarting",
61 route_id
62 ))));
63 continue;
64 }
65 let _ = reply.send(controller.stop_route(&route_id).await);
66 }
67 RouteControllerCommand::RestartRoute { route_id, reply } => {
68 {
71 let mut guard = restarting.lock().expect("restarting mutex poisoned"); if guard.contains(&route_id) {
73 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
74 "route {} is restarting",
75 route_id
76 ))));
77 continue;
78 }
79 guard.insert(route_id.clone());
80 }
81
82 let stop_result = controller.stop_route(&route_id).await;
87 if let Err(ref e) = stop_result {
88 restarting
90 .lock()
91 .expect("restarting mutex poisoned") .remove(&route_id);
93 let _ = reply.send(Err(e.clone()));
94 continue;
95 }
96
97 let tx_clone = tx_for_spawn.clone();
102 let restarting_clone = restarting.clone();
103 let route_id_for_start = route_id.clone();
104 let route_id_for_cleanup = route_id.clone();
105 tokio::spawn(async move {
106 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
107 restarting_clone
113 .lock()
114 .expect("restarting mutex poisoned") .remove(&route_id_for_cleanup);
116 if tx_clone
117 .send(RouteControllerCommand::StartRoute {
118 route_id: route_id_for_start,
119 reply,
120 })
121 .await
122 .is_err()
123 {
124 warn!(
125 "route {} restart: StartRoute send failed (actor shutting down)",
126 route_id_for_cleanup
127 );
128 }
129 });
130 }
132 RouteControllerCommand::SuspendRoute { route_id, reply } => {
133 if restarting
135 .lock()
136 .expect("restarting mutex poisoned") .contains(&route_id)
138 {
139 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
140 "route {} is restarting",
141 route_id
142 ))));
143 continue;
144 }
145 let _ = reply.send(controller.suspend_route(&route_id).await);
146 }
147 RouteControllerCommand::ResumeRoute { route_id, reply } => {
148 if restarting
150 .lock()
151 .expect("restarting mutex poisoned") .contains(&route_id)
153 {
154 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
155 "route {} is restarting",
156 route_id
157 ))));
158 continue;
159 }
160 let _ = reply.send(controller.resume_route(&route_id).await);
161 }
162 RouteControllerCommand::StartAllRoutes { reply } => {
163 let _ = reply.send(controller.start_all_routes().await);
164 }
165 RouteControllerCommand::StopAllRoutes { reply } => {
166 let _ = reply.send(controller.stop_all_routes().await);
167 }
168 RouteControllerCommand::AddRoute { definition, reply } => {
169 let _ = reply.send(controller.add_route(definition).await);
170 }
171 RouteControllerCommand::RemoveRoute { route_id, reply } => {
172 if restarting
174 .lock()
175 .expect("restarting mutex poisoned") .contains(&route_id)
177 {
178 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
179 "route {} is restarting",
180 route_id
181 ))));
182 continue;
183 }
184 let _ = reply.send(controller.remove_route(&route_id).await);
185 }
186 RouteControllerCommand::SwapPipeline {
187 route_id,
188 pipeline,
189 reply,
190 } => {
191 let _ = reply.send(controller.swap_pipeline(&route_id, pipeline));
192 }
193 RouteControllerCommand::SwapPipelineRaw {
194 route_id,
195 pipeline,
196 lifecycle,
197 reply,
198 } => {
199 let _ =
200 reply.send(controller.swap_pipeline_raw(&route_id, pipeline, lifecycle));
201 }
202 RouteControllerCommand::CompileRouteDefinition { definition, reply } => {
203 let _ = reply.send(controller.compile_route_definition(definition));
204 }
205 RouteControllerCommand::CompileRouteDefinitionWithGeneration {
206 definition,
207 generation,
208 reply,
209 } => {
210 let _ = reply.send(
211 controller.compile_route_definition_with_generation(definition, generation),
212 );
213 }
214 RouteControllerCommand::CompileRouteDefinitionPipeline {
215 definition,
216 generation,
217 reply,
218 } => {
219 let _ = reply
220 .send(controller.compile_route_definition_pipeline(definition, generation));
221 }
222 RouteControllerCommand::CompileRouteDefinitionDryPipeline { definition, reply } => {
223 let _ =
224 reply.send(controller.compile_route_definition_dry_pipeline(definition));
225 }
226 RouteControllerCommand::PrepareRouteDefinitionWithGeneration {
227 definition,
228 generation,
229 reply,
230 } => {
231 let _ = reply.send(
232 controller.prepare_route_definition_with_generation(definition, generation),
233 );
234 }
235 RouteControllerCommand::InsertPreparedRoute { prepared, reply } => {
236 let _ = reply.send(controller.insert_prepared_route(prepared));
237 }
238 RouteControllerCommand::DiscardPreparedStaging { route_id, reply } => {
239 controller.discard_prepared_staging(&route_id);
240 let _ = reply.send(Ok(()));
241 }
242 RouteControllerCommand::RemoveRoutePreservingFunctions { route_id, reply } => {
243 if restarting
245 .lock()
246 .expect("restarting mutex poisoned") .contains(&route_id)
248 {
249 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
250 "route {} is restarting",
251 route_id
252 ))));
253 continue;
254 }
255 let _ = reply.send(
256 controller
257 .remove_route_preserving_functions(&route_id)
258 .await,
259 );
260 }
261 RouteControllerCommand::RouteFromUri { route_id, reply } => {
262 let _ = reply.send(controller.route_from_uri(&route_id));
263 }
264 RouteControllerCommand::SetErrorHandler { config } => {
265 controller.set_error_handler(config);
266 }
267 RouteControllerCommand::SetTracerConfig { config } => {
268 controller.set_tracer_config(&config);
269 }
270 RouteControllerCommand::RouteCount { reply } => {
271 let _ = reply.send(controller.route_count());
272 }
273 RouteControllerCommand::InFlightCount { route_id, reply } => {
274 let _ = reply.send(controller.in_flight_count(&route_id));
275 }
276 RouteControllerCommand::RouteExists { route_id, reply } => {
277 let _ = reply.send(controller.route_exists(&route_id));
278 }
279 RouteControllerCommand::RouteIds { reply } => {
280 let _ = reply.send(controller.route_ids());
281 }
282 RouteControllerCommand::ListEndpoints { reply } => {
283 let _ = reply.send(controller.list_endpoint_uris());
284 }
285 RouteControllerCommand::RoutesForEndpoint { uri, reply } => {
286 let _ = reply.send(controller.routes_for_endpoint(&uri));
287 }
288 RouteControllerCommand::HealthCheckEndpoint { uri, reply } => {
289 let route_ids = controller.routes_for_endpoint(&uri);
290 if route_ids.is_empty() {
291 let _ =
292 reply.send(Err(CamelError::RouteError("endpoint not found".into())));
293 } else {
294 let health_registry = controller.health_registry();
295 tokio::spawn(async move {
299 let futures: Vec<_> = route_ids
300 .iter()
301 .map(|rid| health_registry.check_route(rid))
302 .collect();
303 let results = futures::future::join_all(futures).await;
304 let worst = results.into_iter().fold(
305 camel_api::HealthStatus::Healthy,
306 crate::health_registry::combine_worst,
307 );
308 let _ = reply.send(Ok(worst));
309 });
310 }
311 }
312 RouteControllerCommand::AutoStartupRouteIds { reply } => {
313 let _ = reply.send(controller.auto_startup_route_ids());
314 }
315 RouteControllerCommand::ShutdownRouteIds { reply } => {
316 let _ = reply.send(controller.shutdown_route_ids());
317 }
318 RouteControllerCommand::GetPipeline { route_id, reply } => {
319 let _ = reply.send(controller.get_pipeline(&route_id));
320 }
321 RouteControllerCommand::StartRouteReload { route_id, reply } => {
322 if restarting
324 .lock()
325 .expect("restarting mutex poisoned") .contains(&route_id)
327 {
328 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
329 "route {} is restarting",
330 route_id
331 ))));
332 continue;
333 }
334 let _ = reply.send(controller.start_route_reload(&route_id).await);
335 }
336 RouteControllerCommand::StopRouteReload { route_id, reply } => {
337 if restarting
339 .lock()
340 .expect("restarting mutex poisoned") .contains(&route_id)
342 {
343 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
344 "route {} is restarting",
345 route_id
346 ))));
347 continue;
348 }
349 let _ = reply.send(controller.stop_route_reload(&route_id).await);
350 }
351 RouteControllerCommand::SetRuntimeHandle { runtime } => {
352 controller.set_runtime_handle(runtime);
353 }
354 RouteControllerCommand::SetFunctionInvoker { invoker } => {
355 controller.set_function_invoker(invoker);
356 }
357 RouteControllerCommand::RouteSourceHash { route_id, reply } => {
358 let _ = reply.send(controller.route_source_hash(&route_id));
359 }
360 RouteControllerCommand::RouteHasLifecycle { route_id, reply } => {
361 let _ = reply.send(controller.route_has_lifecycle(&route_id));
362 }
363 RouteControllerCommand::Shutdown => {
364 break;
365 }
366 }
367 }
368 });
369 (RouteControllerHandle { tx }, handle)
370}
371
372pub fn spawn_supervision_task(
373 controller: RouteControllerHandle,
374 config: SupervisionConfig,
375 _metrics: Option<Arc<dyn MetricsCollector>>,
376 mut crash_rx: mpsc::Receiver<CrashNotification>,
377) -> JoinHandle<()> {
378 tokio::spawn(async move {
379 let mut attempts: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
380 let mut last_restart_time: std::collections::HashMap<String, Instant> =
381 std::collections::HashMap::new();
382 let mut currently_restarting: std::collections::HashSet<String> =
383 std::collections::HashSet::new();
384
385 debug!("Supervision loop started");
386
387 while let Some(notification) = crash_rx.recv().await {
388 let route_id = notification.route_id;
389 if currently_restarting.contains(&route_id) {
390 continue;
391 }
392
393 if let Some(last_time) = last_restart_time.get(&route_id)
394 && last_time.elapsed() >= config.initial_delay
395 {
396 attempts.insert(route_id.clone(), 0);
397 }
398
399 let current_attempt = attempts.entry(route_id.clone()).or_insert(0);
400 *current_attempt += 1;
401
402 if config
403 .max_attempts
404 .is_some_and(|max| *current_attempt > max)
405 {
406 error!(
408 route_id = %route_id,
409 attempts = *current_attempt,
410 "Route exceeded max restart attempts, giving up"
411 );
412 continue;
413 }
414
415 let delay = config.next_delay(*current_attempt);
416 currently_restarting.insert(route_id.clone());
417 tokio::time::sleep(delay).await;
418
419 match controller.restart_route(route_id.clone()).await {
420 Ok(()) => {
421 info!(route_id = %route_id, "Route restarted successfully");
422 last_restart_time.insert(route_id.clone(), Instant::now());
423 }
424 Err(err) => {
425 error!(route_id = %route_id, error = %err, "Failed to restart route");
427 }
428 }
429
430 currently_restarting.remove(&route_id);
431 }
432
433 debug!("Supervision loop ended");
434 })
435}
436
437#[cfg(test)]
438mod tests {
439 use super::{
440 RouteControllerCommand, RouteControllerHandle, spawn_controller_actor,
441 spawn_supervision_task,
442 };
443 use crate::lifecycle::adapters::route_controller::DefaultRouteController;
444 use crate::lifecycle::adapters::route_helpers::CrashNotification;
445 use crate::lifecycle::application::route_definition::RouteDefinition;
446 use crate::shared::components::domain::Registry;
447 use crate::shared::observability::domain::TracerConfig;
448 use camel_api::function::PrepareToken;
449 use camel_api::{
450 CamelError, ErrorHandlerConfig, Exchange, ExchangePatch, FunctionDefinition, FunctionDiff,
451 FunctionId, FunctionInvocationError, FunctionInvoker, FunctionInvokerSync, RuntimeCommand,
452 RuntimeCommandBus, RuntimeCommandResult, RuntimeQuery, RuntimeQueryBus, RuntimeQueryResult,
453 SupervisionConfig,
454 };
455 use std::sync::Arc;
456 use std::time::Duration;
457 use tokio::sync::mpsc;
458 use tokio::time::sleep;
459
460 fn build_actor_with_components() -> (RouteControllerHandle, tokio::task::JoinHandle<()>) {
461 let registry = Arc::new(std::sync::Mutex::new(Registry::new()));
462 {
463 let mut guard = registry.lock().expect("lock");
464 guard.register(std::sync::Arc::new(
465 camel_component_timer::TimerComponent::new(),
466 ));
467 guard.register(std::sync::Arc::new(
468 camel_component_mock::MockComponent::new(),
469 ));
470 }
471 let controller = DefaultRouteController::new(
472 Arc::clone(®istry),
473 Arc::new(camel_api::NoopPlatformService::default()),
474 );
475 spawn_controller_actor(controller)
476 }
477
478 fn build_empty_actor() -> (RouteControllerHandle, tokio::task::JoinHandle<()>) {
479 let controller = DefaultRouteController::new(
480 Arc::new(std::sync::Mutex::new(Registry::new())),
481 Arc::new(camel_api::NoopPlatformService::default()),
482 );
483 spawn_controller_actor(controller)
484 }
485
486 fn route_def(route_id: &str, from_uri: &str) -> RouteDefinition {
487 RouteDefinition::new(from_uri, vec![]).with_route_id(route_id)
488 }
489
490 struct NoopRuntime;
491 struct NoopInvoker;
492
493 #[async_trait::async_trait]
494 impl RuntimeCommandBus for NoopRuntime {
495 async fn execute(&self, _cmd: RuntimeCommand) -> Result<RuntimeCommandResult, CamelError> {
496 Ok(RuntimeCommandResult::Accepted)
497 }
498 }
499
500 #[async_trait::async_trait]
501 impl RuntimeQueryBus for NoopRuntime {
502 async fn ask(&self, query: RuntimeQuery) -> Result<RuntimeQueryResult, CamelError> {
503 Ok(match query {
504 RuntimeQuery::GetRouteStatus { route_id }
505 | RuntimeQuery::InFlightCount { route_id } => {
506 RuntimeQueryResult::RouteNotFound { route_id }
507 }
508 _ => RuntimeQueryResult::Routes {
510 route_ids: Vec::new(),
511 },
512 })
513 }
514 }
515
516 impl FunctionInvokerSync for NoopInvoker {
517 fn stage_pending(
518 &self,
519 _def: FunctionDefinition,
520 _route_id: Option<&str>,
521 _generation: u64,
522 ) {
523 }
524 fn discard_staging(&self, _generation: u64) {}
525 fn begin_reload(&self) -> u64 {
526 1
527 }
528 fn function_refs_for_route(&self, _route_id: &str) -> Vec<(FunctionId, Option<String>)> {
529 vec![]
530 }
531 fn staged_refs_for_route(
532 &self,
533 _route_id: &str,
534 _generation: u64,
535 ) -> Vec<(FunctionId, Option<String>)> {
536 vec![]
537 }
538 fn staged_defs_for_route(
539 &self,
540 _route_id: &str,
541 _generation: u64,
542 ) -> Vec<(FunctionDefinition, Option<String>)> {
543 vec![]
544 }
545 }
546
547 #[async_trait::async_trait]
548 impl FunctionInvoker for NoopInvoker {
549 async fn register(
550 &self,
551 _def: FunctionDefinition,
552 _route_id: Option<&str>,
553 ) -> Result<(), FunctionInvocationError> {
554 Ok(())
555 }
556 async fn unregister(
557 &self,
558 _id: &FunctionId,
559 _route_id: Option<&str>,
560 ) -> Result<(), FunctionInvocationError> {
561 Ok(())
562 }
563 async fn invoke(
564 &self,
565 _id: &FunctionId,
566 _exchange: &Exchange,
567 ) -> Result<ExchangePatch, FunctionInvocationError> {
568 Ok(ExchangePatch::default())
569 }
570 async fn prepare_reload(
571 &self,
572 _diff: FunctionDiff,
573 _generation: u64,
574 ) -> Result<PrepareToken, FunctionInvocationError> {
575 Ok(PrepareToken::default())
576 }
577 async fn finalize_reload(
578 &self,
579 _diff: &FunctionDiff,
580 _generation: u64,
581 ) -> Result<(), FunctionInvocationError> {
582 Ok(())
583 }
584 async fn rollback_reload(
585 &self,
586 _token: PrepareToken,
587 _generation: u64,
588 ) -> Result<(), FunctionInvocationError> {
589 Ok(())
590 }
591 async fn commit_staged(&self) -> Result<(), FunctionInvocationError> {
592 Ok(())
593 }
594 }
595
596 #[tokio::test]
597 async fn start_route_sends_command_and_returns_reply() {
598 let (tx, mut rx) = mpsc::channel(1);
599 let handle = RouteControllerHandle { tx };
600
601 let task = tokio::spawn(async move { handle.start_route("route-a").await });
602
603 let command = rx.recv().await.expect("command should be received");
604 match command {
605 RouteControllerCommand::StartRoute { route_id, reply } => {
606 assert_eq!(route_id, "route-a");
607 let _ = reply.send(Ok(()));
608 }
609 _ => panic!("unexpected command variant"),
610 }
611
612 let result = task.await.expect("join should succeed");
613 assert!(result.is_ok());
614 }
615
616 #[tokio::test]
617 async fn start_route_returns_error_when_actor_stops() {
618 let (tx, rx) = mpsc::channel(1);
619 drop(rx);
620
621 let handle = RouteControllerHandle { tx };
622 let result = handle.start_route("route-a").await;
623
624 assert!(matches!(result, Err(CamelError::ProcessorError(_))));
625 }
626
627 #[tokio::test]
628 async fn spawn_controller_actor_processes_commands_and_shutdown() {
629 let controller = DefaultRouteController::new(
630 Arc::new(std::sync::Mutex::new(Registry::new())),
631 Arc::new(camel_api::NoopPlatformService::default()),
632 );
633 let (handle, join_handle) = spawn_controller_actor(controller);
634
635 assert_eq!(handle.route_count().await.expect("route_count"), 0);
636 assert_eq!(
637 handle.route_ids().await.expect("route_ids"),
638 Vec::<String>::new()
639 );
640
641 handle.shutdown().await.expect("shutdown send");
642 join_handle.await.expect("actor join");
643 }
644
645 #[tokio::test]
646 async fn actor_handle_introspection_and_mutation_commands() {
647 let (handle, join_handle) = build_actor_with_components();
648 let definition = route_def("h-1", "timer:tick?period=100");
649
650 handle.add_route(definition).await.expect("add route");
651 assert!(handle.route_exists("h-1").await.expect("route exists h-1"));
652 assert!(
653 !handle
654 .route_exists("no-such")
655 .await
656 .expect("route exists no-such")
657 );
658
659 let from_uri = handle.route_from_uri("h-1").await.expect("route_from_uri");
660 assert_eq!(from_uri.as_deref(), Some("timer:tick?period=100"));
661 assert_eq!(handle.route_count().await.expect("route_count"), 1);
662
663 let auto_ids = handle
664 .auto_startup_route_ids()
665 .await
666 .expect("auto_startup_route_ids");
667 assert!(auto_ids.iter().any(|id| id == "h-1"));
668
669 let shutdown_ids = handle
670 .shutdown_route_ids()
671 .await
672 .expect("shutdown_route_ids");
673 assert!(shutdown_ids.iter().any(|id| id == "h-1"));
674
675 let compiled = handle
676 .compile_route_definition(route_def("h-1", "timer:tick?period=100"))
677 .await
678 .expect("compile_route_definition");
679
680 assert!(
681 handle
682 .get_pipeline("h-1")
683 .await
684 .expect("get_pipeline")
685 .is_some()
686 );
687 handle
688 .swap_pipeline("h-1", compiled)
689 .await
690 .expect("swap_pipeline");
691
692 let _ = handle
693 .in_flight_count("h-1")
694 .await
695 .expect("in_flight_count");
696 let _ = handle.route_source_hash("h-1").await;
697
698 handle
699 .set_error_handler(ErrorHandlerConfig::dead_letter_channel("log:dlq"))
700 .await
701 .expect("set_error_handler");
702 handle
703 .set_tracer_config(TracerConfig::default())
704 .await
705 .expect("set_tracer_config");
706 handle
707 .set_runtime_handle(Arc::new(NoopRuntime))
708 .await
709 .expect("set_runtime_handle");
710
711 handle.remove_route("h-1").await.expect("remove_route");
712 assert_eq!(
713 handle
714 .route_count()
715 .await
716 .expect("route_count after remove"),
717 0
718 );
719 handle
720 .stop_all_routes()
721 .await
722 .expect("stop_all_routes on empty");
723
724 handle.shutdown().await.expect("shutdown send");
725 join_handle.await.expect("actor join");
726 }
727
728 #[tokio::test]
729 async fn actor_handle_lifecycle_start_stop_restart_suspend_resume() {
730 let (handle, join_handle) = build_actor_with_components();
731 handle
732 .add_route(route_def("lc-1", "timer:tick?period=50"))
733 .await
734 .expect("add route lc-1");
735
736 handle.start_route("lc-1").await.expect("start_route");
737 sleep(Duration::from_millis(20)).await;
738
739 handle.restart_route("lc-1").await.expect("restart_route");
740 sleep(Duration::from_millis(20)).await;
741
742 handle.suspend_route("lc-1").await.expect("suspend_route");
743 handle.resume_route("lc-1").await.expect("resume_route");
744 sleep(Duration::from_millis(20)).await;
745
746 handle.stop_route("lc-1").await.expect("stop_route");
747 handle.start_all_routes().await.expect("start_all_routes");
748 sleep(Duration::from_millis(20)).await;
749 handle.stop_all_routes().await.expect("stop_all_routes");
750
751 handle
752 .start_route_reload("lc-1")
753 .await
754 .expect("start_route_reload");
755 handle
756 .stop_route_reload("lc-1")
757 .await
758 .expect("stop_route_reload");
759
760 handle.shutdown().await.expect("shutdown send");
761 join_handle.await.expect("actor join");
762 }
763
764 #[tokio::test]
765 async fn spawn_supervision_restarts_route_on_crash() {
766 let (handle, join_handle) = build_actor_with_components();
767 handle
768 .add_route(route_def("sup-1", "timer:tick?period=100"))
769 .await
770 .expect("add route sup-1");
771 handle
772 .start_route("sup-1")
773 .await
774 .expect("start_route sup-1");
775
776 let (crash_tx, crash_rx) = mpsc::channel(8);
777 let supervision = spawn_supervision_task(
778 handle.clone(),
779 SupervisionConfig {
780 initial_delay: Duration::from_millis(10),
781 max_attempts: Some(2),
782 ..SupervisionConfig::default()
783 },
784 None,
785 crash_rx,
786 );
787
788 crash_tx
789 .send(CrashNotification {
790 route_id: "sup-1".to_string(),
791 error: "simulated".to_string(),
792 })
793 .await
794 .expect("send crash notification");
795
796 sleep(Duration::from_millis(150)).await;
797 drop(crash_tx);
798 supervision.await.expect("supervision join");
799
800 handle.shutdown().await.expect("shutdown send");
801 join_handle.await.expect("actor join");
802 }
803
804 #[tokio::test]
805 async fn supervision_skips_duplicate_and_gives_up_after_max_attempts() {
806 let (handle, join_handle) = build_actor_with_components();
807 handle
808 .add_route(route_def("sup-2", "timer:tick?period=100"))
809 .await
810 .expect("add route sup-2");
811 handle
812 .start_route("sup-2")
813 .await
814 .expect("start_route sup-2");
815
816 let (crash_tx, crash_rx) = mpsc::channel(8);
817 let supervision = spawn_supervision_task(
818 handle.clone(),
819 SupervisionConfig {
820 initial_delay: Duration::from_millis(10),
821 max_attempts: Some(1),
822 ..SupervisionConfig::default()
823 },
824 None,
825 crash_rx,
826 );
827
828 crash_tx
829 .send(CrashNotification {
830 route_id: "sup-2".to_string(),
831 error: "attempt-1".to_string(),
832 })
833 .await
834 .expect("send crash attempt-1");
835 crash_tx
836 .send(CrashNotification {
837 route_id: "sup-2".to_string(),
838 error: "attempt-2".to_string(),
839 })
840 .await
841 .expect("send crash attempt-2");
842
843 sleep(Duration::from_millis(200)).await;
844 drop(crash_tx);
845 supervision.await.expect("supervision join");
846
847 handle.shutdown().await.expect("shutdown send");
848 join_handle.await.expect("actor join");
849 }
850
851 #[tokio::test]
852 async fn try_set_runtime_handle_succeeds_on_fresh_actor() {
853 let (handle, join_handle) = build_empty_actor();
854
855 handle
856 .try_set_runtime_handle(Arc::new(NoopRuntime))
857 .expect("try_set_runtime_handle should succeed");
858
859 handle.shutdown().await.expect("shutdown send");
860 join_handle.await.expect("actor join");
861 }
862
863 #[tokio::test]
864 async fn shutdown_returns_error_when_actor_stopped() {
865 let (tx, rx) = mpsc::channel(1);
866 drop(rx);
867
868 let handle = RouteControllerHandle { tx };
869 let result = handle.shutdown().await;
870
871 assert!(matches!(result, Err(CamelError::ProcessorError(_))));
872 }
873
874 #[tokio::test]
875 async fn handle_methods_send_expected_commands_and_receive_replies() {
876 let (tx, mut rx) = mpsc::channel(16);
877 let handle = RouteControllerHandle { tx };
878
879 let stop_task = tokio::spawn({
880 let h = handle.clone();
881 async move { h.stop_route("r-1").await }
882 });
883 let cmd = rx.recv().await.expect("stop command");
884 match cmd {
885 RouteControllerCommand::StopRoute { route_id, reply } => {
886 assert_eq!(route_id, "r-1");
887 let _ = reply.send(Ok(()));
888 }
889 _ => panic!("unexpected command"),
890 }
891 assert!(stop_task.await.expect("join").is_ok());
892
893 let exists_task = tokio::spawn({
894 let h = handle.clone();
895 async move { h.route_exists("r-2").await }
896 });
897 let cmd = rx.recv().await.expect("exists command");
898 match cmd {
899 RouteControllerCommand::RouteExists { route_id, reply } => {
900 assert_eq!(route_id, "r-2");
901 let _ = reply.send(true);
902 }
903 _ => panic!("unexpected command"),
904 }
905 assert!(exists_task.await.expect("join").expect("ok"));
906
907 let hash_task = tokio::spawn({
908 let h = handle.clone();
909 async move { h.route_source_hash("r-3").await }
910 });
911 let cmd = rx.recv().await.expect("hash command");
912 match cmd {
913 RouteControllerCommand::RouteSourceHash { route_id, reply } => {
914 assert_eq!(route_id, "r-3");
915 let _ = reply.send(Some(77));
916 }
917 _ => panic!("unexpected command"),
918 }
919 assert_eq!(hash_task.await.expect("join"), Some(77));
920 }
921
922 #[tokio::test]
923 async fn handle_methods_error_on_dropped_reply_channel() {
924 let (tx, mut rx) = mpsc::channel(16);
925 let handle = RouteControllerHandle { tx };
926
927 let count_task = tokio::spawn({
928 let h = handle.clone();
929 async move { h.route_count().await }
930 });
931 let cmd = rx.recv().await.expect("route_count command");
932 match cmd {
933 RouteControllerCommand::RouteCount { reply } => drop(reply),
934 _ => panic!("unexpected command"),
935 }
936 assert!(matches!(
937 count_task.await.expect("join"),
938 Err(CamelError::ProcessorError(_))
939 ));
940
941 let stop_task = tokio::spawn({
942 let h = handle.clone();
943 async move { h.stop_route("x").await }
944 });
945 let cmd = rx.recv().await.expect("stop command");
946 match cmd {
947 RouteControllerCommand::StopRoute { reply, .. } => drop(reply),
948 _ => panic!("unexpected command"),
949 }
950 assert!(matches!(
951 stop_task.await.expect("join"),
952 Err(CamelError::ProcessorError(_))
953 ));
954
955 let maybe_hash = tokio::spawn({
956 let h = handle.clone();
957 async move { h.route_source_hash("x").await }
958 });
959 let cmd = rx.recv().await.expect("hash command");
960 match cmd {
961 RouteControllerCommand::RouteSourceHash { reply, .. } => drop(reply),
962 _ => panic!("unexpected command"),
963 }
964 assert_eq!(maybe_hash.await.expect("join"), None);
965 }
966
967 #[test]
968 fn try_set_function_invoker_returns_mailbox_full() {
969 let (tx, mut rx) = mpsc::channel(1);
970 tx.try_send(RouteControllerCommand::Shutdown)
971 .expect("fill mailbox");
972 let handle = RouteControllerHandle { tx };
973
974 let result = handle.try_set_function_invoker(Arc::new(NoopInvoker));
975 assert!(matches!(result, Err(CamelError::ProcessorError(_))));
976
977 rx.try_recv().expect("mailbox still has first message");
978 }
979
980 #[tokio::test]
981 async fn test_restart_does_not_block_other_routes() {
982 let (handle, join_handle) = build_actor_with_components();
986 handle
987 .add_route(route_def("route-a", "timer:tick?period=100"))
988 .await
989 .expect("add route-a");
990 handle
991 .add_route(route_def("route-b", "timer:tick?period=100"))
992 .await
993 .expect("add route-b");
994 handle
995 .add_route(route_def("route-c", "timer:tick?period=100"))
996 .await
997 .expect("add route-c");
998
999 handle.start_route("route-a").await.expect("start route-a");
1000 handle.start_route("route-b").await.expect("start route-b");
1001
1002 let restart_handle = handle.clone();
1005 let restart_fut =
1006 tokio::spawn(async move { restart_handle.restart_route("route-a").await });
1007
1008 tokio::task::yield_now().await;
1010
1011 let start_c_t0 = std::time::Instant::now();
1013 let start_result = handle.start_route("route-c").await;
1014 let start_c_elapsed = start_c_t0.elapsed();
1015
1016 assert!(start_result.is_ok(), "route-c start should succeed");
1017 assert!(
1018 start_c_elapsed < Duration::from_millis(80),
1019 "route-c start blocked by route-a restart (took {start_c_elapsed:?}); \
1020 expected to complete before restart's 100ms sleep"
1021 );
1022
1023 let restart_result = restart_fut.await.expect("restart join");
1024 assert!(restart_result.is_ok(), "route-a restart should succeed");
1025
1026 handle.shutdown().await.expect("shutdown send");
1027 join_handle.await.expect("actor join");
1028 }
1029
1030 #[tokio::test]
1031 async fn test_command_to_restarting_route_is_rejected() {
1032 let (handle, join_handle) = build_actor_with_components();
1035 handle
1036 .add_route(route_def("route-a", "timer:tick?period=100"))
1037 .await
1038 .expect("add route-a");
1039 handle.start_route("route-a").await.expect("start route-a");
1040
1041 let restart_handle = handle.clone();
1044 let restart_fut =
1045 tokio::spawn(async move { restart_handle.restart_route("route-a").await });
1046
1047 tokio::time::sleep(Duration::from_millis(20)).await;
1051
1052 let stop_result = handle.stop_route("route-a").await;
1054 assert!(
1055 stop_result.is_err(),
1056 "stop during restart should be rejected"
1057 );
1058 assert!(
1059 stop_result.unwrap_err().to_string().contains("restarting"),
1060 "error should mention restarting"
1061 );
1062
1063 let restart_result = restart_fut.await.expect("restart join");
1065 assert!(restart_result.is_ok(), "route-a restart should succeed");
1066
1067 handle.shutdown().await.expect("shutdown send");
1068 join_handle.await.expect("actor join");
1069 }
1070}