<?php
 class vB_Systemevent_Queue { use vB_Trait_NoSerialize; public const PRIORITY_HIGH = 16; public const PRIORITY_NORMAL = 8; public const PRIORITY_LOW = 4; public const PRIORITY_MINIUM = 1; private vB_Db_Assertor $db; private int $waittime = 10; private int $runtime = 60; private int $timebuffer = 15; public function __construct(vB_Db_Assertor $db) { $this->db = $db; } public function register(vB_Systemevent_Item $event, int $priority = self::PRIORITY_NORMAL, int $runafter = 0) : void { $runafter = max($runafter, vB::getRequest()->getTimeNow()); $this->db->insert('vBEvent:systemevent', [ 'priority' => $priority, 'data' => $event->getData(), 'runafter' => $runafter, 'status' => '', 'errormessage' => '', 'processkey' => '', 'processtime' => 0, ]); } public function deleteErrors(int $olderthanseconds) : void { $cutoff = vB::getRequest()->getTimeNow() - $olderthanseconds; $this->db->delete('vBEvent:systemevent', [ 'status' => 'error', ['field'=>'runafter', 'value' => $cutoff, vB_dB_Query::OPERATOR_KEY => vB_dB_Query::OPERATOR_LTE], ]); } public function isEmpty() : bool { $row = $this->db->getRow('vBEvent:systemevent', [ vB_dB_Query::COLUMNS_KEY => 'systemeventid', vB_dB_Query::CONDITIONS_KEY => [ 'status' => '', ['field' => 'runafter', 'value' => vB::getRequest()->getTimeNow(), vB_dB_Query::OPERATOR_KEY => vB_dB_Query::OPERATOR_LTE] ], ]); return !$row; } public function runQueue(int $waitlimit = 0) : void { $processkey = $this->getProcessKey(); $runtime = $this->configureTimelimit(); $eventid = 0; $currentEvent = null; register_shutdown_function(function() use (&$eventid, &$currentEvent) { $this->processTimeout($eventid, $currentEvent); }); $waitcount = 0; $starttime = time(); $maxtime = $starttime + $runtime; while (time() < $maxtime) { $eventrow = $this->claimNextEvent($processkey); if (!$eventrow) { $waitcount++; if (($waitcount > $waitlimit) OR ((time() + $this->waittime) > $maxtime)) { break; } else { sleep($this->waittime); continue; } } $eventid = $eventrow['systemeventid']; $currentEvent = vB_Systemevent_Item::restoreFromData($eventrow['data']); if ($currentEvent) { try { $currentEvent->run(); } catch (Throwable $e) { if ($e instanceof vB_Exception_Api) { $errors = $e->get_errors(); } else { $errors = [['unexpected_error', $e->getMessage()]]; } $errors[] = ['exception_trace', vB_Exception::getFullTrace($e)]; $phrases = vB_Api::instance('phrase')->renderPhrases($errors); if (isset($phrases['errors'])) { $errorText = 'Could not process exception, real error: ' . $e->getMessage(); } else { $errorText = implode("\n", $phrases['phrases']); } try { $data = [ 'processkey' => '', 'processtime' => 0, 'status' => 'error', 'errormessage' => $errorText, ]; $this->db->update('vBEvent:systemevent', $data, ['systemeventid' => $eventid]); $currentEvent = null; continue; } catch (Throwable $e) { } } $currentEvent = null; } $this->db->delete('vBEvent:systemevent', ['systemeventid' => $eventid]); } } private function configureTimelimit() : int { vB_Utility_Functions::setPhpTimeout(0); $runtime = ini_get('max_execution_time'); if ($runtime == 0) { $runtime = $this->runtime; } else { $runtime = min($runtime - $this->timebuffer, $this->runtime); } return $runtime; } private function claimNextEvent(string $processkey) : ?array { $this->db->assertQuery('vBEvent:claimNextEvent', [ 'processkey' => $processkey, 'timenow' => time(), ]); $eventrow = $this->db->getRow('vBEvent:systemevent', [ 'processkey' => $processkey ]); return $eventrow; } private function processTimeout(int $eventid, ?vB_Systemevent_Item $event) : void { if ($event) { try { $unlockevent = $event->interrupt(); } catch(Throwable $e) { $unlockevent = false; } if ($unlockevent) { $this->db->update('vBEvent:systemevent', [ 'processkey' => '', 'processtime' => 0, ], [ 'systemeventid' => $eventid, ] ); } else { $this->db->delete('vBEvent:systemevent', ['systemeventid' => $eventid]); } } } private function getProcessKey() : string { return uniqid('ev', true); } } 