Skip to main content

camel_core/lifecycle/adapters/
route_controller.rs

1//! Default implementation of RouteController.
2//!
3//! This module provides [`DefaultRouteController`], which manages route lifecycle
4//! including starting, stopping, suspending, and resuming routes.
5
6use std::collections::HashMap;
7use std::sync::{Arc, Weak};
8use std::time::Duration;
9
10use tokio::sync::mpsc;
11use tokio_util::sync::CancellationToken;
12use tower::{Layer, ServiceExt};
13use tracing::{debug, info, warn};
14
15use super::route_controller_trait::{
16    BindExposureAcks, bind_key_from_uri, enforce_bind_exposure_gate,
17};
18use camel_api::error_handler::ErrorHandlerConfig;
19use camel_api::metrics::MetricsCollector;
20#[allow(unused_imports)]
21use camel_api::{
22    BoxProcessor, CamelError, Exchange, FunctionInvoker, IdentityProcessor, NoOpMetrics,
23    NoopPlatformService, PlatformService, ProducerContext, RouteController, RuntimeHandle,
24    StepLifecycle,
25};
26use camel_component_api::{Consumer, ConsumerContext, consumer::ExchangeEnvelope};
27use camel_processor::aggregator::AggregatorService;
28pub use camel_processor::aggregator::SharedLanguageRegistry;
29
30use crate::health_registry::HealthCheckRegistry;
31use crate::intercept::InterceptRules;
32use crate::lifecycle::CohortActivationGate;
33use crate::lifecycle::adapters::controller_component_context::ControllerComponentContext;
34use crate::lifecycle::adapters::route_compiler_ext::{
35    RouteCompilerExt, build_eh_config_pipeline, transport_from_uri,
36};
37use crate::lifecycle::adapters::route_helpers::{
38    AggregateSplitInfo, CrashNotification, ManagedRoute, assert_no_mixed_top_level_splits,
39    handle_is_running, inferred_lifecycle_label, is_pending,
40};
41#[cfg(test)]
42pub(super) use crate::lifecycle::adapters::route_helpers::{
43    emit_start_route_event, set_start_route_event_hook,
44};
45use crate::lifecycle::adapters::route_registry::RouteRegistry;
46use crate::lifecycle::adapters::route_runtime_state;
47use crate::lifecycle::adapters::step_compilers::CompiledStep;
48use crate::lifecycle::application::route_definition::{BuilderStep, RouteDefinition};
49pub(crate) use crate::lifecycle::domain::CompiledPipeline;
50use crate::shared::components::domain::Registry;
51use crate::shared::observability::domain::{DetailLevel, TracerConfig};
52use camel_bean::BeanRegistry;
53
54/// Default implementation of [`RouteController`].
55///
56/// Manages route lifecycle with support for:
57/// - Starting/stopping individual routes
58/// - Suspending and resuming routes
59/// - Auto-startup with startup ordering
60/// - Graceful shutdown
61pub struct DefaultRouteController {
62    /// Routes indexed by route ID.
63    pub(super) routes: RouteRegistry,
64    /// Reference to the component registry for resolving endpoints.
65    pub(super) registry: Arc<std::sync::Mutex<Registry>>,
66    /// Shared language registry for resolving declarative language expressions.
67    pub(super) languages: SharedLanguageRegistry,
68    /// Bean registry for bean method invocation.
69    pub(super) beans: Arc<std::sync::Mutex<BeanRegistry>>,
70    /// Runtime handle injected into ProducerContext for command/query operations.
71    pub(super) runtime: Option<Weak<dyn RuntimeHandle>>,
72    /// Optional global error handler applied to all routes without a per-route handler.
73    pub(super) global_error_handler: Option<ErrorHandlerConfig>,
74    /// Optional crash notifier for supervision.
75    pub(super) crash_notifier: Option<mpsc::Sender<CrashNotification>>,
76    /// Whether tracing is enabled for route pipelines.
77    pub(super) tracing_enabled: bool,
78    /// Detail level for tracing when enabled.
79    pub(super) tracer_detail_level: DetailLevel,
80    /// Metrics collector for tracing processor.
81    pub(super) tracer_metrics: Option<Arc<dyn MetricsCollector>>,
82    pub(super) platform_service: Arc<dyn PlatformService>,
83    pub(super) function_invoker: Option<Arc<dyn FunctionInvoker>>,
84    pub(super) health_registry: Option<Arc<HealthCheckRegistry>>,
85    /// Shared idempotent repository registry. Defaults to an empty registry;
86    /// the CamelContext builder installs a populated handle that includes the
87    /// built-in `"memory"` repository.
88    pub(super) idempotent_repositories: crate::SharedIdempotentRegistry,
89    pub(super) claim_check_repositories: crate::SharedClaimCheckRegistry,
90    pub(super) cache_repositories: crate::SharedCacheRegistry,
91    /// F2 staging: prepared-but-not-inserted ManagedRoutes keyed by route_id.
92    /// `prepare_*` writes here; `insert_prepared_route` drains via `remove()`.
93    /// On insert-failure error paths, the caller (`reload_actions.rs`) must
94    /// explicitly drain to avoid orphan CancellationToken/SharedPipeline leaks.
95    pub(super) prepared_staging: HashMap<String, ManagedRoute>,
96    /// Source endpoint URI to route_id index (one-to-many).
97    pub(super) endpoint_index: super::endpoint_index::EndpointIndex,
98    /// Operator acknowledgements for per-bind public exposure (ADR-0061).
99    /// Empty by default → the gate fails closed on non-loopback binds.
100    pub(super) bind_acks: super::route_controller_trait::BindExposureAcks,
101    /// Route send-point interception rules captured by step compilation.
102    pub(super) intercept: InterceptRules,
103    /// Intercept-rules freeze. Trips on `add_route` success and on the
104    /// `MarkStarted` actor command; never reset (stop/restart included),
105    /// because compiled pipelines capture the rules at compile time.
106    pub(super) frozen: bool,
107    /// Startup-cohort activation barrier (rc-jxkj). Cloned into the
108    /// `RouteControllerHandle` at spawn; reset/activate act on this shared
109    /// gate directly, never through the actor.
110    pub(super) cohort: Arc<CohortActivationGate>,
111}
112
113impl DefaultRouteController {
114    /// Open the startup-cohort activation barrier (rc-jxkj).
115    ///
116    /// The CamelContext lifecycle opens this automatically once the startup
117    /// cohort completes. Consumers that drive a bare `DefaultRouteController`
118    /// (outside a full context) must call this before dispatching
119    /// (typically after starting routes), or pipeline dispatch parks
120    /// every envelope until the caller's call timeout surfaces as a
121    /// failure.
122    pub fn activate_cohort(&self) {
123        self.cohort.open();
124    }
125
126    pub(super) fn health_registry(&self) -> Arc<HealthCheckRegistry> {
127        self.health_registry.clone().unwrap_or_else(|| {
128            debug!("health_registry not configured — creating isolated fallback");
129            Arc::new(HealthCheckRegistry::new(Duration::from_secs(5)))
130        })
131    }
132
133    /// Create a new `DefaultRouteController` with the given registry.
134    pub fn new(
135        registry: Arc<std::sync::Mutex<Registry>>,
136        platform_service: Arc<dyn PlatformService>,
137    ) -> Self {
138        Self::with_beans_and_platform_service(
139            registry,
140            Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
141            platform_service,
142        )
143    }
144
145    /// Create a new `DefaultRouteController` with shared bean registry.
146    pub fn with_beans(
147        registry: Arc<std::sync::Mutex<Registry>>,
148        beans: Arc<std::sync::Mutex<BeanRegistry>>,
149    ) -> Self {
150        Self::with_beans_and_platform_service(
151            registry,
152            beans,
153            Arc::new(NoopPlatformService::default()),
154        )
155    }
156
157    fn with_beans_and_platform_service(
158        registry: Arc<std::sync::Mutex<Registry>>,
159        beans: Arc<std::sync::Mutex<BeanRegistry>>,
160        platform_service: Arc<dyn PlatformService>,
161    ) -> Self {
162        Self {
163            routes: RouteRegistry::new(),
164            registry,
165            languages: Arc::new(std::sync::Mutex::new(HashMap::new())),
166            beans,
167            runtime: None,
168            global_error_handler: None,
169            crash_notifier: None,
170            tracing_enabled: false,
171            tracer_detail_level: DetailLevel::Minimal,
172            tracer_metrics: None,
173            platform_service,
174            function_invoker: None,
175            health_registry: None,
176            idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
177            claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
178            cache_repositories: Arc::new(crate::CacheRegistry::new()),
179            prepared_staging: HashMap::new(),
180            endpoint_index: super::endpoint_index::EndpointIndex::new(),
181            bind_acks: Default::default(),
182            intercept: InterceptRules::default(),
183            frozen: false,
184            cohort: Arc::new(CohortActivationGate::new_closed()),
185        }
186    }
187
188    /// Create a new `DefaultRouteController` with shared language registry.
189    pub fn with_languages(
190        registry: Arc<std::sync::Mutex<Registry>>,
191        languages: SharedLanguageRegistry,
192        platform_service: Arc<dyn PlatformService>,
193    ) -> Self {
194        Self {
195            routes: RouteRegistry::new(),
196            registry,
197            languages,
198            beans: Arc::new(std::sync::Mutex::new(BeanRegistry::new())),
199            runtime: None,
200            global_error_handler: None,
201            crash_notifier: None,
202            tracing_enabled: false,
203            tracer_detail_level: DetailLevel::Minimal,
204            tracer_metrics: None,
205            platform_service,
206            function_invoker: None,
207            health_registry: None,
208            idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
209            claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
210            cache_repositories: Arc::new(crate::CacheRegistry::new()),
211            prepared_staging: HashMap::new(),
212            endpoint_index: super::endpoint_index::EndpointIndex::new(),
213            bind_acks: Default::default(),
214            intercept: InterceptRules::default(),
215            frozen: false,
216            cohort: Arc::new(CohortActivationGate::new_closed()),
217        }
218    }
219
220    pub fn with_languages_and_beans(
221        registry: Arc<std::sync::Mutex<Registry>>,
222        languages: SharedLanguageRegistry,
223        platform_service: Arc<dyn PlatformService>,
224        beans: Arc<std::sync::Mutex<BeanRegistry>>,
225    ) -> Self {
226        Self {
227            routes: RouteRegistry::new(),
228            registry,
229            languages,
230            beans,
231            runtime: None,
232            global_error_handler: None,
233            crash_notifier: None,
234            tracing_enabled: false,
235            tracer_detail_level: DetailLevel::Minimal,
236            tracer_metrics: None,
237            platform_service,
238            function_invoker: None,
239            health_registry: None,
240            idempotent_repositories: Arc::new(crate::IdempotentRegistry::new()),
241            claim_check_repositories: Arc::new(crate::ClaimCheckRegistry::new()),
242            cache_repositories: Arc::new(crate::CacheRegistry::new()),
243            prepared_staging: HashMap::new(),
244            endpoint_index: super::endpoint_index::EndpointIndex::new(),
245            bind_acks: Default::default(),
246            intercept: InterceptRules::default(),
247            frozen: false,
248            cohort: Arc::new(CohortActivationGate::new_closed()),
249        }
250    }
251
252    pub fn with_function_invoker(mut self, function_invoker: Arc<dyn FunctionInvoker>) -> Self {
253        self.function_invoker = Some(function_invoker);
254        self
255    }
256
257    pub(crate) fn set_idempotent_repositories(
258        &mut self,
259        repositories: crate::SharedIdempotentRegistry,
260    ) {
261        self.idempotent_repositories = repositories;
262    }
263
264    pub(crate) fn set_claim_check_repositories(
265        &mut self,
266        repositories: crate::SharedClaimCheckRegistry,
267    ) {
268        self.claim_check_repositories = repositories;
269    }
270
271    pub(crate) fn set_cache_repositories(&mut self, repositories: crate::SharedCacheRegistry) {
272        self.cache_repositories = repositories;
273    }
274
275    pub fn set_health_registry(&mut self, registry: Arc<HealthCheckRegistry>) {
276        self.health_registry = Some(registry);
277    }
278
279    pub fn set_function_invoker(&mut self, invoker: Arc<dyn FunctionInvoker>) {
280        self.function_invoker = Some(invoker);
281    }
282
283    /// Set runtime handle for ProducerContext creation.
284    pub fn set_runtime_handle(&mut self, runtime: Arc<dyn RuntimeHandle>) {
285        self.runtime = Some(Arc::downgrade(&runtime));
286    }
287
288    /// Set the crash notifier for supervision.
289    ///
290    /// When set, the controller will send a `CrashNotification` whenever
291    /// a consumer crashes.
292    pub fn set_crash_notifier(&mut self, tx: mpsc::Sender<CrashNotification>) {
293        self.crash_notifier = Some(tx);
294    }
295
296    /// Set a global error handler applied to all routes without a per-route handler.
297    /// Install operator acknowledgements for per-bind public exposure
298    /// (ADR-0061). Built by the CLI from `CamelConfig.binds`.
299    pub fn set_bind_exposure_acks(&mut self, acks: BindExposureAcks) {
300        self.bind_acks = acks;
301    }
302
303    /// Install route send-point interception rules (pre-first-use only).
304    ///
305    /// Fails with `CamelError::Config` once the freeze has tripped: compiled
306    /// pipelines capture the rules at compile time, so the rule set must not
307    /// change after a route is added or the context is started.
308    pub fn set_intercept_rules(&mut self, rules: InterceptRules) -> Result<(), CamelError> {
309        if self.frozen {
310            return Err(CamelError::Config(
311                "intercept rules are frozen: a route was added or the context was started; \
312                 rules cannot be changed after first use"
313                    .into(),
314            ));
315        }
316        self.intercept = rules;
317        Ok(())
318    }
319
320    /// Builder-style build-time configuration on a fresh controller (which
321    /// is never frozen); mirrors `with_function_invoker`.
322    pub fn with_intercept_rules(mut self, rules: InterceptRules) -> Self {
323        self.intercept = rules;
324        self
325    }
326
327    /// Trip the intercept-rules freeze. Dispatched by the `MarkStarted`
328    /// actor command so the freeze applies even with zero routes. Never
329    /// unset.
330    pub fn mark_started(&mut self) {
331        self.frozen = true;
332    }
333
334    /// All compiled security plans whose routes bind the same listener
335    /// address (bind key), for the per-bind exposure gate. Routes still
336    /// staging (no plan yet) are skipped — classification failures already
337    /// aborted their own staging (Task 1.8).
338    pub(super) fn plans_for_bind(
339        &self,
340        bind_key: &str,
341    ) -> Vec<(String, camel_api::security_policy::RouteSecurityPlan)> {
342        self.routes
343            .iter()
344            .filter_map(|(route_id, managed)| {
345                let plan = managed.compiled.security_plan.as_ref()?;
346                let bind = bind_key_from_uri(&managed.from_uri)?;
347                if bind.key == bind_key {
348                    Some((route_id.clone(), plan.clone()))
349                } else {
350                    None
351                }
352            })
353            .collect()
354    }
355
356    /// Return whether a listener for `bind_key` is already serving a route.
357    ///
358    /// This is deliberately based on the consumer handle, rather than on the
359    /// presence of a compiled route: the exposure gate applies only to late
360    /// registration against an already-running bind (Task 2.2). Startup and
361    /// resume perform their own gate checks in the lifecycle implementation.
362    fn bind_is_running(&self, bind_key: &str) -> bool {
363        self.routes.iter().any(|(_, managed)| {
364            bind_key_from_uri(&managed.from_uri).is_some_and(|bind| {
365                bind.key == bind_key && handle_is_running(&managed.consumer_handle)
366            })
367        })
368    }
369
370    /// Gate a route before it is inserted into a listener that is already
371    /// running. The candidate is included with all sibling plans so the gate
372    /// has the same aggregation semantics as the start path.
373    fn enforce_late_registration_gate(
374        &self,
375        from_uri: &str,
376        route_id: &str,
377        plan: Option<&camel_api::security_policy::RouteSecurityPlan>,
378    ) -> Result<(), CamelError> {
379        let Some(bind) = bind_key_from_uri(from_uri) else {
380            return Ok(());
381        };
382        if !self.bind_is_running(&bind.key) {
383            return Ok(());
384        }
385
386        let mut owned = self.plans_for_bind(&bind.key);
387        if let Some(plan) = plan {
388            owned.push((route_id.to_string(), plan.clone()));
389        }
390        let plans: Vec<(&str, &camel_api::security_policy::RouteSecurityPlan)> =
391            owned.iter().map(|(id, plan)| (id.as_str(), plan)).collect();
392        enforce_bind_exposure_gate(
393            &bind.key,
394            bind.loopback,
395            &plans,
396            self.bind_acks.acknowledged(&bind.key),
397        )
398        .map_err(|err| {
399            CamelError::RouteError(format!(
400                "late registration of route '{route_id}' rejected for bind '{}': {err}",
401                bind.key
402            ))
403        })
404    }
405
406    pub fn set_error_handler(&mut self, config: ErrorHandlerConfig) {
407        self.global_error_handler = Some(config);
408    }
409
410    /// Configure tracing for this route controller.
411    pub fn set_tracer_config(&mut self, config: &TracerConfig) {
412        self.tracing_enabled = config.enabled;
413        self.tracer_detail_level = config.detail_level.clone();
414        self.tracer_metrics = config.metrics_collector.clone();
415    }
416
417    fn build_producer_context(&self, route_id: &str) -> Result<ProducerContext, CamelError> {
418        let mut producer_ctx = ProducerContext::new().with_route_id(route_id);
419        if let Some(runtime) = self.runtime.as_ref().and_then(Weak::upgrade) {
420            producer_ctx = producer_ctx.with_runtime(runtime);
421        }
422        Ok(producer_ctx)
423    }
424
425    /// Create a transient [`RouteCompilerExt`] from this controller's fields.
426    fn route_compiler_ext(&self) -> RouteCompilerExt<'_> {
427        RouteCompilerExt {
428            registry: &self.registry,
429            languages: &self.languages,
430            beans: &self.beans,
431            function_invoker: &self.function_invoker,
432            tracing_enabled: self.tracing_enabled,
433            tracer_detail_level: &self.tracer_detail_level,
434            tracer_metrics: &self.tracer_metrics,
435            platform_service: &self.platform_service,
436            runtime: &self.runtime,
437            global_error_handler: &self.global_error_handler,
438            health_registry: &self.health_registry,
439            route_registry: &self.routes,
440            idempotent_repositories: Arc::clone(&self.idempotent_repositories),
441            claim_check_repositories: Arc::clone(&self.claim_check_repositories),
442            cache_repositories: Arc::clone(&self.cache_repositories),
443            intercept: &self.intercept,
444        }
445    }
446
447    /// Resolve BuilderSteps into BoxProcessors.
448    #[allow(dead_code)] // used by tests and may be needed for future split paths
449    pub(crate) fn resolve_steps(
450        &self,
451        steps: Vec<BuilderStep>,
452        producer_ctx: &ProducerContext,
453        registry: &Arc<std::sync::Mutex<Registry>>,
454        route_id: Option<&str>,
455        staging_mode: &super::step_resolution::FunctionStagingMode,
456    ) -> Result<Vec<CompiledStep>, CamelError> {
457        let component_ctx = Arc::new(ControllerComponentContext::new(
458            Arc::clone(registry),
459            Arc::clone(&self.languages),
460            self.tracer_metrics
461                .clone()
462                .unwrap_or_else(|| Arc::new(NoOpMetrics)),
463            Arc::clone(&self.platform_service),
464            self.health_registry(),
465            route_id.map(|s| s.to_string()),
466        ));
467        let rt: Arc<dyn camel_component_api::RuntimeObservability> =
468            Arc::clone(&component_ctx) as Arc<_>;
469
470        super::step_resolution::resolve_steps(
471            steps,
472            producer_ctx,
473            rt,
474            registry,
475            &self.languages,
476            &self.beans,
477            self.function_invoker.clone(),
478            component_ctx,
479            route_id,
480            staging_mode,
481            &self.idempotent_repositories,
482            &self.claim_check_repositories,
483            &self.cache_repositories,
484            self.intercept.clone(),
485        )
486    }
487
488    /// Add a route definition to the controller.
489    ///
490    /// Steps are resolved immediately using the registry.
491    ///
492    /// # Errors
493    ///
494    /// Returns an error if:
495    /// - A route with the same ID already exists
496    /// - Step resolution fails
497    pub async fn add_route(&mut self, definition: RouteDefinition) -> Result<(), CamelError> {
498        let route_id = definition.route_id().to_string();
499        let from_uri = definition.from_uri().to_string();
500
501        if self.routes.contains_key(&route_id) {
502            return Err(CamelError::RouteError(format!(
503                "duplicate route ID '{route_id}'"
504            )));
505        }
506
507        debug!(route_id = %route_id, "Adding route to controller");
508
509        let managed = match self.build_managed_route(
510            definition,
511            &super::step_resolution::FunctionStagingMode::DirectAdd,
512        ) {
513            Ok(managed) => managed,
514            Err(err) => {
515                self.discard_function_staging();
516                return Err(err);
517            }
518        };
519
520        // A running listener can observe a newly inserted route immediately.
521        // Gate the complete sibling set before committing function staging or
522        // inserting the route, so a rejected late registration is unreachable.
523        if let Err(err) = self.enforce_late_registration_gate(
524            &from_uri,
525            &route_id,
526            managed.compiled.security_plan.as_ref(),
527        ) {
528            self.discard_function_staging();
529            return Err(err);
530        }
531
532        if let Some(invoker) = &self.function_invoker
533            && let Err(err) = invoker.commit_staged().await
534        {
535            invoker.discard_staging(0);
536            return Err(CamelError::Config(err.to_string()));
537        }
538
539        self.routes
540            .insert(managed.definition.route_id().to_string(), managed);
541
542        self.endpoint_index.insert(&from_uri, &route_id);
543        // First successful route registration freezes the intercept rules:
544        // this route's pipeline compiled against the current rule set.
545        self.frozen = true;
546        Ok(())
547    }
548
549    pub(super) fn build_managed_route(
550        &self,
551        definition: RouteDefinition,
552        staging_mode: &super::step_resolution::FunctionStagingMode,
553    ) -> Result<ManagedRoute, CamelError> {
554        let route_id = definition.route_id().to_string();
555
556        let definition_info = definition.to_info();
557
558        // Security plan compilation (Task 1.8): consumer-backed routes get a
559        // plan BEFORE any consumer starts; a declared route that fails
560        // classification aborts staging (never a Public downgrade). Routes
561        // without a provider registry compile against an empty view.
562        let empty_providers;
563        let providers = match &definition.provider_registry {
564            Some(registry) => registry.as_ref(),
565            None => {
566                empty_providers = camel_auth::ProviderRegistry::new();
567                &empty_providers
568            }
569        };
570        let security_plan =
571            super::route_compiler_ext::compile_route_security_plan(&definition, providers)?;
572
573        let RouteDefinition {
574            from_uri,
575            steps,
576            error_handler,
577            circuit_breaker,
578            circuit_breaker_fallback,
579            security_policy,
580            security_authenticator,
581            provider_registry,
582            unit_of_work,
583            concurrency,
584            ..
585        } = definition;
586
587        let producer_ctx = self.build_producer_context(&route_id)?;
588
589        // N2: reject mixed Aggregate + Resequence top-level splits
590        assert_no_mixed_top_level_splits(&steps)?;
591
592        let (aggregate_split, processors_with_contracts) = self
593            .route_compiler_ext()
594            .detect_and_validate_route_split(steps, &producer_ctx, &route_id, staging_mode)?;
595        let mut lifecycle = super::route_helpers::collect_lifecycle(&processors_with_contracts);
596
597        // CB fallback (mirrors on_miss lifecycle packing): attach via the
598        // shared helper, then merge the fallback lifecycle handles into the
599        // route vec.
600        let (circuit_breaker, fallback_lifecycle) = self.route_compiler_ext().attach_cb_fallback(
601            circuit_breaker,
602            circuit_breaker_fallback,
603            &producer_ctx,
604            &route_id,
605            staging_mode,
606        )?;
607        lifecycle.extend(fallback_lifecycle);
608        let route_id_for_tracing = route_id.clone();
609        let eh_config = error_handler.or_else(|| self.global_error_handler.clone());
610        let transport = transport_from_uri(&from_uri);
611
612        let mut pipeline = build_eh_config_pipeline(
613            eh_config.as_ref(),
614            Arc::clone(&self.registry),
615            Arc::clone(&self.languages),
616            self.tracer_metrics.clone(),
617            Arc::clone(&self.platform_service),
618            self.health_registry(),
619            &route_id_for_tracing,
620            &producer_ctx,
621            processors_with_contracts,
622            self.tracing_enabled,
623            self.tracer_detail_level.clone(),
624            security_policy.clone(),
625            transport,
626            circuit_breaker,
627        )?;
628
629        let uow_counter = if let Some(uow_config) = &unit_of_work {
630            let component_ctx = Arc::new(ControllerComponentContext::new(
631                Arc::clone(&self.registry),
632                Arc::clone(&self.languages),
633                self.tracer_metrics
634                    .clone()
635                    .unwrap_or_else(|| Arc::new(NoOpMetrics)),
636                Arc::clone(&self.platform_service),
637                self.health_registry(),
638                Some(route_id.clone()),
639            ));
640            let rt: Arc<dyn camel_component_api::RuntimeObservability> =
641                Arc::clone(&component_ctx) as Arc<_>;
642            let (uow_layer, counter) = super::route_compiler_ext::resolve_uow_layer(
643                uow_config,
644                &producer_ctx,
645                rt,
646                component_ctx.as_ref(),
647                None,
648            )?;
649            pipeline = BoxProcessor::new(uow_layer.layer(pipeline));
650            Some(counter)
651        } else {
652            None
653        };
654
655        Ok(ManagedRoute {
656            definition: definition_info,
657            from_uri,
658            pipeline: super::pipeline_runtime::new_shared_pipeline_with_lifecycle(
659                pipeline, lifecycle,
660            ),
661            concurrency,
662            consumer_handle: None,
663            pipeline_handle: None,
664            consumer_cancel_token: CancellationToken::new(),
665            pipeline_cancel_token: CancellationToken::new(),
666            channel_sender: None,
667            in_flight: uow_counter,
668            drain_in_flight: Arc::new(std::sync::atomic::AtomicU64::new(0)),
669            aggregate_split,
670            agg_service: None,
671            compiled: route_runtime_state::CompiledRoute {
672                security_policy,
673                security_authenticator,
674                provider_registry,
675                security_plan,
676            },
677        })
678    }
679
680    pub async fn add_route_with_generation(
681        &mut self,
682        definition: RouteDefinition,
683        generation: u64,
684    ) -> Result<(), CamelError> {
685        let route_id = definition.route_id().to_string();
686        let from_uri = definition.from_uri().to_string();
687
688        if self.routes.contains_key(&route_id) {
689            return Err(CamelError::RouteError(format!(
690                "duplicate route ID '{route_id}'"
691            )));
692        }
693
694        debug!(route_id = %route_id, generation, "Adding route to controller with generation");
695
696        let managed = self.build_managed_route(
697            definition,
698            &super::step_resolution::FunctionStagingMode::HotReload { generation },
699        )?;
700
701        // Symmetric with `add_route`: an already-running bind must never
702        // observe an ungated insertion through the hot-reload path.
703        if let Err(err) = self.enforce_late_registration_gate(
704            &from_uri,
705            &route_id,
706            managed.compiled.security_plan.as_ref(),
707        ) {
708            self.discard_function_staging();
709            return Err(err);
710        }
711
712        self.routes.insert(route_id.clone(), managed);
713
714        self.endpoint_index.insert(&from_uri, &route_id);
715        Ok(())
716    }
717
718    pub async fn remove_route_preserving_functions(
719        &mut self,
720        route_id: &str,
721    ) -> Result<(), CamelError> {
722        let managed = self.routes.get(route_id).ok_or_else(|| {
723            CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
724        })?;
725        if handle_is_running(&managed.consumer_handle)
726            || handle_is_running(&managed.pipeline_handle)
727        {
728            return Err(CamelError::RouteError(format!(
729                "Route '{}' must be stopped before removal (current execution lifecycle: {})",
730                route_id,
731                inferred_lifecycle_label(managed)
732            )));
733        }
734        self.routes.remove(route_id);
735        if let Some(reg) = &self.health_registry {
736            reg.unregister_for_route(route_id);
737        }
738        self.endpoint_index.remove(route_id);
739        debug!(route_id = %route_id, "Route removed from controller (functions preserved for reload finalize)");
740        Ok(())
741    }
742
743    /// Compile a route definition into a processor pipeline, without adding it
744    /// to the controller. Used for validation and testing.
745    pub fn compile_route_definition(
746        &self,
747        def: RouteDefinition,
748    ) -> Result<BoxProcessor, CamelError> {
749        self.route_compiler_ext().compile_route_definition(def)
750    }
751
752    /// Compile a route definition with a specific generation (for hot-reload).
753    pub fn compile_route_definition_with_generation(
754        &self,
755        def: RouteDefinition,
756        generation: u64,
757    ) -> Result<BoxProcessor, CamelError> {
758        self.route_compiler_ext()
759            .compile_route_definition_with_generation(def, generation)
760    }
761
762    /// Compile a route definition into a [`CompiledPipeline`] (processor +
763    /// lifecycle handles). Used by the hot-reload Restart path so that
764    /// lifecycle handles are threaded through
765    /// [`swap_pipeline_raw`](Self::swap_pipeline_raw).
766    pub(crate) fn compile_route_definition_pipeline(
767        &self,
768        def: RouteDefinition,
769        generation: u64,
770    ) -> Result<CompiledPipeline, CamelError> {
771        self.route_compiler_ext()
772            .compile_route_definition_pipeline(def, generation)
773    }
774
775    /// Compile without function generation, returning full [`CompiledPipeline`].
776    ///
777    /// Oracle Fix 1: used by the stateless hot-reload path so that
778    /// lifecycle-bearing routes have their handles preserved.
779    pub(crate) fn compile_route_definition_dry_pipeline(
780        &self,
781        def: RouteDefinition,
782    ) -> Result<CompiledPipeline, CamelError> {
783        self.route_compiler_ext()
784            .compile_route_definition_dry_pipeline(def)
785    }
786
787    /// Remove a route from the controller map.
788    ///
789    /// The route **must** be stopped before removal (status `Stopped` or `Failed`).
790    /// Returns an error if the route is still running or does not exist.
791    /// Does not cancel any running tasks — call `stop_route` first.
792    pub async fn remove_route(&mut self, route_id: &str) -> Result<(), CamelError> {
793        let managed = self.routes.get(route_id).ok_or_else(|| {
794            CamelError::RouteError(format!("Route '{}' not found for removal", route_id))
795        })?;
796        if handle_is_running(&managed.consumer_handle)
797            || handle_is_running(&managed.pipeline_handle)
798        {
799            return Err(CamelError::RouteError(format!(
800                "Route '{}' must be stopped before removal (current execution lifecycle: {})",
801                route_id,
802                inferred_lifecycle_label(managed)
803            )));
804        }
805        if let Some(invoker) = &self.function_invoker {
806            for (id, rid) in self.collect_function_refs(route_id) {
807                if let Err(e) = invoker.unregister(&id, rid.as_deref()).await {
808                    warn!(route_id = %route_id, error = %e, "Failed to unregister function during route removal");
809                }
810            }
811        }
812        self.routes.remove(route_id);
813        if let Some(reg) = &self.health_registry {
814            reg.unregister_for_route(route_id);
815        }
816        self.endpoint_index.remove(route_id);
817        info!(route_id = %route_id, "Route removed from controller");
818        Ok(())
819    }
820
821    fn collect_function_refs(
822        &self,
823        route_id: &str,
824    ) -> Vec<(camel_api::FunctionId, Option<String>)> {
825        self.function_invoker
826            .as_ref()
827            .map(|invoker| invoker.function_refs_for_route(route_id))
828            .unwrap_or_default()
829    }
830
831    fn discard_function_staging(&self) {
832        if let Some(invoker) = &self.function_invoker {
833            invoker.discard_staging(0);
834        }
835    }
836
837    /// Returns the number of routes in the controller.
838    pub fn route_count(&self) -> usize {
839        self.routes.route_count()
840    }
841
842    pub fn in_flight_count(&self, route_id: &str) -> Option<u64> {
843        self.routes.in_flight_count(route_id)
844    }
845
846    /// Returns `true` if a route with the given ID exists.
847    pub fn route_exists(&self, route_id: &str) -> bool {
848        self.routes.route_exists(route_id)
849    }
850
851    /// Returns all route IDs.
852    pub fn route_ids(&self) -> Vec<String> {
853        self.routes.route_ids()
854    }
855
856    pub fn route_source_hash(&self, route_id: &str) -> Option<u64> {
857        self.routes.route_source_hash(route_id)
858    }
859
860    /// Returns route IDs that should auto-start, sorted by startup order (ascending).
861    pub fn auto_startup_route_ids(&self) -> Vec<String> {
862        self.routes.auto_startup_route_ids()
863    }
864
865    /// Returns route IDs sorted by shutdown order (startup order descending).
866    pub fn shutdown_route_ids(&self) -> Vec<String> {
867        self.routes.shutdown_route_ids()
868    }
869
870    /// Atomically swap the pipeline of a route (zero-downtime).
871    ///
872    /// In-flight requests finish with the old pipeline (kept alive by Arc).
873    /// New requests immediately use the new pipeline.
874    ///
875    /// ## Rejection policy
876    ///
877    /// Returns an error if the route has lifecycle-bearing steps or an active
878    /// aggregate — these require the **Restart path** (stop → swap → start).
879    ///
880    /// The caller (e.g. `reload_actions::apply_swap`) MUST catch this rejection
881    /// and fall back to:
882    /// 1. `stop_route_reload` — drain lifecycle, stop consumer
883    /// 2. `swap_pipeline_raw` — bypass the lifecycle check (route is stopped)
884    /// 3. `start_route_reload` — re-create consumer with the new pipeline
885    ///
886    /// This is the "reject, don't defer" policy (oracle Fix 3): the swap is
887    /// refused upfront rather than silently deferring or partially swapping.
888    pub fn swap_pipeline(
889        &self,
890        route_id: &str,
891        new_pipeline: BoxProcessor,
892    ) -> Result<(), CamelError> {
893        let managed = self
894            .routes
895            .get(route_id)
896            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
897
898        let assembly = managed.pipeline.load();
899        let has_lifecycle = !assembly.lifecycle.is_empty();
900
901        if has_lifecycle || managed.agg_service.is_some() {
902            warn!(
903                route_id = %route_id,
904                "Hot-swap rejected — route has lifecycle/agg steps; use Restart path"
905            );
906            return Err(CamelError::RouteError(format!(
907                "Route '{}' contains stateful steps (lifecycle-bearing). Hot-swap not supported — use restart.",
908                route_id
909            )));
910        }
911
912        drop(assembly);
913
914        if managed.aggregate_split.is_some() {
915            warn!(
916                route_id = %route_id,
917                "swap_pipeline: aggregate routes with timeout do not support hot-reload of pre/post segments"
918            );
919        }
920
921        super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, vec![]);
922        debug!(route_id = %route_id, "Pipeline swapped atomically");
923        Ok(())
924    }
925
926    /// Non-checking raw pipeline swap — bypasses lifecycle/aggregate rejection.
927    ///
928    /// Only for use after the route has been stopped (Restart path).
929    /// Does NOT check for lifecycle handles or aggregate service — the caller
930    /// is responsible for ensuring the route is safe to swap.
931    ///
932    /// Accepts `lifecycle` so that the new pipeline assembly records the
933    /// lifecycle handles from the compiled steps.  When the route is
934    /// subsequently stopped, these handles are drained.
935    pub(crate) fn swap_pipeline_raw(
936        &self,
937        route_id: &str,
938        new_pipeline: BoxProcessor,
939        lifecycle: Vec<Arc<dyn StepLifecycle>>,
940    ) -> Result<(), CamelError> {
941        let managed = self
942            .routes
943            .get(route_id)
944            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
945        super::pipeline_runtime::swap_pipeline_raw(&managed.pipeline, new_pipeline, lifecycle);
946        debug!(route_id = %route_id, "Pipeline swapped (raw — lifecycle bypass)");
947        Ok(())
948    }
949
950    /// Returns the from_uri of a route, if it exists.
951    pub fn route_from_uri(&self, route_id: &str) -> Option<String> {
952        self.routes.route_from_uri(route_id)
953    }
954
955    /// Return all route_ids that consume from the given source endpoint URI.
956    pub fn routes_for_endpoint(&self, uri: &str) -> Vec<String> {
957        self.endpoint_index.routes_for(uri)
958    }
959
960    /// Return all registered source endpoint URIs.
961    pub fn list_endpoint_uris(&self) -> Vec<String> {
962        self.endpoint_index.list_uris()
963    }
964
965    /// Get a clone of the current pipeline for a route.
966    ///
967    /// This is useful for testing and introspection.
968    /// Returns `None` if the route doesn't exist.
969    pub fn get_pipeline(&self, route_id: &str) -> Option<BoxProcessor> {
970        self.routes.get_pipeline(route_id)
971    }
972
973    /// Check whether the running route has lifecycle-bearing steps.
974    ///
975    /// Returns `false` when the route is missing.
976    pub(crate) fn route_has_lifecycle(&self, route_id: &str) -> bool {
977        self.routes
978            .get(route_id)
979            .map(|managed| !managed.pipeline.load().lifecycle.is_empty())
980            .unwrap_or(false)
981    }
982
983    /// Internal stop implementation that can set custom status.
984    pub(super) async fn stop_route_internal(&mut self, route_id: &str) -> Result<(), CamelError> {
985        self.routes.stop_route(route_id).await
986    }
987
988    pub async fn start_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
989        self.start_route(route_id).await
990    }
991
992    pub async fn stop_route_reload(&mut self, route_id: &str) -> Result<(), CamelError> {
993        self.stop_route(route_id).await
994    }
995}
996
997// ── Aggregator route helpers ──
998
999impl DefaultRouteController {
1000    /// Start a route with an aggregate split (pre-pipeline → aggregator → post-pipeline).
1001    ///
1002    /// Spawns a biased-select forward loop that routes exchanges through the
1003    /// pre-pipeline, aggregator, and post-pipeline in sequence, with late-exchange
1004    /// handling and force-completion on stop.
1005    #[allow(clippy::too_many_arguments)]
1006    pub(super) async fn start_aggregate_route(
1007        &mut self,
1008        route_id: &str,
1009        split: AggregateSplitInfo,
1010        consumer: Box<dyn Consumer>,
1011        consumer_ctx: ConsumerContext,
1012        mut rx: mpsc::Receiver<ExchangeEnvelope>,
1013        crash_notifier: Option<mpsc::Sender<CrashNotification>>,
1014        runtime_for_consumer: Option<Weak<dyn RuntimeHandle>>,
1015        tx_for_storage: mpsc::Sender<ExchangeEnvelope>,
1016        // Pipeline cancellation — a child of the managed route's pipeline_cancel_token.
1017        pipeline_cancel: CancellationToken,
1018        drain_in_flight: Arc<std::sync::atomic::AtomicU64>,
1019    ) -> Result<(), CamelError> {
1020        let (late_tx, late_rx) = mpsc::channel::<Exchange>(256);
1021
1022        let route_cancel_clone = pipeline_cancel.clone();
1023        let svc = AggregatorService::new(
1024            split.agg_config.clone(),
1025            late_tx,
1026            Arc::clone(&self.languages),
1027            route_cancel_clone,
1028        );
1029        let agg = Arc::new(svc);
1030
1031        let pipeline_cancel_for_monitor = pipeline_cancel.clone();
1032        // rc-jxkj cohort gate: owned by the forward loop — the envelope arm
1033        // parks dispatch until the startup cohort opens the gate.
1034        let mut cohort_rx = self.cohort.subscribe();
1035        let agg_for_monitor = Arc::clone(&agg);
1036
1037        {
1038            let managed = self
1039                .routes
1040                .get_mut(route_id)
1041                .expect("invariant: route must exist"); // allow-unwrap
1042            managed.agg_service = Some(Arc::clone(&agg));
1043        }
1044
1045        let late_rx = Arc::new(tokio::sync::Mutex::new(late_rx));
1046        let pre_pipeline = split.pre_pipeline;
1047        let post_pipeline = split.post_pipeline;
1048
1049        // Spawn biased select forward loop
1050        let pipeline_handle = tokio::spawn(async move {
1051            loop {
1052                tokio::select! {
1053                    biased;
1054
1055                    // Ungated by design (D3, rc-jxkj): late exchanges exist
1056                    // only after a dispatched envelope traversed the
1057                    // aggregator — transitively post-activation. Gating here
1058                    // would be dead code and a self-deadlock risk.
1059                    late_ex = async {
1060                        let mut rx = late_rx.lock().await;
1061                        rx.recv().await
1062                    } => {
1063                        match late_ex {
1064                            Some(ex) => {
1065                                let pipe = post_pipeline.load();
1066                                if let Err(e) = pipe.processor.clone_inner().oneshot(ex).await {
1067                                    tracing::warn!(error = %e, "late exchange post-pipeline failed");
1068                                }
1069                            }
1070                            None => return,
1071                        }
1072                    }
1073
1074                    envelope_opt = rx.recv() => {
1075                        match envelope_opt {
1076                            Some(envelope) => {
1077                                // rc-jxkj cohort gate: park dispatch until
1078                                // the startup cohort completes (same guard
1079                                // as the non-aggregate drain loops).
1080                                tokio::select! {
1081                                    _ = cohort_rx.wait_for(|open| *open) => {}
1082                                    _ = pipeline_cancel.cancelled() => {
1083                                        // Drop the envelope; reply_tx (if
1084                                        // any) resolves to ChannelClosed for
1085                                        // the send_and_wait waiter.
1086                                        // `continue`, not `return`: the token
1087                                        // is already cancelled, so the next
1088                                        // loop iteration lands in the biased
1089                                        // outer select's cancel arm below,
1090                                        // which runs the force_complete_all +
1091                                        // late_rx cleanup. A `return` would
1092                                        // skip that cleanup and silently
1093                                        // drop a pending bucket across a
1094                                        // stop→restart gate re-arm.
1095                                        continue;
1096                                    }
1097                                }
1098                                let ExchangeEnvelope { exchange, reply_tx } = envelope;
1099                                let _drain_guard = super::route_helpers::DrainGuard::new(Arc::clone(&drain_in_flight));
1100                                let pre_pipe = pre_pipeline.load();
1101                                let ex = match pre_pipe.processor.clone_inner().oneshot(exchange).await {
1102                                    Ok(ex) => ex,
1103                                    Err(e) => {
1104                                        if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1105                                        continue;
1106                                    }
1107                                };
1108
1109                                let ex = {
1110                                    let cloned_svc = agg.as_ref().clone();
1111                                    cloned_svc.oneshot(ex).await
1112                                };
1113
1114                                match ex {
1115                                    Ok(ex) => {
1116                                        if !is_pending(&ex) {
1117                                            let post_pipe = post_pipeline.load();
1118                                            let out = post_pipe.processor.clone_inner().oneshot(ex).await;
1119                                            if let Some(tx) = reply_tx { let _ = tx.send(out); }
1120                                        } else if let Some(tx) = reply_tx {
1121                                            let _ = tx.send(Ok(ex));
1122                                        }
1123                                    }
1124                                    Err(e) => {
1125                                        if let Some(tx) = reply_tx { let _ = tx.send(Err(e)); }
1126                                    }
1127                                }
1128                            }
1129                            None => return,
1130                        }
1131                    }
1132
1133                    _ = pipeline_cancel.cancelled() => {
1134                        agg.force_complete_all();
1135                        let mut rx_guard = late_rx.lock().await;
1136                        while let Ok(late_ex) = rx_guard.try_recv() {
1137                            let pipe = post_pipeline.load();
1138                            let _ = pipe.processor.clone_inner().oneshot(late_ex).await;
1139                        }
1140                        break;
1141                    }
1142                }
1143            }
1144        });
1145        #[cfg(test)]
1146        emit_start_route_event("pipeline_spawned", route_id);
1147
1148        // Start consumer after pipeline loop is spawned to avoid startup races
1149        // where consumers emit exchanges before the route pipeline begins polling.
1150        // rc-kh7c cleanup parity: capture the consumer's cancel token before
1151        // consumer_ctx moves into the task so the failure arm can stop child
1152        // tasks spawned by consumer.start().
1153        let consumer_cancel_for_cleanup = consumer_ctx.cancel_token();
1154        let (consumer_handle, startup_rx, watcher_inputs, outer_inputs) =
1155            super::consumer_management::spawn_consumer_task(
1156                route_id.to_string(),
1157                consumer,
1158                consumer_ctx,
1159                crash_notifier,
1160                runtime_for_consumer,
1161                false,
1162            );
1163
1164        // rc-w1u9: await consumer startup handshake for aggregate routes too
1165        // so bind failures surface as route-start errors.
1166        // For Immediate consumers the receiver is pre-resolved (rc-slvd).
1167        let startup_result =
1168            super::consumer_management::await_consumer_startup(startup_rx, "startup").await;
1169        // rc-kh7c: on failure, abort the orphaned consumer task and cancel the
1170        // pipeline so neither runs detached. The aggregate pipeline loop would
1171        // eventually self-clean via rx-drop + late_tx-drop, but cancelling
1172        // pipeline_cancel also triggers force_complete_all (aggregate cleanup).
1173        if let Err(e) = startup_result {
1174            consumer_handle.abort();
1175            pipeline_cancel_for_monitor.cancel();
1176            // Deliberate Explicit-failure cleanup parity with the trait start arm (rc-kh7c).
1177            consumer_cancel_for_cleanup.cancel();
1178            return Err(e);
1179        }
1180
1181        // Detached failure watcher for Immediate consumers (rc-slvd).
1182        if let Some(inputs) = watcher_inputs {
1183            super::consumer_management::spawn_failure_watcher(inputs);
1184        }
1185
1186        // Detached outer-task watcher for Explicit consumers (rc-a7rh):
1187        // spawned only after the handshake resolved Ok — rollback
1188        // terminations (abort-then-cancel above) happen before this point
1189        // and are never watched. Explicit consumers on aggregate routes
1190        // get identical coverage — no aggregate carve-out.
1191        if let Some(outer) = outer_inputs {
1192            super::consumer_management::spawn_outer_task_watcher(outer);
1193        }
1194
1195        // Extend the stored consumer handle through aggregate force-completion.
1196        // While this monitor drains pending buckets, handle_is_running still reports
1197        // the Route as running because forced exchanges may still be in post-pipeline.
1198        let force_on_stop = agg_for_monitor.config().force_completion_on_stop;
1199        let consumer_handle = tokio::spawn(async move {
1200            let _ = consumer_handle.await;
1201            if !pipeline_cancel_for_monitor.is_cancelled() {
1202                agg_for_monitor.force_complete_all();
1203                if force_on_stop {
1204                    pipeline_cancel_for_monitor.cancel();
1205                }
1206            }
1207        });
1208        #[cfg(test)]
1209        emit_start_route_event("consumer_spawned", route_id);
1210
1211        {
1212            let managed = self
1213                .routes
1214                .get_mut(route_id)
1215                .expect("invariant: route must exist"); // allow-unwrap
1216            managed.consumer_handle = Some(consumer_handle);
1217            managed.pipeline_handle = Some(pipeline_handle);
1218            managed.channel_sender = Some(tx_for_storage);
1219        }
1220
1221        info!(route_id = %route_id, "Route started (aggregate with timeout)");
1222        Ok(())
1223    }
1224
1225    /// Test-only: inject lifecycle handles into an existing route's pipeline
1226    /// assembly.  This makes the route lifecycle-bearing so that swap_pipeline
1227    /// rejects it, forcing callers (like reload_actions::apply_swap) to take
1228    /// the Restart path instead.
1229    #[cfg(test)]
1230    pub(crate) fn set_route_lifecycle_for_test(
1231        &mut self,
1232        route_id: &str,
1233        lifecycle: Vec<Arc<dyn StepLifecycle>>,
1234    ) -> Result<(), CamelError> {
1235        use super::pipeline_runtime::PipelineAssembly;
1236        use camel_api::SyncBoxProcessor;
1237        use std::sync::Arc;
1238
1239        let managed = self
1240            .routes
1241            .get_mut(route_id)
1242            .ok_or_else(|| CamelError::RouteError(format!("Route '{}' not found", route_id)))?;
1243        let old_processor = managed.pipeline.load().processor.clone_inner();
1244        managed.pipeline.store(Arc::new(PipelineAssembly::new(
1245            SyncBoxProcessor::new(old_processor),
1246            lifecycle,
1247        )));
1248        Ok(())
1249    }
1250}
1251
1252#[cfg(test)]
1253impl crate::hot_reload::ports::ReloadIntrospectionPort for DefaultRouteController {
1254    fn route_ids(&self) -> Vec<String> {
1255        self.route_ids() // inherent pub fn
1256    }
1257    fn route_from_uri(&self, route_id: &str) -> Option<String> {
1258        self.route_from_uri(route_id) // inherent pub fn
1259    }
1260    fn route_source_hash(&self, route_id: &str) -> Option<u64> {
1261        self.route_source_hash(route_id) // inherent pub fn
1262    }
1263}
1264
1265#[cfg(test)]
1266#[path = "route_controller_tests.rs"]
1267mod tests;
1268
1269#[cfg(test)]
1270#[path = "cohort_activation_regression.rs"]
1271mod cohort_activation_regression;