index : static-web-server.git

ascending towards madness

author Jose Quintana <1700322+joseluisq@users.noreply.github.com> 2021-10-29 23:31:07.0 +00:00:00
committer GitHub <noreply@github.com> 2021-10-29 23:31:07.0 +00:00:00
commit
0a02da33369da8006b4506d1371e7c4c2509cd95 [patch]
tree
ed1199b3d3ec7183aa7a3a37e218996e5d55d482
parent
802c6fe00405fb5f093f6b3951219b76ef0d2ed1
parent
ef52492d8d899794670fdbaf1d4ed3d69aee5023
download
0a02da33369da8006b4506d1371e7c4c2509cd95.tar.gz

Merge pull request #62 from joseluisq/feature/gracefully_shutdown_server_support

feat: graceful shutdown support for http1 / http2 servers by default

Diff

 Cargo.lock                |  69 +++++++++++++++---------------
 Cargo.toml                |   7 ++-
 docker/alpine/Dockerfile  |   4 ++-
 docker/scratch/Dockerfile |   3 +-
 src/server.rs             | 108 +++++++++++++++++++++++++++++------------------
 src/signals.rs            |  51 ++++++++++++++--------
 6 files changed, 150 insertions(+), 92 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index c17be3b..0d0b2f4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -203,16 +203,6 @@ dependencies = [
]

[[package]]
name = "ctrlc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a19c6cedffdc8c03a3346d723eb20bd85a13362bb96dc2ac000842c6381ec7bf"
dependencies = [
 "nix",
 "winapi",
]

[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -547,15 +537,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"

[[package]]
name = "memoffset"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9"
dependencies = [
 "autocfg",
]

[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -604,19 +585,6 @@ dependencies = [
]

[[package]]
name = "nix"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f305c2c2e4c39a82f7bf0bf65fb557f9070ce06781d4f2454295cc34b1c43188"
dependencies = [
 "bitflags",
 "cc",
 "cfg-if",
 "libc",
 "memoffset",
]

[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -854,6 +822,38 @@ dependencies = [
]

[[package]]
name = "signal-hook"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
dependencies = [
 "cc",
 "libc",
 "signal-hook-registry",
]

[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
 "libc",
]

[[package]]
name = "signal-hook-tokio"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8"
dependencies = [
 "futures-core",
 "libc",
 "signal-hook",
 "tokio",
]

[[package]]
name = "slab"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -889,7 +889,6 @@ dependencies = [
 "async-compression",
 "bcrypt",
 "bytes",
 "ctrlc",
 "futures-util",
 "headers",
 "http",
@@ -901,6 +900,8 @@ dependencies = [
 "num_cpus",
 "percent-encoding",
 "pin-project",
 "signal-hook",
 "signal-hook-tokio",
 "structopt",
 "time",
 "tokio",
@@ -985,7 +986,9 @@ dependencies = [
 "memchr",
 "mio",
 "num_cpus",
 "once_cell",
 "pin-project-lite",
 "signal-hook-registry",
 "tokio-macros",
 "winapi",
]
diff --git a/Cargo.toml b/Cargo.toml
index f99ac8a..273dbba 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,7 +32,6 @@ anyhow = "1.0"
async-compression = { version = "0.3", features = ["brotli", "deflate", "gzip", "tokio"] }
bcrypt = "0.10"
bytes = "1.0"
ctrlc = { version = "3.1", features = ["termination"] }
futures-util = { version = "0.3", default-features = false, features = ["sink"] }
headers = { git = "https://github.com/joseluisq/hyper-headers.git", branch = "headers_encoding" }
http = "0.2"
@@ -45,7 +44,7 @@ percent-encoding = "2.1"
pin-project = "1.0"
structopt = { version = "0.3", default-features = false }
time = "0.1"
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util"], default-features = false }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "fs", "io-util", "signal"], default-features = false }
tokio-rustls = { version = "0.22" }
tokio-util = { version = "0.6", features = ["io"] }
tracing = "0.1"
@@ -54,6 +53,10 @@ tracing-subscriber = "0.2"
[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator]
version = "0.3"

[target.'cfg(not(windows))'.dependencies]
signal-hook = { version = "0.3.4", features = ["extended-siginfo"] }
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"], default-features = false }

[dev-dependencies]
bytes = "1.0"

diff --git a/docker/alpine/Dockerfile b/docker/alpine/Dockerfile
index f8e4708..b0e23b4 100644
--- a/docker/alpine/Dockerfile
+++ b/docker/alpine/Dockerfile
@@ -19,7 +19,11 @@ COPY ./docker/alpine/entrypoint.sh /
COPY ./docker/public /public

EXPOSE 80

STOPSIGNAL SIGQUIT

ENTRYPOINT ["/entrypoint.sh"]

CMD ["static-web-server"]

# Metadata
diff --git a/docker/scratch/Dockerfile b/docker/scratch/Dockerfile
index 2124c10..e89f8a8 100644
--- a/docker/scratch/Dockerfile
+++ b/docker/scratch/Dockerfile
@@ -15,6 +15,9 @@ COPY --from=latest /usr/local/bin/static-web-server /
COPY ./docker/public /public

EXPOSE 80

STOPSIGNAL SIGQUIT

ENTRYPOINT ["/static-web-server"]

# Metadata
diff --git a/src/server.rs b/src/server.rs
index 211de99..191b3eb 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -132,7 +132,7 @@ impl Server {
            },
        });

        // Spawn a new Tokio asynchronous task with its given options
        // Run the corresponding HTTP Server asynchronously with its given options

        if opts.http2 {
            // HTTP/2 + TLS
@@ -140,54 +140,82 @@ impl Server {
            let cert_path = opts.http2_tls_cert.clone();
            let key_path = opts.http2_tls_key.clone();

            tokio::task::spawn(async move {
                tcp_listener
                    .set_nonblocking(true)
                    .expect("cannot set non-blocking");
                let listener = tokio::net::TcpListener::from_std(tcp_listener)
                    .expect("failed to create tokio::net::TcpListener");
                let mut incoming = AddrIncoming::from_listener(listener)?;
                incoming.set_nodelay(true);

                let tls = TlsConfigBuilder::new()
                    .cert_path(cert_path)
                    .key_path(key_path)
                    .build()
                    .expect(
                        "error during TLS server initialization, probably cert or key file missing",
                    );

                let server =
                    HyperServer::builder(TlsAcceptor::new(tls, incoming)).serve(router_service);

                tracing::info!(
                    parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
                    "listening on https://{}",
                    addr_str
            tcp_listener
                .set_nonblocking(true)
                .expect("cannot set non-blocking");
            let listener = tokio::net::TcpListener::from_std(tcp_listener)
                .expect("failed to create tokio::net::TcpListener");
            let mut incoming = AddrIncoming::from_listener(listener)?;
            incoming.set_nodelay(true);

            let tls = TlsConfigBuilder::new()
                .cert_path(cert_path)
                .key_path(key_path)
                .build()
                .expect(
                    "error during TLS server initialization, probably cert or key file missing",
                );

                server.await
            });
            #[cfg(not(windows))]
            let signals = signals::create_signals()?;
            #[cfg(not(windows))]
            let handle = signals.handle();

            let server =
                HyperServer::builder(TlsAcceptor::new(tls, incoming)).serve(router_service);

            #[cfg(not(windows))]
            let server = server.with_graceful_shutdown(signals::wait_for_signals(signals));
            #[cfg(windows)]
            let server = server.with_graceful_shutdown(signals::wait_for_ctrl_c());

            tracing::info!(
                parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
                "listening on https://{}",
                addr_str
            );

            tracing::info!("press ctrl+c to shut down the server");

            server.await?;

            #[cfg(not(windows))]
            handle.close();
        } else {
            // HTTP/1

            tokio::task::spawn(async move {
                let server = HyperServer::from_tcp(tcp_listener)
                    .unwrap()
                    .tcp_nodelay(true)
                    .serve(router_service);
            #[cfg(not(windows))]
            let signals = signals::create_signals()?;
            #[cfg(not(windows))]
            let handle = signals.handle();

                tracing::info!(
                    parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
                    "listening on http://{}",
                    addr_str
                );
            let server = HyperServer::from_tcp(tcp_listener)
                .unwrap()
                .tcp_nodelay(true)
                .serve(router_service);

                server.await
            });
            #[cfg(not(windows))]
            let server = server.with_graceful_shutdown(signals::wait_for_signals(signals));
            #[cfg(windows)]
            let server = server.with_graceful_shutdown(signals::wait_for_ctrl_c());

            tracing::info!(
                parent: tracing::info_span!("Server::start_server", ?addr_str, ?threads),
                "listening on http://{}",
                addr_str
            );

            tracing::info!("press ctrl+c to shut down the server");

            server.await?;

            #[cfg(not(windows))]
            handle.close();
        }

        signals::wait_for_ctrl_c()
        tracing::warn!("termination signal caught, shutting down the server execution");

        Ok(())
    }
}

diff --git a/src/signals.rs b/src/signals.rs
index 49941dd..20fc4da 100644
--- a/src/signals.rs
+++ b/src/signals.rs
@@ -1,21 +1,38 @@
use ctrlc;
use std::sync::mpsc::channel;
#[cfg(not(windows))]
use {
    crate::Result, futures_util::stream::StreamExt, signal_hook::consts::signal::*,
    signal_hook_tokio::Signals,
};

use crate::{Context, Result};

/// It waits for a `Ctrl-C` incoming signal.
pub fn wait_for_ctrl_c() -> Result {
    let (tx, rx) = channel();

    ctrlc::set_handler(move || tx.send(()).expect("could not send signal on channel"))
        .with_context(|| "error setting Ctrl-C handler".to_owned())?;

    tracing::info!("press Ctrl+C to shutdown server");

    rx.recv()
        .with_context(|| "could not receive signal from channel".to_owned())?;
#[cfg(not(windows))]
/// It creates a common list of signals stream for `SIGTERM`, `SIGINT` and `SIGQUIT` to be observed.
pub fn create_signals() -> Result<Signals> {
    Ok(Signals::new(&[SIGHUP, SIGTERM, SIGINT, SIGQUIT])?)
}

    tracing::warn!("Ctrl+C signal caught, shutting down server execution");
#[cfg(not(windows))]
/// It waits for a specific type of incoming signals included `ctrl+c`.
pub async fn wait_for_signals(signals: Signals) {
    let mut signals = signals.fuse();
    while let Some(signal) = signals.next().await {
        match signal {
            SIGHUP => {
                // Note: for now we don't do something for SIGHUPs
                tracing::debug!("SIGHUP caught, nothing to do about")
            }
            SIGTERM | SIGINT | SIGQUIT => {
                tracing::debug!("an incoming SIGTERM received, SIGINT or SIGQUIT signal, delegating graceful shutdown to server");
                break;
            }
            _ => unreachable!(),
        }
    }
}

    Ok(())
#[cfg(windows)]
/// It waits for an incoming `ctrl+c` signal on Windows.
pub async fn wait_for_ctrl_c() {
    tokio::signal::ctrl_c()
        .await
        .expect("failed to install ctrl+c signal handler");
}