Skip to content

Commit 86c86ba

Browse files
committed
feat(clock): add support for psr clock
1 parent f90f8d7 commit 86c86ba

5 files changed

Lines changed: 73 additions & 27 deletions

File tree

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
],
1919
"require": {
2020
"php": "^8.1",
21-
"psr/http-message": "^1.0|^2.0"
21+
"psr/http-message": "^1.0|^2.0",
22+
"psr/clock": "^1.0"
2223
},
2324
"suggest": {
2425
"guzzlehttp/guzzle": "Allows the usage of the Guzzle Middleware",

src/Ganesha/NativeClock.php

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

src/Ganesha/Strategy/Count.php

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
use Ackintosh\Ganesha;
66
use Ackintosh\Ganesha\Configuration;
77
use Ackintosh\Ganesha\Exception\StorageException;
8+
use Ackintosh\Ganesha\NativeClock;
89
use Ackintosh\Ganesha\Storage;
910
use Ackintosh\Ganesha\StrategyInterface;
11+
use Psr\Clock\ClockInterface;
1012

1113
class Count implements StrategyInterface
1214
{
@@ -20,27 +22,37 @@ class Count implements StrategyInterface
2022
*/
2123
private $storage;
2224

23-
private function __construct(Configuration $configuration, Storage $storage)
24-
{
25+
private ClockInterface $clock;
26+
27+
private function __construct(
28+
Configuration $configuration,
29+
Storage $storage,
30+
ClockInterface $clock,
31+
) {
2532
$this->configuration = $configuration;
2633
$this->storage = $storage;
34+
$this->clock = $clock;
2735
}
2836

29-
public static function create(Storage\AdapterInterface $adapter, Configuration $configuration): StrategyInterface
30-
{
37+
public static function create(
38+
Storage\AdapterInterface $adapter,
39+
Configuration $configuration,
40+
?ClockInterface $clock = null,
41+
): StrategyInterface {
3142
return new self(
3243
$configuration,
3344
new Storage(
3445
$adapter,
3546
$configuration->storageKeys(),
3647
null
37-
)
48+
),
49+
$clock ?? new NativeClock(),
3850
);
3951
}
4052

4153
public function recordFailure(string $service): int
4254
{
43-
$this->storage->setLastFailureTime($service, time());
55+
$this->storage->setLastFailureTime($service, $this->clock->now()->getTimestamp());
4456
$this->storage->incrementFailureCount($service);
4557

4658
if ($this->storage->getFailureCount($service) >= $this->configuration->failureCountThreshold()
@@ -95,9 +107,11 @@ private function isHalfOpen(string $service): bool
95107
return false;
96108
}
97109

98-
if ((time() - $lastFailureTime) > $this->configuration->intervalToHalfOpen()) {
110+
$time = $this->clock->now()->getTimestamp();
111+
112+
if (($time - $lastFailureTime) > $this->configuration->intervalToHalfOpen()) {
99113
$this->storage->setFailureCount($service, $this->configuration->failureCountThreshold());
100-
$this->storage->setLastFailureTime($service, time());
114+
$this->storage->setLastFailureTime($service, $time);
101115
return true;
102116
}
103117

src/Ganesha/Strategy/Rate.php

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
use Ackintosh\Ganesha;
66
use Ackintosh\Ganesha\Configuration;
77
use Ackintosh\Ganesha\Exception\StorageException;
8+
use Ackintosh\Ganesha\NativeClock;
89
use Ackintosh\Ganesha\Storage;
910
use Ackintosh\Ganesha\StrategyInterface;
1011
use InvalidArgumentException;
1112
use LogicException;
13+
use Psr\Clock\ClockInterface;
1214

1315
class Rate implements StrategyInterface
1416
{
@@ -22,6 +24,8 @@ class Rate implements StrategyInterface
2224
*/
2325
private $storage;
2426

27+
private ClockInterface $clock;
28+
2529
/**
2630
* @var array
2731
*/
@@ -36,10 +40,14 @@ class Rate implements StrategyInterface
3640
/**
3741
* @param Configuration $configuration
3842
*/
39-
private function __construct(Configuration $configuration, Storage $storage)
40-
{
43+
private function __construct(
44+
Configuration $configuration,
45+
Storage $storage,
46+
ClockInterface $clock,
47+
) {
4148
$this->configuration = $configuration;
4249
$this->storage = $storage;
50+
$this->clock = $clock;
4351
}
4452

4553
/**
@@ -58,23 +66,28 @@ public static function validate(array $params): void
5866
}
5967
}
6068

61-
public static function create(Storage\AdapterInterface $adapter, Configuration $configuration): StrategyInterface
62-
{
63-
$serviceNameDecorator = $adapter instanceof Storage\Adapter\TumblingTimeWindowInterface ? self::serviceNameDecorator($configuration->timeWindow()) : null;
69+
public static function create(
70+
Storage\AdapterInterface $adapter,
71+
Configuration $configuration,
72+
?ClockInterface $clock = null
73+
): StrategyInterface {
74+
$clock = $clock ?? new NativeClock();
75+
$serviceNameDecorator = $adapter instanceof Storage\Adapter\TumblingTimeWindowInterface ? self::serviceNameDecorator($configuration->timeWindow(), $clock) : null;
6476

6577
return new self(
6678
$configuration,
6779
new Storage(
6880
$adapter,
6981
$configuration->storageKeys(),
7082
$serviceNameDecorator
71-
)
83+
),
84+
$clock,
7285
);
7386
}
7487

7588
public function recordFailure(string $service): int
7689
{
77-
$this->storage->setLastFailureTime($service, time());
90+
$this->storage->setLastFailureTime($service, $this->clock->now()->getTimestamp());
7891
$this->storage->incrementFailureCount($service);
7992
if (
8093
$this->storage->getStatus($service) === Ganesha::STATUS_CALMED_DOWN
@@ -158,16 +171,16 @@ private function isClosedInCurrentTimeWindow(string $service): bool
158171

159172
private function isClosedInPreviousTimeWindow(string $service): bool
160173
{
161-
$failure = $this->storage->getFailureCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow()));
174+
$failure = $this->storage->getFailureCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow(), $this->clock));
162175
if (
163176
$failure === 0
164177
|| ($failure / $this->configuration->minimumRequests()) * 100 < $this->configuration->failureRateThreshold()
165178
) {
166179
return true;
167180
}
168181

169-
$success = $this->storage->getSuccessCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow()));
170-
$rejection = $this->storage->getRejectionCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow()));
182+
$success = $this->storage->getSuccessCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow(), $this->clock));
183+
$rejection = $this->storage->getRejectionCountByCustomKey(self::keyForPreviousTimeWindow($service, $this->configuration->timeWindow(), $this->clock));
171184

172185
return $this->isClosedInTimeWindow($failure, $success, $rejection);
173186
}
@@ -190,32 +203,36 @@ private function isClosedInTimeWindow(int $failure, int $success, int $rejection
190203
*/
191204
private function isHalfOpen(string $service): bool
192205
{
206+
$time = $this->clock->now()->getTimestamp();
207+
193208
if (is_null($lastFailureTime = $this->storage->getLastFailureTime($service))) {
194209
return false;
195210
}
196211

197-
if ((time() - $lastFailureTime) > $this->configuration->intervalToHalfOpen()) {
198-
$this->storage->setLastFailureTime($service, time());
212+
if (($time - $lastFailureTime) > $this->configuration->intervalToHalfOpen()) {
213+
$this->storage->setLastFailureTime($service, $time);
199214
return true;
200215
}
201216

202217
return false;
203218
}
204219

205-
private static function serviceNameDecorator(int $timeWindow, $current = true)
220+
private static function serviceNameDecorator(int $timeWindow, ClockInterface $clock, bool $current = true): \Closure
206221
{
207-
return function ($service) use ($timeWindow, $current) {
222+
return function ($service) use ($timeWindow, $clock, $current) {
223+
$time = $clock->now()->getTimestamp();
224+
208225
return sprintf(
209226
'%s.%d',
210227
$service,
211-
$current ? (int)floor(time() / $timeWindow) : (int)floor((time() - $timeWindow) / $timeWindow)
228+
$current ? (int)floor($time / $timeWindow) : (int)floor(($time - $timeWindow) / $timeWindow)
212229
);
213230
};
214231
}
215232

216-
private static function keyForPreviousTimeWindow(string $service, int $timeWindow)
233+
private static function keyForPreviousTimeWindow(string $service, int $timeWindow, ClockInterface $clock): string
217234
{
218-
$f = self::serviceNameDecorator($timeWindow, false);
235+
$f = self::serviceNameDecorator($timeWindow, $clock, false);
219236
return $f($service);
220237
}
221238
}

tests/Ackintosh/Ganesha/Storage/Adapter/MemcachedTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -424,7 +424,7 @@ public function outdatedCountsShouldBeEvictedInCaseOfRateStrategy(): void
424424

425425
$reflection = new \ReflectionMethod(Ganesha\Strategy\Rate::class, 'serviceNameDecorator');
426426
$reflection->setAccessible(true);
427-
$serviceNameDecorator = $reflection->invokeArgs(null, [$timeWindow]);
427+
$serviceNameDecorator = $reflection->invokeArgs(null, [$timeWindow, new Ganesha\NativeClock()]);
428428
$storageKeys = new Ganesha\Storage\StorageKeys();
429429

430430
$successKeyForTheTumblingTimeWindow = $storageKeys->prefix() . $serviceNameDecorator($serviceName) . $storageKeys->success();

0 commit comments

Comments
 (0)