Skip to content

Commit 54c397d

Browse files
committed
Add AtomicOperationInterface.php
1 parent ea83943 commit 54c397d

File tree

11 files changed

+456
-25
lines changed

11 files changed

+456
-25
lines changed

.github/workflows/phpunit.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ jobs:
2424
services:
2525
memcached:
2626
image: memcached
27+
ports:
28+
- "11211:11211"
2729
redis:
2830
image: redis
31+
ports:
32+
- "6379:6379"
2933
options: >-
3034
--health-cmd "redis-cli ping"
3135
--health-interval 10s

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,17 @@ You can use a PSR-11 compatible to retrieve the cache keys.
5252

5353
See more [here](docs/psr11-usage.md)
5454

55+
## Beyond the PSR protocol
56+
57+
The PSR protocol is a good way to standardize the cache access,
58+
but sometimes you need to go beyond the protocol.
59+
60+
Some cache engines have additional features that are not covered by the PSR protocol.
61+
62+
Some examples are:
63+
- [Atomic Operations](docs/atomic-operations.md)
64+
- [Garbage Collection](docs/garbage-collection.md)
65+
5566
## Install
5667

5768
Just type:

docker-compose.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
services:
2+
memcached:
3+
image: memcached
4+
container_name: memcached
5+
ports:
6+
- "11211:11211"
7+
8+
redis:
9+
image: redis
10+
container_name: redis
11+
ports:
12+
- "6379:6379"
13+
healthcheck:
14+
test: ["CMD", "redis-cli", "ping"]
15+
interval: 10s
16+
timeout: 5s
17+
retries: 5

docs/atomic-operations.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Atomic Operations
2+
3+
Some cache engines allow you to do atomic operations such as incrementing or decrementing a value.
4+
5+
Besides this is not cache operation, it is a common operation in cache engines.
6+
7+
The advantage of using atomic operations is that you can avoid race conditions when multiple processes
8+
are trying to update the same value.
9+
10+
The atomic operations are:
11+
- Increment: Increment a value by a given number
12+
- Decrement: Decrement a value by a given number
13+
- Add: Add a value to a list in the cache
14+
15+
The engines that support atomic operations have to implement the `AtomicOperationInterface`.
16+
17+
Some engines that support atomic operations are:
18+
- RedisCachedEngine
19+
- MemcachedEngine
20+
- TmpfsCacheEngine
21+
- FileSystemCacheEngine
22+
23+
## Increment
24+
25+
The increment operation is used to increment a value by a given number.
26+
27+
```php
28+
<?php
29+
/** @var \ByJG\Cache\AtomicOperationInterface $cache */
30+
$cache->increment('my-key', 1);
31+
```
32+
33+
## Decrement
34+
35+
The decrement operation is used to decrement a value by a given number.
36+
37+
```php
38+
<?php
39+
/** @var \ByJG\Cache\AtomicOperationInterface $cache */
40+
$cache->decrement('my-key', 1);
41+
```
42+
43+
## Add
44+
45+
The add operation is used to add a value to a list in the cache.
46+
47+
```php
48+
<?php
49+
/** @var \ByJG\Cache\AtomicOperationInterface $cache */
50+
$cache->add('my-key', 'value1');
51+
$cache->add('my-key', 'value2');
52+
$cache->add('my-key', 'value3');
53+
54+
print_r($cache->get('my-key')); // ['value1', 'value2', 'value3']
55+
```
56+

docs/garbage-collection.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Garbage Collection
2+
3+
Some cache engines need to have a garbage collection process to remove the expired keys.
4+
5+
In some engines like `Memcached` and `Redis` the garbage collection is done automatically by the engine itself.
6+
7+
In other engines like `FileSystem` and `Array` there is no such process. The current implementation
8+
is based on the Best Effort. It means an expired key is removed only when you try to access it.
9+
10+
If the cache engine has a low hit rate, it is recommended to run a garbage collection process
11+
to avoid the cache to grow indefinitely.
12+
13+
The classes that implement the `GarbageCollectionInterface` have the method `collectGarbage()`.
14+
15+
Some engines that support garbage collection are:
16+
- FileSystemCacheEngine
17+
- ArrayCacheEngine
18+
- TmpfsCacheEngine
19+
20+
## Example
21+
22+
```php
23+
<?php
24+
/** @var \ByJG\Cache\GarbageCollectionInterface $cache */
25+
$cache->collectGarbage();
26+
```
27+
28+
Note: The garbage collection process is blocking.
29+
It means the process will be slow if you have a lot of keys to remove.
30+

src/AtomicOperationInterface.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace ByJG\Cache;
4+
5+
use DateInterval;
6+
7+
interface AtomicOperationInterface
8+
{
9+
public function increment(string $key, int $value = 1, DateInterval|int|null $ttl = null): int;
10+
11+
public function decrement(string $key, int $value = 1, DateInterval|int|null $ttl = null): int;
12+
13+
public function add(string $key, $value, DateInterval|int|null $ttl = null): array;
14+
}

src/Psr16/FileSystemCacheEngine.php

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@
22

33
namespace ByJG\Cache\Psr16;
44

5+
use ByJG\Cache\AtomicOperationInterface;
56
use ByJG\Cache\GarbageCollectorInterface;
7+
use Closure;
68
use DateInterval;
79
use Exception;
810
use Psr\Container\ContainerExceptionInterface;
911
use Psr\Container\NotFoundExceptionInterface;
1012
use Psr\Log\LoggerInterface;
1113
use Psr\Log\NullLogger;
1214

13-
class FileSystemCacheEngine extends BaseCacheEngine implements GarbageCollectorInterface
15+
class FileSystemCacheEngine extends BaseCacheEngine implements GarbageCollectorInterface, AtomicOperationInterface
1416
{
1517

1618
protected ?LoggerInterface $logger = null;
@@ -75,21 +77,10 @@ public function set(string $key, mixed $value, DateInterval|int|null $ttl = null
7577
$this->logger->info("[Filesystem cache] Set '$key' in FileSystem");
7678

7779
try {
78-
if (file_exists($fileKey)) {
79-
unlink($fileKey);
80-
}
81-
if (file_exists("$fileKey.ttl")) {
82-
unlink("$fileKey.ttl");
83-
}
84-
85-
if (is_null($value)) {
86-
return false;
87-
}
88-
8980
if (is_string($value) && (strlen($value) === 0)) {
9081
touch($fileKey);
9182
} else {
92-
$this->putContents($fileKey, $value, $this->addToNow($ttl));
83+
return $this->putContents($fileKey, $value, $this->addToNow($ttl));
9384
}
9485
} catch (Exception $ex) {
9586
$this->logger->warning("[Filesystem cache] I could not write to cache on file '" . basename($key) . "'. Switching to nocache=true mode.");
@@ -208,12 +199,34 @@ protected function getContents(string $fileKey, mixed $default = null): mixed
208199
return $content;
209200
}
210201

211-
protected function putContents(string $fileKey, mixed $value, ?string $ttl): void
202+
protected function putContents(string $fileKey, mixed $value, ?int $ttl, ?Closure $operation = null): mixed
212203
{
213-
$fo = fopen($fileKey, 'w');
204+
$returnValue = true;
205+
206+
if (file_exists("$fileKey.ttl")) {
207+
unlink("$fileKey.ttl");
208+
}
209+
210+
if (is_null($value)) {
211+
if (file_exists($fileKey)) {
212+
unlink($fileKey);
213+
}
214+
return false;
215+
}
216+
217+
$fo = fopen($fileKey, 'a+');
214218
$waitIfLocked = 1;
215219
$lock = flock($fo, LOCK_EX, $waitIfLocked);
216220
try {
221+
if (!is_null($operation)) {
222+
if (!file_exists($fileKey)) {
223+
$currentValue = 0;
224+
} else {
225+
$content = file_get_contents($fileKey);
226+
$currentValue = !empty($content) ? unserialize($content) : $content;
227+
}
228+
$value = $returnValue = $operation($currentValue, $value);
229+
}
217230
file_put_contents($fileKey, serialize($value));
218231
if (!is_null($ttl)) {
219232
file_put_contents("$fileKey.ttl", serialize($ttl));
@@ -222,6 +235,8 @@ protected function putContents(string $fileKey, mixed $value, ?string $ttl): voi
222235
flock($fo, LOCK_UN);
223236
fclose($fo);
224237
}
238+
239+
return $returnValue;
225240
}
226241

227242
public function collectGarbage()
@@ -250,4 +265,32 @@ public function getTtl(string $key): ?int
250265
}
251266
return null;
252267
}
268+
269+
public function increment(string $key, int $value = 1, DateInterval|int|null $ttl = null): int
270+
{
271+
return $this->putContents($this->fixKey($key), $value, $ttl, function ($currentValue, $value) {
272+
return intval($currentValue) + $value;
273+
});
274+
}
275+
276+
public function decrement(string $key, int $value = 1, DateInterval|int|null $ttl = null): int
277+
{
278+
return $this->putContents($this->fixKey($key), $value, $ttl, function ($currentValue, $value) {
279+
return intval($currentValue) - $value;
280+
});
281+
}
282+
283+
public function add(string $key, $value, DateInterval|int|null $ttl = null): array
284+
{
285+
return $this->putContents($this->fixKey($key), $value, $ttl, function ($currentValue, $value) {
286+
if (empty($currentValue)) {
287+
return [$value];
288+
}
289+
if (!is_array($currentValue)) {
290+
return [$currentValue, $value];
291+
}
292+
$currentValue[] = $value;
293+
return $currentValue;
294+
});
295+
}
253296
}

src/Psr16/MemcachedEngine.php

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace ByJG\Cache\Psr16;
44

5+
use ByJG\Cache\AtomicOperationInterface;
56
use ByJG\Cache\Exception\InvalidArgumentException;
67
use ByJG\Cache\Exception\StorageErrorException;
78
use DateInterval;
@@ -11,7 +12,7 @@
1112
use Psr\Log\LoggerInterface;
1213
use Psr\Log\NullLogger;
1314

14-
class MemcachedEngine extends BaseCacheEngine
15+
class MemcachedEngine extends BaseCacheEngine implements AtomicOperationInterface
1516
{
1617

1718
/**
@@ -88,7 +89,7 @@ public function get(string $key, mixed $default = null): mixed
8889
return $default;
8990
}
9091

91-
return unserialize($value);
92+
return $value;
9293
}
9394

9495
/**
@@ -107,7 +108,7 @@ public function set(string $key, mixed $value, DateInterval|int|null $ttl = null
107108

108109
$ttl = $this->convertToSeconds($ttl);
109110

110-
$this->memCached->set($this->fixKey($key), serialize($value), is_null($ttl) ? 0 : $ttl);
111+
$this->memCached->set($this->fixKey($key), $value, is_null($ttl) ? 0 : $ttl);
111112
$this->logger->info("[Memcached] Set '$key' result " . $this->memCached->getResultCode());
112113
if ($this->memCached->getResultCode() !== Memcached::RES_SUCCESS) {
113114
$this->logger->error("[Memcached] Set '$key' failed with status " . $this->memCached->getResultCode());
@@ -172,4 +173,73 @@ public function has(string $key): bool
172173
$this->memCached->get($this->fixKey($key));
173174
return ($this->memCached->getResultCode() === Memcached::RES_SUCCESS);
174175
}
176+
177+
public function increment(string $key, int $value = 1, DateInterval|int|null $ttl = null): int
178+
{
179+
$this->lazyLoadMemCachedServers();
180+
181+
$ttl = $this->convertToSeconds($ttl);
182+
183+
if ($this->memCached->get($this->fixKey($key)) === false) {
184+
$this->memCached->set($this->fixKey($key), 0, is_null($ttl) ? 0 : $ttl);
185+
}
186+
187+
$result = $this->memCached->increment($this->fixKey($key), $value);
188+
$this->logger->info("[Memcached] Increment '$key' result " . $this->memCached->getResultCode());
189+
if ($this->memCached->getResultCode() !== Memcached::RES_SUCCESS) {
190+
$this->logger->error("[Memcached] Set '$key' failed with status " . $this->memCached->getResultCode());
191+
}
192+
193+
return $result;
194+
}
195+
196+
public function decrement(string $key, int $value = 1, DateInterval|int|null $ttl = null): int
197+
{
198+
$this->lazyLoadMemCachedServers();
199+
200+
$ttl = $this->convertToSeconds($ttl);
201+
202+
if ($this->memCached->get($this->fixKey($key)) === false) {
203+
$this->memCached->set($this->fixKey($key), 0, is_null($ttl) ? 0 : $ttl);
204+
}
205+
206+
$result = $this->memCached->decrement($this->fixKey($key), $value);
207+
$this->logger->info("[Memcached] Decrement '$key' result " . $this->memCached->getResultCode());
208+
if ($this->memCached->getResultCode() !== Memcached::RES_SUCCESS) {
209+
$this->logger->error("[Memcached] Set '$key' failed with status " . $this->memCached->getResultCode());
210+
}
211+
212+
return $result;
213+
}
214+
215+
public function add(string $key, $value, DateInterval|int|null $ttl = null): array
216+
{
217+
$this->lazyLoadMemCachedServers();
218+
219+
$ttl = $this->convertToSeconds($ttl);
220+
$fixKey = $this->fixKey($key);
221+
222+
if ($this->memCached->get($fixKey) === false) {
223+
$this->memCached->set($fixKey, [], is_null($ttl) ? 0 : $ttl);
224+
}
225+
226+
do {
227+
$data = $this->memCached->get($fixKey, null, Memcached::GET_EXTENDED);
228+
$casToken = $data['cas'];
229+
$currentValue = $data['value'];
230+
231+
if ($currentValue === false) {
232+
$currentValue = [];
233+
}
234+
235+
if (!is_array($currentValue)) {
236+
$currentValue = [$currentValue];
237+
}
238+
239+
$currentValue[] = $value;
240+
$success = $this->memCached->cas($casToken, $fixKey, $currentValue, is_null($ttl) ? 0 : $ttl);
241+
} while (!$success);
242+
243+
return $currentValue;
244+
}
175245
}

0 commit comments

Comments
 (0)