Fix job behavior during summer and winter time change (#2954)

ref/IP/52977

`ts_last_attempt`, which is used in the callback function of
`React\EventLoop\LoopInterface::addPeriodicTimer`, had data type
timestamp and was saved as UTC time but retrieved as system time, and
during DST where the clock is moved forward or set back during DST, you
could expect time jumps in the retrieved `ts_last_attempt` value.

And the `React\EventLoop\LoopInterface::addPeriodicTimer` expects the
time source to be monotonous without any such time jumps. This causes
the jobs to be erratically scheduled after DST. And this happens mostly
when the `ts_last_attempt` saved time time falls in the skipped or time
range that was reset.

To avoid such time jumps the `ts_last_attempt` datatype is changed to
`BIGINT` and the values are saved as Unix time stamp.
This commit is contained in:
Ravi Kumar Kempapura Srinivasa 2025-03-25 16:17:12 +01:00 committed by GitHub
commit cadac72f4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 54 additions and 15 deletions

View File

@ -3,6 +3,7 @@
namespace Icinga\Module\Director\Objects;
use Icinga\Exception\NotFoundError;
use Icinga\Module\Director\Daemon\DaemonUtil;
use Icinga\Module\Director\Daemon\Logger;
use Icinga\Module\Director\Data\Db\DbObjectWithSettings;
use Icinga\Module\Director\Db;
@ -84,7 +85,8 @@ class DirectorJob extends DbObjectWithSettings implements ExportInterface, Insta
public function run()
{
$job = $this->getInstance();
$this->set('ts_last_attempt', date('Y-m-d H:i:s'));
$currentTimestamp = DaemonUtil::timestampWithMilliseconds();
$this->set('ts_last_attempt', $currentTimestamp);
try {
$job->run();
@ -92,7 +94,7 @@ class DirectorJob extends DbObjectWithSettings implements ExportInterface, Insta
$success = true;
} catch (Exception $e) {
Logger::error($e->getMessage());
$this->set('ts_last_error', date('Y-m-d H:i:s'));
$this->set('ts_last_error', $currentTimestamp);
$this->set('last_error_message', $e->getMessage());
$this->set('last_attempt_succeeded', 'n');
$success = false;
@ -127,8 +129,8 @@ class DirectorJob extends DbObjectWithSettings implements ExportInterface, Insta
}
return (
strtotime($this->get('ts_last_attempt')) + $this->get('run_interval') * 2
) < time();
$this->get('ts_last_attempt') + $this->get('run_interval') * 2 * 1000
) < DaemonUtil::timestampWithMilliseconds();
}
public function hasBeenDisabled()
@ -145,7 +147,9 @@ class DirectorJob extends DbObjectWithSettings implements ExportInterface, Insta
return $this->isWithinTimeperiod();
}
if (strtotime($this->get('ts_last_attempt')) + $this->get('run_interval') < time()) {
if (
$this->get('ts_last_attempt') + $this->get('run_interval') * 1000 < DaemonUtil::timestampWithMilliseconds()
) {
return $this->isWithinTimeperiod();
}

View File

@ -4,6 +4,7 @@ namespace Icinga\Module\Director\Web\Table;
use gipfl\IcingaWeb2\Link;
use gipfl\IcingaWeb2\Table\ZfQueryBasedTable;
use Icinga\Module\Director\Daemon\DaemonUtil;
class JobTable extends ZfQueryBasedTable
{
@ -37,11 +38,11 @@ class JobTable extends ZfQueryBasedTable
protected function getJobClasses($row)
{
if ($row->unixts_last_attempt === null) {
if ($row->ts_last_attempt === null) {
return 'pending';
}
if ($row->unixts_last_attempt + $row->run_interval < time()) {
if ($row->ts_last_attempt + $row->run_interval * 1000 < DaemonUtil::timestampWithMilliseconds()) {
return 'pending';
}
@ -73,7 +74,6 @@ class JobTable extends ZfQueryBasedTable
'run_interval' => 'j.run_interval',
'last_attempt_succeeded' => 'j.last_attempt_succeeded',
'ts_last_attempt' => 'j.ts_last_attempt',
'unixts_last_attempt' => 'UNIX_TIMESTAMP(j.ts_last_attempt)',
'ts_last_error' => 'j.ts_last_error',
'last_error_message' => 'j.last_error_message',
]

View File

@ -45,7 +45,7 @@ class JobDetails extends HtmlDocument
$tsLastAttempt = $job->get('ts_last_attempt');
if ($tsLastAttempt) {
$ts = \strtotime($tsLastAttempt);
$ts = $tsLastAttempt / 1000;
$timeAgo = Html::tag('span', [
'class' => 'time-ago',
'title' => DateFormatter::formatDateTime($ts)

View File

@ -0,0 +1,17 @@
ALTER TABLE director_job ADD COLUMN ts_last_attempt_tmp BIGINT(20) DEFAULT NULL;
ALTER TABLE director_job ADD COLUMN ts_last_error_tmp BIGINT(20) DEFAULT NULL;
UPDATE director_job
SET ts_last_attempt_tmp = UNIX_TIMESTAMP(ts_last_attempt) * 1000,
ts_last_error_tmp = UNIX_TIMESTAMP(ts_last_error) * 1000;
ALTER TABLE director_job
DROP COLUMN ts_last_attempt,
DROP COLUMN ts_last_error,
CHANGE ts_last_attempt_tmp ts_last_attempt BIGINT(20) DEFAULT NULL,
CHANGE ts_last_error_tmp ts_last_error BIGINT(20) DEFAULT NULL;
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (189, NOW());

View File

@ -347,8 +347,8 @@ CREATE TABLE director_job (
run_interval INT(10) UNSIGNED NOT NULL, -- seconds
timeperiod_id INT(10) UNSIGNED DEFAULT NULL,
last_attempt_succeeded ENUM('y', 'n') DEFAULT NULL,
ts_last_attempt TIMESTAMP NULL DEFAULT NULL,
ts_last_error TIMESTAMP NULL DEFAULT NULL,
ts_last_attempt BIGINT(20) NULL DEFAULT NULL,
ts_last_error BIGINT(20) NULL DEFAULT NULL,
last_error_message TEXT DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY (job_name),
@ -2446,4 +2446,4 @@ CREATE TABLE branched_icinga_dependency (
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (188, NOW());
VALUES (189, NOW());

View File

@ -0,0 +1,18 @@
ALTER TABLE director_job ADD COLUMN ts_last_attempt_tmp bigint DEFAULT NULL;
ALTER TABLE director_job ADD COLUMN ts_last_error_tmp bigint DEFAULT NULL;
UPDATE director_job
SET ts_last_attempt_tmp = UNIX_TIMESTAMP(ts_last_attempt) * 1000,
ts_last_error_tmp = UNIX_TIMESTAMP(ts_last_error) * 1000;
ALTER TABLE director_job
DROP COLUMN ts_last_attempt,
DROP COLUMN ts_last_error;
ALTER TABLE director_job RENAME COLUMN ts_last_attempt_tmp TO ts_last_attempt;
ALTER TABLE director_job RENAME COLUMN ts_last_error_tmp TO ts_last_error;
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (189, NOW());

View File

@ -448,8 +448,8 @@ CREATE TABLE director_job (
run_interval integer NOT NULL, -- seconds
timeperiod_id integer DEFAULT NULL,
last_attempt_succeeded enum_boolean DEFAULT NULL,
ts_last_attempt timestamp with time zone DEFAULT NULL,
ts_last_error timestamp with time zone DEFAULT NULL,
ts_last_attempt bigint DEFAULT NULL,
ts_last_error bigint DEFAULT NULL,
last_error_message text NULL DEFAULT NULL,
CONSTRAINT director_job_period
FOREIGN KEY (timeperiod_id)
@ -2781,4 +2781,4 @@ CREATE INDEX branched_dependency_search_object_name ON branched_icinga_dependenc
INSERT INTO director_schema_migration
(schema_version, migration_time)
VALUES (187, NOW());
VALUES (189, NOW());