Graceful Termination
Kubernetes (K8s) applications are expected to handle graceful termination. Typically, applications will initiate a graceful termination by handling the TERM
signal when a K8s pod is to be terminated.
At this point in time Julia does not provide user-definable signal handlers and the internal Julia signal handler for TERM
results in the process reporting the signal (with a stack trace) to standard error and exiting. Julia provides users with atexit
to define callbacks when Julia is terminating but unfortunately this callback system only allows for trivial actions to occur when Julia is shutdown due to handling the TERM
signal.
These limitations resulted in K8sDeputy.jl providing an alternative path for handling graceful termination Julia processes. This avoids logging unnecessary error messages and also provides a reliable shutdown callback system for graceful termination.
Interface
The K8sDeputy.jl package provides the graceful_terminator
function for registering a single user callback upon receiving a graceful termination event. The graceful_terminate
function can be used from another Julia process to terminate the graceful_terminator
caller process. For example run the following code in an interactive Julia REPL:
using K8sDeputy
graceful_terminator(() -> (@info "Gracefully terminating..."; exit()))
In another terminal run the following code to initiate graceful termination:
julia -e 'using K8sDeputy; graceful_terminate()'
Once graceful_terminate
has been called the first process will: execute the callback, log the message, and exit the Julia process.
By default the graceful_terminator
function registers the caller Julia process as the "entrypoint" Julia process. Primarily, this allows for out-of-the-box support for Julia applications running as non-init processes but only allows one Julia process to be defined as the "entrypoint". If you require multiple Julia processes to support graceful termination concurrently you can use set_entrypoint=false
(e.g. graceful_terminator(...; set_entrypoint=false)
) and pass in the target process ID to graceful_terminate
.
Deputy Integration
The graceful_terminator
function can be combined with the deputy's shutdown!
function to allow graceful termination of the application and the deputy:
using K8sDeputy
deputy = Deputy(; shutdown_handler=() -> @info "Shutting down")
server = K8sDeputy.serve!(deputy, "0.0.0.0")
graceful_terminator(() -> shutdown!(deputy))
# Application code
Kubernetes Setup
To configure your K8s container resource to call graceful_terminate
when terminating you can configure a preStop
hook:
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
# command: ["/bin/sh", "-c", "julia entrypoint.jl; sleep 1"]
lifecycle:
preStop:
exec:
command: ["julia", "-e", "using K8sDeputy; graceful_terminate()"]
# terminationGracePeriodSeconds: 30
Applications with slow shutdown callbacks may want to consider specifying terminationGracePeriodSeconds
which specifies the maximum duration a pod can take when gracefully terminating. Once the timeout is reached the processes running in the pod are forcibly halted with a KILL
signal.
Finally, the entrypoint for the container should also not directly use the Julia as the init process (PID 1). Instead, users should define their entrypoint similarly to ["/bin/sh", "-c", "julia entrypoint.jl; sleep 1"]
as this allows both the Julia process and the preStop
process to cleanly terminate.
Read-only Filesystem
If you have a read-only filesystem on your container you'll need to configure a writeable /run
directory for K8sDeputy.jl so it can create UNIX-domain sockets for interprocess communication:
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
# command: ["/bin/sh", "-c", "julia entrypoint.jl; sleep 1"]
lifecycle:
preStop:
exec:
command: ["julia", "-e", "using K8sDeputy; graceful_terminate()"]
securityContext:
readOnlyRootFilesystem: true
volumeMounts:
- name: ipc
mountPath: /run
subPath: app # Set the `subPath` to the container name to ensure per-container isolation
volumes:
- name: ipc
emptyDir:
medium: Memory