chrisboulton / php-resque

PHP port of resque (Workers and Queueing)
MIT License
3.43k stars 759 forks source link

Task Timeout #326

Closed eric1234 closed 7 years ago

eric1234 commented 7 years ago

This software looks great and a perfect fit for an application that I am refactoring!

One question, my tasks need to have a timeout. I.E. if they run past a certain time they should fail as I assume something is stuck. Does this library have an option to specify the task time.

If you are interested in such an option I could add it to this software. If not then I will code it into a base class for my jobs.

Implementation-wise I was thinking of using pcntl_alarm and installing a signal handler to kill the process rather than set_time_limit since the later doesn't work if the process is doing some sort of IO or other system operation.

eric1234 commented 7 years ago

After thinking about this a bit more I decided this is probably best accomplished through the event system as callbacks. Could be wrapped up as a plugin if desired. This way resque stays lean. To help anybody else that comes along here is my event handler which implements the timeout. To use all you need to do is add a public static attribute named $timeout to your job class with the desired timeout. It is specified as a string. So 5 minutes, 2 hours, etc.

<?php
# Required to ensure signal is delivered. If in a tight loop PHP will keep
# executing even once the signal is deliver which may mean it will run for much
# longer than intended. The code can manually yield by calling
# `pcntl_signal_dispatch`, but jobs are unlikely to call that. This causes PHP
# to come up for air and deliver the signals.
declare(ticks=1);

# Install the alarm and signal handler before the job is executed. Note, that
# we don't remove the handler and alarm in an `afterPerform` event. Jobs are
# executed in a fork, as soon as the job is done everything is thrown out anyway.
Resque_Event::listen('beforePerform', function($job) {
  # For easy reference
  $logger = $job->worker->logger;

  # Grab the name of the job class handling this job
  $job_class = $job->payload['class'];

  # Check if job class wants a timeout.
  if( property_exists($job_class, 'timeout') ) {
    $timeout = $job_class::$timeout;

    $logger->info("$job timeout in {timeout}", ['timeout' => $timeout]);

    # Requiring jobs to set the timeout in seconds can get tedious. If a string
    # then use strtotime to figure out the number of seconds;
    if( is_string($timeout) ) $timeout = strtotime($timeout) - time();

    # NOTE: The default behavior of SIGALRM is to kill the process which is
    # exactly what I am doing. I install a handler anyway for two reasons:
    #
    # * The default handler is at the OS level and will just kill the process
    #   completely without executing any more PHP. This results in Resque's
    #   `onFailure` handler not being run. The process will just silently die.
    #   Having a handler and manually killing means anything registered in
    #   `onFailure` will run properly.
    # * I can also log a message so even if no handler is installed there is
    #   some bit of feedback as to what happened.
    pcntl_signal(SIGALRM, function() use($job, $logger) {
      $logger->error("$job timed out. Killing process");
      exit(12);
    });

    # Setup alart to go off after the configured time.
    pcntl_alarm($timeout);
  }
});
ancoka commented 7 years ago

That's a good idea. I agree with you.

misterpancn commented 4 years ago

Nice work!👍