From b0d984b35c394a573d615baa7543a87db49149ef Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Mon, 8 May 2023 18:01:55 -0400 Subject: [PATCH] [Scheduler] add `JitterTrigger` Co-authored-by: Uladzimir Tsykun --- .../Component/Scheduler/RecurringMessage.php | 6 +++ .../Tests/Trigger/JitterTriggerTest.php | 42 +++++++++++++++++++ .../Scheduler/Trigger/JitterTrigger.php | 39 +++++++++++++++++ 3 files changed, 87 insertions(+) create mode 100644 src/Symfony/Component/Scheduler/Tests/Trigger/JitterTriggerTest.php create mode 100644 src/Symfony/Component/Scheduler/Trigger/JitterTrigger.php diff --git a/src/Symfony/Component/Scheduler/RecurringMessage.php b/src/Symfony/Component/Scheduler/RecurringMessage.php index 4e89adc8c783a..3553e96119bf8 100644 --- a/src/Symfony/Component/Scheduler/RecurringMessage.php +++ b/src/Symfony/Component/Scheduler/RecurringMessage.php @@ -14,6 +14,7 @@ use Symfony\Component\Scheduler\Exception\InvalidArgumentException; use Symfony\Component\Scheduler\Trigger\CronExpressionTrigger; use Symfony\Component\Scheduler\Trigger\DateIntervalTrigger; +use Symfony\Component\Scheduler\Trigger\JitterTrigger; use Symfony\Component\Scheduler\Trigger\TriggerInterface; /** @@ -59,6 +60,11 @@ public static function trigger(TriggerInterface $trigger, object $message): self return new self($trigger, $message); } + public function withJitter(int $maxSeconds = 60): self + { + return new self(new JitterTrigger($this->trigger, $maxSeconds), $this->message); + } + public function getMessage(): object { return $this->message; diff --git a/src/Symfony/Component/Scheduler/Tests/Trigger/JitterTriggerTest.php b/src/Symfony/Component/Scheduler/Tests/Trigger/JitterTriggerTest.php new file mode 100644 index 0000000000000..52dd7d593959a --- /dev/null +++ b/src/Symfony/Component/Scheduler/Tests/Trigger/JitterTriggerTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Scheduler\Tests\Trigger; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Scheduler\Trigger\JitterTrigger; +use Symfony\Component\Scheduler\Trigger\TriggerInterface; + +class JitterTriggerTest extends TestCase +{ + public function testCanAddJitter() + { + $time = new \DateTimeImmutable(); + $inner = $this->createMock(TriggerInterface::class); + $inner->method('getNextRunDate')->willReturn($time); + + $trigger = new JitterTrigger($inner); + + $values = array_map( + fn () => (int) $trigger->getNextRunDate($time)?->getTimestamp(), + array_fill(0, 100, null) + ); + + foreach ($values as $value) { + $this->assertGreaterThanOrEqual($time->getTimestamp(), $value); + $this->assertLessThanOrEqual($time->getTimestamp() + 60, $value); + } + + $values = array_unique($values); + + $this->assertGreaterThan(1, \count($values)); + } +} diff --git a/src/Symfony/Component/Scheduler/Trigger/JitterTrigger.php b/src/Symfony/Component/Scheduler/Trigger/JitterTrigger.php new file mode 100644 index 0000000000000..639c490c12070 --- /dev/null +++ b/src/Symfony/Component/Scheduler/Trigger/JitterTrigger.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Scheduler\Trigger; + +/** + * @author Kevin Bond + */ +final class JitterTrigger implements TriggerInterface +{ + /** + * @param positive-int $maxSeconds + */ + public function __construct(private readonly TriggerInterface $trigger, private readonly int $maxSeconds = 60) + { + } + + public function __toString(): string + { + return sprintf('%s with 0-%d second jitter', $this->trigger, $this->maxSeconds); + } + + public function getNextRunDate(\DateTimeImmutable $run): ?\DateTimeImmutable + { + if (!$nextRun = $this->trigger->getNextRunDate($run)) { + return null; + } + + return $nextRun->add(new \DateInterval(sprintf('PT%sS', random_int(0, $this->maxSeconds)))); + } +}