Skip to content

Commit f898f56

Browse files
authored
[12.x] Ability to refresh cache locks (#58349)
1 parent dcf70c4 commit f898f56

16 files changed

Lines changed: 447 additions & 2 deletions

src/Illuminate/Cache/ArrayLock.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,31 @@ protected function exists()
5959
return isset($this->store->locks[$this->name]);
6060
}
6161

62+
/**
63+
* Attempt to refresh the lock for the given number of seconds.
64+
*
65+
* @param int|null $seconds
66+
* @return bool
67+
*/
68+
public function refresh($seconds = null)
69+
{
70+
if (! $this->isOwnedByCurrentProcess()) {
71+
return false;
72+
}
73+
74+
$expiresAt = $this->store->locks[$this->name]['expiresAt'];
75+
76+
if ($expiresAt && ! $expiresAt->isFuture()) {
77+
return false;
78+
}
79+
80+
$seconds ??= $this->seconds;
81+
82+
$this->store->locks[$this->name]['expiresAt'] = $seconds === 0 ? null : Carbon::now()->addSeconds($seconds);
83+
84+
return true;
85+
}
86+
6287
/**
6388
* Release the lock.
6489
*

src/Illuminate/Cache/DatabaseLock.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,33 @@ public function acquire()
9797
return $acquired;
9898
}
9999

100+
/**
101+
* Attempt to refresh the lock for the given number of seconds.
102+
*
103+
* @param int|null $seconds
104+
* @return bool
105+
*/
106+
public function refresh($seconds = null)
107+
{
108+
$seconds ??= $this->seconds;
109+
110+
return $this->connection->table($this->table)
111+
->where('key', $this->name)
112+
->where('owner', $this->owner)
113+
->where('expiration', '>', $this->currentTime())
114+
->update(['expiration' => $this->expiresAt($seconds)]) >= 1;
115+
}
116+
100117
/**
101118
* Get the UNIX timestamp indicating when the lock should expire.
102119
*
103120
* @return int
104121
*/
105-
protected function expiresAt()
122+
protected function expiresAt($seconds = null)
106123
{
107-
$lockTimeout = $this->seconds > 0 ? $this->seconds : $this->defaultTimeoutInSeconds;
124+
$seconds ??= $this->seconds;
125+
126+
$lockTimeout = $seconds > 0 ? $seconds : $this->defaultTimeoutInSeconds;
108127

109128
return $this->currentTime() + $lockTimeout;
110129
}

src/Illuminate/Cache/DynamoDbLock.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,23 @@ public function acquire()
4040
return $this->dynamo->add($this->name, $this->owner, 86400);
4141
}
4242

43+
/**
44+
* Attempt to refresh the lock for the given number of seconds.
45+
*
46+
* @param int|null $seconds
47+
* @return bool
48+
*/
49+
public function refresh($seconds = null)
50+
{
51+
$seconds ??= $this->seconds;
52+
53+
if ($seconds <= 0) {
54+
$seconds = 86400;
55+
}
56+
57+
return $this->dynamo->refreshIfOwned($this->name, $this->owner, $seconds);
58+
}
59+
4360
/**
4461
* Release the lock.
4562
*

src/Illuminate/Cache/DynamoDbStore.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,54 @@ public function restoreLock($name, $owner)
400400
return $this->lock($name, 0, $owner);
401401
}
402402

403+
/**
404+
* Atomically refresh the expiration of a key if it matches the expected owner.
405+
*
406+
* @param string $key
407+
* @param mixed $expectedOwner
408+
* @param int $seconds
409+
* @return bool
410+
*/
411+
public function refreshIfOwned($key, $expectedOwner, $seconds)
412+
{
413+
try {
414+
$this->dynamo->updateItem([
415+
'TableName' => $this->table,
416+
'Key' => [
417+
$this->keyAttribute => [
418+
'S' => $this->prefix.$key,
419+
],
420+
],
421+
'ConditionExpression' => 'attribute_exists(#key) AND #value = :owner AND #expires_at > :now',
422+
'UpdateExpression' => 'SET #expires_at = :expires_at',
423+
'ExpressionAttributeNames' => [
424+
'#key' => $this->keyAttribute,
425+
'#value' => $this->valueAttribute,
426+
'#expires_at' => $this->expirationAttribute,
427+
],
428+
'ExpressionAttributeValues' => [
429+
':owner' => [
430+
$this->type($expectedOwner) => $this->serialize($expectedOwner),
431+
],
432+
':now' => [
433+
'N' => (string) $this->currentTime(),
434+
],
435+
':expires_at' => [
436+
'N' => (string) $this->toTimestamp($seconds),
437+
],
438+
],
439+
]);
440+
441+
return true;
442+
} catch (DynamoDbException $e) {
443+
if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) {
444+
return false;
445+
}
446+
447+
throw $e;
448+
}
449+
}
450+
403451
/**
404452
* Remove an item from the cache.
405453
*

src/Illuminate/Cache/FileLock.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,19 @@ public function acquire()
1313
{
1414
return $this->store->add($this->name, $this->owner, $this->seconds);
1515
}
16+
17+
/**
18+
* Attempt to refresh the lock for the given number of seconds.
19+
*
20+
* @param int|null $seconds
21+
* @return bool
22+
*/
23+
public function refresh($seconds = null)
24+
{
25+
return $this->store->refreshIfOwned(
26+
$this->name,
27+
$this->owner,
28+
$seconds ?? $this->seconds
29+
);
30+
}
1631
}

src/Illuminate/Cache/FileStore.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,55 @@ public function restoreLock($name, $owner)
247247
return $this->lock($name, 0, $owner);
248248
}
249249

250+
/**
251+
* Atomically refresh the expiration of a cache key if it matches the expected owner.
252+
*
253+
* @param string $key
254+
* @param mixed $expectedOwner
255+
* @param int $seconds
256+
* @return bool
257+
*/
258+
public function refreshIfOwned($key, $expectedOwner, $seconds)
259+
{
260+
$this->ensureCacheDirectoryExists($path = $this->path($key));
261+
262+
$file = new LockableFile($path, 'c+');
263+
264+
try {
265+
$file->getExclusiveLock();
266+
} catch (LockTimeoutException) {
267+
$file->close();
268+
269+
return false;
270+
}
271+
272+
$contents = $file->read();
273+
274+
if (strlen($contents) < 10) {
275+
$file->close();
276+
277+
return false;
278+
}
279+
280+
$expire = substr($contents, 0, 10);
281+
282+
$currentOwner = unserialize(substr($contents, 10));
283+
284+
if ($currentOwner !== $expectedOwner || $this->currentTime() >= $expire) {
285+
$file->close();
286+
287+
return false;
288+
}
289+
290+
$file->truncate()
291+
->write($this->expiration($seconds).serialize($expectedOwner))
292+
->close();
293+
294+
$this->ensurePermissionsAreCorrect($path);
295+
296+
return true;
297+
}
298+
250299
/**
251300
* Remove an item from the cache.
252301
*

src/Illuminate/Cache/Lock.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Illuminate\Support\InteractsWithTime;
88
use Illuminate\Support\Sleep;
99
use Illuminate\Support\Str;
10+
use RuntimeException;
1011

1112
abstract class Lock implements LockContract
1213
{
@@ -136,6 +137,17 @@ public function block($seconds, $callback = null)
136137
return true;
137138
}
138139

140+
/**
141+
* Attempt to refresh the lock for the given number of seconds.
142+
*
143+
* @param int|null $seconds
144+
* @return bool
145+
*/
146+
public function refresh($seconds = null)
147+
{
148+
throw new RuntimeException('This lock driver does not support refreshing locks.');
149+
}
150+
139151
/**
140152
* Returns the current owner of the lock.
141153
*

src/Illuminate/Cache/LuaScripts.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,32 @@ public static function add()
2020
LUA;
2121
}
2222

23+
/**
24+
* Get the Lua script to atomically refresh a lock's expiration.
25+
*
26+
* KEYS[1] - The name of the lock
27+
* ARGV[1] - The owner key of the lock instance trying to refresh it
28+
* ARGV[2] - The number of seconds the lock should be valid
29+
*
30+
* @return string
31+
*/
32+
public static function refreshLock()
33+
{
34+
return <<<'LUA'
35+
if redis.call("get",KEYS[1]) == ARGV[1] then
36+
if tonumber(ARGV[2]) > 0 then
37+
return redis.call("expire",KEYS[1],ARGV[2])
38+
end
39+
40+
redis.call("persist",KEYS[1])
41+
42+
return 1
43+
else
44+
return 0
45+
end
46+
LUA;
47+
}
48+
2349
/**
2450
* Get the Lua script to atomically release a lock.
2551
*

src/Illuminate/Cache/MemcachedLock.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,25 @@ public function acquire()
3838
);
3939
}
4040

41+
/**
42+
* Attempt to refresh the lock for the given number of seconds.
43+
*
44+
* @param int|null $seconds
45+
* @return bool
46+
*/
47+
public function refresh($seconds = null)
48+
{
49+
$seconds ??= $this->seconds;
50+
51+
$value = $this->memcached->get($this->name, null, \Memcached::GET_EXTENDED);
52+
53+
if ($value === false || ($value['value'] ?? null) !== $this->owner) {
54+
return false;
55+
}
56+
57+
return $this->memcached->cas($value['cas'], $this->name, $this->owner, $seconds);
58+
}
59+
4160
/**
4261
* Release the lock.
4362
*

src/Illuminate/Cache/NoLock.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ public function acquire()
1414
return true;
1515
}
1616

17+
/**
18+
* Attempt to refresh the lock for the given number of seconds.
19+
*
20+
* @param int|null $seconds
21+
* @return bool
22+
*/
23+
public function refresh($seconds = null)
24+
{
25+
return true;
26+
}
27+
1728
/**
1829
* Release the lock.
1930
*

0 commit comments

Comments
 (0)