Skip to content

Commit 5a34797

Browse files
[10.x] Use atomic locks for command mutex (#47624)
* [10.x] use atomic locks for command mutex * fix carbon usage and use correct lock store * fix styleci * fix tests * formatting --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 3cd9bcb commit 5a34797

File tree

2 files changed

+136
-14
lines changed

2 files changed

+136
-14
lines changed

src/Illuminate/Console/CacheCommandMutex.php

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
namespace Illuminate\Console;
44

55
use Carbon\CarbonInterval;
6+
use Illuminate\Cache\DynamoDbStore;
67
use Illuminate\Contracts\Cache\Factory as Cache;
8+
use Illuminate\Contracts\Cache\LockProvider;
9+
use Illuminate\Support\InteractsWithTime;
710

811
class CacheCommandMutex implements CommandMutex
912
{
13+
use InteractsWithTime;
14+
1015
/**
1116
* The cache factory implementation.
1217
*
@@ -39,13 +44,20 @@ public function __construct(Cache $cache)
3944
*/
4045
public function create($command)
4146
{
42-
return $this->cache->store($this->store)->add(
43-
$this->commandMutexName($command),
44-
true,
45-
method_exists($command, 'isolationLockExpiresAt')
46-
? $command->isolationLockExpiresAt()
47-
: CarbonInterval::hour(),
48-
);
47+
$store = $this->cache->store($this->store);
48+
49+
$expiresAt = method_exists($command, 'isolationLockExpiresAt')
50+
? $command->isolationLockExpiresAt()
51+
: CarbonInterval::hour();
52+
53+
if ($this->shouldUseLocks($store->getStore())) {
54+
return $store->getStore()->lock(
55+
$this->commandMutexName($command),
56+
$this->secondsUntil($expiresAt)
57+
)->get();
58+
}
59+
60+
return $store->add($this->commandMutexName($command), true, $expiresAt);
4961
}
5062

5163
/**
@@ -56,9 +68,19 @@ public function create($command)
5668
*/
5769
public function exists($command)
5870
{
59-
return $this->cache->store($this->store)->has(
60-
$this->commandMutexName($command)
61-
);
71+
$store = $this->cache->store($this->store);
72+
73+
if ($this->shouldUseLocks($store->getStore())) {
74+
$lock = $store->getStore()->lock($this->commandMutexName($command));
75+
76+
return tap(! $lock->get(), function ($exists) {
77+
if ($exists) {
78+
$lock->release();
79+
}
80+
});
81+
}
82+
83+
return $this->cache->store($this->store)->has($this->commandMutexName($command));
6284
}
6385

6486
/**
@@ -69,9 +91,13 @@ public function exists($command)
6991
*/
7092
public function forget($command)
7193
{
72-
return $this->cache->store($this->store)->forget(
73-
$this->commandMutexName($command)
74-
);
94+
$store = $this->cache->store($this->store);
95+
96+
if ($this->shouldUseLocks($store->getStore())) {
97+
return $store->getStore()->lock($this->commandMutexName($command))->forceRelease();
98+
}
99+
100+
return $this->cache->store($this->store)->forget($this->commandMutexName($command));
75101
}
76102

77103
/**
@@ -95,4 +121,15 @@ public function useStore($store)
95121

96122
return $this;
97123
}
124+
125+
/**
126+
* Determine if the given store should use locks for command mutexes.
127+
*
128+
* @param \Illuminate\Contracts\Cache\Store $store
129+
* @return bool
130+
*/
131+
protected function shouldUseLocks($store)
132+
{
133+
return $store instanceof LockProvider && ! $store instanceof DynamoDbStore;
134+
}
98135
}

tests/Console/CacheCommandMutexTest.php

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
use Illuminate\Console\CacheCommandMutex;
66
use Illuminate\Console\Command;
77
use Illuminate\Contracts\Cache\Factory;
8+
use Illuminate\Contracts\Cache\LockProvider;
89
use Illuminate\Contracts\Cache\Repository;
910
use Mockery as m;
11+
use Mockery\MockInterface;
1012
use PHPUnit\Framework\TestCase;
1113

1214
class CacheCommandMutexTest extends TestCase
@@ -35,16 +37,22 @@ protected function setUp(): void
3537
{
3638
$this->cacheFactory = m::mock(Factory::class);
3739
$this->cacheRepository = m::mock(Repository::class);
38-
$this->cacheFactory->shouldReceive('store')->andReturn($this->cacheRepository);
3940
$this->mutex = new CacheCommandMutex($this->cacheFactory);
4041
$this->command = new class extends Command
4142
{
4243
protected $name = 'command-name';
4344
};
4445
}
4546

47+
protected function tearDown(): void
48+
{
49+
m::close();
50+
parent::tearDown();
51+
}
52+
4653
public function testCanCreateMutex()
4754
{
55+
$this->mockUsingCacheStore();
4856
$this->cacheRepository->shouldReceive('add')
4957
->andReturn(true)
5058
->once();
@@ -55,6 +63,7 @@ public function testCanCreateMutex()
5563

5664
public function testCannotCreateMutexIfAlreadyExist()
5765
{
66+
$this->mockUsingCacheStore();
5867
$this->cacheRepository->shouldReceive('add')
5968
->andReturn(false)
6069
->once();
@@ -65,6 +74,31 @@ public function testCannotCreateMutexIfAlreadyExist()
6574

6675
public function testCanCreateMutexWithCustomConnection()
6776
{
77+
$this->mockUsingCacheStore();
78+
$this->cacheRepository->shouldReceive('getStore')
79+
->with('test')
80+
->andReturn($this->cacheRepository);
81+
$this->cacheRepository->shouldReceive('add')
82+
->andReturn(false)
83+
->once();
84+
$this->mutex->useStore('test');
85+
86+
$this->mutex->create($this->command);
87+
}
88+
89+
public function testCanCreateMutexWithLockProvider()
90+
{
91+
$lock = $this->mockUsingLockProvider();
92+
$this->acquireLockExpectations($lock, true);
93+
94+
$actual = $this->mutex->create($this->command);
95+
96+
$this->assertTrue($actual);
97+
}
98+
99+
public function testCanCreateMutexWithCustomLockProviderConnection()
100+
{
101+
$this->mockUsingCacheStore();
68102
$this->cacheRepository->shouldReceive('getStore')
69103
->with('test')
70104
->andReturn($this->cacheRepository);
@@ -75,4 +109,55 @@ public function testCanCreateMutexWithCustomConnection()
75109

76110
$this->mutex->create($this->command);
77111
}
112+
113+
public function testCannotCreateMutexIfAlreadyExistWithLockProvider()
114+
{
115+
$lock = $this->mockUsingLockProvider();
116+
$this->acquireLockExpectations($lock, false);
117+
$actual = $this->mutex->create($this->command);
118+
119+
$this->assertFalse($actual);
120+
}
121+
122+
public function testCanCreateMutexWithCustomConnectionWithLockProvider()
123+
{
124+
$lock = m::mock(LockProvider::class);
125+
$this->cacheFactory->expects('store')->once()->with('test')->andReturn($this->cacheRepository);
126+
$this->cacheRepository->expects('getStore')->twice()->andReturn($lock);
127+
128+
$this->acquireLockExpectations($lock, true);
129+
$this->mutex->useStore('test');
130+
131+
$this->mutex->create($this->command);
132+
}
133+
134+
/**
135+
* @return void
136+
*/
137+
private function mockUsingCacheStore(): void
138+
{
139+
$this->cacheFactory->expects('store')->once()->andReturn($this->cacheRepository);
140+
$this->cacheRepository->expects('getStore')->andReturn(null);
141+
}
142+
143+
private function mockUsingLockProvider(): m\MockInterface
144+
{
145+
$lock = m::mock(LockProvider::class);
146+
$this->cacheFactory->expects('store')->once()->andReturn($this->cacheRepository);
147+
$this->cacheRepository->expects('getStore')->twice()->andReturn($lock);
148+
149+
return $lock;
150+
}
151+
152+
private function acquireLockExpectations(MockInterface $lock, bool $acquiresSuccessfully): void
153+
{
154+
$lock->expects('lock')
155+
->once()
156+
->with(m::type('string'), m::type('int'))
157+
->andReturns($lock);
158+
159+
$lock->expects('get')
160+
->once()
161+
->andReturns($acquiresSuccessfully);
162+
}
78163
}

0 commit comments

Comments
 (0)