ecdysis

repository·main·Indexed 43 days ago

https://github.com/cloudflare/ecdysis

A Rust library for graceful process restarts, optimized for socket inheritance and rebinding in network services. It allows updating running code or configuration without disrupting existing client connections by transferring clients to a new process. Version 1.1.1 supports TCP, UDP, and Unix sockets, with optional integration for systemd-notify and systemd named sockets.

Tokens
8.2K
Snippets
16
Records
35
Agent score
55%

What's inside ecdysis

  1. Overview of ecdysis for graceful restarts

    main

    The ecdysis library is a Rust tool designed for performing graceful restarts of network services. It allows you to update running code or configuration without disrupting existing client connections by starting a new process, transferring clients to it, and then exiting the old process.

    Key design goals include:

    • Ensuring no old code remains running after a successful upgrade.
    • Providing a grace period for the new process to perform initialization.
    • Allowing the process to crash safely during the initialization phase.
    • Ensuring only a single upgrade runs in parallel at any time.

    ecdysis is based on the Go library tableflip and is suitable for scenarios where socket inheritance and rebinding are prioritized.

  2. Compare ecdysis and shellflip

    main

    When choosing between ecdysis and shellflip for graceful reloads, consider your primary use case:

    Featureecdysisshellflip
    Primary FocusSocket inheritance and rebindingArbitrary data transfer between parent/child
    OpinionatednessLess opinionated; supports synchronous (non-Tokio) modeHighly opinionated; assumes systemd and tokio
    Data SharingRequires manual serialization (e.g., via named pipes)Provides utilities/abstractions for easy data sharing
    EnvironmentDoes not assume systemd by defaultBuilt specifically for systemd environments

    Use shellflip if you need a user-friendly way to share complex state/data. Use ecdysis if your priority is managing network socket inheritance.

  3. Enable systemd named socket support

    main

    You can enable support for managing and persisting systemd-spawned named sockets by enabling the systemd_sockets crate feature. This is useful for passing network connections from a parent process to a child process via systemd.

    Requirements:

    1. Enable the systemd_sockets feature (this automatically enables tokio_ecdysis and requires the tokio crate).
    2. Use named systemd sockets. ecdysis will ignore anonymous file descriptors or file descriptors with duplicate/special names as defined by sd_listen_fds.

    Note: Failure to meet these requirements will cause ecdysis to fail during initialization.

  4. Enable systemd-notify support

    main

    You can integrate ecdysis with systemd's process status notification capabilities by enabling the systemd_notify crate feature. This enables automatic compatibility with systemd service status notifications.

    Requirements:

    1. Enable the systemd_notify feature (this automatically enables tokio_ecdysis and requires the tokio crate).
    2. Use systemd version v253 or newer.
    3. Set Type=notify-reload in your application's systemd service unit configuration file.

    Note: Failure to meet these requirements will cause ecdysis to fail during initialization.

  5. Understand the ListenerRegistry socket management model

    main

    The ListenerRegistry manages file descriptors (FDs) used for socket listeners during Ecdysis process upgrades. It maintains two distinct collections of ListenerInfo to handle the handoff between parent and child processes:

    1. used_fds: Tracks FDs opened by the parent via Ecdysis::listen_* methods. When adding a listener to this registry, the FD is duplicated (via dup) to ensure the original listener's semantics remain unchanged. These are serialized and sent to the child process.
    2. inherited_fds: Stores FDs received from the parent during a child process upgrade. The child uses the inherit method to retrieve these FDs based on their socket information.

    When a file descriptor is retrieved from inherited_fds via take, the registry automatically sets the CLOEXEC flag on it to ensure safe handling in subsequent executions.

  6. How systemd activated sockets work in ecdysis

    main

    Ecdysis supports acquiring sockets that are created and passed to the process by systemd. This mechanism allows systemd to manage socket lifecycle and pass the file descriptors (FDs) to the service during the fork-exec process.

    The Handover Process

    1. Socket Creation: systemd creates the sockets and removes the FD_CLOEXEC flag so the process can inherit them.
    2. Environment Variables: systemd sets specific LISTEN_* environment variables to communicate socket information:
      • LISTEN_PID: The PID of the process systemd started. Ecdysis verifies this matches the current process PID for safety.
      • LISTEN_FDS: The number of FDs passed. (Note: Ecdysis does not use this variable directly).
      • LISTEN_FDNAMES: A colon-separated list of names for the passed FDs (e.g., sock1:sock2). These names correspond to the FileDescriptorName= setting in the systemd socket service file.

    Ecdysis Behavior and Safety

    • PID Verification: Ecdysis checks LISTEN_PID against the current process ID. If they do not match, it returns a SystemdSocketsReadError::DoesNotMatchListenPid error.
    • CLOEXEC Enforcement: Ecdysis automatically sets the CLOEXEC flag on all inherited sockets. This prevents socket leaks to child processes spawned by ecdysis.
    • Socket Filtering: Only sockets explicitly requested via methods like systemd_listen_unix or systemd_listen_tcp will be passed to child instances. Any other sockets passed by systemd that are not found by these methods will be lost.
    • Duplicate/Special Names:
      • Ecdysis ignores sockets with duplicate names in LISTEN_FDNAMES.
      • Ecdysis ignores sockets with special names defined by sd_listen_fds: unknown, stored, and connection.
    • Environment Cleanup: Ecdysis resets all LISTEN_* environment variables before spawning any child instances to ensure child processes do not incorrectly use them.
  7. Run integration tests using Docker Compose

    main

    To run the integration tests in a Docker container, use docker compose up.

    If you are developing on a machine protected by Cloudflare WARP, you must provide the path to your WARP certificates via the CF_CERTS environment variable. This allows the container to mount the necessary root certificates to prevent network failures during tests. If this variable is not set, the container mounts /dev/null and the initial certificate setup commands will fail silently.

    Note: The init: true setting in the configuration is used to ensure child processes do not become zombies.

    # For Cloudflare employees on WARP-protected machines:
    $ CF_CERTS=~/.local/share/cloudflare-warp-certs docker compose up
    
    # For others (or non-WARP machines):
    $ docker compose up
  8. Use systemd integration with TokioEcdysis

    main

    If the systemd_notify feature is enabled, TokioEcdysis can manage systemd lifecycle notifications.

    Setup

    1. Call enable_systemd_notifications() on the builder to initialize the notifier.
    2. The lifecycle.await future will automatically send READY=1 when the process is ready and RELOADING=1 when an upgrade starts.

    Extending Timeouts

    Use extend_systemd_timeouts(duration) to tell systemd to extend the startup, runtime, or shutdown timeouts. This is useful if your post-Ecdysis shutdown logic is slow.

    Partial Shutdowns

    When a partial_stop_on_signal is triggered, the lifecycle.await future returns ExitMode::PartialStopWithSystemd(SystemdNotifier). This allows the caller to manually handle systemd notifications if desired.

  9. Configure upgrade and shutdown triggers

    main

    The TokioEcdysisBuilder allows you to define what events trigger different lifecycle actions:

    Upgrade Triggers

    Triggers an upgrade (fork-exec to a new process) while attempting to hand over sockets.

    • upgrade_on_socket(path): Triggers an upgrade when a connection is made to the specified Unix Domain Socket.
    • (Default) The SignalKind passed to new() triggers an upgrade.

    Stop (Full Shutdown) Triggers

    Triggers a graceful shutdown of the process.

    • stop_on_signal(signal_kind): Registers a Unix signal to trigger a full stop.
    • stop_on_socket(path): Triggers a full stop when a connection is made to the specified Unix Domain Socket.

    Partial Stop Triggers

    Triggers a partial shutdown where Ecdysis stops monitoring triggers but skips other shutdown steps (like notifying systemd).

    • partial_stop_on_signal(signal_kind)
    • partial_stop_on_socket(path)
  10. Initialize TokioEcdysis with a builder

    main

    To use TokioEcdysis in a Tokio-based application, use the TokioEcdysisBuilder. You must provide a SignalKind during construction which will act as the trigger for an upgrade.

    Steps:

    1. Create a new builder using TokioEcdysisBuilder::new(upgrade_signal_kind).
    2. Configure various triggers (signals, sockets, or PID files).
    3. Call .ready() to consume the builder. This returns an Arc<TokioEcdysis> for creating sockets and a Future that monitors for triggers. You must await this future in your main task to manage the lifecycle (upgrades/shutdowns).
    use tokio_ecdysis::{TokioEcdysisBuilder, ExitMode};
    use tokio::signal::unix::{SignalKind, SignalErrorKind};
    
    #[tokio::main]
    async fn main() -> Result<(), Box<dyn std::error::Error>> {
        // 1. Initialize builder with an upgrade signal (e.g., SIGHUP)
        let mut builder = TokioEcdysisBuilder::new(SignalKind::from(SignalErrorKind::Hangup))?;
    
        // 2. Configure additional triggers
        builder.stop_on_signal(SignalKind::from(SignalErrorKind::Interrupt))?;
        builder.upgrade_on_socket("/tmp/upgrade.sock")?;
    
        // 3. Consume builder
        let (ecdysis, lifecycle) = builder.ready()?;
    
        // 4. Use ecdysis to create sockets
        let listener = ecdysis.listen_tcp(addr)?;
        tokio::spawn(async move { /* handle connections */ });
    
        // 5. Await the lifecycle future to manage upgrades/shutdowns
        let (exit_mode, reason) = lifecycle.await?;
        println!("Exited with mode {:?} due to {:?}", exit_mode, reason);
    
        Ok(())
    }
  11. Initialize Ecdysis for graceful upgrades

    main

    Use Ecdysis::new() to initialize the upgrade manager. This function automatically detects if the current process is a child process resulting from a previous upgrade attempt.

    • If it is a child process, it inherits the listening sockets from the parent.
    • If it is the parent process, it initializes a new registry for managing sockets.

    If the environment is not correctly set up for an upgrade, the process will exit with code 1.

    use ecdysis::Ecdysis;
    
    let ecdysis = Ecdysis::new();
    
    if ecdysis.is_child() {
        // Logic for the new version of your application
    } else {
        // Logic for the current version of your application
    }
  12. Configure WARP certificates for integration tests

    main

    The integration test environment uses the CF_CERTS environment variable to mount Cloudflare root certificates into the container at /opt/cf.

    • Variable Name: CF_CERTS
    • Default Value: /dev/null (if unset)
    • Container Mount Path: /opt/cf
    • Purpose: Provides CloudflareRootCertificate.pem to the container so it can be installed into the system trust store via update-ca-certificates.