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

Skip to content

Commit 0bd266a

Browse files
committed
Check TTL expiration in lock acquisition
1 parent c36262e commit 0bd266a

File tree

7 files changed

+86
-14
lines changed

7 files changed

+86
-14
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 LockConflictedException
20+
{
21+
}

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

Lines 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('TTL expires during the 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 (microtime(true) >= $expireAt) {
128+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock.', $key));
129+
}
130+
120131
if ($this->strategy->isMet($successCount, $storesCount)) {
121132
return;
122133
}

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

Lines changed: 14 additions & 4 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

@@ -58,12 +59,15 @@ public function save(Key $key)
5859
{
5960
$token = $this->getToken($key);
6061

61-
if ($this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
62-
return;
62+
$expireAt = microtime(true) + $this->initialTtl;
63+
if (!$this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
64+
// the lock is already acquired. It could be us. Let's try to put off.
65+
$this->putOffExpiration($key, $this->initialTtl);
6366
}
6467

65-
// the lock is already acquire. It could be us. Let's try to put off.
66-
$this->putOffExpiration($key, $this->initialTtl);
68+
if (microtime(true) >= $expireAt) {
69+
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
70+
}
6771
}
6872

6973
public function waitAndSave(Key $key)
@@ -80,6 +84,8 @@ public function putOffExpiration(Key $key, $ttl)
8084
throw new InvalidArgumentException(sprintf('%s() expects a TTL greater or equals to 1. Got %s.', __METHOD__, $ttl));
8185
}
8286

87+
$expireAt = microtime(true) + $ttl;
88+
8389
// Interface defines a float value but Store required an integer.
8490
$ttl = (int) ceil($ttl);
8591

@@ -105,6 +111,10 @@ public function putOffExpiration(Key $key, $ttl)
105111
if (!$this->memcached->cas($cas, (string) $key, $token, $ttl)) {
106112
throw new LockConflictedException();
107113
}
114+
115+
if (microtime(true) >= $expireAt) {
116+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock.', $key));
117+
}
108118
}
109119

110120
/**

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

Lines changed: 13 additions & 4 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,10 +58,14 @@ public function save(Key $key)
5758
end
5859
';
5960

60-
$expire = (int) ceil($this->initialTtl * 1000);
61-
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) {
61+
$expireAt = microtime(true) + $this->initialTtl;
62+
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($this->initialTtl * 1000)))) {
6263
throw new LockConflictedException();
6364
}
65+
66+
if (microtime(true) >= $expireAt) {
67+
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
68+
}
6469
}
6570

6671
public function waitAndSave(Key $key)
@@ -81,10 +86,14 @@ public function putOffExpiration(Key $key, $ttl)
8186
end
8287
';
8388

84-
$expire = (int) ceil($ttl * 1000);
85-
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) {
89+
$expireAt = microtime(true) + $ttl;
90+
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($ttl * 1000)))) {
8691
throw new LockConflictedException();
8792
}
93+
94+
if (microtime(true) >= $expireAt) {
95+
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock.', $key));
96+
}
8897
}
8998

9099
/**

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: 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

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

0 commit comments

Comments
 (0)