From 96905b5e7726cd90c14bff686aaf8a45488611c1 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Tue, 1 Aug 2023 20:15:13 +0200 Subject: [PATCH 1/2] wip(time-schedule): schedule / time expressions --- lib/Resque/Time/Expression.php | 63 +++++++++++++ lib/Resque/Time/Schedule.php | 84 +++++++++++++++++ test/Resque/Tests/TimeScheduleTest.php | 119 +++++++++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 lib/Resque/Time/Expression.php create mode 100644 lib/Resque/Time/Schedule.php create mode 100644 test/Resque/Tests/TimeScheduleTest.php diff --git a/lib/Resque/Time/Expression.php b/lib/Resque/Time/Expression.php new file mode 100644 index 00000000..37ade89e --- /dev/null +++ b/lib/Resque/Time/Expression.php @@ -0,0 +1,63 @@ + + * @license http://www.opensource.org/licenses/mit-license.php + * + * @see Resque_Time_Schedule + */ +class Resque_Time_Expression +{ + public int $hour; + public int $minute; + + public function __construct(int $hour, int $minute = 0) + { + $this->hour = $hour; + $this->minute = $minute; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Format + + public function __toString(): string + { + return self::pad2($this->hour) . ':' . self::pad2($this->minute); + } + + public static function pad2(int $number): string + { + $strVal = strval($number); + if (strlen($strVal) === 1) + return "0{$strVal}"; + return $strVal; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Parse + + /** + * Tries to parse a time expression, e.g. "12:34" into a Resque_TimeExpression. + * + * @param string $input Time expression with hours and minutes. + * @return Resque_Time_Expression|null Parsed time expression, or NULL if parsing failed. + */ + public static function tryParse(string $input): ?Resque_Time_Expression + { + $parts = explode(':', $input); + + if (count($parts) < 2) + return null; + + $hours = intval($parts[0]); + $minutes = intval($parts[1]); + + if ($hours < 0 || $hours >= 24 || $minutes < 0 || $minutes >= 60) + return null; + + return new Resque_Time_Expression($hours, $minutes); + } +} \ No newline at end of file diff --git a/lib/Resque/Time/Schedule.php b/lib/Resque/Time/Schedule.php new file mode 100644 index 00000000..ca5049c4 --- /dev/null +++ b/lib/Resque/Time/Schedule.php @@ -0,0 +1,84 @@ + + * @license http://www.opensource.org/licenses/mit-license.php + */ +class Resque_Time_Schedule +{ + public Resque_Time_Expression $from; + public Resque_Time_Expression $until; + + public function __construct(Resque_Time_Expression $from, Resque_Time_Expression $until) + { + $this->from = $from; + $this->until = $until; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Schedule checking + + public function isInSchedule(DateTime $now): bool + { + $todayFrom = $this->getFromDateTime($now); + + if ($todayFrom > $now) { + // Outside of start range, check if we are in yesterday's range + $yesterdayFrom = (clone $todayFrom)->modify('-1 day'); + $yesterdayUntil = $this->getUntilDateTime($yesterdayFrom); + + return $now >= $yesterdayFrom && $now <= $yesterdayUntil; + } + + $todayUntil = $this->getUntilDateTime($todayFrom); + return $now <= $todayUntil; + } + + public function getFromDateTime(?DateTime $now = null): DateTime + { + if (!$now) + $now = new DateTime('now'); + + $dt = clone $now; + $dt->setTime($this->from->hour, $this->from->minute, 0, 0); + + return $dt; + } + + public function getUntilDateTime(?DateTime $fromDateTime = null): DateTime + { + if (!$fromDateTime) + $fromDateTime = new DateTime('now'); + + $dt = clone $fromDateTime; + $dt->setTime($this->until->hour, $this->until->minute, 59, 999999); + + if ($dt < $fromDateTime) + // Midnight rollover + $dt->modify('+1 day'); + + return $dt; + } + + // ----------------------------------------------------------------------------------------------------------------- + // Expression parsing + + public static function tryParse(string $input): ?Resque_Time_Schedule + { + $parts = explode('-', $input, 2); + + if (count($parts) !== 2) + return null; + + $from = Resque_Time_Expression::tryParse(trim($parts[0])); + $until = Resque_Time_Expression::tryParse(trim($parts[1])); + + if ($from === null || $until === null) + return null; + + return new Resque_Time_Schedule($from, $until); + } +} \ No newline at end of file diff --git a/test/Resque/Tests/TimeScheduleTest.php b/test/Resque/Tests/TimeScheduleTest.php new file mode 100644 index 00000000..b198693c --- /dev/null +++ b/test/Resque/Tests/TimeScheduleTest.php @@ -0,0 +1,119 @@ + + * @license http://www.opensource.org/licenses/mit-license.php + */ +class Resque_Tests_TimeScheduleTest extends Resque_Tests_TestCase +{ + // ----------------------------------------------------------------------------------------------------------------- + // Resque_Time_Schedule - logic + + public function testScheduleCheck() + { + // Test expression: 10pm-6am + $schedule = new Resque_Time_Schedule( + new Resque_Time_Expression(22, 0), + new Resque_Time_Expression(6, 0), + ); + + $this->assertTrue( + $schedule->isInSchedule(new DateTime('2023-08-01 06:00:00')), + "Schedule: 6am should be accepted for a 10pm-6am schedule" + ); + $this->assertTrue( + $schedule->isInSchedule(new DateTime('2023-08-01 00:00:00')), + "Schedule: 12am should be accepted for a 10pm-6am schedule" + ); + $this->assertTrue( + $schedule->isInSchedule(new DateTime('2023-08-01 22:00:00')), + "Schedule: 10pm should be accepted for a 10pm-6am schedule" + ); + + $this->assertFalse( + $schedule->isInSchedule(new DateTime('2023-08-01 06:01:00')), + "Schedule: 6:01am should be rejected for a 10pm-6am schedule" + ); + $this->assertFalse( + $schedule->isInSchedule(new DateTime('2023-08-01 12:00:00')), + "Schedule: 12pm should be rejected for a 10pm-6am schedule" + ); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Resque_Time_Expression - expression/format + + public function testParseTimeExpression() + { + $this->assertEquals( + new Resque_Time_Expression(12, 34), + Resque_Time_Expression::tryParse("12:34"), + "Valid time expression - tryParse should return parsed result" + ); + $this->assertEquals( + new Resque_Time_Expression(12, 34), + Resque_Time_Expression::tryParse("12:34:56"), + "Valid time expression, with seconds - tryParse should return parsed result discarding seconds" + ); + + $this->assertNull( + Resque_Time_Expression::tryParse("not_valid"), + "Invalid time expression string, bad format - tryParse should return null" + ); + $this->assertNull( + Resque_Time_Expression::tryParse("-1:99"), + "Invalid time expression, impossible values - tryParse should return null" + ); + } + + public function testFormatTimeExpression() + { + $this->assertSame( + "20:19", + (string)(new Resque_Time_Expression(20, 19)) + ); + } + + // ----------------------------------------------------------------------------------------------------------------- + // Resque_Time_Schedule - expression/format + + public function testParseScheduleExpression() + { + $this->assertEquals( + new Resque_Time_Schedule( + new Resque_Time_Expression(22, 0), + new Resque_Time_Expression(6, 0), + ), + Resque_Time_Schedule::tryParse("22:00-06:00"), + "Valid schedule expression - tryParse should return parsed result" + ); + $this->assertEquals( + new Resque_Time_Schedule( + new Resque_Time_Expression(22, 12), + new Resque_Time_Expression(6, 34), + ), + Resque_Time_Schedule::tryParse(" 22:12 - 06:34 "), + "Valid schedule expression - tryParse should return parsed result, trimming excess spaces" + ); + $this->assertEquals( + new Resque_Time_Schedule( + new Resque_Time_Expression(22, 34), + new Resque_Time_Expression(6, 56), + ), + Resque_Time_Schedule::tryParse("22:34:12.999999 - 06:56:12.999999"), + "Valid schedule expression - tryParse should return parsed result, discarding seconds" + ); + + $this->assertNull( + Resque_Time_Schedule::tryParse("not valid"), + "Invalid schedule expression - tryParse should return null" + ); + $this->assertNull( + Resque_Time_Schedule::tryParse("456 - 123"), + "Invalid schedule expression - tryParse should return null" + ); + } +} \ No newline at end of file From b6869f1d7cee8d1a7ce0e3161315c3ee002f94a8 Mon Sep 17 00:00:00 2001 From: Roy de Jong Date: Tue, 1 Aug 2023 20:56:54 +0200 Subject: [PATCH 2/2] wip(time-schedule): worker schedule logic --- README.md | 19 ++++++++------- bin/resque | 12 ++++++++++ lib/Resque/Time/Schedule.php | 13 +++++++++- lib/Resque/Worker.php | 33 ++++++++++++++++++++++++++ test/Resque/Tests/TimeScheduleTest.php | 11 +++++++++ 5 files changed, 78 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 150d7f4a..91200946 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **php-resque is a Redis-based library for enqueuing and running background jobs.** -This is a lightly maintained fork of **chrisboulton/php-resque**, compatible with PHP 8.2+. +This is a lightly maintained fork of **chrisboulton/php-resque**, with fixes and improvements, compatible with PHP 8.2+. ⚠️ Not recommended for new projects. We are only maintaining this for legacy projects. @@ -74,14 +74,15 @@ QUEUE=default php vendor/bin/resque You can set the following environment variables on the worker: -| Name | Description | -|---------------|-------------------------------------------------------------------------------------------------------------------------| -| `QUEUE` | Required. Defines one or more comma-separated queues to process tasks from. Set to `*` to process from any queue. | -| `APP_INCLUDE` | Optional. Defines a bootstrap script to run before starting the worker. | -| `PREFIX` | Optional. Prefix to use in Redis. | -| `COUNT` | Optional. Amount of worker forks to start. If set to > 1, the process will start the workers and then exit immediately. | -| `VERBOSE` | Optional. Forces verbose log output. | -| `VVERBOSE` | Optional. Forces detailed verbose log output. | +| Name | Description | +|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `QUEUE` | Required. Defines one or more comma-separated queues to process tasks from. Set to `*` to process from any queue. | +| `APP_INCLUDE` | Optional. Defines a bootstrap script to run before starting the worker. | +| `PREFIX` | Optional. Prefix to use in Redis. | +| `COUNT` | Optional. Amount of worker forks to start. If set to > 1, the process will start the workers and then exit immediately. | +| `SCHEDULE` | Optional. An expression with a from/until time, e.g. `22:00-06:00` to only run tasks between 10pm and 6am. The worker is paused outside of the schedule. Relative to default timezone (`date_default_timezone_set`). | +| `VERBOSE` | Optional. Forces verbose log output. | +| `VVERBOSE` | Optional. Forces detailed verbose log output. | ### Events diff --git a/bin/resque b/bin/resque index 1d604851..4f3d8476 100755 --- a/bin/resque +++ b/bin/resque @@ -93,6 +93,15 @@ if(!empty($PREFIX)) { Resque_Redis::prefix($PREFIX); } +$SCHEDULE = getenv('SCHEDULE'); +$scheduleParsed = null; +if (!empty($SCHEDULE)) { + $scheduleParsed = Resque_Time_Schedule::tryParse($SCHEDULE); + if (!$scheduleParsed) { + die('SCHEDULE ('.$SCHEDULE.") expression invalid (parse error).\n"); + } +} + if($count > 1) { for($i = 0; $i < $count; ++$i) { $pid = Resque::fork(); @@ -117,6 +126,9 @@ else { $worker = new Resque_Worker($queues); $worker->setLogger($logger); + if ($scheduleParsed) + $worker->setSchedule($scheduleParsed); + $PIDFILE = getenv('PIDFILE'); if ($PIDFILE) { file_put_contents($PIDFILE, getmypid()) or diff --git a/lib/Resque/Time/Schedule.php b/lib/Resque/Time/Schedule.php index ca5049c4..63f71da5 100644 --- a/lib/Resque/Time/Schedule.php +++ b/lib/Resque/Time/Schedule.php @@ -21,8 +21,11 @@ public function __construct(Resque_Time_Expression $from, Resque_Time_Expression // ----------------------------------------------------------------------------------------------------------------- // Schedule checking - public function isInSchedule(DateTime $now): bool + public function isInSchedule(?DateTime $now = null): bool { + if (!$now) + $now = new DateTime('now'); + $todayFrom = $this->getFromDateTime($now); if ($todayFrom > $now) { @@ -81,4 +84,12 @@ public static function tryParse(string $input): ?Resque_Time_Schedule return new Resque_Time_Schedule($from, $until); } + + // ----------------------------------------------------------------------------------------------------------------- + // Schedule formatting + + public function __toString(): string + { + return "{$this->from} - {$this->until}"; + } } \ No newline at end of file diff --git a/lib/Resque/Worker.php b/lib/Resque/Worker.php index fb092c64..2cebf458 100644 --- a/lib/Resque/Worker.php +++ b/lib/Resque/Worker.php @@ -51,6 +51,12 @@ class Resque_Worker */ private $child = null; + /** + * The schedule constraints for this worker. + * If a schedule is set, the worker will not perform any tasks outside of it. + */ + private ?Resque_Time_Schedule $schedule = null; + /** * Instantiate a new worker, given a list of queues that it should be working * on. The list of queues should be supplied in the priority that they should @@ -192,6 +198,28 @@ public function work($interval = Resque::DEFAULT_INTERVAL, $blocking = false) break; } + // Check schedule constraints + if ($this->schedule) { + $didScheduleDelay = false; + while (!$this->schedule->isInSchedule()) { + if (!$didScheduleDelay) { + // Announce schedule pause + $this->logger->log(Psr\Log\LogLevel::NOTICE, "Pausing, outside schedule constraint ({$this->schedule})"); + $this->updateProcLine("Paused for " . implode(',', $this->queues) . " with schedule constraint {$this->schedule}"); + $didScheduleDelay = true; + } + if ($this->shutdown) { + // Interrupted, immediate shutdown + $this->shutdownNow(); + return; + } + usleep(10000000); // 10s + } + if ($didScheduleDelay) { + $this->logger->log(Psr\Log\LogLevel::NOTICE, "Resuming, now within schedule constraint ({$this->schedule})"); + } + } + // Attempt to find and reserve a job $job = false; if(!$this->paused) { @@ -601,4 +629,9 @@ public function setLogger(Psr\Log\LoggerInterface $logger) { $this->logger = $logger; } + + public function setSchedule(?Resque_Time_Schedule $schedule): void + { + $this->schedule = $schedule; + } } diff --git a/test/Resque/Tests/TimeScheduleTest.php b/test/Resque/Tests/TimeScheduleTest.php index b198693c..9ba94396 100644 --- a/test/Resque/Tests/TimeScheduleTest.php +++ b/test/Resque/Tests/TimeScheduleTest.php @@ -116,4 +116,15 @@ public function testParseScheduleExpression() "Invalid schedule expression - tryParse should return null" ); } + + public function testFormatScheduleExpression() + { + $this->assertSame( + "22:34 - 06:56", + (string)new Resque_Time_Schedule( + new Resque_Time_Expression(22, 34), + new Resque_Time_Expression(6, 56), + ) + ); + } } \ No newline at end of file