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::SetBindExposureAcks { acks } => {
271 controller.set_bind_exposure_acks(acks);
272 }
273 RouteControllerCommand::RouteCount { reply } => {
274 let _ = reply.send(controller.route_count());
275 }
276 RouteControllerCommand::InFlightCount { route_id, reply } => {
277 let _ = reply.send(controller.in_flight_count(&route_id));
278 }
279 RouteControllerCommand::RouteExists { route_id, reply } => {
280 let _ = reply.send(controller.route_exists(&route_id));
281 }
282 RouteControllerCommand::RouteIds { reply } => {
283 let _ = reply.send(controller.route_ids());
284 }
285 RouteControllerCommand::ListEndpoints { reply } => {
286 let _ = reply.send(controller.list_endpoint_uris());
287 }
288 RouteControllerCommand::RoutesForEndpoint { uri, reply } => {
289 let _ = reply.send(controller.routes_for_endpoint(&uri));
290 }
291 RouteControllerCommand::HealthCheckEndpoint { uri, reply } => {
292 let route_ids = controller.routes_for_endpoint(&uri);
293 if route_ids.is_empty() {
294 let _ =
295 reply.send(Err(CamelError::RouteError("endpoint not found".into())));
296 } else {
297 let health_registry = controller.health_registry();
298 tokio::spawn(async move {
302 let futures: Vec<_> = route_ids
303 .iter()
304 .map(|rid| health_registry.check_route(rid))
305 .collect();
306 let results = futures::future::join_all(futures).await;
307 let worst = results.into_iter().fold(
308 camel_api::HealthStatus::Healthy,
309 crate::health_registry::combine_worst,
310 );
311 let _ = reply.send(Ok(worst));
312 });
313 }
314 }
315 RouteControllerCommand::AutoStartupRouteIds { reply } => {
316 let _ = reply.send(controller.auto_startup_route_ids());
317 }
318 RouteControllerCommand::ShutdownRouteIds { reply } => {
319 let _ = reply.send(controller.shutdown_route_ids());
320 }
321 RouteControllerCommand::GetPipeline { route_id, reply } => {
322 let _ = reply.send(controller.get_pipeline(&route_id));
323 }
324 RouteControllerCommand::StartRouteReload { route_id, reply } => {
325 if restarting
327 .lock()
328 .expect("restarting mutex poisoned") .contains(&route_id)
330 {
331 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
332 "route {} is restarting",
333 route_id
334 ))));
335 continue;
336 }
337 let _ = reply.send(controller.start_route_reload(&route_id).await);
338 }
339 RouteControllerCommand::StopRouteReload { route_id, reply } => {
340 if restarting
342 .lock()
343 .expect("restarting mutex poisoned") .contains(&route_id)
345 {
346 let _ = reply.send(Err(camel_api::CamelError::RouteError(format!(
347 "route {} is restarting",
348 route_id
349 ))));
350 continue;
351 }
352 let _ = reply.send(controller.stop_route_reload(&route_id).await);
353 }
354 RouteControllerCommand::SetRuntimeHandle { runtime } => {
355 controller.set_runtime_handle(runtime);
356 }
357 RouteControllerCommand::SetInterceptRules { rules, reply } => {
358 let _ = reply.send(controller.set_intercept_rules(rules));
359 }
360 RouteControllerCommand::MarkStarted { reply } => {
361 controller.mark_started();
362 let _ = reply.send(Ok(()));
363 }
364 RouteControllerCommand::SetFunctionInvoker { invoker } => {
365 controller.set_function_invoker(invoker);
366 }
367 RouteControllerCommand::RouteSourceHash { route_id, reply } => {
368 let _ = reply.send(controller.route_source_hash(&route_id));
369 }
370 RouteControllerCommand::RouteHasLifecycle { route_id, reply } => {
371 let _ = reply.send(controller.route_has_lifecycle(&route_id));
372 }
373 RouteControllerCommand::Shutdown => {
374 break;
375 }
376 }
377 }
378 });
379 (RouteControllerHandle { tx }, handle)
380}
381
382pub fn spawn_supervision_task(
383 controller: RouteControllerHandle,
384 config: SupervisionConfig,
385 _metrics: Option<Arc<dyn MetricsCollector>>,
386 mut crash_rx: mpsc::Receiver<CrashNotification>,
387) -> JoinHandle<()> {
388 tokio::spawn(async move {
389 let mut attempts: std::collections::HashMap<String, u32> = std::collections::HashMap::new();
390 let mut last_restart_time: std::collections::HashMap<String, Instant> =
391 std::collections::HashMap::new();
392 let mut currently_restarting: std::collections::HashSet<String> =
393 std::collections::HashSet::new();
394
395 debug!("Supervision loop started");
396
397 while let Some(notification) = crash_rx.recv().await {
398 let route_id = notification.route_id;
399 if currently_restarting.contains(&route_id) {
400 continue;
401 }
402
403 if let Some(last_time) = last_restart_time.get(&route_id)
404 && last_time.elapsed() >= config.initial_delay
405 {
406 attempts.insert(route_id.clone(), 0);
407 }
408
409 let current_attempt = attempts.entry(route_id.clone()).or_insert(0);
410 *current_attempt += 1;
411
412 if config
413 .max_attempts
414 .is_some_and(|max| *current_attempt > max)
415 {
416 error!(
418 route_id = %route_id,
419 attempts = *current_attempt,
420 "Route exceeded max restart attempts, giving up"
421 );
422 continue;
423 }
424
425 let delay = config.next_delay(*current_attempt);
426 currently_restarting.insert(route_id.clone());
427 tokio::time::sleep(delay).await;
428
429 match controller.restart_route(route_id.clone()).await {
430 Ok(()) => {
431 info!(route_id = %route_id, "Route restarted successfully");
432 last_restart_time.insert(route_id.clone(), Instant::now());
433 }
434 Err(err) => {
435 error!(route_id = %route_id, error = %err, "Failed to restart route");
437 }
438 }
439
440 currently_restarting.remove(&route_id);
441 }
442
443 debug!("Supervision loop ended");
444 })
445}
446
447#[cfg(test)]
448mod tests {
449 use super::{
450 RouteControllerCommand, RouteControllerHandle, spawn_controller_actor,
451 spawn_supervision_task,
452 };
453 use crate::lifecycle::adapters::route_controller::DefaultRouteController;
454 use crate::lifecycle::adapters::route_helpers::CrashNotification;
455 use crate::lifecycle::application::route_definition::RouteDefinition;
456 use crate::shared::components::domain::Registry;
457 use crate::shared::observability::domain::TracerConfig;
458 use camel_api::function::PrepareToken;
459 use camel_api::{
460 CamelError, ErrorHandlerConfig, Exchange, ExchangePatch, FunctionDefinition, FunctionDiff,
461 FunctionId, FunctionInvocationError, FunctionInvoker, FunctionInvokerSync, RuntimeCommand,
462 RuntimeCommandBus, RuntimeCommandResult, RuntimeQuery, RuntimeQueryBus, RuntimeQueryResult,
463 SupervisionConfig,
464 };
465 use std::sync::Arc;
466 use std::time::Duration;
467 use tokio::sync::mpsc;
468 use tokio::time::sleep;
469
470 fn build_actor_with_components() -> (RouteControllerHandle, tokio::task::JoinHandle<()>) {
471 let registry = Arc::new(std::sync::Mutex::new(Registry::new()));
472 {
473 let mut guard = registry.lock().expect("lock");
474 guard.register(std::sync::Arc::new(
475 camel_component_timer::TimerComponent::new(),
476 ));
477 guard.register(std::sync::Arc::new(
478 camel_component_mock::MockComponent::new(),
479 ));
480 }
481 let controller = DefaultRouteController::new(
482 Arc::clone(®istry),
483 Arc::new(camel_api::NoopPlatformService::default()),
484 );
485 spawn_controller_actor(controller)
486 }
487
488 fn build_empty_actor() -> (RouteControllerHandle, tokio::task::JoinHandle<()>) {
489 let controller = DefaultRouteController::new(
490 Arc::new(std::sync::Mutex::new(Registry::new())),
491 Arc::new(camel_api::NoopPlatformService::default()),
492 );
493 spawn_controller_actor(controller)
494 }
495
496 fn route_def(route_id: &str, from_uri: &str) -> RouteDefinition {
497 RouteDefinition::new(from_uri, vec![]).with_route_id(route_id)
498 }
499
500 struct NoopRuntime;
501 struct NoopInvoker;
502
503 #[async_trait::async_trait]
504 impl RuntimeCommandBus for NoopRuntime {
505 async fn execute(&self, _cmd: RuntimeCommand) -> Result<RuntimeCommandResult, CamelError> {
506 Ok(RuntimeCommandResult::Accepted)
507 }
508 }
509
510 #[async_trait::async_trait]
511 impl RuntimeQueryBus for NoopRuntime {
512 async fn ask(&self, query: RuntimeQuery) -> Result<RuntimeQueryResult, CamelError> {
513 Ok(match query {
514 RuntimeQuery::GetRouteStatus { route_id }
515 | RuntimeQuery::InFlightCount { route_id } => {
516 RuntimeQueryResult::RouteNotFound { route_id }
517 }
518 _ => RuntimeQueryResult::Routes {
520 route_ids: Vec::new(),
521 },
522 })
523 }
524 }
525
526 impl FunctionInvokerSync for NoopInvoker {
527 fn stage_pending(
528 &self,
529 _def: FunctionDefinition,
530 _route_id: Option<&str>,
531 _generation: u64,
532 ) {
533 }
534 fn discard_staging(&self, _generation: u64) {}
535 fn begin_reload(&self) -> u64 {
536 1
537 }
538 fn function_refs_for_route(&self, _route_id: &str) -> Vec<(FunctionId, Option<String>)> {
539 vec![]
540 }
541 fn staged_refs_for_route(
542 &self,
543 _route_id: &str,
544 _generation: u64,
545 ) -> Vec<(FunctionId, Option<String>)> {
546 vec![]
547 }
548 fn staged_defs_for_route(
549 &self,
550 _route_id: &str,
551 _generation: u64,
552 ) -> Vec<(FunctionDefinition, Option<String>)> {
553 vec![]
554 }
555 }
556
557 #[async_trait::async_trait]
558 impl FunctionInvoker for NoopInvoker {
559 async fn register(
560 &self,
561 _def: FunctionDefinition,
562 _route_id: Option<&str>,
563 ) -> Result<(), FunctionInvocationError> {
564 Ok(())
565 }
566 async fn unregister(
567 &self,
568 _id: &FunctionId,
569 _route_id: Option<&str>,
570 ) -> Result<(), FunctionInvocationError> {
571 Ok(())
572 }
573 async fn invoke(
574 &self,
575 _id: &FunctionId,
576 _exchange: &Exchange,
577 ) -> Result<ExchangePatch, FunctionInvocationError> {
578 Ok(ExchangePatch::default())
579 }
580 async fn prepare_reload(
581 &self,
582 _diff: FunctionDiff,
583 _generation: u64,
584 ) -> Result<PrepareToken, FunctionInvocationError> {
585 Ok(PrepareToken::default())
586 }
587 async fn finalize_reload(
588 &self,
589 _diff: &FunctionDiff,
590 _generation: u64,
591 ) -> Result<(), FunctionInvocationError> {
592 Ok(())
593 }
594 async fn rollback_reload(
595 &self,
596 _token: PrepareToken,
597 _generation: u64,
598 ) -> Result<(), FunctionInvocationError> {
599 Ok(())
600 }
601 async fn commit_staged(&self) -> Result<(), FunctionInvocationError> {
602 Ok(())
603 }
604 }
605
606 #[tokio::test]
607 async fn start_route_sends_command_and_returns_reply() {
608 let (tx, mut rx) = mpsc::channel(1);
609 let handle = RouteControllerHandle { tx };
610
611 let task = tokio::spawn(async move { handle.start_route("route-a").await });
612
613 let command = rx.recv().await.expect("command should be received");
614 match command {
615 RouteControllerCommand::StartRoute { route_id, reply } => {
616 assert_eq!(route_id, "route-a");
617 let _ = reply.send(Ok(()));
618 }
619 _ => panic!("unexpected command variant"),
620 }
621
622 let result = task.await.expect("join should succeed");
623 assert!(result.is_ok());
624 }
625
626 #[tokio::test]
627 async fn start_route_returns_error_when_actor_stops() {
628 let (tx, rx) = mpsc::channel(1);
629 drop(rx);
630
631 let handle = RouteControllerHandle { tx };
632 let result = handle.start_route("route-a").await;
633
634 assert!(matches!(result, Err(CamelError::ProcessorError(_))));
635 }
636
637 #[tokio::test]
638 async fn spawn_controller_actor_processes_commands_and_shutdown() {
639 let controller = DefaultRouteController::new(
640 Arc::new(std::sync::Mutex::new(Registry::new())),
641 Arc::new(camel_api::NoopPlatformService::default()),
642 );
643 let (handle, join_handle) = spawn_controller_actor(controller);
644
645 assert_eq!(handle.route_count().await.expect("route_count"), 0);
646 assert_eq!(
647 handle.route_ids().await.expect("route_ids"),
648 Vec::<String>::new()
649 );
650
651 handle.shutdown().await.expect("shutdown send");
652 join_handle.await.expect("actor join");
653 }
654
655 #[tokio::test]
656 async fn actor_handle_introspection_and_mutation_commands() {
657 let (handle, join_handle) = build_actor_with_components();
658 let definition = route_def("h-1", "timer:tick?period=100");
659
660 handle.add_route(definition).await.expect("add route");
661 assert!(handle.route_exists("h-1").await.expect("route exists h-1"));
662 assert!(
663 !handle
664 .route_exists("no-such")
665 .await
666 .expect("route exists no-such")
667 );
668
669 let from_uri = handle.route_from_uri("h-1").await.expect("route_from_uri");
670 assert_eq!(from_uri.as_deref(), Some("timer:tick?period=100"));
671 assert_eq!(handle.route_count().await.expect("route_count"), 1);
672
673 let auto_ids = handle
674 .auto_startup_route_ids()
675 .await
676 .expect("auto_startup_route_ids");
677 assert!(auto_ids.iter().any(|id| id == "h-1"));
678
679 let shutdown_ids = handle
680 .shutdown_route_ids()
681 .await
682 .expect("shutdown_route_ids");
683 assert!(shutdown_ids.iter().any(|id| id == "h-1"));
684
685 let compiled = handle
686 .compile_route_definition(route_def("h-1", "timer:tick?period=100"))
687 .await
688 .expect("compile_route_definition");
689
690 assert!(
691 handle
692 .get_pipeline("h-1")
693 .await
694 .expect("get_pipeline")
695 .is_some()
696 );
697 handle
698 .swap_pipeline("h-1", compiled)
699 .await
700 .expect("swap_pipeline");
701
702 let _ = handle
703 .in_flight_count("h-1")
704 .await
705 .expect("in_flight_count");
706 let _ = handle.route_source_hash("h-1").await;
707
708 handle
709 .set_error_handler(ErrorHandlerConfig::dead_letter_channel("log:dlq"))
710 .await
711 .expect("set_error_handler");
712 handle
713 .set_tracer_config(TracerConfig::default())
714 .await
715 .expect("set_tracer_config");
716 handle
717 .set_runtime_handle(Arc::new(NoopRuntime))
718 .await
719 .expect("set_runtime_handle");
720
721 handle.remove_route("h-1").await.expect("remove_route");
722 assert_eq!(
723 handle
724 .route_count()
725 .await
726 .expect("route_count after remove"),
727 0
728 );
729 handle
730 .stop_all_routes()
731 .await
732 .expect("stop_all_routes on empty");
733
734 handle.shutdown().await.expect("shutdown send");
735 join_handle.await.expect("actor join");
736 }
737
738 #[tokio::test]
739 async fn actor_handle_lifecycle_start_stop_restart_suspend_resume() {
740 let (handle, join_handle) = build_actor_with_components();
741 handle
742 .add_route(route_def("lc-1", "timer:tick?period=50"))
743 .await
744 .expect("add route lc-1");
745
746 handle.start_route("lc-1").await.expect("start_route");
747 sleep(Duration::from_millis(20)).await;
748
749 handle.restart_route("lc-1").await.expect("restart_route");
750 sleep(Duration::from_millis(20)).await;
751
752 handle.suspend_route("lc-1").await.expect("suspend_route");
753 handle.resume_route("lc-1").await.expect("resume_route");
754 sleep(Duration::from_millis(20)).await;
755
756 handle.stop_route("lc-1").await.expect("stop_route");
757 handle.start_all_routes().await.expect("start_all_routes");
758 sleep(Duration::from_millis(20)).await;
759 handle.stop_all_routes().await.expect("stop_all_routes");
760
761 handle
762 .start_route_reload("lc-1")
763 .await
764 .expect("start_route_reload");
765 handle
766 .stop_route_reload("lc-1")
767 .await
768 .expect("stop_route_reload");
769
770 handle.shutdown().await.expect("shutdown send");
771 join_handle.await.expect("actor join");
772 }
773
774 #[tokio::test]
775 async fn spawn_supervision_restarts_route_on_crash() {
776 let (handle, join_handle) = build_actor_with_components();
777 handle
778 .add_route(route_def("sup-1", "timer:tick?period=100"))
779 .await
780 .expect("add route sup-1");
781 handle
782 .start_route("sup-1")
783 .await
784 .expect("start_route sup-1");
785
786 let (crash_tx, crash_rx) = mpsc::channel(8);
787 let supervision = spawn_supervision_task(
788 handle.clone(),
789 SupervisionConfig {
790 initial_delay: Duration::from_millis(10),
791 max_attempts: Some(2),
792 ..SupervisionConfig::default()
793 },
794 None,
795 crash_rx,
796 );
797
798 crash_tx
799 .send(CrashNotification {
800 route_id: "sup-1".to_string(),
801 error: "simulated".to_string(),
802 })
803 .await
804 .expect("send crash notification");
805
806 sleep(Duration::from_millis(150)).await;
807 drop(crash_tx);
808 supervision.await.expect("supervision join");
809
810 handle.shutdown().await.expect("shutdown send");
811 join_handle.await.expect("actor join");
812 }
813
814 #[tokio::test]
815 async fn supervision_skips_duplicate_and_gives_up_after_max_attempts() {
816 let (handle, join_handle) = build_actor_with_components();
817 handle
818 .add_route(route_def("sup-2", "timer:tick?period=100"))
819 .await
820 .expect("add route sup-2");
821 handle
822 .start_route("sup-2")
823 .await
824 .expect("start_route sup-2");
825
826 let (crash_tx, crash_rx) = mpsc::channel(8);
827 let supervision = spawn_supervision_task(
828 handle.clone(),
829 SupervisionConfig {
830 initial_delay: Duration::from_millis(10),
831 max_attempts: Some(1),
832 ..SupervisionConfig::default()
833 },
834 None,
835 crash_rx,
836 );
837
838 crash_tx
839 .send(CrashNotification {
840 route_id: "sup-2".to_string(),
841 error: "attempt-1".to_string(),
842 })
843 .await
844 .expect("send crash attempt-1");
845 crash_tx
846 .send(CrashNotification {
847 route_id: "sup-2".to_string(),
848 error: "attempt-2".to_string(),
849 })
850 .await
851 .expect("send crash attempt-2");
852
853 sleep(Duration::from_millis(200)).await;
854 drop(crash_tx);
855 supervision.await.expect("supervision join");
856
857 handle.shutdown().await.expect("shutdown send");
858 join_handle.await.expect("actor join");
859 }
860
861 #[tokio::test]
862 async fn try_set_runtime_handle_succeeds_on_fresh_actor() {
863 let (handle, join_handle) = build_empty_actor();
864
865 handle
866 .try_set_runtime_handle(Arc::new(NoopRuntime))
867 .expect("try_set_runtime_handle should succeed");
868
869 handle.shutdown().await.expect("shutdown send");
870 join_handle.await.expect("actor join");
871 }
872
873 #[tokio::test]
874 async fn shutdown_returns_error_when_actor_stopped() {
875 let (tx, rx) = mpsc::channel(1);
876 drop(rx);
877
878 let handle = RouteControllerHandle { tx };
879 let result = handle.shutdown().await;
880
881 assert!(matches!(result, Err(CamelError::ProcessorError(_))));
882 }
883
884 #[tokio::test]
885 async fn handle_methods_send_expected_commands_and_receive_replies() {
886 let (tx, mut rx) = mpsc::channel(16);
887 let handle = RouteControllerHandle { tx };
888
889 let stop_task = tokio::spawn({
890 let h = handle.clone();
891 async move { h.stop_route("r-1").await }
892 });
893 let cmd = rx.recv().await.expect("stop command");
894 match cmd {
895 RouteControllerCommand::StopRoute { route_id, reply } => {
896 assert_eq!(route_id, "r-1");
897 let _ = reply.send(Ok(()));
898 }
899 _ => panic!("unexpected command"),
900 }
901 assert!(stop_task.await.expect("join").is_ok());
902
903 let exists_task = tokio::spawn({
904 let h = handle.clone();
905 async move { h.route_exists("r-2").await }
906 });
907 let cmd = rx.recv().await.expect("exists command");
908 match cmd {
909 RouteControllerCommand::RouteExists { route_id, reply } => {
910 assert_eq!(route_id, "r-2");
911 let _ = reply.send(true);
912 }
913 _ => panic!("unexpected command"),
914 }
915 assert!(exists_task.await.expect("join").expect("ok"));
916
917 let hash_task = tokio::spawn({
918 let h = handle.clone();
919 async move { h.route_source_hash("r-3").await }
920 });
921 let cmd = rx.recv().await.expect("hash command");
922 match cmd {
923 RouteControllerCommand::RouteSourceHash { route_id, reply } => {
924 assert_eq!(route_id, "r-3");
925 let _ = reply.send(Some(77));
926 }
927 _ => panic!("unexpected command"),
928 }
929 assert_eq!(hash_task.await.expect("join"), Some(77));
930 }
931
932 #[tokio::test]
933 async fn handle_methods_error_on_dropped_reply_channel() {
934 let (tx, mut rx) = mpsc::channel(16);
935 let handle = RouteControllerHandle { tx };
936
937 let count_task = tokio::spawn({
938 let h = handle.clone();
939 async move { h.route_count().await }
940 });
941 let cmd = rx.recv().await.expect("route_count command");
942 match cmd {
943 RouteControllerCommand::RouteCount { reply } => drop(reply),
944 _ => panic!("unexpected command"),
945 }
946 assert!(matches!(
947 count_task.await.expect("join"),
948 Err(CamelError::ProcessorError(_))
949 ));
950
951 let stop_task = tokio::spawn({
952 let h = handle.clone();
953 async move { h.stop_route("x").await }
954 });
955 let cmd = rx.recv().await.expect("stop command");
956 match cmd {
957 RouteControllerCommand::StopRoute { reply, .. } => drop(reply),
958 _ => panic!("unexpected command"),
959 }
960 assert!(matches!(
961 stop_task.await.expect("join"),
962 Err(CamelError::ProcessorError(_))
963 ));
964
965 let maybe_hash = tokio::spawn({
966 let h = handle.clone();
967 async move { h.route_source_hash("x").await }
968 });
969 let cmd = rx.recv().await.expect("hash command");
970 match cmd {
971 RouteControllerCommand::RouteSourceHash { reply, .. } => drop(reply),
972 _ => panic!("unexpected command"),
973 }
974 assert_eq!(maybe_hash.await.expect("join"), None);
975 }
976
977 #[test]
978 fn try_set_function_invoker_returns_mailbox_full() {
979 let (tx, mut rx) = mpsc::channel(1);
980 tx.try_send(RouteControllerCommand::Shutdown)
981 .expect("fill mailbox");
982 let handle = RouteControllerHandle { tx };
983
984 let result = handle.try_set_function_invoker(Arc::new(NoopInvoker));
985 assert!(matches!(result, Err(CamelError::ProcessorError(_))));
986
987 rx.try_recv().expect("mailbox still has first message");
988 }
989
990 #[tokio::test]
991 async fn test_restart_does_not_block_other_routes() {
992 let (handle, join_handle) = build_actor_with_components();
996 handle
997 .add_route(route_def("route-a", "timer:tick?period=100"))
998 .await
999 .expect("add route-a");
1000 handle
1001 .add_route(route_def("route-b", "timer:tick?period=100"))
1002 .await
1003 .expect("add route-b");
1004 handle
1005 .add_route(route_def("route-c", "timer:tick?period=100"))
1006 .await
1007 .expect("add route-c");
1008
1009 handle.start_route("route-a").await.expect("start route-a");
1010 handle.start_route("route-b").await.expect("start route-b");
1011
1012 let restart_handle = handle.clone();
1015 let restart_fut =
1016 tokio::spawn(async move { restart_handle.restart_route("route-a").await });
1017
1018 tokio::task::yield_now().await;
1020
1021 let start_c_t0 = std::time::Instant::now();
1023 let start_result = handle.start_route("route-c").await;
1024 let start_c_elapsed = start_c_t0.elapsed();
1025
1026 assert!(start_result.is_ok(), "route-c start should succeed");
1027 assert!(
1028 start_c_elapsed < Duration::from_millis(80),
1029 "route-c start blocked by route-a restart (took {start_c_elapsed:?}); \
1030 expected to complete before restart's 100ms sleep"
1031 );
1032
1033 let restart_result = restart_fut.await.expect("restart join");
1034 assert!(restart_result.is_ok(), "route-a restart should succeed");
1035
1036 handle.shutdown().await.expect("shutdown send");
1037 join_handle.await.expect("actor join");
1038 }
1039
1040 #[tokio::test]
1041 async fn test_command_to_restarting_route_is_rejected() {
1042 let (handle, join_handle) = build_actor_with_components();
1045 handle
1046 .add_route(route_def("route-a", "timer:tick?period=100"))
1047 .await
1048 .expect("add route-a");
1049 handle.start_route("route-a").await.expect("start route-a");
1050
1051 let restart_handle = handle.clone();
1054 let restart_fut =
1055 tokio::spawn(async move { restart_handle.restart_route("route-a").await });
1056
1057 tokio::time::sleep(Duration::from_millis(20)).await;
1061
1062 let stop_result = handle.stop_route("route-a").await;
1064 assert!(
1065 stop_result.is_err(),
1066 "stop during restart should be rejected"
1067 );
1068 assert!(
1069 stop_result.unwrap_err().to_string().contains("restarting"),
1070 "error should mention restarting"
1071 );
1072
1073 let restart_result = restart_fut.await.expect("restart join");
1075 assert!(restart_result.is_ok(), "route-a restart should succeed");
1076
1077 handle.shutdown().await.expect("shutdown send");
1078 join_handle.await.expect("actor join");
1079 }
1080}