diff --git a/src/Symfony/Component/Lock/Key.php b/src/Symfony/Component/Lock/Key.php index 5b53ae9b6b078..5069901761820 100644 --- a/src/Symfony/Component/Lock/Key.php +++ b/src/Symfony/Component/Lock/Key.php @@ -19,6 +19,7 @@ final class Key { private $resource; + private $expiringDate; private $state = array(); /** @@ -70,4 +71,29 @@ public function getState($stateKey) { return $this->state[$stateKey]; } + + /** + * @param float $ttl The expiration delay of locks in seconds. + */ + public function reduceLifetime($ttl) + { + $newExpiringDate = \DateTimeImmutable::createFromFormat('U.u', (string) (microtime(true) + $ttl)); + + if (null === $this->expiringDate || $newExpiringDate < $this->expiringDate) { + $this->expiringDate = $newExpiringDate; + } + } + + public function resetExpiringDate() + { + $this->expiringDate = null; + } + + /** + * @return \DateTimeImmutable + */ + public function getExpiringDate() + { + return $this->expiringDate; + } } diff --git a/src/Symfony/Component/Lock/Lock.php b/src/Symfony/Component/Lock/Lock.php index 5d5342a528002..c42bd2255cf5f 100644 --- a/src/Symfony/Component/Lock/Lock.php +++ b/src/Symfony/Component/Lock/Lock.php @@ -89,6 +89,7 @@ public function refresh() } try { + $this->key->resetExpiringDate(); $this->store->putOffExpiration($this->key, $this->ttl); $this->logger->info('Expiration defined for "{resource}" lock for "{ttl}" seconds.', array('resource' => $this->key, 'ttl' => $this->ttl)); } catch (LockConflictedException $e) { @@ -120,4 +121,21 @@ public function release() throw new LockReleasingException(sprintf('Failed to release the "%s" lock.', $this->key)); } } + + /** + * @return bool + */ + public function isExpired() + { + if (null === $expireDate = $this->key->getExpiringDate()) { + return false; + } + + return $expireDate <= new \DateTime(); + } + + public function getExpiringDate() + { + return $this->key->getExpiringDate(); + } } diff --git a/src/Symfony/Component/Lock/Store/MemcachedStore.php b/src/Symfony/Component/Lock/Store/MemcachedStore.php index a1e31ee63320f..4f8d6f5ff7159 100644 --- a/src/Symfony/Component/Lock/Store/MemcachedStore.php +++ b/src/Symfony/Component/Lock/Store/MemcachedStore.php @@ -58,6 +58,7 @@ public function save(Key $key) { $token = $this->getToken($key); + $key->reduceLifetime($this->initialTtl); if ($this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) { return; } @@ -87,6 +88,7 @@ public function putOffExpiration(Key $key, $ttl) list($value, $cas) = $this->getValueAndCas($key); + $key->reduceLifetime($ttl); // Could happens when we ask a putOff after a timeout but in luck nobody steal the lock if (\Memcached::RES_NOTFOUND === $this->memcached->getResultCode()) { if ($this->memcached->add((string) $key, $token, $ttl)) { diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php index b9ea2a5fb80e3..d67120fe0c0c3 100644 --- a/src/Symfony/Component/Lock/Store/RedisStore.php +++ b/src/Symfony/Component/Lock/Store/RedisStore.php @@ -57,8 +57,8 @@ public function save(Key $key) end '; - $expire = (int) ceil($this->initialTtl * 1000); - if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) { + $key->reduceLifetime($this->initialTtl); + if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($this->initialTtl * 1000)))) { throw new LockConflictedException(); } } @@ -81,8 +81,8 @@ public function putOffExpiration(Key $key, $ttl) end '; - $expire = (int) ceil($ttl * 1000); - if (!$this->evaluate($script, (string) $key, array($this->getToken($key), $expire))) { + $key->reduceLifetime($ttl); + if (!$this->evaluate($script, (string) $key, array($this->getToken($key), (int) ceil($ttl * 1000)))) { throw new LockConflictedException(); } } diff --git a/src/Symfony/Component/Lock/Tests/LockTest.php b/src/Symfony/Component/Lock/Tests/LockTest.php index 58dbdc5820131..fb107232efba6 100644 --- a/src/Symfony/Component/Lock/Tests/LockTest.php +++ b/src/Symfony/Component/Lock/Tests/LockTest.php @@ -153,4 +153,34 @@ public function testReleaseThrowsExceptionIfNotWellDeleted() $lock->release(); } + + /** + * @dataProvider provideExpiredDates + */ + public function testExpiration($ttls, $expected) + { + $key = new Key(uniqid(__METHOD__, true)); + $store = $this->getMockBuilder(StoreInterface::class)->getMock(); + $lock = new Lock($key, $store, 10); + + foreach ($ttls as $ttl) { + if (null === $ttl) { + $key->resetExpiringDate(); + } else { + $key->reduceLifetime($ttl); + } + } + $this->assertSame($expected, $lock->isExpired()); + } + + public function provideExpiredDates() + { + yield array(array(-1.0), true); + yield array(array(1, -1.0), true); + yield array(array(-1.0, 1), true); + + yield array(array(), false); + yield array(array(1), false); + yield array(array(-1.0, null), false); + } } diff --git a/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php index c0d758744ce1a..630ba743cc4e8 100644 --- a/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/AbstractStoreTest.php @@ -49,14 +49,17 @@ public function testSaveWithDifferentResources() $store->save($key1); $this->assertTrue($store->exists($key1)); $this->assertFalse($store->exists($key2)); - $store->save($key2); + $store->save($key2); $this->assertTrue($store->exists($key1)); $this->assertTrue($store->exists($key2)); $store->delete($key1); $this->assertFalse($store->exists($key1)); + $this->assertTrue($store->exists($key2)); + $store->delete($key2); + $this->assertFalse($store->exists($key1)); $this->assertFalse($store->exists($key2)); } @@ -74,7 +77,7 @@ public function testSaveWithDifferentKeysOnSameResources() try { $store->save($key2); - throw new \Exception('The store shouldn\'t save the second key'); + $this->fail('The store shouldn\'t save the second key'); } catch (LockConflictedException $e) { } diff --git a/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php b/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php index 0280aa61739ee..f862f0d4aa300 100644 --- a/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php +++ b/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php @@ -75,4 +75,16 @@ public function testRefreshLock() usleep(2.1 * $clockDelay); $this->assertFalse($store->exists($key)); } + + public function testSetExpiration() + { + $key = new Key(uniqid(__METHOD__, true)); + + /** @var StoreInterface $store */ + $store = $this->getStore(); + + $store->save($key); + $store->putOffExpiration($key, 1); + $this->assertNotNull($key->getExpiringDate()); + } }