Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit aa6c353

Browse filesBrowse files
committed
Check TTL expiration in lock acquisition
1 parent c708d02 commit aa6c353
Copy full SHA for aa6c353

File tree

9 files changed

+98
-16
lines changed
Filter options

9 files changed

+98
-16
lines changed
+21Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Lock\Exception;
13+
14+
/**
15+
* LockExpiredException is thrown when a lock may conflict due to a TTL expiration.
16+
*
17+
* @author Jérémy Derussé <jeremy@derusse.com>
18+
*/
19+
class LockExpiredException extends \RuntimeException implements ExceptionInterface
20+
{
21+
}

‎src/Symfony/Component/Lock/Key.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Key.php
+8Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,12 @@ public function getExpiringDate()
9696
{
9797
return $this->expiringDate;
9898
}
99+
100+
/**
101+
* @return bool
102+
*/
103+
public function isExpired()
104+
{
105+
return null !== $this->expiringDate && $this->expiringDate <= new \DateTime();
106+
}
99107
}

‎src/Symfony/Component/Lock/Lock.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Lock.php
+11-5Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Lock\Exception\InvalidArgumentException;
1818
use Symfony\Component\Lock\Exception\LockAcquiringException;
1919
use Symfony\Component\Lock\Exception\LockConflictedException;
20+
use Symfony\Component\Lock\Exception\LockExpiredException;
2021
use Symfony\Component\Lock\Exception\LockReleasingException;
2122

2223
/**
@@ -64,6 +65,10 @@ public function acquire($blocking = false)
6465
$this->refresh();
6566
}
6667

68+
if ($this->key->isExpired()) {
69+
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $this->key));
70+
}
71+
6772
return true;
6873
} catch (LockConflictedException $e) {
6974
$this->logger->warning('Failed to acquire the "{resource}" lock. Someone else already acquired the lock.', array('resource' => $this->key));
@@ -91,6 +96,11 @@ public function refresh()
9196
try {
9297
$this->key->resetExpiringDate();
9398
$this->store->putOffExpiration($this->key, $this->ttl);
99+
100+
if ($this->key->isExpired()) {
101+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $this->key));
102+
}
103+
94104
$this->logger->info('Expiration defined for "{resource}" lock for "{ttl}" seconds.', array('resource' => $this->key, 'ttl' => $this->ttl));
95105
} catch (LockConflictedException $e) {
96106
$this->logger->warning('Failed to define an expiration for the "{resource}" lock, someone else acquired the lock.', array('resource' => $this->key));
@@ -127,11 +137,7 @@ public function release()
127137
*/
128138
public function isExpired()
129139
{
130-
if (null === $expireDate = $this->key->getExpiringDate()) {
131-
return false;
132-
}
133-
134-
return $expireDate <= new \DateTime();
140+
return $this->key->isExpired();
135141
}
136142

137143
public function getExpiringDate()

‎src/Symfony/Component/Lock/Store/CombinedStore.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Store/CombinedStore.php
+12-1Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Psr\Log\NullLogger;
1717
use Symfony\Component\Lock\Exception\InvalidArgumentException;
1818
use Symfony\Component\Lock\Exception\LockConflictedException;
19+
use Symfony\Component\Lock\Exception\LockExpiredException;
1920
use Symfony\Component\Lock\Exception\NotSupportedException;
2021
use Symfony\Component\Lock\Key;
2122
use Symfony\Component\Lock\Strategy\StrategyInterface;
@@ -102,10 +103,16 @@ public function putOffExpiration(Key $key, $ttl)
102103
$successCount = 0;
103104
$failureCount = 0;
104105
$storesCount = count($this->stores);
106+
$expireAt = microtime(true) + $ttl;
105107

106108
foreach ($this->stores as $store) {
107109
try {
108-
$store->putOffExpiration($key, $ttl);
110+
if (0.0 >= $adjustedTtl = $expireAt - microtime(true)) {
111+
$this->logger->warning('Stores took to long to put off the expiration of the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'ttl' => $ttl));
112+
break;
113+
}
114+
115+
$store->putOffExpiration($key, $adjustedTtl);
109116
++$successCount;
110117
} catch (\Exception $e) {
111118
$this->logger->warning('One store failed to put off the expiration of the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
@@ -117,6 +124,10 @@ public function putOffExpiration(Key $key, $ttl)
117124
}
118125
}
119126

127+
if ($key->isExpired()) {
128+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
129+
}
130+
120131
if ($this->strategy->isMet($successCount, $storesCount)) {
121132
return;
122133
}

‎src/Symfony/Component/Lock/Store/MemcachedStore.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Store/MemcachedStore.php
+11-5Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Lock\Exception\InvalidArgumentException;
1515
use Symfony\Component\Lock\Exception\LockConflictedException;
16+
use Symfony\Component\Lock\Exception\LockExpiredException;
1617
use Symfony\Component\Lock\Key;
1718
use Symfony\Component\Lock\StoreInterface;
1819

@@ -57,14 +58,15 @@ public function __construct(\Memcached $memcached, $initialTtl = 300)
5758
public function save(Key $key)
5859
{
5960
$token = $this->getToken($key);
60-
6161
$key->reduceLifetime($this->initialTtl);
62-
if ($this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
63-
return;
62+
if (!$this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
63+
// the lock is already acquired. It could be us. Let's try to put off.
64+
$this->putOffExpiration($key, $this->initialTtl);
6465
}
6566

66-
// the lock is already acquire. It could be us. Let's try to put off.
67-
$this->putOffExpiration($key, $this->initialTtl);
67+
if ($key->isExpired()) {
68+
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
69+
}
6870
}
6971

7072
public function waitAndSave(Key $key)
@@ -107,6 +109,10 @@ public function putOffExpiration(Key $key, $ttl)
107109
if (!$this->memcached->cas($cas, (string) $key, $token, $ttl)) {
108110
throw new LockConflictedException();
109111
}
112+
113+
if ($key->isExpired()) {
114+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
115+
}
110116
}
111117

112118
/**

‎src/Symfony/Component/Lock/Store/RedisStore.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Store/RedisStore.php
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\Lock\Exception\InvalidArgumentException;
1515
use Symfony\Component\Lock\Exception\LockConflictedException;
16+
use Symfony\Component\Lock\Exception\LockExpiredException;
1617
use Symfony\Component\Lock\Key;
1718
use Symfony\Component\Lock\StoreInterface;
1819

@@ -61,6 +62,10 @@ public function save(Key $key)
6162
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($this->initialTtl * 1000)))) {
6263
throw new LockConflictedException();
6364
}
65+
66+
if ($key->isExpired()) {
67+
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
68+
}
6469
}
6570

6671
public function waitAndSave(Key $key)
@@ -85,6 +90,10 @@ public function putOffExpiration(Key $key, $ttl)
8590
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($ttl * 1000)))) {
8691
throw new LockConflictedException();
8792
}
93+
94+
if ($key->isExpired()) {
95+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
96+
}
8897
}
8998

9099
/**

‎src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Tests/Store/CombinedStoreTest.php
+5-5Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,12 @@ public function testputOffExpirationThrowsExceptionOnFailure()
175175
$this->store1
176176
->expects($this->once())
177177
->method('putOffExpiration')
178-
->with($key, $ttl)
178+
->with($key, $this->lessThanOrEqual($ttl))
179179
->willThrowException(new LockConflictedException());
180180
$this->store2
181181
->expects($this->once())
182182
->method('putOffExpiration')
183-
->with($key, $ttl)
183+
->with($key, $this->lessThanOrEqual($ttl))
184184
->willThrowException(new LockConflictedException());
185185

186186
$this->strategy
@@ -203,12 +203,12 @@ public function testputOffExpirationCleanupOnFailure()
203203
$this->store1
204204
->expects($this->once())
205205
->method('putOffExpiration')
206-
->with($key, $ttl)
206+
->with($key, $this->lessThanOrEqual($ttl))
207207
->willThrowException(new LockConflictedException());
208208
$this->store2
209209
->expects($this->once())
210210
->method('putOffExpiration')
211-
->with($key, $ttl)
211+
->with($key, $this->lessThanOrEqual($ttl))
212212
->willThrowException(new LockConflictedException());
213213

214214
$this->store1
@@ -242,7 +242,7 @@ public function testputOffExpirationAbortWhenStrategyCantBeMet()
242242
$this->store1
243243
->expects($this->once())
244244
->method('putOffExpiration')
245-
->with($key, $ttl)
245+
->with($key, $this->lessThanOrEqual($ttl))
246246
->willThrowException(new LockConflictedException());
247247
$this->store2
248248
->expects($this->never())

‎src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php
+16Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ public function testExpiration()
5252
$this->assertFalse($store->exists($key));
5353
}
5454

55+
/**
56+
* Tests the store thrown exception when TTL expires.
57+
*
58+
* @expectedException \Symfony\Component\Lock\Exception\LockExpiredException
59+
*/
60+
public function testAbortAfterExpiration()
61+
{
62+
$key = new Key(uniqid(__METHOD__, true));
63+
64+
/** @var StoreInterface $store */
65+
$store = $this->getStore();
66+
67+
$store->save($key);
68+
$store->putOffExpiration($key, 1 / 1000000);
69+
}
70+
5571
/**
5672
* Tests the refresh can push the limits to the expiration.
5773
*

‎src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/Tests/Store/MemcachedStoreTest.php
+5Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,9 @@ public function getStore()
4949

5050
return new MemcachedStore($memcached);
5151
}
52+
53+
public function testAbortAfterExpiration()
54+
{
55+
$this->markTestSkipped('Memcached expecte a TTL greater than 1 sec. Simulating a slow network is too hard');
56+
}
5257
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.