2017-11-05 05:33:46 -05:00
< ? php
2017-11-19 17:04:40 -05:00
/**
* @ file src / Core / Worker . php
*/
2017-11-05 05:33:46 -05:00
namespace Friendica\Core ;
2018-10-21 01:53:47 -04:00
use Friendica\BaseObject ;
2018-10-29 17:20:46 -04:00
use Friendica\Core\Logger ;
2018-07-20 08:19:26 -04:00
use Friendica\Database\DBA ;
2018-01-15 19:08:28 -05:00
use Friendica\Model\Process ;
2018-01-26 21:38:34 -05:00
use Friendica\Util\DateTimeFormat ;
2018-06-26 17:42:26 -04:00
use Friendica\Util\Network ;
2017-11-05 05:33:46 -05:00
/**
2017-11-05 07:15:53 -05:00
* @ file src / Core / Worker . php
2017-11-05 05:33:46 -05:00
*
2017-11-06 10:32:34 -05:00
* @ brief Contains the class for the worker background job processing
2017-11-05 05:33:46 -05:00
*/
/**
* @ brief Worker methods
*/
2017-11-19 17:04:40 -05:00
class Worker
{
2017-11-05 05:33:46 -05:00
private static $up_start ;
private static $db_duration ;
private static $last_update ;
private static $lock_duration ;
/**
* @ brief Processes the tasks that are in the workerqueue table
*
* @ param boolean $run_cron Should the cron processes be executed ?
2017-11-19 17:04:40 -05:00
* @ return void
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
public static function processQueue ( $run_cron = true )
{
2018-12-27 19:22:35 -05:00
$a = \get_app ();
2017-11-05 05:33:46 -05:00
self :: $up_start = microtime ( true );
2017-11-05 10:28:55 -05:00
// At first check the maximum load. We shouldn't continue with a high load
2018-06-30 14:07:01 -04:00
if ( $a -> isMaxLoadReached ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Pre check: maximum load reached, quitting.' , Logger :: DEBUG );
2017-11-05 10:28:55 -05:00
return ;
}
// We now start the process. This is done after the load check since this could increase the load.
2018-01-15 19:08:28 -05:00
self :: startProcess ();
2017-11-05 10:28:55 -05:00
2017-11-05 05:33:46 -05:00
// Kill stale processes every 5 minutes
2018-06-01 18:09:27 -04:00
$last_cleanup = Config :: get ( 'system' , 'worker_last_cleaned' , 0 );
2017-11-05 05:33:46 -05:00
if ( time () > ( $last_cleanup + 300 )) {
2018-06-01 18:09:27 -04:00
Config :: set ( 'system' , 'worker_last_cleaned' , time ());
2017-11-05 05:33:46 -05:00
self :: killStaleWorkers ();
}
// Count active workers and compare them with a maximum value that depends on the load
if ( self :: tooMuchWorkers ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Pre check: Active worker limit reached, quitting.' , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return ;
}
// Do we have too few memory?
2018-10-09 13:58:58 -04:00
if ( $a -> isMinMemoryReached ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Pre check: Memory limit reached, quitting.' , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return ;
}
// Possibly there are too much database connections
if ( self :: maxConnectionsReached ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Pre check: maximum connections reached, quitting.' , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return ;
}
// Possibly there are too much database processes that block the system
2018-06-30 14:07:01 -04:00
if ( $a -> isMaxProcessesReached ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Pre check: maximum processes reached, quitting.' , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return ;
}
// Now we start additional cron processes if we should do so
if ( $run_cron ) {
self :: runCron ();
}
$starttime = time ();
// We fetch the next queue entry that is about to be executed
while ( $r = self :: workerProcess ( $passing_slow )) {
// When we are processing jobs with a lower priority, we don't refetch new jobs
// Otherwise fast jobs could wait behind slow ones and could be blocked.
$refetched = $passing_slow ;
2017-11-19 17:04:40 -05:00
foreach ( $r as $entry ) {
2017-11-05 05:33:46 -05:00
// Assure that the priority is an integer value
$entry [ 'priority' ] = ( int ) $entry [ 'priority' ];
// The work will be done
if ( ! self :: execute ( $entry )) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Process execution failed, quitting.' , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return ;
}
// If possible we will fetch new jobs for this worker
2018-07-05 14:57:31 -04:00
if ( ! $refetched && Lock :: acquire ( 'worker_process' , 0 )) {
2017-11-05 05:33:46 -05:00
$stamp = ( float ) microtime ( true );
$refetched = self :: findWorkerProcesses ( $passing_slow );
self :: $db_duration += ( microtime ( true ) - $stamp );
2018-07-05 14:57:31 -04:00
Lock :: release ( 'worker_process' );
2017-11-05 05:33:46 -05:00
}
}
2017-11-19 16:47:21 -05:00
// To avoid the quitting of multiple workers only one worker at a time will execute the check
2018-07-05 14:57:31 -04:00
if ( Lock :: acquire ( 'worker' , 0 )) {
2017-11-05 05:33:46 -05:00
$stamp = ( float ) microtime ( true );
// Count active workers and compare them with a maximum value that depends on the load
if ( self :: tooMuchWorkers ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Active worker limit reached, quitting.' , Logger :: DEBUG );
2018-09-06 02:11:18 -04:00
Lock :: release ( 'worker' );
2017-11-05 05:33:46 -05:00
return ;
}
// Check free memory
2018-10-09 13:58:58 -04:00
if ( $a -> isMinMemoryReached ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Memory limit reached, quitting.' , Logger :: DEBUG );
2018-09-06 02:11:18 -04:00
Lock :: release ( 'worker' );
2017-11-05 05:33:46 -05:00
return ;
}
2018-07-05 14:57:31 -04:00
Lock :: release ( 'worker' );
2017-11-05 05:33:46 -05:00
self :: $db_duration += ( microtime ( true ) - $stamp );
}
2017-11-19 16:47:21 -05:00
// Quit the worker once every 5 minutes
2017-11-05 05:33:46 -05:00
if ( time () > ( $starttime + 300 )) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Process lifetime reached, quitting.' , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return ;
}
}
2018-06-15 14:18:20 -04:00
// Cleaning up. Possibly not needed, but it doesn't harm anything.
2018-06-01 18:09:27 -04:00
if ( Config :: get ( 'system' , 'worker_daemon_mode' , false )) {
self :: IPCSetJobState ( false );
}
2018-10-30 09:58:45 -04:00
Logger :: log ( " Couldn't select a workerqueue entry, quitting process " . getmypid () . " . " , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
}
2018-10-23 16:38:28 -04:00
/**
* @ brief Returns the number of deferred entries in the worker queue
*
* @ return integer Number of deferred entries in the worker queue
*/
private static function deferredEntries ()
{
return DBA :: count ( 'workerqueue' , [ " `executed` <= ? AND NOT `done` AND `next_try` > ? " ,
DBA :: NULL_DATETIME , DateTimeFormat :: utcNow ()]);
}
2017-11-05 05:33:46 -05:00
/**
* @ brief Returns the number of non executed entries in the worker queue
*
* @ return integer Number of non executed entries in the worker queue
*/
2017-11-19 17:04:40 -05:00
private static function totalEntries ()
{
2018-10-15 01:19:35 -04:00
return DBA :: count ( 'workerqueue' , [ " `executed` <= ? AND NOT `done` AND `next_try` < ? " ,
2018-10-21 01:53:47 -04:00
DBA :: NULL_DATETIME , DateTimeFormat :: utcNow ()]);
2017-11-05 05:33:46 -05:00
}
/**
* @ brief Returns the highest priority in the worker queue that isn ' t executed
*
2017-11-19 16:47:21 -05:00
* @ return integer Number of active worker processes
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
private static function highestPriority ()
{
2018-10-21 01:53:47 -04:00
$condition = [ " `executed` <= ? AND NOT `done` AND `next_try` < ? " , DBA :: NULL_DATETIME , DateTimeFormat :: utcNow ()];
2018-07-20 08:19:26 -04:00
$workerqueue = DBA :: selectFirst ( 'workerqueue' , [ 'priority' ], $condition , [ 'order' => [ 'priority' ]]);
2018-07-21 08:46:04 -04:00
if ( DBA :: isResult ( $workerqueue )) {
2018-01-11 03:26:30 -05:00
return $workerqueue [ " priority " ];
2017-11-05 05:33:46 -05:00
} else {
return 0 ;
}
}
/**
* @ brief Returns if a process with the given priority is running
*
* @ param integer $priority The priority that should be checked
*
* @ return integer Is there a process running with that priority ?
*/
2017-11-19 17:04:40 -05:00
private static function processWithPriorityActive ( $priority )
{
2018-10-15 01:19:35 -04:00
$condition = [ " `priority` <= ? AND `executed` > ? AND NOT `done` AND `next_try` < ? " ,
2018-10-21 01:53:47 -04:00
$priority , DBA :: NULL_DATETIME , DateTimeFormat :: utcNow ()];
2018-07-20 08:19:26 -04:00
return DBA :: exists ( 'workerqueue' , $condition );
2017-11-05 05:33:46 -05:00
}
/**
* @ brief Execute a worker entry
*
* @ param array $queue Workerqueue entry
*
* @ return boolean " true " if further processing should be stopped
*/
2017-11-19 17:04:40 -05:00
public static function execute ( $queue )
{
2018-12-27 19:22:35 -05:00
$a = \get_app ();
2017-11-05 05:33:46 -05:00
$mypid = getmypid ();
// Quit when in maintenance
2018-03-04 08:01:46 -05:00
if ( Config :: get ( 'system' , 'maintenance' , false , true )) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Maintenance mode - quit process " . $mypid , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return false ;
}
// Constantly check the number of parallel database processes
2018-06-30 14:07:01 -04:00
if ( $a -> isMaxProcessesReached ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Max processes reached for process " . $mypid , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return false ;
}
// Constantly check the number of available database connections to let the frontend be accessible at any time
if ( self :: maxConnectionsReached ()) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Max connection reached for process " . $mypid , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
return false ;
}
2018-02-11 11:18:39 -05:00
$argv = json_decode ( $queue [ " parameter " ], true );
2017-11-05 05:33:46 -05:00
// Check for existance and validity of the include file
$include = $argv [ 0 ];
2017-11-12 02:21:23 -05:00
if ( method_exists ( sprintf ( 'Friendica\Worker\%s' , $include ), 'execute' )) {
// We constantly update the "executed" date every minute to avoid being killed too soon
if ( ! isset ( self :: $last_update )) {
self :: $last_update = strtotime ( $queue [ " executed " ]);
}
$age = ( time () - self :: $last_update ) / 60 ;
self :: $last_update = time ();
if ( $age > 1 ) {
$stamp = ( float ) microtime ( true );
2018-07-20 08:19:26 -04:00
DBA :: update ( 'workerqueue' , [ 'executed' => DateTimeFormat :: utcNow ()], [ 'pid' => $mypid , 'done' => false ]);
2017-11-12 02:21:23 -05:00
self :: $db_duration += ( microtime ( true ) - $stamp );
}
array_shift ( $argv );
self :: execFunction ( $queue , $include , $argv , true );
$stamp = ( float ) microtime ( true );
2018-10-22 23:54:18 -04:00
$condition = [ " `id` = ? AND `next_try` < ? " , $queue [ 'id' ], DateTimeFormat :: utcNow ()];
if ( DBA :: update ( 'workerqueue' , [ 'done' => true ], $condition )) {
2018-06-01 18:09:27 -04:00
Config :: set ( 'system' , 'last_worker_execution' , DateTimeFormat :: utcNow ());
2017-11-12 02:21:23 -05:00
}
self :: $db_duration = ( microtime ( true ) - $stamp );
return true ;
}
2017-11-05 05:33:46 -05:00
// The script could be provided as full path or only with the function name
if ( $include == basename ( $include )) {
2017-11-14 17:13:33 -05:00
$include = " include/ " . $include . " .php " ;
2017-11-05 05:33:46 -05:00
}
if ( ! validate_include ( $include )) {
2018-10-29 17:20:46 -04:00
Logger :: log ( " Include file " . $argv [ 0 ] . " is not valid! " );
2018-07-20 08:19:26 -04:00
DBA :: delete ( 'workerqueue' , [ 'id' => $queue [ " id " ]]);
2017-11-05 05:33:46 -05:00
return true ;
}
2017-11-19 17:04:40 -05:00
require_once $include ;
2017-11-05 05:33:46 -05:00
$funcname = str_replace ( " .php " , " " , basename ( $argv [ 0 ])) . " _run " ;
if ( function_exists ( $funcname )) {
// We constantly update the "executed" date every minute to avoid being killed too soon
if ( ! isset ( self :: $last_update )) {
self :: $last_update = strtotime ( $queue [ " executed " ]);
}
$age = ( time () - self :: $last_update ) / 60 ;
self :: $last_update = time ();
if ( $age > 1 ) {
$stamp = ( float ) microtime ( true );
2018-07-20 08:19:26 -04:00
DBA :: update ( 'workerqueue' , [ 'executed' => DateTimeFormat :: utcNow ()], [ 'pid' => $mypid , 'done' => false ]);
2017-11-05 05:33:46 -05:00
self :: $db_duration += ( microtime ( true ) - $stamp );
}
2017-11-12 02:21:23 -05:00
self :: execFunction ( $queue , $funcname , $argv , false );
2017-11-05 05:33:46 -05:00
$stamp = ( float ) microtime ( true );
2018-07-20 08:19:26 -04:00
if ( DBA :: update ( 'workerqueue' , [ 'done' => true ], [ 'id' => $queue [ " id " ]])) {
2018-06-01 18:09:27 -04:00
Config :: set ( 'system' , 'last_worker_execution' , DateTimeFormat :: utcNow ());
2017-11-05 05:33:46 -05:00
}
self :: $db_duration = ( microtime ( true ) - $stamp );
} else {
2018-10-29 17:20:46 -04:00
Logger :: log ( " Function " . $funcname . " does not exist " );
2018-07-20 08:19:26 -04:00
DBA :: delete ( 'workerqueue' , [ 'id' => $queue [ " id " ]]);
2017-11-05 05:33:46 -05:00
}
return true ;
}
/**
* @ brief Execute a function from the queue
*
2017-11-19 17:04:40 -05:00
* @ param array $queue Workerqueue entry
* @ param string $funcname name of the function
* @ param array $argv Array of values to be passed to the function
* @ param boolean $method_call boolean
* @ return void
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
private static function execFunction ( $queue , $funcname , $argv , $method_call )
{
2018-12-27 19:22:35 -05:00
$a = \get_app ();
2017-11-05 05:33:46 -05:00
$mypid = getmypid ();
$argc = count ( $argv );
2018-07-10 18:32:47 -04:00
$new_process_id = System :: processID ( " wrk " );
2017-11-05 05:33:46 -05:00
2018-10-29 17:20:46 -04:00
Logger :: log ( " Process " . $mypid . " - Prio " . $queue [ " priority " ] . " - ID " . $queue [ " id " ] . " : " . $funcname . " " . $queue [ " parameter " ] . " - Process PID: " . $new_process_id );
2017-11-05 05:33:46 -05:00
$stamp = ( float ) microtime ( true );
// We use the callstack here to analyze the performance of executed worker entries.
// For this reason the variables have to be initialized.
if ( Config :: get ( " system " , " profiler " )) {
$a -> performance [ " start " ] = microtime ( true );
$a -> performance [ " database " ] = 0 ;
$a -> performance [ " database_write " ] = 0 ;
2018-03-01 01:25:39 -05:00
$a -> performance [ " cache " ] = 0 ;
$a -> performance [ " cache_write " ] = 0 ;
2017-11-05 05:33:46 -05:00
$a -> performance [ " network " ] = 0 ;
$a -> performance [ " file " ] = 0 ;
$a -> performance [ " rendering " ] = 0 ;
$a -> performance [ " parser " ] = 0 ;
$a -> performance [ " marktime " ] = 0 ;
$a -> performance [ " markstart " ] = microtime ( true );
2018-01-15 08:05:12 -05:00
$a -> callstack = [];
2017-11-05 05:33:46 -05:00
}
// For better logging create a new process id for every worker call
// But preserve the old one for the worker
$old_process_id = $a -> process_id ;
$a -> process_id = $new_process_id ;
$a -> queue = $queue ;
$up_duration = number_format ( microtime ( true ) - self :: $up_start , 3 );
// Reset global data to avoid interferences
unset ( $_SESSION );
2017-11-12 02:21:23 -05:00
if ( $method_call ) {
call_user_func_array ( sprintf ( 'Friendica\Worker\%s::execute' , $funcname ), $argv );
} else {
$funcname ( $argv , $argc );
}
2017-11-05 05:33:46 -05:00
$a -> process_id = $old_process_id ;
unset ( $a -> queue );
2018-07-15 14:36:20 -04:00
$duration = ( microtime ( true ) - $stamp );
2017-11-05 05:33:46 -05:00
self :: $up_start = microtime ( true );
/* With these values we can analyze how effective the worker is .
* The database and rest time should be low since this is the unproductive time .
* The execution time is the productive time .
* By changing parameters like the maximum number of workers we can check the effectivness .
*/
2018-10-29 17:20:46 -04:00
Logger :: log (
2017-11-19 17:04:40 -05:00
'DB: ' . number_format ( self :: $db_duration , 2 ) .
2017-11-05 05:33:46 -05:00
' - Lock: ' . number_format ( self :: $lock_duration , 2 ) .
' - Rest: ' . number_format ( $up_duration - self :: $db_duration - self :: $lock_duration , 2 ) .
2017-11-19 17:04:40 -05:00
' - Execution: ' . number_format ( $duration , 2 ),
2018-10-30 09:58:45 -04:00
Logger :: DEBUG
2017-11-19 17:04:40 -05:00
);
2017-11-05 05:33:46 -05:00
self :: $lock_duration = 0 ;
if ( $duration > 3600 ) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Prio " . $queue [ " priority " ] . " : " . $queue [ " parameter " ] . " - longer than 1 hour ( " . round ( $duration / 60 , 3 ) . " ) " , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
} elseif ( $duration > 600 ) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Prio " . $queue [ " priority " ] . " : " . $queue [ " parameter " ] . " - longer than 10 minutes ( " . round ( $duration / 60 , 3 ) . " ) " , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
} elseif ( $duration > 300 ) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Prio " . $queue [ " priority " ] . " : " . $queue [ " parameter " ] . " - longer than 5 minutes ( " . round ( $duration / 60 , 3 ) . " ) " , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
} elseif ( $duration > 120 ) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Prio " . $queue [ " priority " ] . " : " . $queue [ " parameter " ] . " - longer than 2 minutes ( " . round ( $duration / 60 , 3 ) . " ) " , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
}
2018-10-29 17:20:46 -04:00
Logger :: log ( " Process " . $mypid . " - Prio " . $queue [ " priority " ] . " - ID " . $queue [ " id " ] . " : " . $funcname . " - done in " . $duration . " seconds. Process PID: " . $new_process_id );
2017-11-05 05:33:46 -05:00
// Write down the performance values into the log
if ( Config :: get ( " system " , " profiler " )) {
$duration = microtime ( true ) - $a -> performance [ " start " ];
2018-02-14 00:05:00 -05:00
$o = '' ;
2017-11-05 05:33:46 -05:00
if ( Config :: get ( " rendertime " , " callstack " )) {
if ( isset ( $a -> callstack [ " database " ])) {
2018-02-14 00:05:00 -05:00
$o .= " \n Database Read: \n " ;
2017-11-19 17:04:40 -05:00
foreach ( $a -> callstack [ " database " ] as $func => $time ) {
2017-11-05 05:33:46 -05:00
$time = round ( $time , 3 );
if ( $time > 0 ) {
$o .= $func . " : " . $time . " \n " ;
}
}
}
if ( isset ( $a -> callstack [ " database_write " ])) {
$o .= " \n Database Write: \n " ;
2017-11-19 17:04:40 -05:00
foreach ( $a -> callstack [ " database_write " ] as $func => $time ) {
2017-11-05 05:33:46 -05:00
$time = round ( $time , 3 );
if ( $time > 0 ) {
$o .= $func . " : " . $time . " \n " ;
}
}
}
2018-03-01 01:25:39 -05:00
if ( isset ( $a -> callstack [ " dache " ])) {
$o .= " \n Cache Read: \n " ;
foreach ( $a -> callstack [ " dache " ] as $func => $time ) {
$time = round ( $time , 3 );
if ( $time > 0 ) {
$o .= $func . " : " . $time . " \n " ;
}
}
}
if ( isset ( $a -> callstack [ " dache_write " ])) {
$o .= " \n Cache Write: \n " ;
foreach ( $a -> callstack [ " dache_write " ] as $func => $time ) {
$time = round ( $time , 3 );
if ( $time > 0 ) {
$o .= $func . " : " . $time . " \n " ;
}
}
}
2017-11-05 05:33:46 -05:00
if ( isset ( $a -> callstack [ " network " ])) {
$o .= " \n Network: \n " ;
2017-11-19 17:04:40 -05:00
foreach ( $a -> callstack [ " network " ] as $func => $time ) {
2017-11-05 05:33:46 -05:00
$time = round ( $time , 3 );
if ( $time > 0 ) {
$o .= $func . " : " . $time . " \n " ;
}
}
}
}
2018-10-29 17:20:46 -04:00
Logger :: log (
2017-11-19 17:04:40 -05:00
" ID " . $queue [ " id " ] . " : " . $funcname . " : " . sprintf (
2018-03-01 01:25:39 -05:00
" DB: %s/%s, Cache: %s/%s, Net: %s, I/O: %s, Other: %s, Total: %s " . $o ,
2017-11-19 17:04:40 -05:00
number_format ( $a -> performance [ " database " ] - $a -> performance [ " database_write " ], 2 ),
number_format ( $a -> performance [ " database_write " ], 2 ),
2018-03-01 01:25:39 -05:00
number_format ( $a -> performance [ " cache " ], 2 ),
number_format ( $a -> performance [ " cache_write " ], 2 ),
2017-11-19 17:04:40 -05:00
number_format ( $a -> performance [ " network " ], 2 ),
number_format ( $a -> performance [ " file " ], 2 ),
2018-03-01 01:25:39 -05:00
number_format ( $duration - ( $a -> performance [ " database " ]
+ $a -> performance [ " cache " ] + $a -> performance [ " cache_write " ]
+ $a -> performance [ " network " ] + $a -> performance [ " file " ]), 2 ),
2017-11-19 17:04:40 -05:00
number_format ( $duration , 2 )
),
2018-10-30 09:58:45 -04:00
Logger :: DEBUG
2017-11-19 17:04:40 -05:00
);
2017-11-05 05:33:46 -05:00
}
$cooldown = Config :: get ( " system " , " worker_cooldown " , 0 );
if ( $cooldown > 0 ) {
2018-10-29 17:20:46 -04:00
Logger :: log ( " Process " . $mypid . " - Prio " . $queue [ " priority " ] . " - ID " . $queue [ " id " ] . " : " . $funcname . " - in cooldown for " . $cooldown . " seconds " );
2017-11-05 05:33:46 -05:00
sleep ( $cooldown );
}
}
/**
* @ brief Checks if the number of database connections has reached a critical limit .
*
* @ return bool Are more than 3 / 4 of the maximum connections used ?
*/
2017-11-19 17:04:40 -05:00
private static function maxConnectionsReached ()
{
2017-11-05 05:33:46 -05:00
// Fetch the max value from the config. This is needed when the system cannot detect the correct value by itself.
$max = Config :: get ( " system " , " max_connections " );
2017-11-19 16:47:21 -05:00
// Fetch the percentage level where the worker will get active
2017-11-05 05:33:46 -05:00
$maxlevel = Config :: get ( " system " , " max_connections_level " , 75 );
if ( $max == 0 ) {
// the maximum number of possible user connections can be a system variable
2018-07-20 22:01:53 -04:00
$r = DBA :: fetchFirst ( " SHOW VARIABLES WHERE `variable_name` = 'max_user_connections' " );
2018-07-21 08:46:04 -04:00
if ( DBA :: isResult ( $r )) {
2017-11-05 12:13:37 -05:00
$max = $r [ " Value " ];
2017-11-05 05:33:46 -05:00
}
// Or it can be granted. This overrides the system variable
2018-07-20 08:19:26 -04:00
$r = DBA :: p ( 'SHOW GRANTS' );
while ( $grants = DBA :: fetch ( $r )) {
2017-11-05 12:13:37 -05:00
$grant = array_pop ( $grants );
if ( stristr ( $grant , " GRANT USAGE ON " )) {
if ( preg_match ( " /WITH MAX_USER_CONNECTIONS ( \ d*)/ " , $grant , $match )) {
$max = $match [ 1 ];
2017-11-05 05:33:46 -05:00
}
}
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $r );
2017-11-05 05:33:46 -05:00
}
// If $max is set we will use the processlist to determine the current number of connections
// The processlist only shows entries of the current user
if ( $max != 0 ) {
2018-07-20 08:19:26 -04:00
$r = DBA :: p ( 'SHOW PROCESSLIST' );
2018-07-20 22:05:12 -04:00
$used = DBA :: numRows ( $r );
2018-07-20 08:19:26 -04:00
DBA :: close ( $r );
2017-11-05 05:33:46 -05:00
2018-10-30 09:58:45 -04:00
Logger :: log ( " Connection usage (user values): " . $used . " / " . $max , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
$level = ( $used / $max ) * 100 ;
if ( $level >= $maxlevel ) {
2018-10-29 17:20:46 -04:00
Logger :: log ( " Maximum level ( " . $maxlevel . " %) of user connections reached: " . $used . " / " . $max );
2017-11-05 05:33:46 -05:00
return true ;
}
}
// We will now check for the system values.
// This limit could be reached although the user limits are fine.
2018-07-20 22:01:53 -04:00
$r = DBA :: fetchFirst ( " SHOW VARIABLES WHERE `variable_name` = 'max_connections' " );
2018-07-21 08:46:04 -04:00
if ( ! DBA :: isResult ( $r )) {
2017-11-05 05:33:46 -05:00
return false ;
}
2017-11-05 12:13:37 -05:00
$max = intval ( $r [ " Value " ]);
2017-11-05 05:33:46 -05:00
if ( $max == 0 ) {
return false ;
}
2018-07-20 22:01:53 -04:00
$r = DBA :: fetchFirst ( " SHOW STATUS WHERE `variable_name` = 'Threads_connected' " );
2018-07-21 08:46:04 -04:00
if ( ! DBA :: isResult ( $r )) {
2017-11-05 05:33:46 -05:00
return false ;
}
2017-11-05 12:13:37 -05:00
$used = intval ( $r [ " Value " ]);
2017-11-05 05:33:46 -05:00
if ( $used == 0 ) {
return false ;
}
2018-10-30 09:58:45 -04:00
Logger :: log ( " Connection usage (system values): " . $used . " / " . $max , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
$level = $used / $max * 100 ;
if ( $level < $maxlevel ) {
return false ;
}
2018-10-29 17:20:46 -04:00
Logger :: log ( " Maximum level ( " . $level . " %) of system connections reached: " . $used . " / " . $max );
2017-11-05 05:33:46 -05:00
return true ;
}
/**
* @ brief fix the queue entry if the worker process died
2017-11-19 17:04:40 -05:00
* @ return void
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
private static function killStaleWorkers ()
{
2018-07-20 08:19:26 -04:00
$entries = DBA :: select (
2017-11-19 17:04:40 -05:00
'workerqueue' ,
2018-01-15 08:05:12 -05:00
[ 'id' , 'pid' , 'executed' , 'priority' , 'parameter' ],
2018-10-21 01:53:47 -04:00
[ '`executed` > ? AND NOT `done` AND `pid` != 0' , DBA :: NULL_DATETIME ],
2018-01-15 08:05:12 -05:00
[ 'order' => [ 'priority' , 'created' ]]
2017-11-19 17:04:40 -05:00
);
2018-07-20 08:19:26 -04:00
while ( $entry = DBA :: fetch ( $entries )) {
2017-11-05 05:33:46 -05:00
if ( ! posix_kill ( $entry [ " pid " ], 0 )) {
2018-07-20 08:19:26 -04:00
DBA :: update (
2017-11-20 11:29:55 -05:00
'workerqueue' ,
2018-10-21 01:53:47 -04:00
[ 'executed' => DBA :: NULL_DATETIME , 'pid' => 0 ],
2018-01-15 08:05:12 -05:00
[ 'id' => $entry [ " id " ]]
2017-11-20 11:29:55 -05:00
);
2017-11-05 05:33:46 -05:00
} else {
// Kill long running processes
// Check if the priority is in a valid range
2018-01-15 08:05:12 -05:00
if ( ! in_array ( $entry [ " priority " ], [ PRIORITY_CRITICAL , PRIORITY_HIGH , PRIORITY_MEDIUM , PRIORITY_LOW , PRIORITY_NEGLIGIBLE ])) {
2017-11-05 05:33:46 -05:00
$entry [ " priority " ] = PRIORITY_MEDIUM ;
}
// Define the maximum durations
2018-01-15 08:05:12 -05:00
$max_duration_defaults = [ PRIORITY_CRITICAL => 720 , PRIORITY_HIGH => 10 , PRIORITY_MEDIUM => 60 , PRIORITY_LOW => 180 , PRIORITY_NEGLIGIBLE => 720 ];
2017-11-05 05:33:46 -05:00
$max_duration = $max_duration_defaults [ $entry [ " priority " ]];
2018-02-11 11:18:39 -05:00
$argv = json_decode ( $entry [ " parameter " ], true );
2017-11-05 05:33:46 -05:00
$argv [ 0 ] = basename ( $argv [ 0 ]);
// How long is the process already running?
$duration = ( time () - strtotime ( $entry [ " executed " ])) / 60 ;
if ( $duration > $max_duration ) {
2018-10-29 17:20:46 -04:00
Logger :: log ( " Worker process " . $entry [ " pid " ] . " ( " . substr ( json_encode ( $argv ), 0 , 50 ) . " ) took more than " . $max_duration . " minutes. It will be killed now. " );
2017-11-05 05:33:46 -05:00
posix_kill ( $entry [ " pid " ], SIGTERM );
// We killed the stale process.
// To avoid a blocking situation we reschedule the process at the beginning of the queue.
// Additionally we are lowering the priority. (But not PRIORITY_CRITICAL)
2018-02-14 00:05:00 -05:00
$new_priority = $entry [ " priority " ];
2017-11-05 05:33:46 -05:00
if ( $entry [ " priority " ] == PRIORITY_HIGH ) {
$new_priority = PRIORITY_MEDIUM ;
} elseif ( $entry [ " priority " ] == PRIORITY_MEDIUM ) {
$new_priority = PRIORITY_LOW ;
} elseif ( $entry [ " priority " ] != PRIORITY_CRITICAL ) {
$new_priority = PRIORITY_NEGLIGIBLE ;
}
2018-07-20 08:19:26 -04:00
DBA :: update (
2017-11-19 17:04:40 -05:00
'workerqueue' ,
2018-10-21 01:53:47 -04:00
[ 'executed' => DBA :: NULL_DATETIME , 'created' => DateTimeFormat :: utcNow (), 'priority' => $new_priority , 'pid' => 0 ],
2018-01-15 08:05:12 -05:00
[ 'id' => $entry [ " id " ]]
2017-11-19 17:04:40 -05:00
);
2017-11-05 05:33:46 -05:00
} else {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Worker process " . $entry [ " pid " ] . " ( " . substr ( json_encode ( $argv ), 0 , 50 ) . " ) now runs for " . round ( $duration ) . " of " . $max_duration . " allowed minutes. That's okay. " , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
}
}
}
}
/**
* @ brief Checks if the number of active workers exceeds the given limits
*
* @ return bool Are there too much workers running ?
*/
2017-11-19 17:04:40 -05:00
public static function tooMuchWorkers ()
{
2017-11-05 05:33:46 -05:00
$queues = Config :: get ( " system " , " worker_queues " , 4 );
$maxqueues = $queues ;
$active = self :: activeWorkers ();
// Decrease the number of workers at higher load
2018-10-13 12:57:31 -04:00
$load = System :: currentLoad ();
2017-11-05 05:33:46 -05:00
if ( $load ) {
$maxsysload = intval ( Config :: get ( " system " , " maxloadavg " , 50 ));
2018-06-20 00:38:50 -04:00
2018-06-20 06:43:57 -04:00
/* Default exponent 3 causes queues to rapidly decrease as load increases .
* If you have 20 max queues at idle , then you get only 5 queues at 37.1 % of $maxsysload .
* For some environments , this rapid decrease is not needed .
* With exponent 1 , you could have 20 max queues at idle and 13 at 37 % of $maxsysload .
*/
2018-06-20 06:22:53 -04:00
$exponent = intval ( Config :: get ( 'system' , 'worker_load_exponent' , 3 ));
2018-06-20 06:06:20 -04:00
$slope = pow ( max ( 0 , $maxsysload - $load ) / $maxsysload , $exponent );
$queues = intval ( ceil ( $slope * $maxqueues ));
2018-01-01 15:51:02 -05:00
$processlist = '' ;
2017-11-05 05:33:46 -05:00
if ( Config :: get ( 'system' , 'worker_debug' )) {
// Create a list of queue entries grouped by their priority
2018-01-15 08:05:12 -05:00
$listitem = [];
2017-11-05 05:33:46 -05:00
// Adding all processes with no workerqueue entry
2018-07-20 08:19:26 -04:00
$processes = DBA :: p (
2017-11-19 17:04:40 -05:00
" SELECT COUNT(*) AS `running` FROM `process` WHERE NOT EXISTS
2017-11-05 05:33:46 -05:00
( SELECT id FROM `workerqueue`
2017-11-19 17:04:40 -05:00
WHERE `workerqueue` . `pid` = `process` . `pid` AND NOT `done` AND `pid` != ? ) " ,
getmypid ()
);
2018-07-20 08:19:26 -04:00
if ( $process = DBA :: fetch ( $processes )) {
2017-11-05 05:33:46 -05:00
$listitem [ 0 ] = " 0: " . $process [ " running " ];
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $processes );
2017-11-05 05:33:46 -05:00
// Now adding all processes with workerqueue entries
2018-11-04 05:51:01 -05:00
$entries = DBA :: p ( " SELECT COUNT(*) AS `entries`, `priority` FROM `workerqueue` WHERE NOT `done` AND `next_try` < ? GROUP BY `priority` " , DateTimeFormat :: utcNow ());
2018-07-20 08:19:26 -04:00
while ( $entry = DBA :: fetch ( $entries )) {
2018-11-04 05:51:01 -05:00
$processes = DBA :: p ( " SELECT COUNT(*) AS `running` FROM `process` INNER JOIN `workerqueue` ON `workerqueue`.`pid` = `process`.`pid` WHERE NOT `done` AND `next_try` < ? AND `priority` = ? " ,
DateTimeFormat :: utcNow (), $entry [ " priority " ]);
2018-07-20 08:19:26 -04:00
if ( $process = DBA :: fetch ( $processes )) {
2017-11-05 05:33:46 -05:00
$listitem [ $entry [ " priority " ]] = $entry [ " priority " ] . " : " . $process [ " running " ] . " / " . $entry [ " entries " ];
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $processes );
2017-11-05 05:33:46 -05:00
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $entries );
2017-11-05 05:33:46 -05:00
2018-01-15 08:05:12 -05:00
$intervals = [ 1 , 10 , 60 ];
$jobs_per_minute = [];
2017-11-19 17:04:40 -05:00
foreach ( $intervals as $interval ) {
2018-07-20 08:19:26 -04:00
$jobs = DBA :: p ( " SELECT COUNT(*) AS `jobs` FROM `workerqueue` WHERE `done` AND `executed` > UTC_TIMESTAMP() - INTERVAL " . intval ( $interval ) . " MINUTE " );
if ( $job = DBA :: fetch ( $jobs )) {
2017-11-05 05:33:46 -05:00
$jobs_per_minute [ $interval ] = number_format ( $job [ 'jobs' ] / $interval , 0 );
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $jobs );
2017-11-05 05:33:46 -05:00
}
$processlist = ' - jpm: ' . implode ( '/' , $jobs_per_minute ) . ' (' . implode ( ', ' , $listitem ) . ')' ;
}
$entries = self :: totalEntries ();
2018-10-23 16:38:28 -04:00
$deferred = self :: deferredEntries ();
2017-11-05 05:33:46 -05:00
if ( Config :: get ( " system " , " worker_fastlane " , false ) && ( $queues > 0 ) && ( $entries > 0 ) && ( $active >= $queues )) {
$top_priority = self :: highestPriority ();
$high_running = self :: processWithPriorityActive ( $top_priority );
if ( ! $high_running && ( $top_priority > PRIORITY_UNDEFINED ) && ( $top_priority < PRIORITY_NEGLIGIBLE )) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " There are jobs with priority " . $top_priority . " waiting but none is executed. Open a fastlane. " , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
$queues = $active + 1 ;
}
}
2018-10-30 09:58:45 -04:00
Logger :: log ( " Load: " . $load . " / " . $maxsysload . " - processes: " . $deferred . " / " . $active . " / " . $entries . $processlist . " - maximum: " . $queues . " / " . $maxqueues , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
// Are there fewer workers running as possible? Then fork a new one.
2018-05-12 22:40:52 -04:00
if ( ! Config :: get ( " system " , " worker_dont_fork " , false ) && ( $queues > ( $active + 1 )) && ( $entries > 1 )) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Active workers: " . $active . " / " . $queues . " Fork a new worker. " , Logger :: DEBUG );
2018-06-15 14:18:20 -04:00
if ( Config :: get ( 'system' , 'worker_daemon_mode' , false )) {
self :: IPCSetJobState ( true );
} else {
self :: spawnWorker ();
}
2017-11-05 05:33:46 -05:00
}
}
2018-06-19 18:53:02 -04:00
// if there are too much worker, we don't spawn a new one.
if ( Config :: get ( 'system' , 'worker_daemon_mode' , false ) && ( $active > $queues )) {
2018-06-15 14:18:20 -04:00
self :: IPCSetJobState ( false );
}
2018-06-19 18:53:02 -04:00
return $active > $queues ;
2017-11-05 05:33:46 -05:00
}
/**
2017-11-19 16:47:21 -05:00
* @ brief Returns the number of active worker processes
2017-11-05 05:33:46 -05:00
*
2017-11-19 16:47:21 -05:00
* @ return integer Number of active worker processes
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
private static function activeWorkers ()
{
2018-07-20 08:19:26 -04:00
return DBA :: count ( 'process' , [ 'command' => 'Worker.php' ]);
2017-11-05 05:33:46 -05:00
}
/**
* @ brief Check if we should pass some slow processes
*
* When the active processes of the highest priority are using more than 2 / 3
* of all processes , we let pass slower processes .
*
* @ param string $highest_priority Returns the currently highest priority
* @ return bool We let pass a slower process than $highest_priority
*/
2017-11-19 17:04:40 -05:00
private static function passingSlow ( & $highest_priority )
{
2017-11-05 05:33:46 -05:00
$highest_priority = 0 ;
2018-07-20 08:19:26 -04:00
$r = DBA :: p (
2017-11-19 17:04:40 -05:00
" SELECT `priority`
2017-11-05 12:13:37 -05:00
FROM `process`
2017-11-19 17:04:40 -05:00
INNER JOIN `workerqueue` ON `workerqueue` . `pid` = `process` . `pid` AND NOT `done` "
);
2017-11-05 05:33:46 -05:00
// No active processes at all? Fine
2018-07-21 08:46:04 -04:00
if ( ! DBA :: isResult ( $r )) {
2017-11-05 05:33:46 -05:00
return false ;
}
2018-01-15 08:05:12 -05:00
$priorities = [];
2018-07-20 08:19:26 -04:00
while ( $line = DBA :: fetch ( $r )) {
2017-11-05 05:33:46 -05:00
$priorities [] = $line [ " priority " ];
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $r );
2017-11-05 12:13:37 -05:00
2017-11-05 05:33:46 -05:00
// Should not happen
if ( count ( $priorities ) == 0 ) {
return false ;
}
$highest_priority = min ( $priorities );
// The highest process is already the slowest one?
// Then we quit
if ( $highest_priority == PRIORITY_NEGLIGIBLE ) {
return false ;
}
$high = 0 ;
2017-11-19 17:04:40 -05:00
foreach ( $priorities as $priority ) {
2017-11-05 05:33:46 -05:00
if ( $priority == $highest_priority ) {
++ $high ;
}
}
2018-10-30 09:58:45 -04:00
Logger :: log ( " Highest priority: " . $highest_priority . " Total processes: " . count ( $priorities ) . " Count high priority processes: " . $high , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
$passing_slow = (( $high / count ( $priorities )) > ( 2 / 3 ));
if ( $passing_slow ) {
2018-10-30 09:58:45 -04:00
Logger :: log ( " Passing slower processes than priority " . $highest_priority , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
}
return $passing_slow ;
}
/**
* @ brief Find and claim the next worker process for us
*
* @ param boolean $passing_slow Returns if we had passed low priority processes
* @ return boolean Have we found something ?
*/
2017-11-19 17:04:40 -05:00
private static function findWorkerProcesses ( & $passing_slow )
{
2017-11-05 05:33:46 -05:00
$mypid = getmypid ();
// Check if we should pass some low priority process
$highest_priority = 0 ;
$found = false ;
$passing_slow = false ;
// The higher the number of parallel workers, the more we prefetch to prevent concurring access
// We decrease the limit with the number of entries left in the queue
$worker_queues = Config :: get ( " system " , " worker_queues " , 4 );
$queue_length = Config :: get ( 'system' , 'worker_fetch_limit' , 1 );
$lower_job_limit = $worker_queues * $queue_length * 2 ;
$jobs = self :: totalEntries ();
2018-10-23 16:38:28 -04:00
$deferred = self :: deferredEntries ();
2017-11-05 05:33:46 -05:00
// Now do some magic
$exponent = 2 ;
$slope = $queue_length / pow ( $lower_job_limit , $exponent );
$limit = min ( $queue_length , ceil ( $slope * pow ( $jobs , $exponent )));
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Deferred: ' . $deferred . ' - Total: ' . $jobs . ' - Maximum: ' . $queue_length . ' - jobs per queue: ' . $limit , Logger :: DEBUG );
2018-02-14 00:05:00 -05:00
$ids = [];
2017-11-05 05:33:46 -05:00
if ( self :: passingSlow ( $highest_priority )) {
// Are there waiting processes with a higher priority than the currently highest?
2018-07-20 08:19:26 -04:00
$result = DBA :: select (
2017-11-19 17:04:40 -05:00
'workerqueue' ,
2018-01-09 22:20:33 -05:00
[ 'id' ],
2018-10-15 01:19:35 -04:00
[ " `executed` <= ? AND `priority` < ? AND NOT `done` AND `next_try` < ? " ,
2018-10-21 01:53:47 -04:00
DBA :: NULL_DATETIME , $highest_priority , DateTimeFormat :: utcNow ()],
2018-01-09 22:20:33 -05:00
[ 'limit' => $limit , 'order' => [ 'priority' , 'created' ]]
2017-11-19 17:04:40 -05:00
);
2017-11-05 05:33:46 -05:00
2018-07-20 08:19:26 -04:00
while ( $id = DBA :: fetch ( $result )) {
2017-11-05 05:33:46 -05:00
$ids [] = $id [ " id " ];
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $result );
2017-11-05 05:33:46 -05:00
$found = ( count ( $ids ) > 0 );
if ( ! $found ) {
// Give slower processes some processing time
2018-07-20 08:19:26 -04:00
$result = DBA :: select (
2017-11-19 17:04:40 -05:00
'workerqueue' ,
2018-01-09 22:20:33 -05:00
[ 'id' ],
2018-10-15 01:19:35 -04:00
[ " `executed` <= ? AND `priority` > ? AND NOT `done` AND `next_try` < ? " ,
2018-10-21 01:53:47 -04:00
DBA :: NULL_DATETIME , $highest_priority , DateTimeFormat :: utcNow ()],
2018-01-09 22:20:33 -05:00
[ 'limit' => $limit , 'order' => [ 'priority' , 'created' ]]
2017-11-19 17:04:40 -05:00
);
2017-11-05 05:33:46 -05:00
2018-07-20 08:19:26 -04:00
while ( $id = DBA :: fetch ( $result )) {
2017-11-05 05:33:46 -05:00
$ids [] = $id [ " id " ];
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $result );
2017-11-05 05:33:46 -05:00
$found = ( count ( $ids ) > 0 );
$passing_slow = $found ;
}
}
// If there is no result (or we shouldn't pass lower processes) we check without priority limit
if ( ! $found ) {
2018-07-20 08:19:26 -04:00
$result = DBA :: select (
2017-11-19 17:04:40 -05:00
'workerqueue' ,
2018-01-09 22:20:33 -05:00
[ 'id' ],
2018-10-15 01:19:35 -04:00
[ " `executed` <= ? AND NOT `done` AND `next_try` < ? " ,
2018-10-21 01:53:47 -04:00
DBA :: NULL_DATETIME , DateTimeFormat :: utcNow ()],
2018-01-09 22:20:33 -05:00
[ 'limit' => $limit , 'order' => [ 'priority' , 'created' ]]
2017-11-19 17:04:40 -05:00
);
2017-11-05 05:33:46 -05:00
2018-07-20 08:19:26 -04:00
while ( $id = DBA :: fetch ( $result )) {
2017-11-05 05:33:46 -05:00
$ids [] = $id [ " id " ];
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $result );
2017-11-05 05:33:46 -05:00
$found = ( count ( $ids ) > 0 );
}
if ( $found ) {
$condition = " `id` IN ( " . substr ( str_repeat ( " ?, " , count ( $ids )), 0 , - 2 ) . " ) AND `pid` = 0 AND NOT `done` " ;
array_unshift ( $ids , $condition );
2018-07-20 08:19:26 -04:00
DBA :: update ( 'workerqueue' , [ 'executed' => DateTimeFormat :: utcNow (), 'pid' => $mypid ], $ids );
2017-11-05 05:33:46 -05:00
}
return $found ;
}
/**
* @ brief Returns the next worker process
*
* @ param boolean $passing_slow Returns if we had passed low priority processes
* @ return string SQL statement
*/
2017-11-19 17:04:40 -05:00
public static function workerProcess ( & $passing_slow )
{
2017-11-05 05:33:46 -05:00
$stamp = ( float ) microtime ( true );
// There can already be jobs for us in the queue.
2018-07-20 08:19:26 -04:00
$r = DBA :: select ( 'workerqueue' , [], [ 'pid' => getmypid (), 'done' => false ]);
2018-07-21 08:46:04 -04:00
if ( DBA :: isResult ( $r )) {
2017-11-05 05:33:46 -05:00
self :: $db_duration += ( microtime ( true ) - $stamp );
2018-07-20 22:03:40 -04:00
return DBA :: toArray ( $r );
2017-11-05 05:33:46 -05:00
}
2018-07-20 08:19:26 -04:00
DBA :: close ( $r );
2017-11-05 05:33:46 -05:00
$stamp = ( float ) microtime ( true );
2018-07-05 14:57:31 -04:00
if ( ! Lock :: acquire ( 'worker_process' )) {
2017-11-05 05:33:46 -05:00
return false ;
}
self :: $lock_duration = ( microtime ( true ) - $stamp );
$stamp = ( float ) microtime ( true );
$found = self :: findWorkerProcesses ( $passing_slow );
self :: $db_duration += ( microtime ( true ) - $stamp );
2018-07-05 14:57:31 -04:00
Lock :: release ( 'worker_process' );
2017-11-05 05:33:46 -05:00
if ( $found ) {
2018-07-20 08:19:26 -04:00
$r = DBA :: select ( 'workerqueue' , [], [ 'pid' => getmypid (), 'done' => false ]);
2018-07-20 22:03:40 -04:00
return DBA :: toArray ( $r );
2017-11-05 05:33:46 -05:00
}
2017-11-05 12:13:37 -05:00
return false ;
2017-11-05 05:33:46 -05:00
}
/**
* @ brief Removes a workerqueue entry from the current process
2017-11-19 17:04:40 -05:00
* @ return void
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
public static function unclaimProcess ()
{
2017-11-05 05:33:46 -05:00
$mypid = getmypid ();
2018-10-21 01:53:47 -04:00
DBA :: update ( 'workerqueue' , [ 'executed' => DBA :: NULL_DATETIME , 'pid' => 0 ], [ 'pid' => $mypid , 'done' => false ]);
2017-11-05 05:33:46 -05:00
}
/**
* @ brief Call the front end worker
2017-11-19 17:04:40 -05:00
* @ return void
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
public static function callWorker ()
{
2017-11-05 05:33:46 -05:00
if ( ! Config :: get ( " system " , " frontend_worker " )) {
return ;
}
$url = System :: baseUrl () . " /worker " ;
2018-01-27 11:13:41 -05:00
Network :: fetchUrl ( $url , false , $redirects , 1 );
2017-11-05 05:33:46 -05:00
}
/**
* @ brief Call the front end worker if there aren ' t any active
2017-11-19 17:04:40 -05:00
* @ return void
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
public static function executeIfIdle ()
{
2017-11-05 05:33:46 -05:00
if ( ! Config :: get ( " system " , " frontend_worker " )) {
return ;
}
2017-11-19 16:47:21 -05:00
// Do we have "proc_open"? Then we can fork the worker
2017-11-05 05:33:46 -05:00
if ( function_exists ( " proc_open " )) {
// When was the last time that we called the worker?
// Less than one minute? Then we quit
if (( time () - Config :: get ( " system " , " worker_started " )) < 60 ) {
return ;
}
2017-11-06 21:22:52 -05:00
Config :: set ( " system " , " worker_started " , time ());
2017-11-05 05:33:46 -05:00
// Do we have enough running workers? Then we quit here.
if ( self :: tooMuchWorkers ()) {
// Cleaning dead processes
self :: killStaleWorkers ();
2018-01-15 19:08:28 -05:00
Process :: deleteInactive ();
2017-11-05 05:33:46 -05:00
return ;
}
self :: runCron ();
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Call worker' , Logger :: DEBUG );
2017-11-05 10:28:55 -05:00
self :: spawnWorker ();
2017-11-05 05:33:46 -05:00
return ;
}
// We cannot execute background processes.
// We now run the processes from the frontend.
// This won't work with long running processes.
self :: runCron ();
self :: clearProcesses ();
2018-06-19 17:33:07 -04:00
$workers = self :: activeWorkers ();
2017-11-05 05:33:46 -05:00
2018-06-19 17:33:07 -04:00
if ( $workers == 0 ) {
2017-11-05 05:33:46 -05:00
self :: callWorker ();
}
}
/**
* @ brief Removes long running worker processes
2017-11-19 17:04:40 -05:00
* @ return void
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
public static function clearProcesses ()
{
2017-11-05 05:33:46 -05:00
$timeout = Config :: get ( " system " , " frontend_worker_timeout " , 10 );
/// @todo We should clean up the corresponding workerqueue entries as well
2018-01-15 08:05:12 -05:00
$condition = [ " `created` < ? AND `command` = 'worker.php' " ,
2018-01-26 21:38:34 -05:00
DateTimeFormat :: utc ( " now - " . $timeout . " minutes " )];
2018-07-20 08:19:26 -04:00
DBA :: delete ( 'process' , $condition );
2017-11-05 05:33:46 -05:00
}
/**
* @ brief Runs the cron processes
2017-11-19 17:04:40 -05:00
* @ return void
2017-11-05 05:33:46 -05:00
*/
2017-11-19 17:04:40 -05:00
private static function runCron ()
{
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Add cron entries' , Logger :: DEBUG );
2017-11-05 05:33:46 -05:00
// Check for spooled items
2017-11-18 02:44:38 -05:00
self :: add ( PRIORITY_HIGH , " SpoolPost " );
2017-11-05 05:33:46 -05:00
// Run the cron job that calls all other jobs
2017-11-14 16:50:16 -05:00
self :: add ( PRIORITY_MEDIUM , " Cron " );
2017-11-05 05:33:46 -05:00
// Cleaning dead processes
self :: killStaleWorkers ();
}
2017-11-19 17:33:07 -05:00
/**
2018-05-14 01:02:18 -04:00
* @ brief Spawns a new worker
2017-11-19 17:33:07 -05:00
* @ return void
*/
2018-06-05 23:48:04 -04:00
public static function spawnWorker ( $do_cron = false )
2017-11-19 17:33:07 -05:00
{
2018-07-23 07:40:52 -04:00
$command = 'bin/worker.php' ;
2018-06-05 23:48:04 -04:00
2018-07-24 02:15:58 -04:00
$args = [ 'no_cron' => ! $do_cron ];
2018-06-05 23:48:04 -04:00
2018-07-23 07:40:52 -04:00
get_app () -> proc_run ( $command , $args );
2018-06-15 14:18:20 -04:00
// after spawning we have to remove the flag.
if ( Config :: get ( 'system' , 'worker_daemon_mode' , false )) {
self :: IPCSetJobState ( false );
}
2017-11-05 10:28:55 -05:00
}
2017-11-05 05:33:46 -05:00
/**
* @ brief Adds tasks to the worker queue
*
2017-11-06 10:38:15 -05:00
* @ param ( integer | array ) priority or parameter array , strings are deprecated and are ignored
2017-11-05 05:33:46 -05:00
*
* next args are passed as $cmd command line
2017-11-19 13:59:55 -05:00
* or : Worker :: add ( PRIORITY_HIGH , " Notifier " , " drop " , $drop_id );
2017-11-19 14:47:04 -05:00
* or : Worker :: add ( array ( 'priority' => PRIORITY_HIGH , 'dont_fork' => true ), " CreateShadowEntry " , $post_id );
2017-11-05 05:33:46 -05:00
*
* @ note $cmd and string args are surrounded with " "
*
* @ hooks 'proc_run'
* array $arr
*
* @ return boolean " false " if proc_run couldn ' t be executed
*/
2017-11-19 17:04:40 -05:00
public static function add ( $cmd )
{
2018-02-11 11:18:39 -05:00
$args = func_get_args ();
2017-11-05 05:33:46 -05:00
2018-02-11 11:18:39 -05:00
if ( ! count ( $args )) {
2017-11-05 05:33:46 -05:00
return false ;
}
2018-01-15 08:05:12 -05:00
$arr = [ 'args' => $args , 'run_cmd' => true ];
2017-11-05 05:33:46 -05:00
2018-01-17 13:42:40 -05:00
Addon :: callHooks ( " proc_run " , $arr );
2017-11-05 05:33:46 -05:00
if ( ! $arr [ 'run_cmd' ] || ! count ( $args )) {
return true ;
}
$priority = PRIORITY_MEDIUM ;
2018-05-12 22:40:52 -04:00
$dont_fork = Config :: get ( " system " , " worker_dont_fork " , false );
2018-01-26 21:38:34 -05:00
$created = DateTimeFormat :: utcNow ();
2017-11-05 05:33:46 -05:00
2018-02-12 21:26:35 -05:00
$run_parameter = array_shift ( $args );
2017-11-05 05:33:46 -05:00
if ( is_int ( $run_parameter )) {
$priority = $run_parameter ;
} elseif ( is_array ( $run_parameter )) {
if ( isset ( $run_parameter [ 'priority' ])) {
$priority = $run_parameter [ 'priority' ];
}
if ( isset ( $run_parameter [ 'created' ])) {
$created = $run_parameter [ 'created' ];
}
if ( isset ( $run_parameter [ 'dont_fork' ])) {
$dont_fork = $run_parameter [ 'dont_fork' ];
}
}
2018-02-11 11:18:39 -05:00
$parameters = json_encode ( $args );
2018-07-20 08:19:26 -04:00
$found = DBA :: exists ( 'workerqueue' , [ 'parameter' => $parameters , 'done' => false ]);
2017-11-05 05:33:46 -05:00
// Quit if there was a database error - a precaution for the update process to 3.5.3
2018-07-20 08:19:26 -04:00
if ( DBA :: errorNo () != 0 ) {
2017-11-05 05:33:46 -05:00
return false ;
}
if ( ! $found ) {
2018-07-20 08:19:26 -04:00
DBA :: insert ( 'workerqueue' , [ 'parameter' => $parameters , 'created' => $created , 'priority' => $priority ]);
2017-11-05 05:33:46 -05:00
}
2018-06-02 01:17:32 -04:00
// Should we quit and wait for the worker to be called as a cronjob?
if ( $dont_fork ) {
2018-06-01 18:09:27 -04:00
return true ;
}
2017-11-05 05:33:46 -05:00
// If there is a lock then we don't have to check for too much worker
2018-07-05 14:57:31 -04:00
if ( ! Lock :: acquire ( 'worker' , 0 )) {
2017-11-05 05:33:46 -05:00
return true ;
}
// If there are already enough workers running, don't fork another one
$quit = self :: tooMuchWorkers ();
2018-07-05 14:57:31 -04:00
Lock :: release ( 'worker' );
2017-11-05 05:33:46 -05:00
if ( $quit ) {
return true ;
}
2018-06-15 14:18:20 -04:00
// We tell the daemon that a new job entry exists
if ( Config :: get ( 'system' , 'worker_daemon_mode' , false )) {
// We don't have to set the IPC flag - this is done in "tooMuchWorkers"
return true ;
}
2017-11-19 16:47:21 -05:00
// Now call the worker to execute the jobs that we just added to the queue
2017-11-05 10:28:55 -05:00
self :: spawnWorker ();
2017-11-05 05:33:46 -05:00
return true ;
}
2018-01-15 19:08:28 -05:00
2018-10-15 01:19:35 -04:00
/**
* Defers the current worker entry
*/
public static function defer ()
{
if ( empty ( BaseObject :: getApp () -> queue )) {
return ;
}
$queue = BaseObject :: getApp () -> queue ;
$retrial = $queue [ 'retrial' ];
$id = $queue [ 'id' ];
if ( $retrial > 14 ) {
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Id ' . $id . ' had been tried 14 times. We stop now.' , Logger :: DEBUG );
2018-10-22 23:54:18 -04:00
return ;
2018-10-15 01:19:35 -04:00
}
// Calculate the delay until the next trial
$delay = (( $retrial + 3 ) ** 4 ) + ( rand ( 1 , 30 ) * ( $retrial + 1 ));
$next = DateTimeFormat :: utc ( 'now + ' . $delay . ' seconds' );
2018-10-30 09:58:45 -04:00
Logger :: log ( 'Defer execution ' . $retrial . ' of id ' . $id . ' to ' . $next , Logger :: DEBUG );
2018-10-15 01:19:35 -04:00
2018-10-21 01:53:47 -04:00
$fields = [ 'retrial' => $retrial + 1 , 'next_try' => $next , 'executed' => DBA :: NULL_DATETIME , 'pid' => 0 ];
2018-10-15 01:19:35 -04:00
DBA :: update ( 'workerqueue' , $fields , [ 'id' => $id ]);
}
2018-01-15 19:08:28 -05:00
/**
* Log active processes into the " process " table
*
* @ brief Log active processes into the " process " table
*/
public static function startProcess ()
{
$trace = debug_backtrace ( DEBUG_BACKTRACE_IGNORE_ARGS , 1 );
$command = basename ( $trace [ 0 ][ 'file' ]);
Process :: deleteInactive ();
Process :: insert ( $command );
}
/**
* Remove the active process from the " process " table
*
* @ brief Remove the active process from the " process " table
* @ return bool
*/
public static function endProcess ()
{
return Process :: deleteByPid ();
}
2018-06-01 18:09:27 -04:00
2018-06-02 01:03:23 -04:00
/**
* Set the flag if some job is waiting
*
* @ brief Set the flag if some job is waiting
* @ param boolean $jobs Is there a waiting job ?
*/
2018-06-01 18:09:27 -04:00
public static function IPCSetJobState ( $jobs )
{
2018-07-20 08:19:26 -04:00
DBA :: update ( 'worker-ipc' , [ 'jobs' => $jobs ], [ 'key' => 1 ], true );
2018-06-01 18:09:27 -04:00
}
2018-06-02 01:03:23 -04:00
/**
* Checks if some worker job waits to be executed
*
* @ brief Checks if some worker job waits to be executed
* @ return bool
*/
2018-06-01 18:09:27 -04:00
public static function IPCJobsExists ()
{
2018-07-20 08:19:26 -04:00
$row = DBA :: selectFirst ( 'worker-ipc' , [ 'jobs' ], [ 'key' => 1 ]);
2018-06-01 18:09:27 -04:00
// When we don't have a row, no job is running
2018-07-21 08:46:04 -04:00
if ( ! DBA :: isResult ( $row )) {
2018-06-01 18:09:27 -04:00
return false ;
}
return ( bool ) $row [ 'jobs' ];
}
2017-11-05 05:33:46 -05:00
}