use std::future::{Future, IntoFuture};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use futures::{FutureExt, select};
use proxima_primitives::sync::Notify;
use crate::app::Shutdown;
use crate::control_plane::{ControlPlane, DynControlPlane, PipeStatus};
use crate::error::ProximaError;
use crate::telemetry::MetricsSnapshot;
use proxima_primitives::sync::shutdown::ShutdownReport;
struct ServerInner {
control: DynControlPlane,
shutdown_notify: Arc<Notify>,
shutdown: Mutex<Option<Shutdown>>,
}
#[derive(Clone)]
pub struct Server {
inner: Arc<ServerInner>,
}
impl Server {
#[must_use]
pub fn new(shutdown: Shutdown, control: DynControlPlane) -> Self {
Self {
inner: Arc::new(ServerInner {
control,
shutdown_notify: Arc::new(Notify::new()),
shutdown: Mutex::new(Some(shutdown)),
}),
}
}
pub async fn run_until_signal(self) {
let notify = self.inner.shutdown_notify.clone();
select! {
_ = shutdown_signal().fuse() => {}
_ = notify.notified().fuse() => {}
}
if let Some(shutdown) = take_shutdown(&self.inner.shutdown) {
shutdown.stop();
}
}
pub async fn run_until_signal_with_drain(self) -> ShutdownReport {
let notify = self.inner.shutdown_notify.clone();
select! {
_ = shutdown_signal().fuse() => {}
_ = notify.notified().fuse() => {}
}
match take_shutdown(&self.inner.shutdown) {
Some(shutdown) => shutdown.drain().await,
None => ShutdownReport::default(),
}
}
pub fn stop(&self) {
self.inner.shutdown_notify.notify_waiters();
if let Some(shutdown) = take_shutdown(&self.inner.shutdown) {
shutdown.stop();
}
}
pub async fn drain(self) -> ShutdownReport {
let shutdown = take_shutdown(&self.inner.shutdown);
match shutdown {
Some(s) => s.drain().await,
None => ShutdownReport::default(),
}
}
}
fn take_shutdown(mutex: &Mutex<Option<Shutdown>>) -> Option<Shutdown> {
match mutex.lock() {
Ok(mut guard) => guard.take(),
Err(poisoned) => poisoned.into_inner().take(),
}
}
impl IntoFuture for Server {
type Output = Result<(), ProximaError>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(async move {
self.run_until_signal().await;
Ok(())
})
}
}
impl ControlPlane for Server {
fn list_pipes<'lifetime>(
&'lifetime self,
) -> Pin<Box<dyn Future<Output = Result<Vec<PipeStatus>, ProximaError>> + Send + 'lifetime>>
{
self.inner.control.list_pipes()
}
fn status<'lifetime>(
&'lifetime self,
name: &'lifetime str,
) -> Pin<Box<dyn Future<Output = Result<PipeStatus, ProximaError>> + Send + 'lifetime>> {
self.inner.control.status(name)
}
fn snapshot_metrics<'lifetime>(
&'lifetime self,
) -> Pin<Box<dyn Future<Output = Result<MetricsSnapshot, ProximaError>> + Send + 'lifetime>>
{
self.inner.control.snapshot_metrics()
}
fn start<'lifetime>(
&'lifetime self,
name: &'lifetime str,
) -> Pin<Box<dyn Future<Output = Result<PipeStatus, ProximaError>> + Send + 'lifetime>> {
self.inner.control.start(name)
}
fn stop<'lifetime>(
&'lifetime self,
name: &'lifetime str,
) -> Pin<Box<dyn Future<Output = Result<PipeStatus, ProximaError>> + Send + 'lifetime>> {
self.inner.control.stop(name)
}
fn restart<'lifetime>(
&'lifetime self,
name: &'lifetime str,
) -> Pin<Box<dyn Future<Output = Result<PipeStatus, ProximaError>> + Send + 'lifetime>> {
self.inner.control.restart(name)
}
fn reload<'lifetime>(
&'lifetime self,
) -> Pin<Box<dyn Future<Output = Result<(), ProximaError>> + Send + 'lifetime>> {
self.inner.control.reload()
}
fn shutdown<'lifetime>(
&'lifetime self,
) -> Pin<Box<dyn Future<Output = Result<(), ProximaError>> + Send + 'lifetime>> {
Server::stop(self);
Box::pin(async move { Ok(()) })
}
fn apply<'lifetime>(
&'lifetime self,
name: &'lifetime str,
spec: serde_json::Value,
) -> Pin<Box<dyn Future<Output = Result<PipeStatus, ProximaError>> + Send + 'lifetime>> {
self.inner.control.apply(name, spec)
}
fn logs<'lifetime>(
&'lifetime self,
name: &'lifetime str,
max_lines: Option<usize>,
) -> Pin<Box<dyn Future<Output = Result<Vec<String>, ProximaError>> + Send + 'lifetime>> {
self.inner.control.logs(name, max_lines)
}
}
#[cfg(all(unix, feature = "tokio"))]
async fn shutdown_signal() {
use tokio::signal::unix::{SignalKind, signal};
let mut sigterm = match signal(SignalKind::terminate()) {
Ok(s) => s,
Err(_) => return,
};
let mut sigint = match signal(SignalKind::interrupt()) {
Ok(s) => s,
Err(_) => return,
};
select! {
_ = sigterm.recv().fuse() => {}
_ = sigint.recv().fuse() => {}
}
}
#[cfg(all(not(unix), feature = "tokio"))]
async fn shutdown_signal() {
let _ = tokio::signal::ctrl_c().await;
}
#[cfg(all(unix, not(feature = "tokio"), feature = "runtime-prime-reactor"))]
async fn shutdown_signal() {
let _ = prime::os::signal::wait_for_signal().await;
}
#[cfg(not(any(feature = "tokio", all(unix, feature = "runtime-prime-reactor"))))]
async fn shutdown_signal() {
core::future::pending::<()>().await;
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
#[cfg(all(unix, feature = "runtime-prime-reactor"))]
use std::task::Poll;
use super::*;
use crate::control_plane::{PipeState, StaticControlPlane};
fn fixture_control() -> DynControlPlane {
Arc::new(StaticControlPlane::new(vec![PipeStatus {
name: "echo".into(),
state: PipeState::Running,
uptime_ms: Some(1234),
restart_count: 0,
last_message: None,
}]))
}
#[proxima::test]
async fn clone_shares_control_plane_state() {
let (_tx, rx) = futures::channel::oneshot::channel();
let shutdown = Shutdown::for_test(rx);
let server = Server::new(shutdown, fixture_control());
let clone = server.clone();
let listed_a = server.list_pipes().await.expect("list a");
let listed_b = clone.list_pipes().await.expect("list b");
assert_eq!(listed_a.len(), listed_b.len());
assert_eq!(listed_a[0].name, "echo");
assert_eq!(listed_b[0].name, "echo");
}
#[proxima::test]
async fn stop_from_clone_signals_listener() {
let (tx, mut rx) = futures::channel::oneshot::channel();
let shutdown = Shutdown::for_test_with_tx(tx);
let server = Server::new(shutdown, fixture_control());
let clone = server.clone();
clone.stop();
let outcome = proxima_core::time::timeout(std::time::Duration::from_millis(100), &mut rx)
.await
.expect("oneshot fires within 100ms");
assert!(outcome.is_ok(), "shutdown sender fired");
}
#[proxima::test]
async fn stop_is_idempotent_across_clones() {
let (tx, _rx) = futures::channel::oneshot::channel();
let shutdown = Shutdown::for_test_with_tx(tx);
let server = Server::new(shutdown, fixture_control());
let clone = server.clone();
server.stop();
clone.stop(); }
#[cfg(all(unix, feature = "runtime-prime-reactor", not(feature = "tokio")))]
#[proxima::test]
async fn run_until_signal_resolves_on_real_sigint_prime_path() {
let (tx, mut rx) = futures::channel::oneshot::channel();
let shutdown = Shutdown::for_test_with_tx(tx);
let server = Server::new(shutdown, fixture_control());
let mut driving = std::pin::pin!(server.run_until_signal());
{
let waker = std::task::Waker::noop();
let mut context = std::task::Context::from_waker(waker);
assert_eq!(
driving.as_mut().poll(&mut context),
Poll::Pending,
"run_until_signal must park on the OS signal, not resolve immediately"
);
}
unsafe {
libc::raise(libc::SIGINT);
}
proxima_core::time::timeout(std::time::Duration::from_secs(5), driving)
.await
.expect("run_until_signal must resolve within 5s of a real SIGINT");
let outcome = proxima_core::time::timeout(std::time::Duration::from_millis(100), &mut rx)
.await
.expect("Shutdown::stop() must fire the shutdown sender");
assert!(outcome.is_ok(), "shutdown sender fired");
}
#[cfg(all(unix, feature = "runtime-prime-reactor", not(feature = "tokio")))]
#[proxima::test]
async fn run_until_signal_resolves_on_real_sigterm_prime_path() {
let (tx, mut rx) = futures::channel::oneshot::channel();
let shutdown = Shutdown::for_test_with_tx(tx);
let server = Server::new(shutdown, fixture_control());
let mut driving = std::pin::pin!(server.run_until_signal());
{
let waker = std::task::Waker::noop();
let mut context = std::task::Context::from_waker(waker);
assert_eq!(
driving.as_mut().poll(&mut context),
Poll::Pending,
"run_until_signal must park on the OS signal, not resolve immediately"
);
}
unsafe {
libc::raise(libc::SIGTERM);
}
proxima_core::time::timeout(std::time::Duration::from_secs(5), driving)
.await
.expect("run_until_signal must resolve within 5s of a real SIGTERM");
let outcome = proxima_core::time::timeout(std::time::Duration::from_millis(100), &mut rx)
.await
.expect("Shutdown::stop() must fire the shutdown sender");
assert!(outcome.is_ok(), "shutdown sender fired");
}
#[cfg(all(unix, feature = "tokio", feature = "runtime-prime-reactor"))]
#[proxima::test(runtime = "tokio")]
async fn run_until_signal_resolves_on_real_sigint_tokio_path() {
let (tx, mut rx) = futures::channel::oneshot::channel();
let shutdown = Shutdown::for_test_with_tx(tx);
let server = Server::new(shutdown, fixture_control());
let mut driving = std::pin::pin!(server.run_until_signal());
{
let waker = std::task::Waker::noop();
let mut context = std::task::Context::from_waker(waker);
assert_eq!(
driving.as_mut().poll(&mut context),
Poll::Pending,
"run_until_signal must park on the OS signal, not resolve immediately"
);
}
unsafe {
libc::raise(libc::SIGINT);
}
proxima_core::time::timeout(std::time::Duration::from_secs(5), driving)
.await
.expect("run_until_signal must resolve within 5s of a real SIGINT");
let outcome = proxima_core::time::timeout(std::time::Duration::from_millis(100), &mut rx)
.await
.expect("Shutdown::stop() must fire the shutdown sender");
assert!(outcome.is_ok(), "shutdown sender fired");
}
}