Jeff Ober

NAME

Proc::tored - Service management using a pid file and touch files

VERSION

version 0.17

SYNOPSIS

  use Proc::tored;
  use Getopt::Long;

  my %opt = (
    pause  => 0,
    resume => 0,
    stop   => 0,
    zap    => 0,
    start  => 0,
    run    => 0,
  );

  GetOptions(
    'pause'  => \$opt{pause},
    'resume' => \$opt{resume},
    'stop'   => \$opt{stop},
    'zap'    => \$opt{zap},
    'start'  => \$opt{start},
    'run'    => \$opt{run},
  );

  my $service = service 'stuff-doer', in '/var/run';
  my $pid = running $service;

  print "Running service found with pid $pid\n"
    if $pid;

  if ($opt{pause}) {
    # Set the paused flag, causing a running service to block until unset
    pause $service;
  }
  elsif ($opt{resume}) {
    # Unset the paused flag, unblocking any running service
    resume $service;
  }
  elsif ($opt{stop}) {
    # Set the stopped state, preventing new processes from starting the service
    # and causing running processes to self-terminate
    stop $service;
  }
  elsif ($opt{zap}) {
    # Terminate a running process, timing out after 15s
    zap $service, 15
      or die "stuff_doer $pid is being stubborn";
  }
  elsif ($opt{start}) {
    # Allow the service to start running again
    start $service;
  }

  if ($opt{run}) {
    # Run service (if not stopped)
    run { do_stuff() } $service;
  }

DESCRIPTION

A Proc::tored service is voluntarily managed by a pid file and touch files.

Proc::tored services are specified with a name and a path. Any services created using the same name and path are considered the same service and will be aware of other processes via their "PID FILE" and respect service control "FLAGS".

EXPORTED SUBROUTINES

All routines are exported by default.

service

in

trap

A proctored service is defined using the service function. The name given to the service is used in the naming of various files used to control the service (e.g., pid file and touch files). The in function is used to specify the local directory where these files will be created and looked for. Signals may be trapped using trap on non-MSWin32 systems.

  my $service = service 'name-of-service', in '/var/run', trap ['TERM', 'INT'];

pid

Reads and returns the contents of the pid file. Does not check to determine whether the pid is valid. Returns 0 if the pid file is not found or is empty.

  printf "service may be running under pid %d", pid $service;

running

Reads and returns the contents of the pid file after checking that the process identified still exists. Essentially the same as kill(0, pid $service). Returns 0 if the pid is not found or cannot be signalled.

  if (my $pid = running $service) {
    warn "service is already running under pid $pid";
  }

run

Begins the service in the current process. The service, specified as a code block, will be called until it returns false or the "stopped" flag is set.

If the "paused" flag is set, the loop will continue to run without executing the code block until it has been "resume"d. If the "paused" flag is set at the time run is called, the loop will start but will not begin executing the code block until the flag is cleared.

If the "stopped" flag is set, the loop will terminate at the completion of the current iteration. If the "stopped" flag is set at the time run is called, run will return false immediately. The behavior under "stopped" takes priority over that of "paused".

  my $started = time;
  my $max_run_time = 300;

  run {
    if (time - $started > $max_run_time) {
      warn "Max run time ($max_run_time seconds) exceeded\n";
      warn "  -shutting down\n";
      return 0;
    }
    else {
      do_some_work();
    }

    return 1;
  } $service;

zap

Sets the "stopped" flag (see "stop"), then blocks until a running service exits. Returns immediately (after setting the "stopped" flag) if the "running" service is the current process.

  sub stop_service {
    if (my $pid = running $service) {
      print "Attempting to stop running service running under process $pid\n";

      if (zap $pid, 30) {
        print "  -Service shut down\n";
        return 1;
      }
      else {
        print "  -Timed out before service shut down\n";
        return 0;
      }
    }
  }

stop

start

stopped

Controls and inspects the "stopped" flag for the service.

  # Stop a running service
  if (!stopped $service && running $service) {
    stop $service;
  }

  do_work_while_stopped();

  # Allow service to start
  # Note that this does not launch the service process. It simply clears the
  # "stopped" flag that would have prevented it from running again.
  start $service;

pause

resume

paused

Controls and inspects the "paused" flag for the service. In general, this should never be done inside the "run" loop (see the warning in "Pause and resume").

  # Pause a running service
  # Note that the running service will not exit. Instead, it will stop
  # executing its main loop until the "paused" flag is cleared.
  if (!paused $service && running $service) {
    pause $service;
  }

  do_work_while_paused();

  # Allow service to resume execution
  resume $service;

PID FILE

A pid file is used to identify a running service. While the service is running, barring any outside interference, the pid will contain the pid of the running process and a newline. After the service process stops, the pid file will be truncated. The file will be located in the directory specified by "in". Its name is the concatenation of the service name and ".pid".

FLAGS

Service control flags are persistent until unset. Their status is determined by the existence of a touch file.

stopped

A touch file indicating that a running service should self-terminate and that new processes should not start is created with "stop" and removed with "start". It is located in the directory specified by "in". Its name is the concatenation of the service name and ".stopped".

paused

A touch file indicating that a running service should temporarily stop executing and that new processes should start but not yet execute any service code is created with "pause" and removed with "resume". It is located in the directory specified by "in". Its name is the concatenation of the service name and ".paused".

BUGS AND LIMITATIONS

Pause and resume

When a service is "paused", the code block passed to "run" is no longer executed until something calls "resume". This can lead to deadlock if there is no external actor willing to "resume" the service.

For example, this service will never resume:

  run {
    my $empty = out_of_tasks();

    if ($empty) {
      pause $service;
    }
    elsif (paused $service && !$empty) {
      # This line is never reached because this code block is no longer
      # executed after being paused above.
      resume $service;
    }

    do_next_task();
    return 1;
  } $service;

In most cases, pausing and resuming a service should be handled from outside of "run".

AUTHOR

Jeff Ober <jeffober@gmail.com>

COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Jeff Ober.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.