Skip to content

Commit b78880a

Browse files
authored
[7.x] Cache::lock support for the database cache driver (#32639)
Add atomic lock support for the database driver.
1 parent c4f877e commit b78880a

File tree

4 files changed

+263
-2
lines changed

4 files changed

+263
-2
lines changed

src/Illuminate/Cache/CacheManager.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,11 @@ protected function createDatabaseDriver(array $config)
214214

215215
return $this->repository(
216216
new DatabaseStore(
217-
$connection, $config['table'], $this->getPrefix($config)
217+
$connection,
218+
$config['table'],
219+
$this->getPrefix($config),
220+
$config['lock_table'] ?? 'cache_locks',
221+
$config['lock_lottery'] ?? [2, 100]
218222
)
219223
);
220224
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
namespace Illuminate\Cache;
4+
5+
use Illuminate\Database\Connection;
6+
use Illuminate\Database\QueryException;
7+
8+
class DatabaseLock extends Lock
9+
{
10+
/**
11+
* The database connection instance.
12+
*
13+
* @var \Illuminate\Database\Connection
14+
*/
15+
protected $connection;
16+
17+
/**
18+
* The database table name.
19+
*
20+
* @var string
21+
*/
22+
protected $table;
23+
24+
/**
25+
* The prune probability odds.
26+
*
27+
* @var array
28+
*/
29+
protected $lottery;
30+
31+
/**
32+
* Create a new lock instance.
33+
*
34+
* @param \Illuminate\Database\Connection $connection
35+
* @param string $table
36+
* @param string $name
37+
* @param int $seconds
38+
* @param string|null $owner
39+
* @param array $lottery
40+
* @return void
41+
*/
42+
public function __construct(Connection $connection, $table, $name, $seconds, $owner = null, $lottery = [2, 100])
43+
{
44+
parent::__construct($name, $seconds, $owner);
45+
46+
$this->connection = $connection;
47+
$this->table = $table;
48+
$this->lottery = $lottery;
49+
}
50+
51+
/**
52+
* Attempt to acquire the lock.
53+
*
54+
* @return bool
55+
*/
56+
public function acquire()
57+
{
58+
$acquired = false;
59+
60+
try {
61+
$this->connection->table($this->table)->insert([
62+
'id' => $this->name,
63+
'owner' => $this->owner,
64+
'expires_at' => $this->expiresAt(),
65+
]);
66+
67+
$acquired = true;
68+
} catch (QueryException $e) {
69+
$updated = $this->connection->table($this->table)
70+
->where('id', $this->name)
71+
->where(function ($query) {
72+
return $query->where('owner', $this->owner)->orWhere('expires_at', '<=', time());
73+
})->update([
74+
'owner' => $this->owner,
75+
'expires_at' => $this->expiresAt(),
76+
]);
77+
78+
$acquired = $updated >= 1;
79+
}
80+
81+
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
82+
$this->connection->table($this->table)->where('expires_at', '<=', time())->delete();
83+
}
84+
85+
return $acquired;
86+
}
87+
88+
/**
89+
* Get the UNIX timestamp indicating when the lock should expire.
90+
*
91+
* @return int
92+
*/
93+
protected function expiresAt()
94+
{
95+
return $this->seconds > 0 ? time() + $this->seconds : now()->addDays(1)->getTimestamp();
96+
}
97+
98+
/**
99+
* Release the lock.
100+
*
101+
* @return bool
102+
*/
103+
public function release()
104+
{
105+
if ($this->isOwnedByCurrentProcess()) {
106+
$this->connection->table($this->table)
107+
->where('id', $this->name)
108+
->where('owner', $this->owner)
109+
->delete();
110+
111+
return true;
112+
}
113+
114+
return false;
115+
}
116+
117+
/**
118+
* Releases this lock in disregard of ownership.
119+
*
120+
* @return void
121+
*/
122+
public function forceRelease()
123+
{
124+
$this->connection->table($this->table)
125+
->where('id', $this->name)
126+
->delete();
127+
}
128+
129+
/**
130+
* Returns the owner value written into the driver for this lock.
131+
*
132+
* @return string
133+
*/
134+
protected function getCurrentOwner()
135+
{
136+
return optional($this->connection->table($this->table)->where('id', $this->name)->first())->owner;
137+
}
138+
}

src/Illuminate/Cache/DatabaseStore.php

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,19 +35,41 @@ class DatabaseStore implements Store
3535
*/
3636
protected $prefix;
3737

38+
/**
39+
* The name of the cache locks table.
40+
*
41+
* @var string
42+
*/
43+
protected $lockTable;
44+
45+
/**
46+
* A array representation of the lock lottery odds.
47+
*
48+
* @var array
49+
*/
50+
protected $lockLottery;
51+
3852
/**
3953
* Create a new database store.
4054
*
4155
* @param \Illuminate\Database\ConnectionInterface $connection
4256
* @param string $table
4357
* @param string $prefix
58+
* @param string $lockTable
59+
* @param array $lockLottery
4460
* @return void
4561
*/
46-
public function __construct(ConnectionInterface $connection, $table, $prefix = '')
62+
public function __construct(ConnectionInterface $connection,
63+
$table,
64+
$prefix = '',
65+
$lockTable = 'cache_locks',
66+
$lockLottery = [2, 100])
4767
{
4868
$this->table = $table;
4969
$this->prefix = $prefix;
5070
$this->connection = $connection;
71+
$this->lockTable = $lockTable;
72+
$this->lockLottery = $lockLottery;
5173
}
5274

5375
/**
@@ -205,6 +227,38 @@ public function forever($key, $value)
205227
return $this->put($key, $value, 315360000);
206228
}
207229

230+
/**
231+
* Get a lock instance.
232+
*
233+
* @param string $name
234+
* @param int $seconds
235+
* @param string|null $owner
236+
* @return \Illuminate\Contracts\Cache\Lock
237+
*/
238+
public function lock($name, $seconds = 0, $owner = null)
239+
{
240+
return new DatabaseLock(
241+
$this->connection,
242+
$this->lockTable,
243+
$this->prefix.$name,
244+
$seconds,
245+
$owner,
246+
$this->lockLottery
247+
);
248+
}
249+
250+
/**
251+
* Restore a lock instance using the owner identifier.
252+
*
253+
* @param string $name
254+
* @param string $owner
255+
* @return \Illuminate\Contracts\Cache\Lock
256+
*/
257+
public function restoreLock($name, $owner)
258+
{
259+
return $this->lock($name, 0, $owner);
260+
}
261+
208262
/**
209263
* Remove an item from the cache.
210264
*
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Database;
4+
5+
use Illuminate\Database\Schema\Blueprint;
6+
use Illuminate\Support\Facades\Cache;
7+
use Illuminate\Support\Facades\DB;
8+
use Illuminate\Support\Facades\Schema;
9+
10+
/**
11+
* @group integration
12+
*/
13+
class DatabaseLockTest extends DatabaseTestCase
14+
{
15+
protected function setUp(): void
16+
{
17+
parent::setUp();
18+
19+
Schema::create('cache_locks', function (Blueprint $table) {
20+
$table->string('id')->primary();
21+
$table->string('owner');
22+
$table->integer('expires_at');
23+
});
24+
}
25+
26+
public function testLockCanBeAcquired()
27+
{
28+
$lock = Cache::driver('database')->lock('foo');
29+
$this->assertTrue($lock->get());
30+
31+
$otherLock = Cache::driver('database')->lock('foo');
32+
$this->assertFalse($otherLock->get());
33+
34+
$lock->release();
35+
36+
$otherLock = Cache::driver('database')->lock('foo');
37+
$this->assertTrue($otherLock->get());
38+
39+
$otherLock->release();
40+
}
41+
42+
public function testLockCanBeForceReleased()
43+
{
44+
$lock = Cache::driver('database')->lock('foo');
45+
$this->assertTrue($lock->get());
46+
47+
$otherLock = Cache::driver('database')->lock('foo');
48+
$otherLock->forceRelease();
49+
$this->assertTrue($otherLock->get());
50+
51+
$otherLock->release();
52+
}
53+
54+
public function testExpiredLockCanBeRetrieved()
55+
{
56+
$lock = Cache::driver('database')->lock('foo');
57+
$this->assertTrue($lock->get());
58+
DB::table('cache_locks')->update(['expires_at' => now()->subDays(1)->getTimestamp()]);
59+
60+
$otherLock = Cache::driver('database')->lock('foo');
61+
$this->assertTrue($otherLock->get());
62+
63+
$otherLock->release();
64+
}
65+
}

0 commit comments

Comments
 (0)