From 9f960f34e7785d3bde67b850ac4754cc46f853bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Deruss=C3=A9?= Date: Mon, 17 Jun 2019 16:05:50 +0200 Subject: [PATCH] Fix expired lock not cleaned --- src/Symfony/Component/Lock/Lock.php | 10 +++++++ .../Component/Lock/Store/CombinedStore.php | 8 ++--- .../Lock/Store/ExpiringStoreTrait.php | 30 +++++++++++++++++++ .../Component/Lock/Store/MemcachedStore.php | 11 +++---- .../Component/Lock/Store/RedisStore.php | 11 +++---- .../Tests/Store/ExpiringStoreTestTrait.php | 25 ++++++++++++++++ 6 files changed, 77 insertions(+), 18 deletions(-) create mode 100644 src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php diff --git a/src/Symfony/Component/Lock/Lock.php b/src/Symfony/Component/Lock/Lock.php index 1a6c0e26ee0c..0d5714bde20c 100644 --- a/src/Symfony/Component/Lock/Lock.php +++ b/src/Symfony/Component/Lock/Lock.php @@ -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)); } @@ -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)); } diff --git a/src/Symfony/Component/Lock/Store/CombinedStore.php b/src/Symfony/Component/Lock/Store/CombinedStore.php index 38f3f35e750e..6038b3be93d3 100644 --- a/src/Symfony/Component/Lock/Store/CombinedStore.php +++ b/src/Symfony/Component/Lock/Store/CombinedStore.php @@ -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; @@ -30,6 +29,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface { use LoggerAwareTrait; + use ExpiringStoreTrait; /** @var StoreInterface[] */ private $stores; @@ -78,6 +78,8 @@ public function save(Key $key) } } + $this->checkNotExpired($key); + if ($this->strategy->isMet($successCount, $storesCount)) { return; } @@ -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; diff --git a/src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php b/src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php new file mode 100644 index 000000000000..e22c4405e415 --- /dev/null +++ b/src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php @@ -0,0 +1,30 @@ + + * + * 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)); + } + } +} diff --git a/src/Symfony/Component/Lock/Store/MemcachedStore.php b/src/Symfony/Component/Lock/Store/MemcachedStore.php index 9b795504c990..7a51d6149b30 100644 --- a/src/Symfony/Component/Lock/Store/MemcachedStore.php +++ b/src/Symfony/Component/Lock/Store/MemcachedStore.php @@ -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; @@ -24,6 +23,8 @@ */ class MemcachedStore implements StoreInterface { + use ExpiringStoreTrait; + private $memcached; private $initialTtl; /** @var bool */ @@ -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) @@ -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); } /** diff --git a/src/Symfony/Component/Lock/Store/RedisStore.php b/src/Symfony/Component/Lock/Store/RedisStore.php index 2d7ba4536678..6070c2e74e76 100644 --- a/src/Symfony/Component/Lock/Store/RedisStore.php +++ b/src/Symfony/Component/Lock/Store/RedisStore.php @@ -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; @@ -25,6 +24,8 @@ */ class RedisStore implements StoreInterface { + use ExpiringStoreTrait; + private $redis; private $initialTtl; @@ -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) @@ -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); } /** diff --git a/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php b/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php index f523780ce144..bc96ff66f5df 100644 --- a/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php +++ b/src/Symfony/Component/Lock/Tests/Store/ExpiringStoreTestTrait.php @@ -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; @@ -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)); + } }