feature #22543 [Lock] Expose an expiringDate and isExpired method in Lock (jderusse)

This PR was squashed before being merged into the 3.4 branch (closes #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 | #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
-------

279430800e [Lock] Expose an expiringDate and isExpired method in Lock
This commit is contained in:
Fabien Potencier 2017-08-29 13:57:32 -07:00
commit dda57e6e21
7 changed files with 97 additions and 6 deletions

View File

@ -19,6 +19,7 @@ namespace Symfony\Component\Lock;
final class Key
{
private $resource;
private $expiringDate;
private $state = array();
/**
@ -70,4 +71,29 @@ final class Key
{
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;
}
}

View File

@ -89,6 +89,7 @@ final class Lock implements LockInterface, LoggerAwareInterface
}
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 @@ final class Lock implements LockInterface, LoggerAwareInterface
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();
}
}

View File

@ -58,6 +58,7 @@ class MemcachedStore implements StoreInterface
{
$token = $this->getToken($key);
$key->reduceLifetime($this->initialTtl);
if ($this->memcached->add((string) $key, $token, (int) ceil($this->initialTtl))) {
return;
}
@ -87,6 +88,7 @@ class MemcachedStore implements StoreInterface
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)) {

View File

@ -57,8 +57,8 @@ class RedisStore implements StoreInterface
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 @@ class RedisStore implements StoreInterface
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();
}
}

View File

@ -153,4 +153,34 @@ class LockTest extends TestCase
$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);
}
}

View File

@ -49,14 +49,17 @@ abstract class AbstractStoreTest extends TestCase
$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 @@ abstract class AbstractStoreTest extends TestCase
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) {
}

View File

@ -75,4 +75,16 @@ trait ExpiringStoreTestTrait
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());
}
}