Skip to content

Commit 7a87643

Browse files
authored
Merge branch '4.8' into feat/lazy-cache-ttl
2 parents 250df35 + 3f91249 commit 7a87643

15 files changed

Lines changed: 400 additions & 11 deletions

File tree

system/Cache/Exceptions/CacheException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,14 @@ public static function forHandlerNotFound()
5959
{
6060
return new static(lang('Cache.handlerNotFound'));
6161
}
62+
63+
/**
64+
* Thrown when the handler cannot provide a lock store.
65+
*
66+
* @return static
67+
*/
68+
public static function forUnsupportedLockStore()
69+
{
70+
return new static(lang('Cache.unsupportedLockStore'));
71+
}
6272
}

system/Cache/Handlers/MemcachedHandler.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313

1414
namespace CodeIgniter\Cache\Handlers;
1515

16+
use CodeIgniter\Cache\Exceptions\CacheException;
17+
use CodeIgniter\Cache\LockStoreInterface;
18+
use CodeIgniter\Cache\LockStoreProviderInterface;
19+
use CodeIgniter\Cache\LockStores\MemcachedLockStore;
1620
use CodeIgniter\Exceptions\BadMethodCallException;
1721
use CodeIgniter\Exceptions\CriticalError;
1822
use CodeIgniter\I18n\Time;
@@ -26,7 +30,7 @@
2630
*
2731
* @see \CodeIgniter\Cache\Handlers\MemcachedHandlerTest
2832
*/
29-
class MemcachedHandler extends BaseHandler
33+
class MemcachedHandler extends BaseHandler implements LockStoreProviderInterface
3034
{
3135
/**
3236
* The memcached object
@@ -35,6 +39,8 @@ class MemcachedHandler extends BaseHandler
3539
*/
3640
protected $memcached;
3741

42+
private ?LockStoreInterface $lockStore = null;
43+
3844
/**
3945
* Memcached Configuration
4046
*
@@ -62,6 +68,7 @@ public function initialize(): void
6268
try {
6369
if (class_exists(Memcached::class)) {
6470
$this->memcached = new Memcached();
71+
$this->lockStore = null;
6572

6673
if ($this->config['raw']) {
6774
$this->memcached->setOption(Memcached::OPT_BINARY_PROTOCOL, true);
@@ -82,6 +89,7 @@ public function initialize(): void
8289
}
8390
} elseif (class_exists(Memcache::class)) {
8491
$this->memcached = new Memcache();
92+
$this->lockStore = null;
8593

8694
if (! $this->memcached->connect($this->config['host'], $this->config['port'])) {
8795
throw new CriticalError('Cache: Memcache connection failed.');
@@ -219,6 +227,15 @@ public function isSupported(): bool
219227
return extension_loaded('memcached') || extension_loaded('memcache');
220228
}
221229

230+
public function lockStore(): LockStoreInterface
231+
{
232+
if (! $this->memcached instanceof Memcached) {
233+
throw CacheException::forUnsupportedLockStore();
234+
}
235+
236+
return $this->lockStore ??= new MemcachedLockStore($this->memcached, $this->prefix);
237+
}
238+
222239
public function ping(): bool
223240
{
224241
$version = $this->memcached->getVersion();
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Cache\LockStores;
15+
16+
use CodeIgniter\Cache\Handlers\MemcachedHandler;
17+
use CodeIgniter\Cache\LockStoreInterface;
18+
use Memcached;
19+
20+
class MemcachedLockStore implements LockStoreInterface
21+
{
22+
private const RELEASE_TTL = 2;
23+
24+
public function __construct(
25+
private readonly Memcached $memcached,
26+
private readonly string $prefix = '',
27+
) {
28+
}
29+
30+
public function acquireLock(string $key, string $owner, int $ttl): bool
31+
{
32+
$key = MemcachedHandler::validateKey($key, $this->prefix);
33+
34+
return $this->memcached->add($key, $owner, $ttl);
35+
}
36+
37+
public function releaseLock(string $key, string $owner): bool
38+
{
39+
$key = MemcachedHandler::validateKey($key, $this->prefix);
40+
41+
[$value, $cas] = $this->getValueAndCas($key);
42+
43+
if ($value !== $owner || $cas === null) {
44+
return false;
45+
}
46+
47+
// Memcached has no atomic compare-and-delete command. CAS narrows the
48+
// release race by first shortening only the current owner's value.
49+
if (! $this->memcached->cas($cas, $key, $owner, self::RELEASE_TTL)) {
50+
return false;
51+
}
52+
53+
return $this->memcached->delete($key);
54+
}
55+
56+
public function forceReleaseLock(string $key): bool
57+
{
58+
$key = MemcachedHandler::validateKey($key, $this->prefix);
59+
60+
if ($this->memcached->delete($key)) {
61+
return true;
62+
}
63+
64+
return $this->memcached->getResultCode() === Memcached::RES_NOTFOUND;
65+
}
66+
67+
public function refreshLock(string $key, string $owner, int $ttl): bool
68+
{
69+
$key = MemcachedHandler::validateKey($key, $this->prefix);
70+
71+
[$value, $cas] = $this->getValueAndCas($key);
72+
73+
if ($value !== $owner || $cas === null) {
74+
return false;
75+
}
76+
77+
return $this->memcached->cas($cas, $key, $owner, $ttl);
78+
}
79+
80+
public function getLockOwner(string $key): ?string
81+
{
82+
$key = MemcachedHandler::validateKey($key, $this->prefix);
83+
$owner = $this->memcached->get($key);
84+
85+
if ($this->memcached->getResultCode() !== Memcached::RES_SUCCESS) {
86+
return null;
87+
}
88+
89+
return is_string($owner) ? $owner : null;
90+
}
91+
92+
/**
93+
* @return array{0: mixed, 1: float|int|null}
94+
*/
95+
private function getValueAndCas(string $key): array
96+
{
97+
$extended = $this->memcached->get($key, null, Memcached::GET_EXTENDED);
98+
99+
if (! is_array($extended) || ! array_key_exists('value', $extended) || ! array_key_exists('cas', $extended)) {
100+
return [null, null];
101+
}
102+
103+
return [$extended['value'], $extended['cas']];
104+
}
105+
}

system/Database/BaseConnection.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,14 @@ public function transStatus(): bool
10241024
return $this->transStatus;
10251025
}
10261026

1027+
/**
1028+
* Checks whether this connection is inside an active transaction.
1029+
*/
1030+
public function inTransaction(): bool
1031+
{
1032+
return $this->transDepth > 0;
1033+
}
1034+
10271035
/**
10281036
* Register a callback to run after the outermost transaction commits.
10291037
*

system/Database/ConnectionInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,11 @@ public function query(string $sql, $binds = null);
115115
*/
116116
public function simpleQuery(string $sql);
117117

118+
/**
119+
* Checks whether this connection is inside an active CodeIgniter-managed transaction.
120+
*/
121+
public function inTransaction(): bool;
122+
118123
/**
119124
* Register a callback to run after the outermost transaction commits.
120125
*

system/Language/en/Cache.php

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313

1414
// Cache language settings
1515
return [
16-
'unableToWrite' => 'Cache unable to write to "{0}".',
17-
'invalidHandler' => 'Cache driver "{0}" is not a valid cache handler.',
18-
'invalidHandlers' => 'Cache config must have an array of $validHandlers.',
19-
'noBackup' => 'Cache config must have a handler and backupHandler set.',
20-
'handlerNotFound' => 'Cache config has an invalid handler or backup handler specified.',
16+
'unableToWrite' => 'Cache unable to write to "{0}".',
17+
'invalidHandler' => 'Cache driver "{0}" is not a valid cache handler.',
18+
'invalidHandlers' => 'Cache config must have an array of $validHandlers.',
19+
'noBackup' => 'Cache config must have a handler and backupHandler set.',
20+
'handlerNotFound' => 'Cache config has an invalid handler or backup handler specified.',
21+
'unsupportedLockStore' => 'The cache handler cannot provide a lock store with the current runtime client.',
2122
];

system/Lock/LockManager.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace CodeIgniter\Lock;
1515

1616
use CodeIgniter\Cache\CacheInterface;
17+
use CodeIgniter\Cache\Exceptions\CacheException;
1718
use CodeIgniter\Cache\LockStoreInterface;
1819
use CodeIgniter\Cache\LockStoreProviderInterface;
1920
use CodeIgniter\Exceptions\InvalidArgumentException;
@@ -36,7 +37,11 @@ public function __construct(CacheInterface $cache)
3637
throw LockException::forUnsupportedStore($cache::class);
3738
}
3839

39-
$this->store = $cache->lockStore();
40+
try {
41+
$this->store = $cache->lockStore();
42+
} catch (CacheException) {
43+
throw LockException::forUnsupportedStore($cache::class);
44+
}
4045
}
4146

4247
public function create(string $name, int $ttl = 300, ?string $owner = null): LockInterface

tests/system/Cache/Handlers/MemcachedHandlerTest.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace CodeIgniter\Cache\Handlers;
1515

1616
use CodeIgniter\Cache\CacheFactory;
17+
use CodeIgniter\Cache\LockStoreInterface;
18+
use CodeIgniter\Cache\LockStoreProviderInterface;
1719
use CodeIgniter\CLI\CLI;
1820
use CodeIgniter\Exceptions\BadMethodCallException;
1921
use CodeIgniter\Exceptions\InvalidArgumentException;
@@ -148,6 +150,53 @@ public function testSave(): void
148150
$this->assertTrue($this->handler->save(self::$key1, 'value'));
149151
}
150152

153+
public function testLockOperations(): void
154+
{
155+
$handler = $this->handler;
156+
157+
$this->assertInstanceOf(LockStoreProviderInterface::class, $handler);
158+
159+
$store = $handler->lockStore();
160+
161+
$this->assertInstanceOf(LockStoreInterface::class, $store);
162+
$this->assertTrue($store->acquireLock(self::$key1, 'owner1', 60));
163+
$this->assertFalse($store->acquireLock(self::$key1, 'owner2', 60));
164+
$this->assertSame('owner1', $store->getLockOwner(self::$key1));
165+
$this->assertFalse($store->releaseLock(self::$key1, 'owner2'));
166+
$this->assertFalse($store->refreshLock(self::$key1, 'owner2', 120));
167+
$this->assertTrue($store->refreshLock(self::$key1, 'owner1', 120));
168+
$this->assertTrue($store->releaseLock(self::$key1, 'owner1'));
169+
$this->assertNull($store->getLockOwner(self::$key1));
170+
$this->assertTrue($store->acquireLock(self::$key1, 'owner1', 60));
171+
$this->assertTrue($store->forceReleaseLock(self::$key1));
172+
$this->assertNull($store->getLockOwner(self::$key1));
173+
$this->assertTrue($store->forceReleaseLock(self::$key1));
174+
}
175+
176+
/**
177+
* This test waits for 2 seconds before reacquiring the lock.
178+
*
179+
* @timeLimit 2.5
180+
*/
181+
public function testExpiredLockCanBeAcquiredByNewOwner(): void
182+
{
183+
$handler = $this->handler;
184+
185+
$this->assertInstanceOf(LockStoreProviderInterface::class, $handler);
186+
187+
$store = $handler->lockStore();
188+
189+
$this->assertTrue($store->acquireLock(self::$key1, 'owner1', 1));
190+
191+
CLI::wait(2);
192+
193+
$this->assertTrue($store->acquireLock(self::$key1, 'owner2', 60));
194+
$this->assertSame('owner2', $store->getLockOwner(self::$key1));
195+
$this->assertFalse($store->releaseLock(self::$key1, 'owner1'));
196+
$this->assertFalse($store->refreshLock(self::$key1, 'owner1', 120));
197+
$this->assertTrue($store->releaseLock(self::$key1, 'owner2'));
198+
}
199+
151200
public function testSavePermanent(): void
152201
{
153202
$this->assertTrue($this->handler->save(self::$key1, 'value', 0));
@@ -244,11 +293,18 @@ public function testPing(): void
244293

245294
public function testReconnect(): void
246295
{
296+
$handler = $this->handler;
297+
298+
$this->assertInstanceOf(LockStoreProviderInterface::class, $handler);
299+
300+
$lockStore = $handler->lockStore();
301+
247302
$this->handler->save(self::$key1, 'value');
248303
$this->assertSame('value', $this->handler->get(self::$key1));
249304

250305
$this->assertTrue($this->handler->reconnect());
251306

252307
$this->assertSame('value', $this->handler->get(self::$key1));
308+
$this->assertNotSame($lockStore, $handler->lockStore());
253309
}
254310
}

0 commit comments

Comments
 (0)