systemd reload and hypnotoad reload needs a wrapper script to work together properly
Note: This workaround applies to Mojolicious 6.x. SystemD compatibility was fixed in Mojolicious 7.x
If you are wanting to reload under systemd, then a standard hypnotoad reload (i.e. hypnotoad myapp.pl
) or kill -USR $MAINPID
is not preferrable. SystemD assumes that once the command to reload the hypnotoad server returns, the reload is finished. But, instead, both of these options exit immediately, and do not wait for the reload to finish.
When I have tried these standard reload options, systemd completes the execution of the reload command, and then, afterward, monitors that the pid file is removed and replaced when hypnotoad does the "Starting zero downtime software upgrade > upgrade successful".
If you have systemd configured to always run the hypnotoad app, systemd will then restart (stop and start) the application again - after hypnotoad did it gracefully.
Here is what happens:
- systemd reload hypnotoad-app (ExecReload=hypnotoad myapp.pl)
- hypnotoad-app exits after being told to reload
- hypnotoad starts up a new process with the newest myapp.pl
- systemd thinks the reload just completed, and sees that the pid did not change in the PIDFile
- hypnotoad see no load on the old processes, kills them, and deletes and creates a new PIDFile with the PID of the new process when the "Upgrade [is] successful" (This is hypnotoad's "zero downtime" reload procedure)
- systemd sees the pid file removed (it ignores that it is recreated, probably because by design, systemd wants to be in control of the forking)
- systemd waits for the HOLDOFF (
RestartSec=<seconds>
) if configured
- systemd does a full unit start of the hypnotoad-app
systemd documentation specifically says that when the ExecReload=
command returns, systemd expects the process to already finished reloading, and the pid file to have the new PID of the reloaded process. But Hypnotoad does not work synchronously like that. So systemd and hypnotoad do not work together nicely.
https://www.freedesktop.org/software/systemd/man/systemd.service.html
Note however that reloading a daemon by sending a signal (as with the
example line above) is usually not a good choice, because this is an
asynchronous operation and hence not suitable to order reloads of
multiple services against each other. It is strongly recommended to
set ExecReload= to a command that not only triggers a configuration
reload of the daemon, but also synchronously waits for it to complete.
The solution is to write a wrapper around the reload process.
#!/bin/bash
SERVER="/path/to/myapp.pl"
HYPNOTOAD="/usr/bin/hypnotoad"
PIDFILE="/var/run/myapp.pid"
# Timeout == LOOPSAFE x SLEEPTIME
SLEEPTIME="0.5"
LOOPSAFE=20
LOOPCOUNT=0
#
if [ ! -f "${PIDFILE}" ]; then
# The PID files does not exist, maybe $SERVER is not running
exit 1
fi
OLDPID=$(cat ${PIDFILE})
NEWPID=$OLDPID
# Reload the application
${HYPNOTOAD} ${SERVER}
while (( $LOOPCOUNT <= $LOOPSAFE )); do
let LOOPCOUNT++
if [ -f ${PIDFILE} ]; then
NEWPID=$(cat ${PIDFILE})
if (( $NEWPID > 1 )) && (( $NEWPID != $OLDPID )); then
exit 0
fi
fi
sleep ${SLEEPTIME}
done
exit 1
Here is the systemd unit file needed for Hypnotoad
[Unit]
Description=Hypnotoad-app
Requires=network.target
After=network.target
[Service]
Type=simple
SyslogIdentifier=hypnotoad-app
PIDFile=/var/run/myapp.pid
EnvironmentFile=-/etc/sysconfig/myapp
ExecStart=/usr/bin/hypnotoad --foreground /path/to/myapp.pl
ExecStop=/usr/bin/hypnotoad --stop /path/to/myapp.pl
ExecReload=/path/to/reload-myapp.sh
KillMode=process
Restart=always
RestartSec=5
User=myuser
Group=mygroup
[Install]
WantedBy=multi-user.target
Here is how the reload procedure now works
- systemd reload hypnotoad-app
- systemd executes the reload script
- reload script reloads hypnotoad-app, and waits for the "successful upgrade" - essentially sleeping until the PID file has the PID of the newly reloaded myapp.pl process
- systemd now monitors the pid file with the new PID, which does not change any more - so now systemd does not try to restart it