module = $module; $this->func = $func; } function __toString() { return $this->module.':'.$this->func; } } class Task { const CODE_MESSAGE = 0; const CODE_EXCEPTION = 1; const CODE_WARN = 2; const CODE_NSTART = 124; const CODE_STUCK = 125; const CODE_GONE = 126; const CODE_HUNG = 127; const CODE_DONE = 128; static function code2string(int $code) : string { switch($code) { case self::CODE_MESSAGE: return "message"; case self::CODE_EXCEPTION: return "exception"; case self::CODE_WARN: return "warning"; case self::CODE_STUCK: return "stuck"; case self::CODE_NSTART: return "nstart"; case self::CODE_GONE: return "gone"; case self::CODE_HUNG: return "hung"; case self::CODE_DONE: return "done"; } return "???"; } public ?Plan $plan = null; //async execution id public int $run_id = 0; //'true' if async coroutine is resolved public bool $run_status = false; //async execution exception if any public ?object $run_error = null; //attached device public ?string $device = null; public int $attempts = 1; //signal to dispatcher to re-start execution of the task public bool $need_reschedule = false; public Cmd $cmd; public array $args = array(); public string $title = ''; public float $start_time = 0; public float $reset_time = 0; public float $last_progress = 0; public int $last_done_arg_idx = -1; public float $last_alive_check_time = 0; public float $last_stuck_check_time = 1; public float $last_ext_status_item_time = 0; //array of tuples [code, msg] public array $status_codes = array(); function __construct(Cmd $cmd, array $args, $title = '') { $this->cmd = $cmd; $this->args = $args; $this->title = $title; } function getCmd() : Cmd { return $this->cmd; } function getCmdArgs() : array { return $this->args; } function getProgress() : float { if($this->last_progress == 0 && $this->isDone()) return 1; return $this->last_progress; } static function isProblemCode($code) : bool { return $code > self::CODE_MESSAGE && $code < self::CODE_DONE; } static function isFatalCode($code) : bool { return $code == Task::CODE_STUCK || $code == Task::CODE_NSTART || $code == Task::CODE_GONE || $code == Task::CODE_HUNG || $code == Task::CODE_EXCEPTION; } function hasFatalProblem() : bool { return $this->getLastFatalProblem() != 0; } function getLastFatalProblem() : int { for($i=sizeof($this->status_codes);$i-- > 0;) { $item = $this->status_codes[$i]; if(self::isFatalCode($item[0])) return $item[0]; } return 0; } function hasStatusCode($code) { foreach($this->status_codes as $item) { if($item[0] === $code) return true; } return false; } function isDone() { return $this->hasStatusCode(Task::CODE_DONE); } function addStatusCode($code, $msg = '') { //plan stores history of all status codes $this->plan->addStatusCode($code, $msg); $this->status_codes[] = array($code, $msg); } function onFatalProblem() { if($this->attempts < 3) $this->reschedule(); } function onProgress($jzon) { try { $data = jzon_parse(trim(str_replace('\"', '"', $jzon))); if(isset($data['p'])) $this->last_progress = floatval(str_replace(',', '.', $data['p'])); if(isset($data['arg_idx'])) $this->last_done_arg_idx = (int)$data['arg_idx']; } catch(Exception $e) { echo $e->getMessage() . "\n"; } } function _resetCheckTimes() { $this->reset_time = microtime(true); $this->last_alive_check_time = $this->reset_time; $this->last_stuck_check_time = $this->reset_time; $this->last_ext_status_item_time = 0; } function reschedule() { ++$this->attempts; $this->device = null; $this->need_reschedule = true; } function start() { $this->_resetCheckTimes(); $this->status_codes = array(); $this->start_time = time(); } function getDuration() { return time() - $this->start_time; } } class TestLevelsTask extends Task { public $arg_offset = 0; private $rseeds = array(); private $seed = null; private $timescale = null; function __construct(Cmd $cmd, array $args, $with_rseeds = false, $seed = null, $timescale = null, $title = '') { $level_ids = array(); //TODO: should not really be here if($with_rseeds) { foreach($args as $item) { $level_ids[] = $item[0]; $this->rseeds[] = $item[1]; } } else $level_ids = $args; $this->seed = $seed; $this->timescale = $timescale; parent::__construct($cmd, $level_ids, $title); } function getCmdArgs() : array { $args = array(); if($this->rseeds) { foreach($this->args as $idx => $arg) { $args[] = $arg; $args[] = $this->rseeds[$idx]; } } else $args = $this->args; //appending arg. offset as a first argument array_unshift($args, ''.$this->arg_offset); array_unshift($args, '--offset'); if($this->rseeds) { array_unshift($args, 'true'); array_unshift($args, '--levels-with-rseed'); } if($this->seed !== null) { array_unshift($args, ''.$this->seed); array_unshift($args, '--seed'); } if($this->timescale !== null) { array_unshift($args, ''.$this->timescale); array_unshift($args, '--timescale'); } return $args; } function onFatalProblem() { $start_arg = isset($this->args[$this->arg_offset]) ? $this->args[$this->arg_offset] : '?'; $done_arg = isset($this->args[$this->last_done_arg_idx]) ? $this->args[$this->last_done_arg_idx] : '?'; $this->plan->post("Handling error, progress:".round($this->getProgress(),2).", last done arg:{$done_arg}, start arg:{$start_arg}, arg offset:{$this->arg_offset}, args:".sizeof($this->args)." *$this->device*"); //we're allowed to skip only in case of exceptions if($this->getLastFatalProblem() == Task::CODE_EXCEPTION) { if($this->last_done_arg_idx > $this->arg_offset) $this->arg_offset = $this->last_done_arg_idx; ++$this->arg_offset; } //let's reschedule the task if it still makes sense if($this->arg_offset < sizeof($this->args)) $this->reschedule(); } }