Crate shellflip

source ·
Expand description

Graceful restart management inspired by tableflip, but more basic.

To implement restarts, the simplest thing to do is to generate a RestartConfig from command-line values or hardcoded defaults, then call RestartConfig::try_into_restart_task. If you implement a restart command using unix sockets for interactive error reporting, call RestartConfig::request_restart and return the Result in your main() function.

The process is automatically placed into the ready state the first time the restart task is polled. This should be put into a select statement with other futures your app may await on. The restart task will resolve with Ok(()) if a restart signal was sent and the new process spawned successfully. If the task is unable to handle future restart signals for any reason, it will resolve to an Err.

The process can also be restarted by sending it SIGUSR1. After any kind of restart request, the old process will terminate if the new process starts up successfully, otherwise it will continue if possible.

For coordinating graceful shutdown of the old process, see ShutdownCoordinator in the shutdown module.

Restart thread

Process restarts are handled by a dedicated thread which is spawned when calling either RestartConfig::try_into_restart_task or spawn_restart_task. If you are dropping privileges, capabilities or using seccomp policies to limit the syscalls that can execute, it is a good idea to call the aforementioned functions before locking down the main & future child threads. You likely don’t want the restart thread to have the same restrictions and limitations that may otherwise prevent you from calling execve() or doing certain I/O operations.

Transferring state to the new process

It is possible for the old process to serialise state and send it to the new process to continue processing. Your code must set an implementation of LifecycleHandler on the RestartConfig. After a new process is spawned, shellflip will call LifecycleHandler::send_to_new_process which gives you a unidirectional pipe to write data to the new process, which receives this data by calling the receive_from_old_process function.

The data should be received and validated by the new process before it signals readiness by polling the restart task, in case the data is unusable e.g. if the data format changed slightly between versions causing serialisation to fail. If the new process fails to signal readiness, LifecycleHandler::new_process_failed is called and you can undo any changes you made in preparation for handover. If the new process succeeds, however, the restart task will resolve and you may terminate the process as usual.

Re-exports

Modules

Structs

Enums

Functions

  • When the proxy restarts itself, it sets the child’s LISTEN_PID env to a special value so that the child can replace it with the real child PID. Doing this is easier than reimplementing rust’s process spawn code just so we can call execvpe to replace the environment in the forked process.
  • Spawns a thread that can be used to restart the process. Returns a future that resolves when a restart succeeds, or if restart becomes impossible. The child spawner thread needs to be created before seccomp locks down fork/exec.
  • Notify systemd and the parent process (if any) that the proxy has started successfully. Returns an error if there was a parent process and we failed to notify it.

Type Aliases