1use std::any::{Any, TypeId};
13use std::collections::HashMap;
14use std::sync::Arc;
15
16use crate::cache::Cache;
17use crate::time::{ClockSource, SystemClock};
18
19pub struct GlobalCacheEntry(pub Arc<dyn Cache>);
22
23use crate::actuator;
24use crate::authorization::{ForbiddenResponse, Policy, PolicyRegistry, Scope};
25#[cfg(feature = "ws")]
26use crate::channels::Channels;
27#[cfg(feature = "db")]
28use crate::db::DbState;
29use crate::middleware;
30#[cfg(feature = "presence")]
31use crate::presence::Presence;
32use crate::probe;
33#[cfg(feature = "ws")]
34use tokio_util::sync::CancellationToken;
35
36#[derive(Clone)]
57#[non_exhaustive]
58pub struct AppState {
59 pub(crate) extensions: Arc<std::sync::RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>>,
62
63 #[cfg(feature = "db")]
66 pub(crate) pool:
67 Option<diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>,
68
69 #[cfg(feature = "db")]
71 pub(crate) replica_pool:
72 Option<diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>,
73
74 #[cfg(feature = "db")]
78 pub(crate) shards: Option<crate::sharding::ShardSet>,
79
80 pub(crate) profile: Option<String>,
82
83 pub(crate) role: crate::config::ProcessRole,
90
91 pub(crate) started_at: std::time::Instant,
93
94 pub(crate) health_detailed: bool,
96
97 pub(crate) probes: probe::ProbeState,
99
100 pub(crate) metrics: middleware::MetricsCollector,
102
103 pub(crate) log_levels: actuator::LogLevels,
105
106 pub(crate) task_registry: actuator::TaskRegistry,
108 pub(crate) job_registry: actuator::JobRegistry,
110
111 pub(crate) config_props: actuator::ConfigProperties,
113
114 pub(crate) metrics_source_registry: actuator::MetricsSourceRegistry,
117
118 pub(crate) health_indicator_registry: actuator::HealthIndicatorRegistry,
121
122 #[cfg(feature = "ws")]
127 pub(crate) channels: Channels,
128
129 #[cfg(feature = "presence")]
134 pub(crate) presence: Presence,
135
136 #[cfg(feature = "ws")]
141 pub(crate) shutdown: CancellationToken,
142
143 pub(crate) policy_registry: PolicyRegistry,
146
147 pub(crate) forbidden_response: ForbiddenResponse,
151
152 pub(crate) auth_session_key: String,
157
158 pub(crate) shared_cache: Option<Arc<dyn Cache>>,
161
162 pub(crate) clock: Arc<dyn ClockSource>,
165
166 pub(crate) app_id: u64,
176}
177
178static NEXT_APP_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
181
182impl crate::authorization::ProvideAuthorizationState for AppState {
183 fn policy_registry(&self) -> &crate::authorization::PolicyRegistry {
184 &self.policy_registry
185 }
186
187 fn auth_session_key(&self) -> &str {
188 &self.auth_session_key
189 }
190
191 fn forbidden_response(&self) -> &crate::authorization::ForbiddenResponse {
192 &self.forbidden_response
193 }
194
195 #[cfg(feature = "db")]
196 fn pool(
197 &self,
198 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
199 {
200 self.pool.as_ref()
201 }
202}
203
204impl AppState {
205 pub fn insert_extension<T>(&self, value: T)
214 where
215 T: Any + Send + Sync + 'static,
216 {
217 self.extensions
218 .write()
219 .expect("app state extension lock poisoned")
220 .insert(TypeId::of::<T>(), Arc::new(value));
221 }
222
223 #[must_use]
232 pub fn extension<T>(&self) -> Option<Arc<T>>
233 where
234 T: Any + Send + Sync + 'static,
235 {
236 self.extensions
237 .read()
238 .expect("app state extension lock poisoned")
239 .get(&TypeId::of::<T>())
240 .cloned()
241 .and_then(|value| Arc::downcast::<T>(value).ok())
242 }
243
244 pub fn extension_or_insert_with<T>(&self, f: impl FnOnce() -> T) -> Arc<T>
252 where
253 T: Any + Send + Sync + 'static,
254 {
255 if let Some(existing) = self.extension::<T>() {
256 return existing;
257 }
258 let mut map = self
259 .extensions
260 .write()
261 .expect("app state extension lock poisoned");
262 if let Some(existing) = map
263 .get(&TypeId::of::<T>())
264 .cloned()
265 .and_then(|value| Arc::downcast::<T>(value).ok())
266 {
267 return existing;
268 }
269 let arc = Arc::new(f());
270 map.insert(TypeId::of::<T>(), arc.clone() as Arc<dyn Any + Send + Sync>);
271 arc
272 }
273
274 #[cfg(feature = "reporting")]
281 #[must_use]
282 pub(crate) fn error_reporters(
283 &self,
284 ) -> Vec<std::sync::Arc<dyn crate::reporting::ErrorReporter>> {
285 self.extension::<crate::reporting::RegisteredReporters>()
286 .map(|reporters| reporters.0.clone())
287 .unwrap_or_default()
288 }
289
290 #[cfg(feature = "db")]
292 #[must_use]
293 pub const fn pool(
294 &self,
295 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
296 {
297 self.pool.as_ref()
298 }
299
300 #[cfg(feature = "db")]
302 #[must_use]
303 pub const fn replica_pool(
304 &self,
305 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
306 {
307 self.replica_pool.as_ref()
308 }
309
310 #[cfg(feature = "db")]
316 #[must_use]
317 pub const fn shards(&self) -> Option<&crate::sharding::ShardSet> {
318 self.shards.as_ref()
319 }
320
321 #[cfg(feature = "db")]
323 #[must_use]
324 pub fn read_pool(
325 &self,
326 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
327 {
328 if self.replica_pool.is_some() && self.probes.should_route_reads_to_replica() {
329 self.replica_pool.as_ref()
330 } else if self.replica_pool.is_some() && self.probes.should_fallback_reads_to_primary() {
331 self.pool.as_ref()
332 } else if self.replica_pool.is_some() {
333 None
334 } else {
335 self.pool.as_ref()
336 }
337 }
338
339 #[must_use]
341 pub const fn metrics(&self) -> &middleware::MetricsCollector {
342 &self.metrics
343 }
344
345 #[must_use]
347 pub const fn log_levels(&self) -> &actuator::LogLevels {
348 &self.log_levels
349 }
350
351 #[must_use]
353 pub const fn task_registry(&self) -> &actuator::TaskRegistry {
354 &self.task_registry
355 }
356
357 #[must_use]
359 pub const fn job_registry(&self) -> &actuator::JobRegistry {
360 &self.job_registry
361 }
362
363 #[must_use]
365 pub const fn config_props(&self) -> &actuator::ConfigProperties {
366 &self.config_props
367 }
368
369 #[must_use]
371 pub const fn metrics_source_registry(&self) -> &actuator::MetricsSourceRegistry {
372 &self.metrics_source_registry
373 }
374
375 #[must_use]
377 pub const fn health_indicator_registry(&self) -> &actuator::HealthIndicatorRegistry {
378 &self.health_indicator_registry
379 }
380
381 #[must_use]
386 pub fn config(&self) -> crate::config::AutumnConfig {
387 self.extension::<crate::config::AutumnConfig>()
388 .map_or_else(crate::config::AutumnConfig::default, |arc| (*arc).clone())
389 }
390
391 pub(crate) fn next_app_id() -> u64 {
398 NEXT_APP_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
399 }
400
401 #[must_use]
406 pub(crate) const fn app_id(&self) -> u64 {
407 self.app_id
408 }
409
410 #[must_use]
412 pub const fn probes(&self) -> &probe::ProbeState {
413 &self.probes
414 }
415
416 pub fn mark_startup_complete(&self) {
418 self.probes.mark_startup_complete();
419 }
420
421 pub fn begin_shutdown(&self) {
423 self.probes.begin_shutdown();
424 }
425
426 #[cfg(feature = "db")]
428 #[must_use]
429 pub fn with_pool(
430 mut self,
431 pool: diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>,
432 ) -> Self {
433 self.pool = Some(pool);
434 self
435 }
436
437 #[cfg(feature = "db")]
439 #[must_use]
440 pub fn with_replica_pool(
441 mut self,
442 pool: diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>,
443 ) -> Self {
444 self.replica_pool = Some(pool);
445 self
446 }
447
448 #[cfg(feature = "db")]
450 #[must_use]
451 pub fn with_shards(mut self, shards: crate::sharding::ShardSet) -> Self {
452 self.shards = Some(shards);
453 self
454 }
455
456 #[must_use]
458 pub fn with_extension<T>(self, value: T) -> Self
459 where
460 T: Any + Send + Sync + 'static,
461 {
462 self.insert_extension(value);
463 self
464 }
465
466 #[must_use]
473 pub fn cache(&self) -> Option<Arc<dyn Cache>> {
474 self.extension::<GlobalCacheEntry>()
475 .map(|e| e.0.clone())
476 .or_else(|| self.shared_cache.clone())
477 }
478
479 #[must_use]
481 pub fn with_cache(mut self, cache: Arc<dyn Cache>) -> Self {
482 self.shared_cache = Some(cache);
483 self
484 }
485
486 #[must_use]
492 pub fn clock(&self) -> &dyn ClockSource {
493 self.clock.as_ref()
494 }
495
496 #[must_use]
498 pub fn with_clock(mut self, clock: Arc<dyn ClockSource>) -> Self {
499 self.clock = clock;
500 self
501 }
502
503 pub fn set_cache(&self, cache: Arc<dyn Cache>) {
508 crate::cache::set_global_cache(cache.clone());
509 self.insert_extension(GlobalCacheEntry(cache));
510 }
511
512 #[must_use]
514 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
515 self.profile = Some(profile.into());
516 self
517 }
518
519 #[must_use]
521 pub const fn policy_registry(&self) -> &PolicyRegistry {
522 &self.policy_registry
523 }
524
525 #[must_use]
527 pub fn policy<R: Send + Sync + 'static>(&self) -> Option<std::sync::Arc<dyn Policy<R>>> {
528 self.policy_registry.policy::<R>()
529 }
530
531 #[must_use]
533 pub fn scope<R: Send + Sync + 'static>(&self) -> Option<std::sync::Arc<dyn Scope<R>>> {
534 self.policy_registry.scope::<R>()
535 }
536
537 #[must_use]
541 pub const fn forbidden_response(&self) -> ForbiddenResponse {
542 self.forbidden_response
543 }
544
545 #[must_use]
548 pub fn auth_session_key(&self) -> &str {
549 &self.auth_session_key
550 }
551
552 #[doc(hidden)]
554 #[must_use]
555 pub const fn with_forbidden_response(mut self, value: ForbiddenResponse) -> Self {
556 self.forbidden_response = value;
557 self
558 }
559
560 #[doc(hidden)]
562 #[must_use]
563 pub fn with_auth_session_key(mut self, value: impl Into<String>) -> Self {
564 self.auth_session_key = value.into();
565 self
566 }
567
568 #[doc(hidden)]
570 #[must_use]
571 pub fn with_startup_complete(self, startup_complete: bool) -> Self {
572 self.probes.set_startup_complete(startup_complete);
573 self
574 }
575
576 #[doc(hidden)]
578 #[must_use]
579 pub fn with_draining(self, draining: bool) -> Self {
580 self.probes.set_draining(draining);
581 self
582 }
583
584 #[must_use]
586 pub fn profile(&self) -> &str {
587 self.profile.as_deref().unwrap_or("default")
588 }
589
590 #[must_use]
612 pub const fn role(&self) -> crate::config::ProcessRole {
613 self.role
614 }
615
616 #[must_use]
618 pub fn uptime(&self) -> std::time::Duration {
619 self.started_at.elapsed()
620 }
621
622 #[must_use]
624 pub fn uptime_display(&self) -> String {
625 let secs = self.started_at.elapsed().as_secs();
626 if secs < 60 {
627 format!("{secs}s")
628 } else if secs < 3600 {
629 format!("{}m {}s", secs / 60, secs % 60)
630 } else {
631 let hours = secs / 3600;
632 let mins = (secs % 3600) / 60;
633 format!("{hours}h {mins}m")
634 }
635 }
636
637 #[cfg(feature = "ws")]
641 #[must_use]
642 pub const fn channels(&self) -> &Channels {
643 &self.channels
644 }
645
646 #[cfg(feature = "presence")]
648 #[must_use]
649 pub const fn presence(&self) -> &Presence {
650 &self.presence
651 }
652
653 #[cfg(feature = "ws")]
655 #[must_use]
656 pub fn broadcast(&self) -> crate::channels::Broadcast {
657 self.channels.broadcast()
658 }
659
660 #[cfg(feature = "ws")]
665 #[must_use]
666 pub fn shutdown_token(&self) -> CancellationToken {
667 self.shutdown.child_token()
668 }
669
670 #[cfg(feature = "ws")]
672 #[doc(hidden)]
673 pub fn trigger_shutdown_for_test(&self) {
674 self.begin_shutdown();
675 self.shutdown.cancel();
676 }
677
678 #[doc(hidden)]
680 pub fn set_startup_complete_for_test(&self, startup_complete: bool) {
681 self.probes.set_startup_complete(startup_complete);
682 }
683
684 #[doc(hidden)]
686 pub fn set_draining_for_test(&self, draining: bool) {
687 self.probes.set_draining(draining);
688 }
689
690 #[doc(hidden)]
692 pub fn begin_shutdown_for_test(&self) {
693 self.set_draining_for_test(true);
694 }
695
696 #[must_use]
702 pub fn detached() -> Self {
703 #[cfg(feature = "ws")]
704 let channels = Channels::new(32);
705 Self {
706 extensions: Arc::new(std::sync::RwLock::new(HashMap::new())),
707 #[cfg(feature = "db")]
708 pool: None,
709 #[cfg(feature = "db")]
710 replica_pool: None,
711 #[cfg(feature = "db")]
712 shards: None,
713 profile: None,
714 role: crate::config::ProcessRole::Combined,
715 started_at: std::time::Instant::now(),
716 health_detailed: true,
717 probes: probe::ProbeState::ready_for_test(),
718 metrics: middleware::MetricsCollector::new(),
719 log_levels: actuator::LogLevels::new("info"),
720 task_registry: actuator::TaskRegistry::new(),
721 job_registry: actuator::JobRegistry::new(),
722 config_props: actuator::ConfigProperties::default(),
723 metrics_source_registry: actuator::MetricsSourceRegistry::new(),
724 health_indicator_registry: actuator::HealthIndicatorRegistry::new(),
725 #[cfg(feature = "presence")]
726 presence: Presence::new(channels.clone()),
727 #[cfg(feature = "ws")]
728 channels,
729 #[cfg(feature = "ws")]
730 shutdown: CancellationToken::new(),
731 policy_registry: PolicyRegistry::default(),
732 forbidden_response: ForbiddenResponse::default(),
733 auth_session_key: "user_id".to_owned(),
734 shared_cache: None,
735 clock: Arc::new(SystemClock),
736 app_id: Self::next_app_id(),
737 }
738 }
739
740 #[allow(dead_code)]
743 #[must_use]
744 pub fn for_test() -> Self {
745 Self::detached()
746 }
747}
748
749#[cfg(feature = "db")]
750impl DbState for AppState {
751 fn metrics(&self) -> Option<&crate::middleware::MetricsCollector> {
752 Some(&self.metrics)
753 }
754
755 fn pool(
756 &self,
757 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
758 {
759 self.pool.as_ref()
760 }
761
762 fn replica_pool(
763 &self,
764 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
765 {
766 self.replica_pool.as_ref()
767 }
768
769 fn read_pool(
770 &self,
771 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
772 {
773 Self::read_pool(self)
774 }
775
776 fn shards(&self) -> Option<&crate::sharding::ShardSet> {
777 self.shards.as_ref()
778 }
779
780 fn db_interceptors(
781 &self,
782 ) -> Vec<std::sync::Arc<dyn crate::interceptor::DbConnectionInterceptor>> {
783 self.extension::<Arc<dyn crate::interceptor::DbConnectionInterceptor>>()
784 .map(|arc| vec![(*arc).clone()])
785 .unwrap_or_default()
786 }
787 fn statement_timeout(&self) -> Option<std::time::Duration> {
788 self.extension::<crate::config::AutumnConfig>()
789 .and_then(|cfg| cfg.database.statement_timeout)
790 }
791
792 fn slow_query_threshold(&self) -> std::time::Duration {
793 self.extension::<crate::config::AutumnConfig>().map_or_else(
794 || std::time::Duration::from_millis(500),
795 |cfg| cfg.database.slow_query_threshold,
796 )
797 }
798}
799
800impl crate::probe::ProvideProbeState for AppState {
801 fn probes(&self) -> &crate::probe::ProbeState {
802 &self.probes
803 }
804
805 fn health_detailed(&self) -> bool {
806 self.health_detailed
807 }
808
809 fn profile(&self) -> &str {
810 self.profile()
811 }
812
813 fn uptime_display(&self) -> String {
814 self.uptime_display()
815 }
816
817 #[cfg(feature = "db")]
818 fn pool(
819 &self,
820 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
821 {
822 self.pool.as_ref()
823 }
824
825 #[cfg(feature = "db")]
826 fn replica_pool(
827 &self,
828 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
829 {
830 self.replica_pool.as_ref()
831 }
832
833 fn health_indicator_registry(&self) -> Option<&crate::actuator::HealthIndicatorRegistry> {
834 Some(&self.health_indicator_registry)
835 }
836}
837
838impl crate::actuator::ProvideActuatorState for AppState {
839 fn metrics(&self) -> &crate::middleware::MetricsCollector {
840 &self.metrics
841 }
842
843 fn log_levels(&self) -> &crate::actuator::LogLevels {
844 &self.log_levels
845 }
846
847 fn task_registry(&self) -> &crate::actuator::TaskRegistry {
848 &self.task_registry
849 }
850
851 fn job_registry(&self) -> &crate::actuator::JobRegistry {
852 &self.job_registry
853 }
854
855 fn config_props(&self) -> &crate::actuator::ConfigProperties {
856 &self.config_props
857 }
858
859 fn profile(&self) -> &str {
860 self.profile()
861 }
862
863 fn uptime_display(&self) -> String {
864 self.uptime_display()
865 }
866
867 fn metrics_source_registry(&self) -> Option<&crate::actuator::MetricsSourceRegistry> {
868 Some(&self.metrics_source_registry)
869 }
870
871 fn health_indicator_registry(&self) -> Option<&crate::actuator::HealthIndicatorRegistry> {
872 Some(&self.health_indicator_registry)
873 }
874
875 fn health_detailed(&self) -> bool {
876 self.health_detailed
877 }
878
879 fn deploy_version(&self) -> String {
880 self.extension::<crate::canary::CanaryState>().map_or_else(
881 || crate::canary::STABLE.to_owned(),
882 |c| c.version().to_owned(),
883 )
884 }
885
886 #[cfg(feature = "ws")]
887 fn channels(&self) -> &crate::channels::Channels {
888 &self.channels
889 }
890
891 #[cfg(feature = "ws")]
892 fn shutdown_token(&self) -> tokio_util::sync::CancellationToken {
893 self.shutdown_token()
894 }
895
896 #[cfg(feature = "db")]
897 fn pool(
898 &self,
899 ) -> Option<&diesel_async::pooled_connection::deadpool::Pool<crate::db::RuntimeConnection>>
900 {
901 self.pool.as_ref()
902 }
903
904 #[cfg(feature = "db")]
905 fn shards(&self) -> Option<&crate::sharding::ShardSet> {
906 self.shards.as_ref()
907 }
908 #[cfg(feature = "http-client")]
915 fn webhook_outbound(&self) -> Option<crate::webhook_outbound::WebhookOutboundManager> {
916 self.extension::<crate::webhook_outbound::WebhookOutboundManager>()
917 .map(|x| (*x).clone())
918 }
919
920 fn log_buffer(&self) -> Option<crate::log::capture::LogBuffer> {
921 self.extension::<crate::log::capture::LogBuffer>()
922 .map(|x| (*x).clone())
923 }
924}
925
926impl std::fmt::Debug for AppState {
927 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
928 let mut s = f.debug_struct("AppState");
929 #[cfg(feature = "db")]
930 s.field(
931 "pool",
932 &self
933 .pool
934 .as_ref()
935 .map(|p| format!("Pool(max={})", p.status().max_size)),
936 );
937 s.field(
938 "extensions",
939 &self
940 .extensions
941 .read()
942 .map_or(0, |extensions| extensions.len()),
943 );
944 s.field("profile", &self.profile)
945 .field("started_at", &self.started_at)
946 .field("health_detailed", &self.health_detailed)
947 .field("probes", &self.probes)
948 .field("metrics", &"MetricsCollector")
949 .field("log_levels", &"LogLevels")
950 .field("task_registry", &"TaskRegistry")
951 .finish_non_exhaustive()
952 }
953}
954
955#[cfg(test)]
956mod tests {
957 use super::*;
958 #[cfg(feature = "db")]
959 use crate::config;
960 #[cfg(feature = "db")]
961 use crate::db;
962
963 #[test]
964 fn app_state_debug_without_pool() {
965 let state = AppState::for_test().with_profile("dev");
966 let debug = format!("{state:?}");
967 assert!(debug.contains("AppState"));
968 assert!(debug.contains("dev"));
969 }
970
971 #[cfg(feature = "db")]
972 #[test]
973 fn app_state_debug_with_pool() {
974 let config = config::DatabaseConfig {
975 url: Some("postgres://localhost/test".into()),
976 pool_size: 5,
977 ..Default::default()
978 };
979 let pool = db::create_pool(&config).unwrap().unwrap();
980 let state = AppState::for_test().with_pool(pool);
981 let debug = format!("{state:?}");
982 assert!(debug.contains("Pool(max=5)"));
983 }
984
985 #[cfg(feature = "db")]
986 #[test]
987 fn database_topology_state_exposes_replica_as_read_pool() {
988 let primary_config = config::DatabaseConfig {
989 url: Some("postgres://localhost/primary".into()),
990 pool_size: 5,
991 ..Default::default()
992 };
993 let replica_config = config::DatabaseConfig {
994 url: Some("postgres://localhost/replica".into()),
995 pool_size: 2,
996 ..Default::default()
997 };
998 let primary = db::create_pool(&primary_config).unwrap().unwrap();
999 let replica = db::create_pool(&replica_config).unwrap().unwrap();
1000
1001 let state = AppState::for_test()
1002 .with_pool(primary)
1003 .with_replica_pool(replica);
1004
1005 assert_eq!(state.pool().expect("primary pool").status().max_size, 5);
1006 assert_eq!(
1007 state
1008 .replica_pool()
1009 .expect("replica pool")
1010 .status()
1011 .max_size,
1012 2
1013 );
1014 assert_eq!(state.read_pool().expect("read pool").status().max_size, 2);
1015 }
1016
1017 #[cfg(feature = "db")]
1018 #[test]
1019 fn read_pool_uses_primary_when_replica_is_unready_and_policy_allows_fallback() {
1020 let primary_config = config::DatabaseConfig {
1021 url: Some("postgres://localhost/primary".into()),
1022 pool_size: 5,
1023 ..Default::default()
1024 };
1025 let replica_config = config::DatabaseConfig {
1026 url: Some("postgres://localhost/replica".into()),
1027 pool_size: 2,
1028 ..Default::default()
1029 };
1030 let primary = db::create_pool(&primary_config).unwrap().unwrap();
1031 let replica = db::create_pool(&replica_config).unwrap().unwrap();
1032
1033 let state = AppState::for_test()
1034 .with_pool(primary)
1035 .with_replica_pool(replica);
1036 state
1037 .probes()
1038 .configure_replica_dependency(config::ReplicaFallback::Primary);
1039 state
1040 .probes()
1041 .mark_replica_unready("replica migrations lag primary");
1042
1043 assert_eq!(state.read_pool().expect("read pool").status().max_size, 5);
1044 assert_eq!(
1045 db::DbState::read_pool(&state)
1046 .expect("trait read pool")
1047 .status()
1048 .max_size,
1049 5
1050 );
1051 }
1052
1053 #[cfg(feature = "db")]
1054 #[test]
1055 fn read_pool_does_not_route_to_unready_replica_when_policy_fails_readiness() {
1056 let primary_config = config::DatabaseConfig {
1057 url: Some("postgres://localhost/primary".into()),
1058 pool_size: 5,
1059 ..Default::default()
1060 };
1061 let replica_config = config::DatabaseConfig {
1062 url: Some("postgres://localhost/replica".into()),
1063 pool_size: 2,
1064 ..Default::default()
1065 };
1066 let primary = db::create_pool(&primary_config).unwrap().unwrap();
1067 let replica = db::create_pool(&replica_config).unwrap().unwrap();
1068
1069 let state = AppState::for_test()
1070 .with_pool(primary)
1071 .with_replica_pool(replica);
1072 state
1073 .probes()
1074 .configure_replica_dependency(config::ReplicaFallback::FailReadiness);
1075 state
1076 .probes()
1077 .mark_replica_unready("replica connection failed");
1078
1079 assert!(state.read_pool().is_none());
1080 }
1081
1082 #[cfg(feature = "db")]
1083 #[tokio::test]
1084 async fn readiness_fails_when_app_state_replica_is_unready_and_policy_is_fail_readiness() {
1085 let primary_config = config::DatabaseConfig {
1086 url: Some("postgres://localhost/primary".into()),
1087 pool_size: 5,
1088 ..Default::default()
1089 };
1090 let replica_config = config::DatabaseConfig {
1091 url: Some("postgres://localhost/replica".into()),
1092 pool_size: 2,
1093 ..Default::default()
1094 };
1095 let primary = db::create_pool(&primary_config).unwrap().unwrap();
1096 let replica = db::create_pool(&replica_config).unwrap().unwrap();
1097
1098 let state = AppState::for_test()
1099 .with_pool(primary)
1100 .with_replica_pool(replica);
1101 state
1102 .probes()
1103 .configure_replica_dependency(config::ReplicaFallback::FailReadiness);
1104 state
1105 .probes()
1106 .mark_replica_unready("replica migrations lag primary");
1107
1108 let (status, _) = crate::probe::readiness_response(&state).await;
1109
1110 assert_eq!(status, http::StatusCode::SERVICE_UNAVAILABLE);
1111 }
1112
1113 #[test]
1114 fn detached_state_starts_without_profile() {
1115 let state = AppState::detached();
1116
1117 assert_eq!(state.profile(), "default");
1118 }
1119
1120 fn require_clone<T: Clone>(t: &T) -> T {
1121 t.clone()
1122 }
1123
1124 #[test]
1125 fn app_state_is_clone() {
1126 let state = AppState::for_test();
1127 let _cloned = require_clone(&state);
1128 }
1129
1130 #[test]
1131 fn app_state_profile_accessor() {
1132 let state = AppState::for_test().with_profile("staging");
1133 assert_eq!(state.profile(), "staging");
1134 }
1135
1136 #[test]
1137 fn app_state_deploy_version_defaults_to_stable() {
1138 use crate::actuator::ProvideActuatorState;
1139 let state = AppState::for_test();
1140 assert_eq!(state.deploy_version(), crate::canary::STABLE);
1141 }
1142
1143 #[test]
1144 fn app_state_deploy_version_reads_canary_extension() {
1145 use crate::actuator::ProvideActuatorState;
1146 let state = AppState::for_test();
1147 state.insert_extension(crate::canary::CanaryState::new(crate::canary::CANARY));
1148 assert_eq!(state.deploy_version(), crate::canary::CANARY);
1149 }
1150
1151 #[test]
1152 fn app_state_profile_default() {
1153 let state = AppState::for_test();
1154 assert_eq!(state.profile(), "default");
1155 }
1156
1157 #[test]
1158 fn app_state_uptime_display() {
1159 let state = AppState::for_test();
1160 let display = state.uptime_display();
1161 assert!(
1162 display.contains('s'),
1163 "uptime should contain 's': {display}"
1164 );
1165 }
1166
1167 #[test]
1168 fn app_state_accessors() {
1169 let state = AppState::for_test();
1170
1171 let _metrics = state.metrics();
1173 let _log_levels = state.log_levels();
1174 let _task_registry = state.task_registry();
1175 let _config_props = state.config_props();
1176
1177 #[cfg(feature = "db")]
1178 {
1179 let _pool = state.pool();
1180 }
1181 let _missing = state.extension::<String>();
1182 }
1183
1184 #[test]
1185 fn app_state_runtime_extensions_round_trip() {
1186 let state = AppState::for_test();
1187 state.insert_extension(String::from("haunted"));
1188
1189 let stored = state
1190 .extension::<String>()
1191 .expect("runtime extension should be installed");
1192
1193 assert_eq!(stored.as_str(), "haunted");
1194 }
1195}