Skip to content

Commit dda57e6

Browse files
committed
feature symfony#22543 [Lock] Expose an expiringDate and isExpired method in Lock (jderusse)
This PR was squashed before being merged into the 3.4 branch (closes symfony#22543). Discussion ---------- [Lock] Expose an expiringDate and isExpired method in Lock | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | symfony#22452 | License | MIT | Doc PR | NA This PR store the expiration date of the lock in the key and exposes public method to let the user know the state of the Lock expiration. Commits ------- 2794308 [Lock] Expose an expiringDate and isExpired method in Lock
2 parents 173b747 + 2794308 commit dda57e6

File tree

7 files changed

+97
-6
lines changed

7 files changed

+97
-6
lines changed

src/Symfony/Component/Lock/Key.php

+26
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
final class Key
2020
{
2121
private $resource;
22+
private $expiringDate;
2223
private $state = array();
2324

2425
/**
@@ -70,4 +71,29 @@ public function getState($stateKey)
7071
{
7172
return $this->state[$stateKey];
7273
}
74+
75+
/**
76+
* @param float $ttl The expiration delay of locks in seconds.
77+
*/
78+
public function reduceLifetime($ttl)
79+
{
80+
$newExpiringDate = \DateTimeImmutable::createFromFormat('U.u', (string) (microtime(true) + $ttl));
81+
82+
if (null === $this->expiringDate || $newExpiringDate < $this->expiringDate) {
83+
$this->expiringDate = $newExpiringDate;
84+
}
85+
}
86+
87+
public function resetExpiringDate()
88+
{
89+
$this->expiringDate = null;
90+
}
91+
92+
/**
93+
* @return \DateTimeImmutable
94+
*/
95+
public function getExpiringDate()
96+
{
97+
return $this->expiringDate;
98+
}
7399
}

src/Symfony/Component/Lock/Lock.php

+18
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ public function refresh()
8989
}
9090

9191
try {
92+
$this->key->resetExpiringDate();
9293
$this->store->putOffExpiration($this->key, $this->ttl);
9394
$this->logger->info('Expiration defined for "{resource}" lock for "{ttl}" seconds.', array('resource' => $this->key, 'ttl' => $this->ttl));
9495
} catch (LockConflictedException $e) {
@@ -120,4 +121,21 @@ public function release()
120121
throw new LockReleasingException(sprintf('Failed to release the "%s" lock.', $this->key));
121122
}
122123
}
124+
125+
/**
126+
* @return bool
127+
*/
128+
public function isExpired()
129+
{
130+
if (null === $expireDate = $this->key->getExpiringDate()) {
131+
return false;
132+
}
133+
134+
return $expireDate <= new \DateTime();
135+
}
136+
137+
public function getExpiringDate()
138+
{
139+
return $this->key->getExpiringDate();
140+
}
123141
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ public function save(Key $key)
5858
{
5959
$token = $this->getToken($key);
6060

61+
$key->reduceLifetime($this->initialTtl);
6162
if ($this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
6263
return;
6364
}
@@ -87,6 +88,7 @@ public function putOffExpiration(Key $key, $ttl)
8788

8889
list($value, $cas) = $this->getValueAndCas($key);
8990

91+
$key->reduceLifetime($ttl);
9092
// Could happens when we ask a putOff after a timeout but in luck nobody steal the lock
9193
if (\Memcached::RES_NOTFOUND === $this->memcached->getResultCode()) {
9294
if ($this->memcached->add((string) $key, $token, $ttl)) {

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ public function save(Key $key)
5757
end
5858
';
5959

60-
$expire = (int) ceil($this->initialTtl * 1000);
61-
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) {
60+
$key->reduceLifetime($this->initialTtl);
61+
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($this->initialTtl * 1000)))) {
6262
throw new LockConflictedException();
6363
}
6464
}
@@ -81,8 +81,8 @@ public function putOffExpiration(Key $key, $ttl)
8181
end
8282
';
8383

84-
$expire = (int) ceil($ttl * 1000);
85-
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) {
84+
$key->reduceLifetime($ttl);
85+
if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($ttl * 1000)))) {
8686
throw new LockConflictedException();
8787
}
8888
}

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

+30
Original file line numberDiff line numberDiff line change
@@ -153,4 +153,34 @@ public function testReleaseThrowsExceptionIfNotWellDeleted()
153153

154154
$lock->release();
155155
}
156+
157+
/**
158+
* @dataProvider provideExpiredDates
159+
*/
160+
public function testExpiration($ttls, $expected)
161+
{
162+
$key = new Key(uniqid(__METHOD__, true));
163+
$store = $this->getMockBuilder(StoreInterface::class)->getMock();
164+
$lock = new Lock($key, $store, 10);
165+
166+
foreach ($ttls as $ttl) {
167+
if (null === $ttl) {
168+
$key->resetExpiringDate();
169+
} else {
170+
$key->reduceLifetime($ttl);
171+
}
172+
}
173+
$this->assertSame($expected, $lock->isExpired());
174+
}
175+
176+
public function provideExpiredDates()
177+
{
178+
yield array(array(-1.0), true);
179+
yield array(array(1, -1.0), true);
180+
yield array(array(-1.0, 1), true);
181+
182+
yield array(array(), false);
183+
yield array(array(1), false);
184+
yield array(array(-1.0, null), false);
185+
}
156186
}

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

+5-2
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,17 @@ public function testSaveWithDifferentResources()
4949
$store->save($key1);
5050
$this->assertTrue($store->exists($key1));
5151
$this->assertFalse($store->exists($key2));
52-
$store->save($key2);
5352

53+
$store->save($key2);
5454
$this->assertTrue($store->exists($key1));
5555
$this->assertTrue($store->exists($key2));
5656

5757
$store->delete($key1);
5858
$this->assertFalse($store->exists($key1));
59+
$this->assertTrue($store->exists($key2));
60+
5961
$store->delete($key2);
62+
$this->assertFalse($store->exists($key1));
6063
$this->assertFalse($store->exists($key2));
6164
}
6265

@@ -74,7 +77,7 @@ public function testSaveWithDifferentKeysOnSameResources()
7477

7578
try {
7679
$store->save($key2);
77-
throw new \Exception('The store shouldn\'t save the second key');
80+
$this->fail('The store shouldn\'t save the second key');
7881
} catch (LockConflictedException $e) {
7982
}
8083

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

+12
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,16 @@ public function testRefreshLock()
7575
usleep(2.1 * $clockDelay);
7676
$this->assertFalse($store->exists($key));
7777
}
78+
79+
public function testSetExpiration()
80+
{
81+
$key = new Key(uniqid(__METHOD__, true));
82+
83+
/** @var StoreInterface $store */
84+
$store = $this->getStore();
85+
86+
$store->save($key);
87+
$store->putOffExpiration($key, 1);
88+
$this->assertNotNull($key->getExpiringDate());
89+
}
7890
}

0 commit comments

Comments
 (0)