1use crate::infrastructure::di::ServiceContainer;
3#[cfg(feature = "multi-tenant")]
4use crate::infrastructure::web::tenant_api::*;
5use crate::{
6 domain::repositories::TenantRepository,
7 infrastructure::{
8 cluster::{
9 ClusterManager, ClusterMember, GeoReplicationManager, GeoSyncRequest, VoteRequest,
10 },
11 replication::{WalReceiver, WalShipper},
12 security::{
13 auth::AuthManager,
14 middleware::{AuthState, RateLimitState, auth_middleware, rate_limit_middleware},
15 rate_limit::RateLimiter,
16 },
17 web::{audit_api::*, auth_api::*, config_api::*},
18 },
19 store::EventStore,
20};
21use axum::{
22 Json, Router,
23 extract::{Path, State},
24 middleware,
25 response::IntoResponse,
26 routing::{delete, get, post, put},
27};
28use std::sync::Arc;
29use tower_http::{
30 cors::{Any, CorsLayer},
31 trace::TraceLayer,
32};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
36#[serde(rename_all = "lowercase")]
37pub enum NodeRole {
38 Leader,
39 Follower,
40}
41
42impl NodeRole {
43 pub fn from_env() -> Self {
49 if let Ok(role) = std::env::var("ALLSOURCE_ROLE") {
50 match role.to_lowercase().as_str() {
51 "follower" => return NodeRole::Follower,
52 "leader" => return NodeRole::Leader,
53 other => {
54 tracing::warn!(
55 "Unknown ALLSOURCE_ROLE value '{}', defaulting to leader",
56 other
57 );
58 return NodeRole::Leader;
59 }
60 }
61 }
62 if let Ok(read_only) = std::env::var("ALLSOURCE_READ_ONLY")
63 && (read_only == "true" || read_only == "1")
64 {
65 return NodeRole::Follower;
66 }
67 NodeRole::Leader
68 }
69
70 pub fn is_follower(self) -> bool {
71 self == NodeRole::Follower
72 }
73
74 fn to_u8(self) -> u8 {
75 match self {
76 NodeRole::Leader => 0,
77 NodeRole::Follower => 1,
78 }
79 }
80
81 fn from_u8(v: u8) -> Self {
82 match v {
83 1 => NodeRole::Follower,
84 _ => NodeRole::Leader,
85 }
86 }
87}
88
89impl std::fmt::Display for NodeRole {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 match self {
92 NodeRole::Leader => write!(f, "leader"),
93 NodeRole::Follower => write!(f, "follower"),
94 }
95 }
96}
97
98#[derive(Clone)]
103pub struct AtomicNodeRole(Arc<std::sync::atomic::AtomicU8>);
104
105impl AtomicNodeRole {
106 pub fn new(role: NodeRole) -> Self {
107 Self(Arc::new(std::sync::atomic::AtomicU8::new(role.to_u8())))
108 }
109
110 pub fn load(&self) -> NodeRole {
111 NodeRole::from_u8(self.0.load(std::sync::atomic::Ordering::Relaxed))
112 }
113
114 pub fn store(&self, role: NodeRole) {
115 self.0
116 .store(role.to_u8(), std::sync::atomic::Ordering::Relaxed);
117 }
118}
119
120#[derive(Clone)]
122pub struct AppState {
123 pub store: Arc<EventStore>,
124 pub auth_manager: Arc<AuthManager>,
125 pub tenant_repo: Arc<dyn TenantRepository>,
126 pub service_container: ServiceContainer,
128 pub role: AtomicNodeRole,
130 pub wal_shipper: Arc<tokio::sync::RwLock<Option<Arc<WalShipper>>>>,
133 pub wal_receiver: Option<Arc<WalReceiver>>,
135 pub replication_port: u16,
137 pub cluster_manager: Option<Arc<ClusterManager>>,
139 pub geo_replication: Option<Arc<GeoReplicationManager>>,
141}
142
143impl axum::extract::FromRef<AppState> for Arc<EventStore> {
146 fn from_ref(state: &AppState) -> Self {
147 state.store.clone()
148 }
149}
150
151pub async fn serve_v1(
152 store: Arc<EventStore>,
153 auth_manager: Arc<AuthManager>,
154 tenant_repo: Arc<dyn TenantRepository>,
155 rate_limiter: Arc<RateLimiter>,
156 service_container: ServiceContainer,
157 addr: &str,
158 role: NodeRole,
159 wal_shipper: Option<Arc<WalShipper>>,
160 wal_receiver: Option<Arc<WalReceiver>>,
161 replication_port: u16,
162 cluster_manager: Option<Arc<ClusterManager>>,
163 geo_replication: Option<Arc<GeoReplicationManager>>,
164) -> anyhow::Result<()> {
165 let app_state = AppState {
166 store,
167 auth_manager: auth_manager.clone(),
168 tenant_repo,
169 service_container,
170 role: AtomicNodeRole::new(role),
171 wal_shipper: Arc::new(tokio::sync::RwLock::new(wal_shipper)),
172 wal_receiver,
173 replication_port,
174 cluster_manager,
175 geo_replication,
176 };
177
178 let auth_state = AuthState {
179 auth_manager: auth_manager.clone(),
180 };
181
182 let rate_limit_state = RateLimitState { rate_limiter };
183
184 let app = Router::new()
185 .route("/health", get(health_v1))
187 .route("/metrics", get(super::api::prometheus_metrics))
188 .route("/api/v1/auth/register", post(register_handler))
190 .route("/api/v1/auth/login", post(login_handler))
191 .route("/api/v1/auth/me", get(me_handler))
192 .route("/api/v1/auth/api-keys", post(create_api_key_handler))
193 .route("/api/v1/auth/api-keys", get(list_api_keys_handler))
194 .route("/api/v1/auth/api-keys/{id}", delete(revoke_api_key_handler))
195 .route("/api/v1/auth/users", get(list_users_handler))
196 .route("/api/v1/auth/users/{id}", delete(delete_user_handler))
197 ;
199 #[cfg(feature = "multi-tenant")]
200 let app = app
201 .route("/api/v1/tenants", post(create_tenant_handler))
202 .route("/api/v1/tenants", get(list_tenants_handler))
203 .route("/api/v1/tenants/{id}", get(get_tenant_handler))
204 .route("/api/v1/tenants/{id}", put(update_tenant_handler))
205 .route("/api/v1/tenants/{id}/stats", get(get_tenant_stats_handler))
206 .route("/api/v1/tenants/{id}/quotas", put(update_quotas_handler))
207 .route(
208 "/api/v1/tenants/{id}/schema-enforcement",
209 put(update_schema_enforcement_handler),
210 )
211 .route(
212 "/api/v1/tenants/{id}/deactivate",
213 post(deactivate_tenant_handler),
214 )
215 .route(
216 "/api/v1/tenants/{id}/activate",
217 post(activate_tenant_handler),
218 )
219 .route("/api/v1/tenants/{id}", delete(delete_tenant_handler));
220 let app = app
221 .route("/api/v1/audit/events", post(log_audit_event))
223 .route("/api/v1/audit/events", get(query_audit_events))
224 .route("/api/v1/config", get(list_configs))
226 .route("/api/v1/config", post(set_config))
227 .route("/api/v1/config/{key}", get(get_config))
228 .route("/api/v1/config/{key}", put(update_config))
229 .route("/api/v1/config/{key}", delete(delete_config))
230 .route(
232 "/api/v1/demo/seed",
233 post(super::demo_api::demo_seed_handler),
234 )
235 .route("/api/v1/events", post(super::api::ingest_event_v1))
237 .route(
238 "/api/v1/events/batch",
239 post(super::api::ingest_events_batch_v1),
240 )
241 .route("/api/v1/events/query", get(super::api::query_events))
242 .route(
243 "/api/v1/events/{event_id}",
244 get(super::api::get_event_by_id),
245 )
246 .route("/api/v1/events/stream", get(super::api::events_websocket))
247 .route("/api/v1/entities", get(super::api::list_entities))
248 .route(
249 "/api/v1/entities/duplicates",
250 get(super::api::detect_duplicates),
251 )
252 .route(
253 "/api/v1/entities/{entity_id}/state",
254 get(super::api::get_entity_state),
255 )
256 .route(
257 "/api/v1/entities/{entity_id}/snapshot",
258 get(super::api::get_entity_snapshot),
259 )
260 .route("/api/v1/stats", get(super::api::get_stats))
261 .route("/api/v1/streams", get(super::api::list_streams))
263 .route("/api/v1/event-types", get(super::api::list_event_types))
264 .route(
266 "/api/v1/analytics/frequency",
267 get(super::api::analytics_frequency),
268 )
269 .route(
270 "/api/v1/analytics/summary",
271 get(super::api::analytics_summary),
272 )
273 .route(
274 "/api/v1/analytics/correlation",
275 get(super::api::analytics_correlation),
276 )
277 .route("/api/v1/snapshots", post(super::api::create_snapshot))
279 .route("/api/v1/snapshots", get(super::api::list_snapshots))
280 .route(
281 "/api/v1/snapshots/{entity_id}/latest",
282 get(super::api::get_latest_snapshot),
283 )
284 .route(
286 "/api/v1/compaction/trigger",
287 post(super::api::trigger_compaction),
288 )
289 .route(
290 "/api/v1/compaction/stats",
291 get(super::api::compaction_stats),
292 )
293 .route("/api/v1/schemas", post(super::api::register_schema))
295 .route("/api/v1/schemas", get(super::api::list_subjects))
296 .route("/api/v1/schemas/{subject}", get(super::api::get_schema))
297 .route(
298 "/api/v1/schemas/{subject}/versions",
299 get(super::api::list_schema_versions),
300 )
301 .route(
302 "/api/v1/schemas/validate",
303 post(super::api::validate_event_schema),
304 )
305 .route(
306 "/api/v1/schemas/{subject}/compatibility",
307 put(super::api::set_compatibility_mode),
308 )
309 .route("/api/v1/replay", post(super::api::start_replay))
311 .route("/api/v1/replay", get(super::api::list_replays))
312 .route(
313 "/api/v1/replay/{replay_id}",
314 get(super::api::get_replay_progress),
315 )
316 .route(
317 "/api/v1/replay/{replay_id}/cancel",
318 post(super::api::cancel_replay),
319 )
320 .route(
321 "/api/v1/replay/{replay_id}",
322 delete(super::api::delete_replay),
323 )
324 .route("/api/v1/pipelines", post(super::api::register_pipeline))
326 .route("/api/v1/pipelines", get(super::api::list_pipelines))
327 .route(
328 "/api/v1/pipelines/stats",
329 get(super::api::all_pipeline_stats),
330 )
331 .route(
332 "/api/v1/pipelines/{pipeline_id}",
333 get(super::api::get_pipeline),
334 )
335 .route(
336 "/api/v1/pipelines/{pipeline_id}",
337 delete(super::api::remove_pipeline),
338 )
339 .route(
340 "/api/v1/pipelines/{pipeline_id}/stats",
341 get(super::api::get_pipeline_stats),
342 )
343 .route(
344 "/api/v1/pipelines/{pipeline_id}/reset",
345 put(super::api::reset_pipeline),
346 )
347 .route("/api/v1/projections", get(super::api::list_projections))
349 .route(
350 "/api/v1/projections/{name}",
351 get(super::api::get_projection),
352 )
353 .route(
354 "/api/v1/projections/{name}",
355 delete(super::api::delete_projection),
356 )
357 .route(
358 "/api/v1/projections/{name}/state",
359 get(super::api::get_projection_state_summary),
360 )
361 .route(
362 "/api/v1/projections/{name}/reset",
363 post(super::api::reset_projection),
364 )
365 .route(
366 "/api/v1/projections/{name}/pause",
367 post(super::api::pause_projection),
368 )
369 .route(
370 "/api/v1/projections/{name}/start",
371 post(super::api::start_projection),
372 )
373 .route(
374 "/api/v1/projections/{name}/{entity_id}/state",
375 get(super::api::get_projection_state),
376 )
377 .route(
378 "/api/v1/projections/{name}/{entity_id}/state",
379 post(super::api::save_projection_state),
380 )
381 .route(
382 "/api/v1/projections/{name}/{entity_id}/state",
383 put(super::api::save_projection_state),
384 )
385 .route(
386 "/api/v1/projections/{name}/bulk",
387 post(super::api::bulk_get_projection_states),
388 )
389 .route(
390 "/api/v1/projections/{name}/bulk/save",
391 post(super::api::bulk_save_projection_states),
392 )
393 .route("/api/v1/webhooks", post(super::api::register_webhook))
395 .route("/api/v1/webhooks", get(super::api::list_webhooks))
396 .route(
397 "/api/v1/webhooks/{webhook_id}",
398 get(super::api::get_webhook),
399 )
400 .route(
401 "/api/v1/webhooks/{webhook_id}",
402 put(super::api::update_webhook),
403 )
404 .route(
405 "/api/v1/webhooks/{webhook_id}",
406 delete(super::api::delete_webhook),
407 )
408 .route(
409 "/api/v1/webhooks/{webhook_id}/deliveries",
410 get(super::api::list_webhook_deliveries),
411 )
412 .route("/api/v1/consumers", post(super::api::register_consumer))
414 .route(
415 "/api/v1/consumers/{consumer_id}",
416 get(super::api::get_consumer),
417 )
418 .route(
419 "/api/v1/consumers/{consumer_id}/events",
420 get(super::api::poll_consumer_events),
421 )
422 .route(
423 "/api/v1/consumers/{consumer_id}/ack",
424 post(super::api::ack_consumer),
425 )
426 .route("/api/v1/cluster/status", get(cluster_status_handler))
428 .route("/api/v1/cluster/members", get(cluster_list_members_handler))
429 .route("/api/v1/cluster/members", post(cluster_add_member_handler))
430 .route(
431 "/api/v1/cluster/members/{node_id}",
432 delete(cluster_remove_member_handler),
433 )
434 .route(
435 "/api/v1/cluster/members/{node_id}/heartbeat",
436 post(cluster_heartbeat_handler),
437 )
438 .route("/api/v1/cluster/vote", post(cluster_vote_handler))
439 .route("/api/v1/cluster/election", post(cluster_election_handler))
440 .route(
441 "/api/v1/cluster/partitions",
442 get(cluster_partitions_handler),
443 )
444 .route("/api/v1/graphql", post(super::api::graphql_query))
446 .route("/api/v1/geospatial/query", post(super::api::geo_query))
447 .route("/api/v1/geospatial/stats", get(super::api::geo_stats))
448 .route(
449 "/api/v1/exactly-once/stats",
450 get(super::api::exactly_once_stats),
451 )
452 .route(
453 "/api/v1/schema-evolution/history/{event_type}",
454 get(super::api::schema_evolution_history),
455 )
456 .route(
457 "/api/v1/schema-evolution/schema/{event_type}",
458 get(super::api::schema_evolution_schema),
459 )
460 .route(
461 "/api/v1/schema-evolution/stats",
462 get(super::api::schema_evolution_stats),
463 )
464 .route("/api/v1/geo/status", get(geo_status_handler))
466 .route("/api/v1/geo/sync", post(geo_sync_handler))
467 .route("/api/v1/geo/peers", get(geo_peers_handler))
468 .route("/api/v1/geo/failover", post(geo_failover_handler));
469 #[cfg(feature = "replication")]
471 let app = app
472 .route("/internal/promote", post(promote_handler))
473 .route("/internal/repoint", post(repoint_handler));
474 let app = app;
475
476 #[cfg(feature = "embedded-sync")]
478 let app = app
479 .route("/api/v1/sync/pull", post(super::api::sync_pull_handler))
480 .route("/api/v1/sync/push", post(super::api::sync_push_handler));
481
482 let app = app
483 .with_state(app_state.clone())
484 .layer(middleware::from_fn_with_state(
487 app_state,
488 read_only_middleware,
489 ))
490 .layer(middleware::from_fn_with_state(
491 rate_limit_state,
492 rate_limit_middleware,
493 ))
494 .layer(middleware::from_fn_with_state(auth_state, auth_middleware))
495 .layer(
496 CorsLayer::new()
497 .allow_origin(Any)
498 .allow_methods(Any)
499 .allow_headers(Any),
500 )
501 .layer(TraceLayer::new_for_http());
502
503 #[cfg(feature = "prime")]
505 let app = {
506 let data_dir =
507 std::env::var("PRIME_DATA_DIR").unwrap_or_else(|_| "/tmp/prime-data".to_string());
508 match crate::prime::Prime::open(&data_dir).await {
509 Ok(prime) => {
510 let prime_state = Arc::new(super::prime_api::PrimeState { prime });
511 tracing::info!("Prime API enabled at /api/v1/prime/*");
512 app.nest(
513 "/api/v1/prime",
514 super::prime_api::prime_router().with_state(prime_state),
515 )
516 }
517 Err(e) => {
518 tracing::warn!("Prime API disabled: failed to open Prime: {e}");
519 app
520 }
521 }
522 };
523
524 let listener = tokio::net::TcpListener::bind(addr).await?;
525
526 axum::serve(listener, app)
528 .with_graceful_shutdown(shutdown_signal())
529 .await?;
530
531 tracing::info!("🛑 AllSource Core shutdown complete");
532 Ok(())
533}
534
535const WRITE_PATHS: &[&str] = &[
537 "/api/v1/events",
538 "/api/v1/events/batch",
539 "/api/v1/snapshots",
540 "/api/v1/projections/",
541 "/api/v1/schemas",
542 "/api/v1/replay",
543 "/api/v1/pipelines",
544 "/api/v1/compaction/trigger",
545 "/api/v1/audit/events",
546 "/api/v1/config",
547 "/api/v1/webhooks",
548 "/api/v1/demo/seed",
549];
550
551fn is_write_request(method: &axum::http::Method, path: &str) -> bool {
553 use axum::http::Method;
554 if method != Method::POST && method != Method::PUT && method != Method::DELETE {
556 return false;
557 }
558 WRITE_PATHS
559 .iter()
560 .any(|write_path| path.starts_with(write_path))
561}
562
563fn is_internal_request(path: &str) -> bool {
565 path.starts_with("/internal/")
566 || path.starts_with("/api/v1/cluster/")
567 || path.starts_with("/api/v1/geo/")
568}
569
570async fn read_only_middleware(
576 State(state): State<AppState>,
577 request: axum::extract::Request,
578 next: axum::middleware::Next,
579) -> axum::response::Response {
580 let path = request.uri().path();
581 if state.role.load().is_follower()
582 && is_write_request(request.method(), path)
583 && !is_internal_request(path)
584 {
585 return (
586 axum::http::StatusCode::CONFLICT,
587 axum::Json(serde_json::json!({
588 "error": "read_only",
589 "message": "This node is a read-only follower"
590 })),
591 )
592 .into_response();
593 }
594 next.run(request).await
595}
596
597async fn health_v1(State(state): State<AppState>) -> impl IntoResponse {
602 let has_system_repos = state.service_container.has_system_repositories();
603
604 let system_streams = if has_system_repos {
605 let (tenant_count, config_count, total_events) =
606 if let Some(store) = state.service_container.system_store() {
607 use crate::domain::value_objects::system_stream::SystemDomain;
608 (
609 store.count_stream(SystemDomain::Tenant),
610 store.count_stream(SystemDomain::Config),
611 store.total_events(),
612 )
613 } else {
614 (0, 0, 0)
615 };
616
617 serde_json::json!({
618 "status": "healthy",
619 "mode": "event-sourced",
620 "total_events": total_events,
621 "tenant_events": tenant_count,
622 "config_events": config_count,
623 })
624 } else {
625 serde_json::json!({
626 "status": "disabled",
627 "mode": "in-memory",
628 })
629 };
630
631 let replication = {
632 #[cfg(feature = "replication")]
633 {
634 let shipper_guard = state.wal_shipper.read().await;
635 if let Some(ref shipper) = *shipper_guard {
636 serde_json::to_value(shipper.status()).unwrap_or_default()
637 } else if let Some(ref receiver) = state.wal_receiver {
638 serde_json::to_value(receiver.status()).unwrap_or_default()
639 } else {
640 serde_json::json!(null)
641 }
642 }
643 #[cfg(not(feature = "replication"))]
644 serde_json::json!({"edition": "community", "status": "not_available"})
645 };
646
647 let current_role = state.role.load();
648
649 Json(serde_json::json!({
650 "status": "healthy",
651 "service": "allsource-core",
652 "version": env!("CARGO_PKG_VERSION"),
653 "role": current_role,
654 "system_streams": system_streams,
655 "replication": replication,
656 }))
657}
658
659#[cfg(feature = "replication")]
665async fn promote_handler(State(state): State<AppState>) -> impl IntoResponse {
666 let current_role = state.role.load();
667 if current_role == NodeRole::Leader {
668 return (
669 axum::http::StatusCode::OK,
670 Json(serde_json::json!({
671 "status": "already_leader",
672 "message": "This node is already the leader",
673 })),
674 );
675 }
676
677 tracing::info!("PROMOTE: Switching role from follower to leader");
678
679 state.role.store(NodeRole::Leader);
681
682 if let Some(ref receiver) = state.wal_receiver {
684 receiver.shutdown();
685 tracing::info!("PROMOTE: WAL receiver shutdown signalled");
686 }
687
688 let replication_port = state.replication_port;
690 let (mut shipper, tx) = WalShipper::new();
691 state.store.enable_wal_replication(tx);
692 shipper.set_store(Arc::clone(&state.store));
693 shipper.set_metrics(state.store.metrics());
694 let shipper = Arc::new(shipper);
695
696 {
698 let mut shipper_guard = state.wal_shipper.write().await;
699 *shipper_guard = Some(Arc::clone(&shipper));
700 }
701
702 let shipper_clone = Arc::clone(&shipper);
704 tokio::spawn(async move {
705 if let Err(e) = shipper_clone.serve(replication_port).await {
706 tracing::error!("Promoted WAL shipper error: {}", e);
707 }
708 });
709
710 tracing::info!(
711 "PROMOTE: Now accepting writes. WAL shipper listening on port {}",
712 replication_port,
713 );
714
715 (
716 axum::http::StatusCode::OK,
717 Json(serde_json::json!({
718 "status": "promoted",
719 "role": "leader",
720 "replication_port": replication_port,
721 })),
722 )
723}
724
725#[cfg(feature = "replication")]
730async fn repoint_handler(
731 State(state): State<AppState>,
732 axum::extract::Query(params): axum::extract::Query<std::collections::HashMap<String, String>>,
733) -> impl IntoResponse {
734 let current_role = state.role.load();
735 if current_role != NodeRole::Follower {
736 return (
737 axum::http::StatusCode::CONFLICT,
738 Json(serde_json::json!({
739 "error": "not_follower",
740 "message": "Repoint only applies to follower nodes",
741 })),
742 );
743 }
744
745 let new_leader = match params.get("leader") {
746 Some(l) if !l.is_empty() => l.clone(),
747 _ => {
748 return (
749 axum::http::StatusCode::BAD_REQUEST,
750 Json(serde_json::json!({
751 "error": "missing_leader",
752 "message": "Query parameter 'leader' is required (e.g. ?leader=new-leader:3910)",
753 })),
754 );
755 }
756 };
757
758 tracing::info!("REPOINT: Switching replication target to {}", new_leader);
759
760 if let Some(ref receiver) = state.wal_receiver {
761 receiver.repoint(&new_leader);
762 tracing::info!("REPOINT: WAL receiver repointed to {}", new_leader);
763 } else {
764 tracing::warn!("REPOINT: No WAL receiver to repoint");
765 }
766
767 (
768 axum::http::StatusCode::OK,
769 Json(serde_json::json!({
770 "status": "repointed",
771 "new_leader": new_leader,
772 })),
773 )
774}
775
776async fn cluster_status_handler(State(state): State<AppState>) -> impl IntoResponse {
782 let Some(ref cm) = state.cluster_manager else {
783 return (
784 axum::http::StatusCode::SERVICE_UNAVAILABLE,
785 Json(serde_json::json!({
786 "error": "cluster_not_enabled",
787 "message": "Cluster mode is not enabled on this node"
788 })),
789 );
790 };
791
792 let status = cm.status().await;
793 (
794 axum::http::StatusCode::OK,
795 Json(serde_json::to_value(status).unwrap()),
796 )
797}
798
799async fn cluster_list_members_handler(State(state): State<AppState>) -> impl IntoResponse {
801 let Some(ref cm) = state.cluster_manager else {
802 return (
803 axum::http::StatusCode::SERVICE_UNAVAILABLE,
804 Json(serde_json::json!({
805 "error": "cluster_not_enabled",
806 "message": "Cluster mode is not enabled on this node"
807 })),
808 );
809 };
810
811 let members = cm.all_members();
812 (
813 axum::http::StatusCode::OK,
814 Json(serde_json::json!({
815 "members": members,
816 "count": members.len(),
817 })),
818 )
819}
820
821async fn cluster_add_member_handler(
823 State(state): State<AppState>,
824 Json(member): Json<ClusterMember>,
825) -> impl IntoResponse {
826 let Some(ref cm) = state.cluster_manager else {
827 return (
828 axum::http::StatusCode::SERVICE_UNAVAILABLE,
829 Json(serde_json::json!({
830 "error": "cluster_not_enabled",
831 "message": "Cluster mode is not enabled on this node"
832 })),
833 );
834 };
835
836 let node_id = member.node_id;
837 cm.add_member(member).await;
838
839 tracing::info!("Cluster member {} added", node_id);
840 (
841 axum::http::StatusCode::OK,
842 Json(serde_json::json!({
843 "status": "added",
844 "node_id": node_id,
845 })),
846 )
847}
848
849async fn cluster_remove_member_handler(
851 State(state): State<AppState>,
852 Path(node_id): Path<u32>,
853) -> impl IntoResponse {
854 let Some(ref cm) = state.cluster_manager else {
855 return (
856 axum::http::StatusCode::SERVICE_UNAVAILABLE,
857 Json(serde_json::json!({
858 "error": "cluster_not_enabled",
859 "message": "Cluster mode is not enabled on this node"
860 })),
861 );
862 };
863
864 match cm.remove_member(node_id).await {
865 Some(_) => {
866 tracing::info!("Cluster member {} removed", node_id);
867 (
868 axum::http::StatusCode::OK,
869 Json(serde_json::json!({
870 "status": "removed",
871 "node_id": node_id,
872 })),
873 )
874 }
875 None => (
876 axum::http::StatusCode::NOT_FOUND,
877 Json(serde_json::json!({
878 "error": "not_found",
879 "message": format!("Node {} not found in cluster", node_id),
880 })),
881 ),
882 }
883}
884
885#[derive(serde::Deserialize)]
887struct HeartbeatRequest {
888 wal_offset: u64,
889 #[serde(default = "default_true")]
890 healthy: bool,
891}
892
893fn default_true() -> bool {
894 true
895}
896
897async fn cluster_heartbeat_handler(
898 State(state): State<AppState>,
899 Path(node_id): Path<u32>,
900 Json(req): Json<HeartbeatRequest>,
901) -> impl IntoResponse {
902 let Some(ref cm) = state.cluster_manager else {
903 return (
904 axum::http::StatusCode::SERVICE_UNAVAILABLE,
905 Json(serde_json::json!({
906 "error": "cluster_not_enabled",
907 "message": "Cluster mode is not enabled on this node"
908 })),
909 );
910 };
911
912 cm.update_member_heartbeat(node_id, req.wal_offset, req.healthy);
913 (
914 axum::http::StatusCode::OK,
915 Json(serde_json::json!({
916 "status": "updated",
917 "node_id": node_id,
918 })),
919 )
920}
921
922async fn cluster_vote_handler(
924 State(state): State<AppState>,
925 Json(request): Json<VoteRequest>,
926) -> impl IntoResponse {
927 let Some(ref cm) = state.cluster_manager else {
928 return (
929 axum::http::StatusCode::SERVICE_UNAVAILABLE,
930 Json(serde_json::json!({
931 "error": "cluster_not_enabled",
932 "message": "Cluster mode is not enabled on this node"
933 })),
934 );
935 };
936
937 let response = cm.handle_vote_request(&request).await;
938 (
939 axum::http::StatusCode::OK,
940 Json(serde_json::to_value(response).unwrap()),
941 )
942}
943
944async fn cluster_election_handler(State(state): State<AppState>) -> impl IntoResponse {
946 let Some(ref cm) = state.cluster_manager else {
947 return (
948 axum::http::StatusCode::SERVICE_UNAVAILABLE,
949 Json(serde_json::json!({
950 "error": "cluster_not_enabled",
951 "message": "Cluster mode is not enabled on this node"
952 })),
953 );
954 };
955
956 let candidate = cm.select_leader_candidate();
958
959 match candidate {
960 Some(candidate_id) => {
961 let new_term = cm.start_election().await;
962 tracing::info!(
963 "Cluster election started: term={}, candidate={}",
964 new_term,
965 candidate_id,
966 );
967
968 if candidate_id == cm.self_id() {
971 cm.become_leader(new_term).await;
972 tracing::info!("Node {} became leader at term {}", candidate_id, new_term);
973 }
974
975 (
976 axum::http::StatusCode::OK,
977 Json(serde_json::json!({
978 "status": "election_started",
979 "term": new_term,
980 "candidate_id": candidate_id,
981 "self_is_leader": candidate_id == cm.self_id(),
982 })),
983 )
984 }
985 None => (
986 axum::http::StatusCode::CONFLICT,
987 Json(serde_json::json!({
988 "error": "no_candidates",
989 "message": "No healthy members available for leader election",
990 })),
991 ),
992 }
993}
994
995async fn cluster_partitions_handler(State(state): State<AppState>) -> impl IntoResponse {
997 let Some(ref cm) = state.cluster_manager else {
998 return (
999 axum::http::StatusCode::SERVICE_UNAVAILABLE,
1000 Json(serde_json::json!({
1001 "error": "cluster_not_enabled",
1002 "message": "Cluster mode is not enabled on this node"
1003 })),
1004 );
1005 };
1006
1007 let registry = cm.registry();
1008 let distribution = registry.partition_distribution();
1009 let total_partitions: usize = distribution.values().map(std::vec::Vec::len).sum();
1010
1011 (
1012 axum::http::StatusCode::OK,
1013 Json(serde_json::json!({
1014 "total_partitions": total_partitions,
1015 "node_count": registry.node_count(),
1016 "healthy_node_count": registry.healthy_node_count(),
1017 "distribution": distribution,
1018 })),
1019 )
1020}
1021
1022async fn geo_status_handler(State(state): State<AppState>) -> impl IntoResponse {
1028 let Some(ref geo) = state.geo_replication else {
1029 return (
1030 axum::http::StatusCode::SERVICE_UNAVAILABLE,
1031 Json(serde_json::json!({
1032 "error": "geo_replication_not_enabled",
1033 "message": "Geo-replication is not enabled on this node"
1034 })),
1035 );
1036 };
1037
1038 let status = geo.status();
1039 (
1040 axum::http::StatusCode::OK,
1041 Json(serde_json::to_value(status).unwrap()),
1042 )
1043}
1044
1045async fn geo_sync_handler(
1047 State(state): State<AppState>,
1048 Json(request): Json<GeoSyncRequest>,
1049) -> impl IntoResponse {
1050 let Some(ref geo) = state.geo_replication else {
1051 return (
1052 axum::http::StatusCode::SERVICE_UNAVAILABLE,
1053 Json(serde_json::json!({
1054 "error": "geo_replication_not_enabled",
1055 "message": "Geo-replication is not enabled on this node"
1056 })),
1057 );
1058 };
1059
1060 tracing::info!(
1061 "Geo-sync received from region '{}': {} events",
1062 request.source_region,
1063 request.events.len(),
1064 );
1065
1066 let response = geo.receive_sync(&request);
1067 (
1068 axum::http::StatusCode::OK,
1069 Json(serde_json::to_value(response).unwrap()),
1070 )
1071}
1072
1073async fn geo_peers_handler(State(state): State<AppState>) -> impl IntoResponse {
1075 let Some(ref geo) = state.geo_replication else {
1076 return (
1077 axum::http::StatusCode::SERVICE_UNAVAILABLE,
1078 Json(serde_json::json!({
1079 "error": "geo_replication_not_enabled",
1080 "message": "Geo-replication is not enabled on this node"
1081 })),
1082 );
1083 };
1084
1085 let status = geo.status();
1086 (
1087 axum::http::StatusCode::OK,
1088 Json(serde_json::json!({
1089 "region_id": status.region_id,
1090 "peers": status.peers,
1091 })),
1092 )
1093}
1094
1095async fn geo_failover_handler(State(state): State<AppState>) -> impl IntoResponse {
1097 let Some(ref geo) = state.geo_replication else {
1098 return (
1099 axum::http::StatusCode::SERVICE_UNAVAILABLE,
1100 Json(serde_json::json!({
1101 "error": "geo_replication_not_enabled",
1102 "message": "Geo-replication is not enabled on this node"
1103 })),
1104 );
1105 };
1106
1107 match geo.select_failover_region() {
1108 Some(failover_region) => {
1109 tracing::info!(
1110 "Geo-failover: selected region '{}' as failover target",
1111 failover_region,
1112 );
1113 (
1114 axum::http::StatusCode::OK,
1115 Json(serde_json::json!({
1116 "status": "failover_target_selected",
1117 "failover_region": failover_region,
1118 "message": "Region selected for failover. DNS/routing update required externally.",
1119 })),
1120 )
1121 }
1122 None => (
1123 axum::http::StatusCode::CONFLICT,
1124 Json(serde_json::json!({
1125 "error": "no_healthy_peers",
1126 "message": "No healthy peer regions available for failover",
1127 })),
1128 ),
1129 }
1130}
1131
1132async fn shutdown_signal() {
1134 let ctrl_c = async {
1135 tokio::signal::ctrl_c()
1136 .await
1137 .expect("failed to install Ctrl+C handler");
1138 };
1139
1140 #[cfg(unix)]
1141 let terminate = async {
1142 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
1143 .expect("failed to install SIGTERM handler")
1144 .recv()
1145 .await;
1146 };
1147
1148 #[cfg(not(unix))]
1149 let terminate = std::future::pending::<()>();
1150
1151 tokio::select! {
1152 () = ctrl_c => {
1153 tracing::info!("📤 Received Ctrl+C, initiating graceful shutdown...");
1154 }
1155 () = terminate => {
1156 tracing::info!("📤 Received SIGTERM, initiating graceful shutdown...");
1157 }
1158 }
1159}