Skip to content

Commit a74e575

Browse files
authored
[12.x] Fix deadlock in cache_locks on cleanup (#58071)
* Update DatabaseLock.php * add a test this was a bit painful * clean up a bit * cs
1 parent cdb0106 commit a74e575

File tree

2 files changed

+54
-1
lines changed

2 files changed

+54
-1
lines changed

src/Illuminate/Cache/DatabaseLock.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
namespace Illuminate\Cache;
44

55
use Illuminate\Database\Connection;
6+
use Illuminate\Database\DetectsConcurrencyErrors;
67
use Illuminate\Database\QueryException;
8+
use Throwable;
79

810
class DatabaseLock extends Lock
911
{
12+
use DetectsConcurrencyErrors;
13+
1014
/**
1115
* The database connection instance.
1216
*
@@ -60,6 +64,8 @@ public function __construct(Connection $connection, $table, $name, $seconds, $ow
6064
* Attempt to acquire the lock.
6165
*
6266
* @return bool
67+
*
68+
* @throws Throwable
6369
*/
6470
public function acquire()
6571
{
@@ -85,7 +91,15 @@ public function acquire()
8591
}
8692

8793
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
88-
$this->connection->table($this->table)->where('expiration', '<=', $this->currentTime())->delete();
94+
try {
95+
$this->connection->table($this->table)
96+
->where('expiration', '<=', $this->currentTime())
97+
->delete();
98+
} catch (Throwable $e) {
99+
if (! $this->causedByConcurrencyError($e)) {
100+
throw $e;
101+
}
102+
}
89103
}
90104

91105
return $acquired;

tests/Integration/Database/DatabaseLockTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22

33
namespace Illuminate\Tests\Integration\Database;
44

5+
use Illuminate\Cache\DatabaseLock;
6+
use Illuminate\Database\Connection;
7+
use Illuminate\Database\Query\Builder;
8+
use Illuminate\Database\QueryException;
59
use Illuminate\Support\Facades\Cache;
610
use Illuminate\Support\Facades\DB;
11+
use Mockery as m;
712
use Orchestra\Testbench\Attributes\WithMigration;
13+
use PDOException;
14+
use PHPUnit\Framework\Attributes\TestWith;
815

916
#[WithMigration('cache')]
1017
class DatabaseLockTest extends DatabaseTestCase
@@ -68,4 +75,36 @@ public function testOtherOwnerDoesNotOwnLockAfterRestore()
6875
$this->assertTrue($secondLock->isOwnedBy($firstLock->owner()));
6976
$this->assertFalse($secondLock->isOwnedByCurrentProcess());
7077
}
78+
79+
#[TestWith(['Deadlock found when trying to get lock', 1213, true])]
80+
#[TestWith(['Table does not exist', 1146, false])]
81+
public function testIgnoresConcurrencyException(string $message, int $code, bool $hasConcurrenyError)
82+
{
83+
$connection = m::mock(Connection::class);
84+
$insertBuilder = m::mock(Builder::class);
85+
$deleteBuilder = m::mock(Builder::class);
86+
87+
$insertBuilder->shouldReceive('insert')->once()->andReturn(true);
88+
89+
$deleteBuilder->shouldReceive('where')->with('expiration', '<=', m::any())->once()->andReturnSelf();
90+
$deleteBuilder->shouldReceive('delete')->once()->andThrow(
91+
new QueryException(
92+
'mysql',
93+
'delete from cache_locks where expiration <= ?',
94+
[],
95+
new PDOException($message, $code)
96+
)
97+
);
98+
99+
$connection->shouldReceive('table')->with('cache_locks')->andReturn($insertBuilder, $deleteBuilder);
100+
101+
$lock = new DatabaseLock($connection, 'cache_locks', 'foo', 0, lottery: [1, 1]);
102+
103+
if ($hasConcurrenyError) {
104+
$this->assertTrue($lock->acquire());
105+
} else {
106+
$this->expectException(QueryException::class);
107+
$this->assertFalse($lock->acquire());
108+
}
109+
}
71110
}

0 commit comments

Comments
 (0)