Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2e4f434

Browse files
committed
Check TTL expiration in lock acquisition
1 parent c708d02 commit 2e4f434

10 files changed

+112
-29
lines changed
Lines 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 <[email protected]>
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é <[email protected]>
18+
*/
19+
class LockExpiredException extends \RuntimeException implements ExceptionInterface
20+
{
21+
}

src/Symfony/Component/Lock/Key.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
final class Key
2020
{
2121
private $resource;
22-
private $expiringDate;
22+
private $expiringTime;
2323
private $state = array();
2424

2525
/**
@@ -77,23 +77,31 @@ public function getState($stateKey)
7777
*/
7878
public function reduceLifetime($ttl)
7979
{
80-
$newExpiringDate = \DateTimeImmutable::createFromFormat('U.u', (string) (microtime(true) + $ttl));
80+
$newTime = microtime(true) + $ttl;
8181

82-
if (null === $this->expiringDate || $newExpiringDate < $this->expiringDate) {
83-
$this->expiringDate = $newExpiringDate;
82+
if (null === $this->expiringTime || $this->expiringTime > $newTime) {
83+
$this->expiringTime = $newTime;
8484
}
8585
}
8686

8787
public function resetExpiringDate()
8888
{
89-
$this->expiringDate = null;
89+
$this->expiringTime = null;
9090
}
9191

9292
/**
9393
* @return \DateTimeImmutable
9494
*/
9595
public function getExpiringDate()
9696
{
97-
return $this->expiringDate;
97+
return \DateTimeImmutable::createFromFormat('U.u', (string) $this->expiringTime);
98+
}
99+
100+
/**
101+
* @return bool
102+
*/
103+
public function isExpired()
104+
{
105+
return null !== $this->expiringTime && $this->expiringTime <= microtime(true);
98106
}
99107
}

src/Symfony/Component/Lock/Lock.php

Lines 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

Lines changed: 13 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,17 @@ 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+
$key->reduceLifetime(0);
113+
break;
114+
}
115+
116+
$store->putOffExpiration($key, $adjustedTtl);
109117
++$successCount;
110118
} catch (\Exception $e) {
111119
$this->logger->warning('One store failed to put off the expiration of the "{resource}" lock.', array('resource' => $key, 'store' => $store, 'exception' => $e));
@@ -117,6 +125,10 @@ public function putOffExpiration(Key $key, $ttl)
117125
}
118126
}
119127

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

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

Lines 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

Lines 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/LockTest.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -175,12 +175,12 @@ public function testExpiration($ttls, $expected)
175175

176176
public function provideExpiredDates()
177177
{
178-
yield array(array(-1.0), true);
179-
yield array(array(1, -1.0), true);
180-
yield array(array(-1.0, 1), true);
178+
yield array(array(-0.1), true);
179+
yield array(array(0.1, -0.1), true);
180+
yield array(array(-0.1, 0.1), true);
181181

182182
yield array(array(), false);
183-
yield array(array(1), false);
184-
yield array(array(-1.0, null), false);
183+
yield array(array(0.1), false);
184+
yield array(array(-0.1, null), false);
185185
}
186186
}

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

Lines 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

Lines changed: 18 additions & 2 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
*
@@ -69,10 +85,10 @@ public function testRefreshLock()
6985
$store = $this->getStore();
7086

7187
$store->save($key);
72-
$store->putOffExpiration($key, 1.0 * $clockDelay / 1000000);
88+
$store->putOffExpiration($key, $clockDelay / 1000000);
7389
$this->assertTrue($store->exists($key));
7490

75-
usleep(2.1 * $clockDelay);
91+
usleep(2 * $clockDelay);
7692
$this->assertFalse($store->exists($key));
7793
}
7894

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

Lines 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 expects a TTL greater than 1 sec. Simulating a slow network is too hard');
56+
}
5257
}

0 commit comments

Comments
 (0)