mirror of https://github.com/synctv-org/synctv
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
291 lines
12 KiB
Rust
291 lines
12 KiB
Rust
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
use async_trait::async_trait;
|
|
use tonic::codec::CompressionEncoding;
|
|
use tonic::transport::{Channel, Endpoint};
|
|
use tonic::Request;
|
|
|
|
use crate::http_error::AppError;
|
|
use synctv_realtime::fanout::RealtimeEventService;
|
|
mod server_state_cluster;
|
|
mod slice_cache;
|
|
use server_state_cluster::cluster_proto_to_server_state_node;
|
|
pub use server_state_cluster::ServerStateGrpcService;
|
|
pub use slice_cache::slice_cache_management_runtime_from_router_options;
|
|
|
|
pub use synctv_core::service::{
|
|
livestream_snapshot_from_publishers, validate_server_state_selection, ServerStateCluster,
|
|
ServerStateClusterNode, ServerStateClusterRuntime, ServerStateClusterStatus,
|
|
ServerStateClusterTarget, ServerStateCpu, ServerStateCpuStatus, ServerStateDatabase,
|
|
ServerStateDatabasePool, ServerStateDatabaseStatus, ServerStateEmail, ServerStateEmailStatus,
|
|
ServerStateError, ServerStateFailure, ServerStateLivestream, ServerStateLivestreamRuntime,
|
|
ServerStateLivestreamSnapshot, ServerStateLivestreamStatus, ServerStateMemory,
|
|
ServerStateMemoryStatus, ServerStateNode, ServerStateNodeStatus, ServerStateRealtime,
|
|
ServerStateRealtimeMetrics, ServerStateRealtimeRuntime, ServerStateRedis,
|
|
ServerStateRedisStatus, ServerStateRemoteClient, ServerStateResponse, ServerStateResult,
|
|
ServerStateScope, ServerStateSelection, ServerStateService, ServerStateServiceDependencies,
|
|
ServerStateSliceCache, ServerStateSliceCacheRuntime, ServerStateSliceCacheStatus,
|
|
ServerStateSummary, ServerStateWebRtc, ServerStateWebRtcStatus, ServerStateWsTicket,
|
|
ServerStateWsTicketStatus, SliceCacheConfigInfo, SliceCacheEvictExpiredNodeResult,
|
|
SliceCacheEvictExpiredResponse, SliceCacheManagementClusterRuntime, SliceCacheManagementError,
|
|
SliceCacheManagementLocalRuntime, SliceCacheManagementRemoteClient, SliceCacheManagementResult,
|
|
SliceCacheManagementService, SliceCacheManagementServiceDependencies, SliceCacheNodeFailure,
|
|
SliceCachePurgeNodeResult, SliceCachePurgeResponse, SliceCachePurgeResult, SliceCacheSelection,
|
|
SliceCacheStats, SliceCacheStatsNode, SliceCacheStatsResponse,
|
|
};
|
|
use synctv_realtime::sync::ConnectionRuntime;
|
|
|
|
const SERVER_STATE_REMOTE_CONNECT_TIMEOUT: Duration = Duration::from_secs(2);
|
|
const SERVER_STATE_REMOTE_REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
|
|
|
|
type ClusterServerStateClient =
|
|
synctv_cluster::grpc::ServerStateServiceClient<tonic::transport::Channel>;
|
|
|
|
impl From<ServerStateError> for AppError {
|
|
fn from(error: ServerStateError) -> Self {
|
|
match error {
|
|
ServerStateError::InvalidSelection => AppError::bad_request(error.to_string()),
|
|
ServerStateError::ClusterUnavailable(_)
|
|
| ServerStateError::MissingClusterSecret
|
|
| ServerStateError::InvalidClusterSecret
|
|
| ServerStateError::Cluster(_)
|
|
| ServerStateError::RemoteRequest { .. }
|
|
| ServerStateError::RemoteDecode { .. } => AppError::new(
|
|
axum::http::StatusCode::SERVICE_UNAVAILABLE,
|
|
error.to_string(),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn server_state_runtime_from_router_options(
|
|
config: &crate::app_state::RouterOptions,
|
|
) -> ServerStateService {
|
|
let cluster_runtime = config.cluster_client.as_ref().map(|client| {
|
|
Arc::new(ApiServerStateClusterRuntime {
|
|
client: client.clone(),
|
|
}) as Arc<dyn ServerStateClusterRuntime>
|
|
});
|
|
let remote_client = config.cluster_client.as_ref().map(|_| {
|
|
Arc::new(ApiServerStateRemoteClient {
|
|
runtime_settings: config.runtime_settings.clone(),
|
|
}) as Arc<dyn ServerStateRemoteClient>
|
|
});
|
|
|
|
ServerStateService::new(ServerStateServiceDependencies {
|
|
runtime_params: Arc::new(config.runtime_settings.server_state.clone()),
|
|
user_service: config.user_service.clone(),
|
|
realtime_runtime: Arc::new(ApiServerStateRealtimeRuntime {
|
|
event_service: config.event_service.clone(),
|
|
connection_manager: config.connection_manager.clone(),
|
|
}),
|
|
ws_ticket_service: config.ws_ticket_service.clone(),
|
|
redis_runtime: config.redis_runtime.clone(),
|
|
email_service: config.email_service.clone(),
|
|
live_streaming_configured: config.live_streaming_infrastructure.is_some(),
|
|
livestream_runtime: config
|
|
.live_streaming_infrastructure
|
|
.as_ref()
|
|
.map(|infrastructure| {
|
|
Arc::new(ApiServerStateLivestreamRuntime {
|
|
infrastructure: infrastructure.clone(),
|
|
local_node_id: config.event_service.node_id().to_string(),
|
|
}) as Arc<dyn ServerStateLivestreamRuntime>
|
|
}),
|
|
cluster_runtime,
|
|
remote_client,
|
|
slice_cache_runtime: Arc::new(ApiServerStateSliceCacheRuntime {
|
|
cache: config.proxy_slice_cache.clone(),
|
|
}),
|
|
webrtc_status: config.webrtc_status.clone(),
|
|
})
|
|
}
|
|
|
|
struct ApiServerStateRealtimeRuntime {
|
|
event_service: Arc<dyn RealtimeEventService>,
|
|
connection_manager: Arc<dyn ConnectionRuntime>,
|
|
}
|
|
|
|
impl ServerStateRealtimeRuntime for ApiServerStateRealtimeRuntime {
|
|
fn metrics(&self) -> ServerStateRealtimeMetrics {
|
|
let metrics = self.event_service.metrics();
|
|
ServerStateRealtimeMetrics {
|
|
distributed_enabled: metrics.distributed_enabled,
|
|
connection_count: saturating_u64(self.connection_manager.connection_count()),
|
|
}
|
|
}
|
|
|
|
fn node_id(&self) -> &str {
|
|
self.event_service.node_id()
|
|
}
|
|
}
|
|
|
|
struct ApiServerStateLivestreamRuntime {
|
|
infrastructure: Arc<synctv_livestream::LiveStreamingInfrastructure>,
|
|
local_node_id: String,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ServerStateLivestreamRuntime for ApiServerStateLivestreamRuntime {
|
|
async fn snapshot(&self) -> ServerStateLivestreamSnapshot {
|
|
match self.infrastructure.list_active_generations().await {
|
|
Ok(publishers) => livestream_snapshot_from_publishers(
|
|
publishers
|
|
.into_iter()
|
|
.map(|publisher| (publisher.room_id, publisher.generation.node_id)),
|
|
&self.local_node_id,
|
|
),
|
|
Err(error) => {
|
|
tracing::warn!(%error, "failed to collect livestream publisher metrics");
|
|
ServerStateLivestreamSnapshot::default()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ApiServerStateClusterRuntime {
|
|
client: Arc<synctv_cluster::grpc::ClusterClient>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ServerStateClusterRuntime for ApiServerStateClusterRuntime {
|
|
async fn resolve_routable_node(
|
|
&self,
|
|
target_node_id: &str,
|
|
) -> ServerStateResult<ServerStateClusterTarget> {
|
|
let node = self
|
|
.client
|
|
.resolve_routable_node(target_node_id)
|
|
.await
|
|
.map_err(|error| ServerStateError::Cluster(error.to_string()))?;
|
|
Ok(cluster_node_to_target(node))
|
|
}
|
|
|
|
async fn remote_routable_nodes(&self) -> ServerStateResult<Vec<ServerStateClusterTarget>> {
|
|
let nodes = self
|
|
.client
|
|
.remote_routable_nodes()
|
|
.await
|
|
.map_err(|error| ServerStateError::Cluster(error.to_string()))?;
|
|
Ok(nodes.into_iter().map(cluster_node_to_target).collect())
|
|
}
|
|
}
|
|
|
|
struct ApiServerStateRemoteClient {
|
|
runtime_settings: Arc<crate::ApiRuntimeSettings>,
|
|
}
|
|
|
|
#[async_trait]
|
|
impl ServerStateRemoteClient for ApiServerStateRemoteClient {
|
|
async fn remote_node_server_state(
|
|
&self,
|
|
node: &ServerStateClusterTarget,
|
|
) -> ServerStateResult<ServerStateNode> {
|
|
let mut request =
|
|
Request::new(synctv_cluster::grpc::synctv::cluster::GetServerStateRequest {});
|
|
attach_cluster_secret(&mut request, &self.runtime_settings.cluster.secret)?;
|
|
let mut client = server_state_client(&self.runtime_settings, &node.cluster_address).await?;
|
|
let response = client.get_server_state(request).await.map_err(|error| {
|
|
ServerStateError::RemoteRequest {
|
|
node_id: node.node_id.clone(),
|
|
error: error.to_string(),
|
|
}
|
|
})?;
|
|
cluster_proto_to_server_state_node(response.into_inner()).map_err(|error| {
|
|
ServerStateError::RemoteDecode {
|
|
node_id: node.node_id.clone(),
|
|
error,
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
struct ApiServerStateSliceCacheRuntime {
|
|
cache: Arc<synctv_proxy::slice_cache::SliceCache>,
|
|
}
|
|
|
|
impl ServerStateSliceCacheRuntime for ApiServerStateSliceCacheRuntime {
|
|
fn snapshot(&self) -> ServerStateSliceCache {
|
|
let stats = self.cache.stats();
|
|
ServerStateSliceCache {
|
|
status: if stats.engine_enabled {
|
|
ServerStateSliceCacheStatus::Healthy
|
|
} else {
|
|
ServerStateSliceCacheStatus::Disabled
|
|
},
|
|
engine_enabled: stats.engine_enabled,
|
|
backend: stats.backend,
|
|
file_cache_dir: stats.file_cache_dir.unwrap_or_default(),
|
|
slice_size: stats.slice_size,
|
|
max_cache_size: stats.max_cache_size,
|
|
segment_ttl_secs: stats.segment_ttl_secs,
|
|
stale_max_age_secs: stats.stale_max_age_secs,
|
|
stale_while_revalidate: stats.stale_while_revalidate,
|
|
eviction_interval_secs: stats.eviction_interval_secs,
|
|
watermark_ratio: stats.watermark_ratio,
|
|
current_size_bytes: stats.current_size_bytes,
|
|
entry_count: stats.entry_count,
|
|
metadata_entries: stats.metadata_entries,
|
|
updating_entries: stats.updating_entries,
|
|
lock_count: stats.lock_count,
|
|
usage_ratio: stats.usage_ratio,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cluster_node_to_target(node: synctv_cluster::discovery::NodeInfo) -> ServerStateClusterTarget {
|
|
ServerStateClusterTarget {
|
|
node_id: node.node_id,
|
|
cluster_address: node.cluster_address,
|
|
last_heartbeat: node.last_heartbeat.timestamp(),
|
|
epoch: node.epoch,
|
|
}
|
|
}
|
|
|
|
fn server_state_uri(address: &str) -> String {
|
|
if address.starts_with("http://") || address.starts_with("https://") {
|
|
address.to_string()
|
|
} else {
|
|
format!("http://{address}")
|
|
}
|
|
}
|
|
|
|
async fn server_state_client(
|
|
runtime_settings: &crate::ApiRuntimeSettings,
|
|
address: &str,
|
|
) -> ServerStateResult<ClusterServerStateClient> {
|
|
let endpoint = Endpoint::from_shared(server_state_uri(address))
|
|
.map_err(|error| ServerStateError::Cluster(format!("invalid node address: {error}")))?
|
|
.connect_timeout(SERVER_STATE_REMOTE_CONNECT_TIMEOUT)
|
|
.timeout(SERVER_STATE_REMOTE_REQUEST_TIMEOUT);
|
|
let channel: Channel = endpoint.connect().await.map_err(|error| {
|
|
ServerStateError::Cluster(format!("failed to connect to {address}: {error}"))
|
|
})?;
|
|
let client = synctv_cluster::grpc::ServerStateServiceClient::new(channel)
|
|
.max_decoding_message_size(runtime_settings.server.grpc_max_message_size_bytes)
|
|
.max_encoding_message_size(runtime_settings.server.grpc_max_message_size_bytes);
|
|
let client = if runtime_settings.server.grpc_compression_enabled {
|
|
client
|
|
.accept_compressed(CompressionEncoding::Gzip)
|
|
.send_compressed(CompressionEncoding::Gzip)
|
|
} else {
|
|
client
|
|
};
|
|
Ok(client)
|
|
}
|
|
|
|
fn attach_cluster_secret<T>(request: &mut Request<T>, secret: &str) -> ServerStateResult<()> {
|
|
if secret.is_empty() {
|
|
return Err(ServerStateError::MissingClusterSecret);
|
|
}
|
|
synctv_cluster::grpc::attach_cluster_secret(request, secret)
|
|
.map_err(|_| ServerStateError::InvalidClusterSecret)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn saturating_u64(value: usize) -> u64 {
|
|
u64::try_from(value).unwrap_or(u64::MAX)
|
|
}
|