Writing Advanced Daemons That Aren’t Daemons

This is the second and final article in a set of two. The first covered a pluggable locking toolkit. Here, we’ll explore more advanced patterns. At Booking.com, we use Perl and IPC::ConcurrencyLimit[3]. You may use another language and another locking API: the strategies described here should be transferable.

- Run once per minute (or once every couple).
- Attempt to get run lock.
- If failed to get run lock, then exit.
- If succeeded,
then execute main loop until life time reached,
then exit.
use 5.14.2;
use warnings;
use IPC::ConcurrencyLimit;

use constant LIFE_TIME => 5*60; # 5 min process life time

my $limit = IPC::ConcurrencyLimit->new(
type => 'Flock',
max_procs => 1,
path => '/var/run/myapp',
);

my $lock_id = $limit->get_lock;
if (not $lock_id) {
# Other process running.
exit(0);
}
else {
my $end_time = time() + LIFE_TIME;

while (1) {
process_work_unit();
last if time() >= $end_time;
}

exit(0);
}
use constant LIFE_TIME => 5 * 60 + int(rand(60)); # 5-6 min life time
- Run once per minute (or once every couple).
- Attempt to get run lock.
- If succeeded to get run lock,
then execute main loop until life time reached,
then exit.
- If failed to get run lock, then
- Attempt to get standby lock.
- If failed, exit.
- If succeeded, then attempt to get run lock in a loop
with short retry interval.
use 5.14.2;
use warnings;
use IPC::ConcurrencyLimit;
use IPC::ConcurrencyLimit::WithStandby;

# 5-6 min process life time
use constant LIFE_TIME => 5 * 60 + int(rand(60)); # in seconds

# Attempt to get main lock every 1/2 second
use constant MAIN_LOCK_INTERVAL => 1/2; # in seconds

# Keep retrying for ~3x lifetime of the worker process
use constant MAIN_LOCK_RETRIES => 1 + 3*int(LIFE_TIME / MAIN_LOCK_INTERVAL);

my $limit = IPC::ConcurrencyLimit::WithStandby->new(
type => 'Flock',
path => '/var/run/myapp',
max_procs => 1,
standby_path => '/var/run/myapp/standby',
standby_max_procs => 1,
interval => MAIN_LOCK_INTERVAL,
retries => MAIN_LOCK_RETRIES,
);

my $lock_id = $limit->get_lock;
if (not $lock_id) {
# Other process running.
exit(0);
}
else {
my $end_time = time() + LIFE_TIME;

while (1) {
process_work_unit();
last if time() >= $end_time;
}

exit(0);
}
S/k > L/k > C                          (2)
use 5.14.2;
use warnings;
use IPC::ConcurrencyLimit;
use IPC::ConcurrencyLimit::WithStandby;
use POSIX qw(ceil);
use Time::HiRes qw(sleep);

# external settings
use constant LIFE_TIME => 5*60; # base life time in seconds
use constant MAX_WORKERS => 16; # max concurrent workers
use constant CRON_RATE => 60; # cron spawns more every 60s

# This asserts relation (2), but not necessarily relation (1).
use constant NCHILDREN => 2 + ceil(2 * MAX_WORKERS * CRON_RATE / LIFE_TIME);

# Attempt to get main lock every 1/2 second
use constant MAIN_LOCK_INTERVAL => 1/2; # in seconds

# Set the standby life time: Keep retrying for ~3x lifetime of the worker process
use constant MAIN_LOCK_RETRIES => 1 + int(3 * LIFE_TIME / MAIN_LOCK_INTERVAL);

# Fork the decided number of workers
my $is_child;
for (1 .. NCHILDREN) {
$is_child = fork_child();
last if $is_child;
sleep(0.2); # no need to have artificial contention on the locks
}
exit(0) if not $is_child; # all children daemonized

my $limit = IPC::ConcurrencyLimit::WithStandby->new(
type => 'Flock',
path => '/var/run/myapp',
max_procs => MAX_WORKERS,
standby_path => '/var/run/myapp/standby',
standby_max_procs => MAX_WORKERS,
interval => MAIN_LOCK_INTERVAL,
retries => MAIN_LOCK_RETRIES,
process_name_change => 1,
);

my $lock_id = $limit->get_lock;
if (not $lock_id) {
# Other process running.
exit(0);
}
else {
my $end_time = time() + LIFE_TIME*(1+rand(0.1));

while (1) {
process_work_unit();
last if time() >= $end_time;
}

exit(0);
}

# mostly standard daemonization from the perlipc manpage
sub fork_child {
use autodie;
defined(my $pid = fork)
or die "Can't fork: $!";
return() if $pid;
chdir '/';
open STDIN, '/dev/null';
open STDOUT, '>/dev/null';
die "Can't start a new session: $!"
if POSIX::setsid == -1;
open STDERR, '>&STDOUT';
return 1;
}

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store