Skip to content

Commit

Permalink
bug #32071 Fix expired lock not cleaned (jderusse)
Browse files Browse the repository at this point in the history
This PR was merged into the 3.4 branch.

Discussion
----------

Fix expired lock not cleaned

| Q             | A
| ------------- | ---
| Branch?       | 3.4
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #31426
| License       | MIT
| Doc PR        | NA

When a lock is acquired BUT not as fast as expected, a LockExpiredException is thrown.
Issue is, that the lock is not removed which avoid other process to acquire that lock.

This PR clean state of store when a LockExpiredException is triggered.

note: same bug should be fixed in 4.3 in PDO and Zookeeper

Commits
-------

9f960f3 Fix expired lock not cleaned
  • Loading branch information
fabpot committed Jun 17, 2019
2 parents a8520ae + 9f960f3 commit cfc8ac0
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 18 deletions.
10 changes: 10 additions & 0 deletions src/Symfony/Component/Lock/Lock.php
Expand Up @@ -83,6 +83,11 @@ public function acquire($blocking = false)
}

if ($this->key->isExpired()) {
try {
$this->release();
} catch (\Exception $e) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $this->key));
}

Expand Down Expand Up @@ -117,6 +122,11 @@ public function refresh()
$this->dirty = true;

if ($this->key->isExpired()) {
try {
$this->release();
} catch (\Exception $e) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $this->key));
}

Expand Down
8 changes: 4 additions & 4 deletions src/Symfony/Component/Lock/Store/CombinedStore.php
Expand Up @@ -16,7 +16,6 @@
use Psr\Log\NullLogger;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Exception\NotSupportedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
Expand All @@ -30,6 +29,7 @@
class CombinedStore implements StoreInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
use ExpiringStoreTrait;

/** @var StoreInterface[] */
private $stores;
Expand Down Expand Up @@ -78,6 +78,8 @@ public function save(Key $key)
}
}

$this->checkNotExpired($key);

if ($this->strategy->isMet($successCount, $storesCount)) {
return;
}
Expand Down Expand Up @@ -125,9 +127,7 @@ public function putOffExpiration(Key $key, $ttl)
}
}

if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
}
$this->checkNotExpired($key);

if ($this->strategy->isMet($successCount, $storesCount)) {
return;
Expand Down
30 changes: 30 additions & 0 deletions src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php
@@ -0,0 +1,30 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\Lock\Store;

use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;

trait ExpiringStoreTrait
{
private function checkNotExpired(Key $key)
{
if ($key->isExpired()) {
try {
$this->delete($key);
} catch (\Exception $e) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
}
}
11 changes: 4 additions & 7 deletions src/Symfony/Component/Lock/Store/MemcachedStore.php
Expand Up @@ -13,7 +13,6 @@

use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;

Expand All @@ -24,6 +23,8 @@
*/
class MemcachedStore implements StoreInterface
{
use ExpiringStoreTrait;

private $memcached;
private $initialTtl;
/** @var bool */
Expand Down Expand Up @@ -64,9 +65,7 @@ public function save(Key $key)
$this->putOffExpiration($key, $this->initialTtl);
}

if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
$this->checkNotExpired($key);
}

public function waitAndSave(Key $key)
Expand Down Expand Up @@ -110,9 +109,7 @@ public function putOffExpiration(Key $key, $ttl)
throw new LockConflictedException();
}

if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
}
$this->checkNotExpired($key);
}

/**
Expand Down
11 changes: 4 additions & 7 deletions src/Symfony/Component/Lock/Store/RedisStore.php
Expand Up @@ -14,7 +14,6 @@
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;

Expand All @@ -25,6 +24,8 @@
*/
class RedisStore implements StoreInterface
{
use ExpiringStoreTrait;

private $redis;
private $initialTtl;

Expand Down Expand Up @@ -66,9 +67,7 @@ public function save(Key $key)
throw new LockConflictedException();
}

if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
$this->checkNotExpired($key);
}

public function waitAndSave(Key $key)
Expand All @@ -94,9 +93,7 @@ public function putOffExpiration(Key $key, $ttl)
throw new LockConflictedException();
}

if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
}
$this->checkNotExpired($key);
}

/**
Expand Down
25 changes: 25 additions & 0 deletions src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Lock\Tests\Store;

use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;

Expand Down Expand Up @@ -105,4 +106,28 @@ public function testSetExpiration()
$this->assertGreaterThanOrEqual(0, $key->getRemainingLifetime());
$this->assertLessThanOrEqual(1, $key->getRemainingLifetime());
}

public function testExpiredLockCleaned()
{
$resource = uniqid(__METHOD__, true);

$key1 = new Key($resource);
$key2 = new Key($resource);

/** @var StoreInterface $store */
$store = $this->getStore();
$key1->reduceLifetime(0);

$this->assertTrue($key1->isExpired());
try {
$store->save($key1);
$this->fail('The store shouldn\'t have save an expired key');
} catch (LockExpiredException $e) {
}

$this->assertFalse($store->exists($key1));

$store->save($key2);
$this->assertTrue($store->exists($key2));
}
}

0 comments on commit cfc8ac0

Please sign in to comment.