Skip to content

Commit 6fbaf2f

Browse files
authored
Merge pull request #58 from patchlevel/psr-clock
Use a PSR-20 clock for time measurement
2 parents b98385f + 80495ae commit 6fbaf2f

10 files changed

Lines changed: 199 additions & 97 deletions

‎composer.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
],
2121
"require": {
2222
"php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
23+
"psr/clock": "^1.0",
2324
"psr/log": "^1.1 || ^2.0 || ^3.0",
2425
"symfony/event-dispatcher": "^5.4.26 || ^6.4.0 || ^7.0.0 || ^8.0.0"
2526
},

‎composer.lock‎

Lines changed: 49 additions & 49 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎docs/getting-started.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,24 @@ Iteration details use the `debug` level; stop reasons (limit exceeded, signal re
8585

8686
With the `ConsoleLogger` from the [Symfony command example](integration.md#symfony), run the command with `-v`
8787
to see stop reasons or `-vvv` to see everything.
88+
89+
## Clock
90+
91+
The worker uses a [PSR-20](https://www.php-fig.org/psr/psr-20/) clock to measure the job's run time
92+
and to check the `timeLimit`. By default it uses the system clock. You can pass your own clock to `create`,
93+
e.g. a mock clock to test time-dependent behaviour without actually waiting:
94+
95+
```php
96+
use Patchlevel\Worker\DefaultWorker;
97+
use Symfony\Component\Clock\MockClock;
98+
99+
$worker = DefaultWorker::create(
100+
$job,
101+
['timeLimit' => 3600],
102+
$logger,
103+
clock: new MockClock(),
104+
);
105+
```
106+
:::note
107+
The clock is only used for measuring time. The sleep between iterations still waits for real.
108+
:::

‎src/DefaultWorker.php‎

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,29 @@
1212
use Patchlevel\Worker\Listener\StopWorkerOnMemoryLimitListener;
1313
use Patchlevel\Worker\Listener\StopWorkerOnSignalListener;
1414
use Patchlevel\Worker\Listener\StopWorkerOnTimeLimitListener;
15+
use Psr\Clock\ClockInterface;
1516
use Psr\Log\LoggerInterface;
1617
use Psr\Log\NullLogger;
1718
use Symfony\Component\EventDispatcher\EventDispatcher;
1819
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
1920

2021
use function max;
21-
use function microtime;
22-
use function round;
2322
use function usleep;
2423

2524
final class DefaultWorker implements Worker
2625
{
2726
private bool $shouldStop = false;
2827

29-
/** @var Closure():int */
30-
private Closure $timeMeasure;
28+
private readonly ClockInterface $clock;
3129

3230
/** @param Closure(Closure):void $job */
3331
public function __construct(
3432
private readonly Closure $job,
3533
private readonly EventDispatcherInterface $eventDispatcher,
3634
private readonly LoggerInterface|null $logger = null,
35+
ClockInterface|null $clock = null,
3736
) {
38-
$this->timeMeasure = static fn () => (int)round(microtime(true) * 1000);
37+
$this->clock = $clock ?? new SystemClock();
3938
}
4039

4140
/** @param positive-int|0 $sleepTimer in milliseconds */
@@ -48,11 +47,11 @@ public function run(int $sleepTimer = 1000): void
4847
while (!$this->shouldStop) {
4948
$this->logger?->debug('Worker starting job run');
5049

51-
$startTime = ($this->timeMeasure)();
50+
$startTime = $this->milliseconds();
5251

5352
($this->job)($this->stop(...));
5453

55-
$endTime = ($this->timeMeasure)();
54+
$endTime = $this->milliseconds();
5655
$ranTime = $endTime - $startTime;
5756

5857
$this->logger?->debug('Worker finished job run ({ranTime}ms)', ['ranTime' => $ranTime]);
@@ -97,6 +96,7 @@ public static function create(
9796
array $options = [],
9897
LoggerInterface $logger = new NullLogger(),
9998
EventDispatcherInterface|null $eventDispatcher = null,
99+
ClockInterface|null $clock = null,
100100
): self {
101101
if ($eventDispatcher === null) {
102102
$eventDispatcher = new EventDispatcher();
@@ -118,14 +118,20 @@ public static function create(
118118

119119
if (isset($options['timeLimit'])) {
120120
$eventDispatcher->addSubscriber(
121-
new StopWorkerOnTimeLimitListener($options['timeLimit'], $logger),
121+
new StopWorkerOnTimeLimitListener($options['timeLimit'], $logger, $clock),
122122
);
123123
}
124124

125125
return new self(
126126
$job,
127127
$eventDispatcher,
128128
$logger,
129+
$clock,
129130
);
130131
}
132+
133+
private function milliseconds(): int
134+
{
135+
return (int)$this->clock->now()->format('Uv');
136+
}
131137
}

‎src/Listener/StopWorkerOnTimeLimitListener.php‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,38 @@
44

55
namespace Patchlevel\Worker\Listener;
66

7+
use DateInterval;
8+
use DateTimeImmutable;
79
use Patchlevel\Worker\Event\WorkerRunningEvent;
810
use Patchlevel\Worker\Event\WorkerStartedEvent;
11+
use Patchlevel\Worker\SystemClock;
12+
use Psr\Clock\ClockInterface;
913
use Psr\Log\LoggerInterface;
1014
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
1115

12-
use function time;
13-
1416
final class StopWorkerOnTimeLimitListener implements EventSubscriberInterface
1517
{
16-
private float $endTime = 0;
18+
private DateTimeImmutable|null $endTime = null;
19+
20+
private readonly ClockInterface $clock;
1721

1822
/** @param positive-int $timeLimit in seconds */
1923
public function __construct(
2024
private readonly int $timeLimit,
2125
private readonly LoggerInterface|null $logger = null,
26+
ClockInterface|null $clock = null,
2227
) {
28+
$this->clock = $clock ?? new SystemClock();
2329
}
2430

2531
public function onWorkerStarted(): void
2632
{
27-
$this->endTime = time() + $this->timeLimit;
33+
$this->endTime = $this->clock->now()->add(new DateInterval('PT' . $this->timeLimit . 'S'));
2834
}
2935

3036
public function onWorkerRunning(WorkerRunningEvent $event): void
3137
{
32-
if ($this->endTime >= time()) {
38+
if ($this->endTime !== null && $this->clock->now() < $this->endTime) {
3339
return;
3440
}
3541

‎src/SystemClock.php‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Worker;
6+
7+
use DateTimeImmutable;
8+
use Psr\Clock\ClockInterface;
9+
10+
final class SystemClock implements ClockInterface
11+
{
12+
public function now(): DateTimeImmutable
13+
{
14+
return new DateTimeImmutable();
15+
}
16+
}

‎tests/TestClock.php‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Patchlevel\Worker\Tests;
6+
7+
use DateTimeImmutable;
8+
use Psr\Clock\ClockInterface;
9+
10+
use function intdiv;
11+
use function sprintf;
12+
13+
final class TestClock implements ClockInterface
14+
{
15+
private int $milliseconds = 1_767_225_600_000;
16+
17+
/** @param int $tick milliseconds the clock moves forward on every now() call */
18+
public function __construct(
19+
private readonly int $tick = 0,
20+
) {
21+
}
22+
23+
public function now(): DateTimeImmutable
24+
{
25+
$now = new DateTimeImmutable(sprintf('@%d.%03d', intdiv($this->milliseconds, 1000), $this->milliseconds % 1000));
26+
27+
$this->milliseconds += $this->tick;
28+
29+
return $now;
30+
}
31+
32+
public function advance(int $milliseconds): void
33+
{
34+
$this->milliseconds += $milliseconds;
35+
}
36+
}

0 commit comments

Comments
 (0)