Skip to content

Commit 2eedc1f

Browse files
jderussefabpot
authored andcommitted
[Lock] Automaticaly release lock when user forget it
1 parent 084e49f commit 2eedc1f

File tree

4 files changed

+72
-12
lines changed

4 files changed

+72
-12
lines changed

src/Symfony/Component/Lock/Factory.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,15 @@ public function __construct(StoreInterface $store)
3636
/**
3737
* Creates a lock for the given resource.
3838
*
39-
* @param string $resource The resource to lock
40-
* @param float $ttl maximum expected lock duration
39+
* @param string $resource The resource to lock
40+
* @param float $ttl Maximum expected lock duration in seconds
41+
* @param bool $autoRelease Whether to automatically release the lock or not when the lock instance is destroyed
4142
*
4243
* @return Lock
4344
*/
44-
public function createLock($resource, $ttl = 300.0)
45+
public function createLock($resource, $ttl = 300.0, $autoRelease = true)
4546
{
46-
$lock = new Lock(new Key($resource), $this->store, $ttl);
47+
$lock = new Lock(new Key($resource), $this->store, $ttl, $autoRelease);
4748
$lock->setLogger($this->logger);
4849

4950
return $lock;

src/Symfony/Component/Lock/Lock.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,37 @@ final class Lock implements LockInterface, LoggerAwareInterface
3232
private $store;
3333
private $key;
3434
private $ttl;
35+
private $autoRelease;
36+
private $dirty = false;
3537

3638
/**
37-
* @param Key $key
38-
* @param StoreInterface $store
39-
* @param float|null $ttl
39+
* @param Key $key Resource to lock
40+
* @param StoreInterface $store Store used to handle lock persistence
41+
* @param float|null $ttl Maximum expected lock duration in seconds
42+
* @param bool $autoRelease Whether to automatically release the lock or not when the lock instance is destroyed
4043
*/
41-
public function __construct(Key $key, StoreInterface $store, $ttl = null)
44+
public function __construct(Key $key, StoreInterface $store, $ttl = null, $autoRelease = true)
4245
{
4346
$this->store = $store;
4447
$this->key = $key;
4548
$this->ttl = $ttl;
49+
$this->autoRelease = (bool) $autoRelease;
4650

4751
$this->logger = new NullLogger();
4852
}
4953

54+
/**
55+
* Automatically releases the underlying lock when the object is destructed.
56+
*/
57+
public function __destruct()
58+
{
59+
if (!$this->autoRelease || !$this->dirty || !$this->isAcquired()) {
60+
return;
61+
}
62+
63+
$this->release();
64+
}
65+
5066
/**
5167
* {@inheritdoc}
5268
*/
@@ -59,6 +75,7 @@ public function acquire($blocking = false)
5975
$this->store->waitAndSave($this->key);
6076
}
6177

78+
$this->dirty = true;
6279
$this->logger->info('Successfully acquired the "{resource}" lock.', array('resource' => $this->key));
6380

6481
if ($this->ttl) {
@@ -71,6 +88,7 @@ public function acquire($blocking = false)
7188

7289
return true;
7390
} catch (LockConflictedException $e) {
91+
$this->dirty = false;
7492
$this->logger->warning('Failed to acquire the "{resource}" lock. Someone else already acquired the lock.', array('resource' => $this->key));
7593

7694
if ($blocking) {
@@ -96,13 +114,15 @@ public function refresh()
96114
try {
97115
$this->key->resetLifetime();
98116
$this->store->putOffExpiration($this->key, $this->ttl);
117+
$this->dirty = true;
99118

100119
if ($this->key->isExpired()) {
101120
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $this->key));
102121
}
103122

104123
$this->logger->info('Expiration defined for "{resource}" lock for "{ttl}" seconds.', array('resource' => $this->key, 'ttl' => $this->ttl));
105124
} catch (LockConflictedException $e) {
125+
$this->dirty = false;
106126
$this->logger->warning('Failed to define an expiration for the "{resource}" lock, someone else acquired the lock.', array('resource' => $this->key));
107127
throw $e;
108128
} catch (\Exception $e) {
@@ -116,7 +136,7 @@ public function refresh()
116136
*/
117137
public function isAcquired()
118138
{
119-
return $this->store->exists($this->key);
139+
return $this->dirty = $this->store->exists($this->key);
120140
}
121141

122142
/**
@@ -125,6 +145,7 @@ public function isAcquired()
125145
public function release()
126146
{
127147
$this->store->delete($this->key);
148+
$this->dirty = false;
128149

129150
if ($this->store->exists($this->key)) {
130151
$this->logger->warning('Failed to release the "{resource}" lock.', array('resource' => $this->key));

src/Symfony/Component/Lock/Store/SemaphoreStore.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public function delete(Key $key)
116116
*/
117117
public function putOffExpiration(Key $key, $ttl)
118118
{
119-
// do nothing, the flock locks forever.
119+
// do nothing, the semaphore locks forever.
120120
}
121121

122122
/**

src/Symfony/Component/Lock/Tests/LockTest.php

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,10 @@ public function testIsAquired()
103103
$lock = new Lock($key, $store, 10);
104104

105105
$store
106-
->expects($this->once())
106+
->expects($this->any())
107107
->method('exists')
108108
->with($key)
109-
->willReturn(true);
109+
->will($this->onConsecutiveCalls(true, false));
110110

111111
$this->assertTrue($lock->isAcquired());
112112
}
@@ -131,6 +131,44 @@ public function testRelease()
131131
$lock->release();
132132
}
133133

134+
public function testReleaseOnDestruction()
135+
{
136+
$key = new Key(uniqid(__METHOD__, true));
137+
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
138+
$lock = new Lock($key, $store, 10);
139+
140+
$store
141+
->method('exists')
142+
->willReturnOnConsecutiveCalls(array(true, false))
143+
;
144+
$store
145+
->expects($this->once())
146+
->method('delete')
147+
;
148+
149+
$lock->acquire(false);
150+
unset($lock);
151+
}
152+
153+
public function testNoAutoReleaseWhenNotConfigured()
154+
{
155+
$key = new Key(uniqid(__METHOD__, true));
156+
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
157+
$lock = new Lock($key, $store, 10, false);
158+
159+
$store
160+
->method('exists')
161+
->willReturnOnConsecutiveCalls(array(true, false))
162+
;
163+
$store
164+
->expects($this->never())
165+
->method('delete')
166+
;
167+
168+
$lock->acquire(false);
169+
unset($lock);
170+
}
171+
134172
/**
135173
* @expectedException \Symfony\Component\Lock\Exception\LockReleasingException
136174
*/

0 commit comments

Comments
 (0)