use hydracache::HydraCache;
use hydracache_client_transport_axum::{ClientSurfaceDrain, ClientSurfaceRuntime};
use serde::Serialize;
use thiserror::Error;
use crate::config::{ServerConfig, ServerConfigError, ServerRole};
use crate::services::{DrainOutcome, GracefulShutdown, ServiceSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum ServerState {
Created,
Running,
Draining,
Stopped,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerHealth {
pub status: &'static str,
pub state: ServerState,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerReadiness {
pub ready: bool,
pub storage_open: bool,
pub cluster_ready: bool,
pub accepting: bool,
pub client_surface_ready: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerAdminStatus {
pub leader: Option<String>,
pub term: u64,
pub quorum_ok: bool,
pub members: u32,
pub reshard_phase: String,
pub draining: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerAdminAction {
pub action: &'static str,
pub outcome: &'static str,
pub detail: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ServerAdminActionError {
#[error("server is not ready for admin action: {0}")]
NotReady(&'static str),
#[error("{0} requires member mode")]
RequiresMember(&'static str),
#[error("backup admin action requires backup.enabled and backup.location")]
BackupDisabled,
}
#[derive(Debug, Clone)]
pub struct ServerRuntime {
config: ServerConfig,
cache: HydraCache,
services: ServiceSet,
state: ServerState,
storage_open: bool,
cluster_ready: bool,
accepting: bool,
flushed: bool,
client_surface: Option<ClientSurfaceRuntime>,
last_client_surface_drain: Option<ClientSurfaceDrain>,
last_drain: Option<DrainOutcome>,
}
impl ServerRuntime {
pub fn new(config: ServerConfig) -> Result<Self, ServerConfigError> {
config.validate()?;
let client_surface = if config.client_api.enabled {
Some(
ClientSurfaceRuntime::new(config.client_api.limits)
.map_err(|error| ServerConfigError::InvalidClientApi(error.to_string()))?,
)
} else {
None
};
Ok(Self {
config,
cache: HydraCache::local().build(),
services: ServiceSet::default(),
state: ServerState::Created,
storage_open: false,
cluster_ready: false,
accepting: false,
flushed: false,
client_surface,
last_client_surface_drain: None,
last_drain: None,
})
}
pub fn start(mut self) -> Self {
self.storage_open = true;
self.cluster_ready = matches!(
self.config.role,
ServerRole::Local | ServerRole::Member | ServerRole::Client
);
self.accepting = true;
if let Some(surface) = self.client_surface.as_mut() {
surface.start();
}
self.services.start();
self.state = ServerState::Running;
self
}
pub fn health(&self) -> ServerHealth {
ServerHealth {
status: if self.state == ServerState::Stopped {
"stopped"
} else {
"ok"
},
state: self.state,
}
}
pub fn ready(&self) -> ServerReadiness {
ServerReadiness {
ready: self.can_serve(),
storage_open: self.storage_open,
cluster_ready: self.cluster_ready,
accepting: self.accepting,
client_surface_ready: self.client_surface_ready(),
}
}
pub fn can_serve(&self) -> bool {
self.state == ServerState::Running
&& self.storage_open
&& self.cluster_ready
&& self.accepting
}
pub fn is_draining(&self) -> bool {
self.state == ServerState::Draining
}
pub fn begin_request(&mut self) -> bool {
if !self.accepting {
return false;
}
self.services.begin_request();
true
}
pub fn finish_request(&mut self) {
self.services.finish_request();
}
pub fn client_surface_ready(&self) -> bool {
self.client_surface
.as_ref()
.is_some_and(ClientSurfaceRuntime::accepting)
}
pub fn begin_client_subscription(&self) -> bool {
self.client_surface
.as_ref()
.is_some_and(|surface| surface.begin_subscription().is_ok())
}
pub fn client_active_subscriptions(&self) -> u64 {
self.client_surface
.as_ref()
.map_or(0, |surface| surface.state().active_subscriptions())
}
pub fn client_surface_drain(&self) -> Option<ClientSurfaceDrain> {
self.last_client_surface_drain
}
pub fn begin_drain(&mut self) {
if matches!(self.state, ServerState::Stopped) {
return;
}
self.accepting = false;
self.state = ServerState::Draining;
if let Some(surface) = self.client_surface.as_mut() {
if self
.last_client_surface_drain
.is_none_or(|drain| drain.remaining > 0)
{
self.last_client_surface_drain = Some(surface.shutdown());
}
}
}
pub fn graceful_shutdown(&mut self) -> DrainOutcome {
if self.state == ServerState::Stopped {
return self.last_drain.unwrap_or(DrainOutcome {
started_with: 0,
remaining: 0,
timed_out: false,
});
}
self.begin_drain();
let outcome = GracefulShutdown::new(self.config.drain_timeout()).drain(&mut self.services);
self.flushed = true;
self.storage_open = false;
self.cluster_ready = false;
self.services.stop();
self.state = ServerState::Stopped;
self.last_drain = Some(outcome);
outcome
}
pub fn shutdown(&mut self) -> DrainOutcome {
self.graceful_shutdown()
}
pub fn admin_status(&self) -> ServerAdminStatus {
let cluster_ready = self.cluster_ready && self.state != ServerState::Stopped;
ServerAdminStatus {
leader: cluster_ready.then(|| "local".to_owned()),
term: u64::from(cluster_ready),
quorum_ok: cluster_ready && !self.is_draining(),
members: u32::from(cluster_ready),
reshard_phase: "idle".to_owned(),
draining: self.is_draining(),
}
}
pub fn request_reshard(&self) -> Result<ServerAdminAction, ServerAdminActionError> {
if !self.can_serve() {
return Err(ServerAdminActionError::NotReady("reshard"));
}
if !matches!(self.config.role, ServerRole::Member) {
return Err(ServerAdminActionError::RequiresMember("reshard"));
}
Ok(ServerAdminAction {
action: "reshard",
outcome: "accepted",
detail: "reshard request accepted by member runtime".to_owned(),
})
}
pub fn request_backup(&self) -> Result<ServerAdminAction, ServerAdminActionError> {
if !self.can_serve() {
return Err(ServerAdminActionError::NotReady("backup"));
}
if !self.config.backup.enabled
|| self
.config
.backup
.location
.as_deref()
.unwrap_or("")
.trim()
.is_empty()
{
return Err(ServerAdminActionError::BackupDisabled);
}
Ok(ServerAdminAction {
action: "backup",
outcome: "accepted",
detail: "backup request accepted by configured runtime".to_owned(),
})
}
pub fn flushed(&self) -> bool {
self.flushed
}
pub fn cache(&self) -> &HydraCache {
&self.cache
}
pub fn config(&self) -> &ServerConfig {
&self.config
}
}