From c5db81853e00c51b96af43fb0f31ed745386eb72 Mon Sep 17 00:00:00 2001 From: G81BVfaN <131437174+G81BVfaN@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:28:18 +0200 Subject: [PATCH 1/7] - Adding the possibility to specify a TTL (Time To Live) for the lock. --- src/Event.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Event.php b/src/Event.php index 0f67428..999e2e2 100644 --- a/src/Event.php +++ b/src/Event.php @@ -702,10 +702,11 @@ public function user($user) * that will be responsible for the locking. * * @param PersistingStoreInterface|object $store - * + * @param int|null $ttl * @return $this + * @throws \Crunz\Exception\CrunzException */ - public function preventOverlapping(?object $store = null) + public function preventOverlapping(?object $store = null, ?int $ttl = 30) { if (null !== $store && !($store instanceof PersistingStoreInterface)) { $expectedClass = PersistingStoreInterface::class; @@ -721,8 +722,8 @@ public function preventOverlapping(?object $store = null) $this->lockFactory = new LockFactory($lockStore); // Skip the event if it's locked (processing) - $this->skip(function () { - $lock = $this->createLockObject(); + $this->skip(function () use ($ttl) { + $lock = $this->createLockObject($ttl); $lock->acquire(); return !$lock->isAcquired(); @@ -1094,15 +1095,14 @@ public function everySixHours(): self /** * Get the symfony lock object for the task. * + * @param int|null $ttl * @return Lock */ - protected function createLockObject() + protected function createLockObject(?int $ttl = 30) { $this->checkLockFactory(); if (null === $this->lock && null !== $this->lockFactory) { - $ttl = 30; - $this->lock = $this->lockFactory ->createLock($this->lockKey(), $ttl); } From 352094425226299e77796b2345a1f6e8d765a4ff Mon Sep 17 00:00:00 2001 From: G81BVfaN <131437174+G81BVfaN@users.noreply.github.com> Date: Wed, 16 Oct 2024 13:30:44 +0200 Subject: [PATCH 2/7] - Adding the possibility to specify a TTL (Time To Live) for the lock. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 6a93f1e..1b85a7a 100644 --- a/README.md +++ b/README.md @@ -508,6 +508,11 @@ $task ->preventOverlapping($store); ``` +Optionally, you can specify a TTL (Time To Live) for the lock + +```php +$task->preventOverlapping($store, 60); // Lock expires after 60 seconds +``` ## Keeping the Output From 6dd28e56f1ddd9a44b0520493d90adff259bdc56 Mon Sep 17 00:00:00 2001 From: akhateeb Date: Thu, 17 Oct 2024 11:32:34 +0200 Subject: [PATCH 3/7] - A little fix suggested from PHP CS Fixer --- src/Event.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Event.php b/src/Event.php index 999e2e2..65704c8 100644 --- a/src/Event.php +++ b/src/Event.php @@ -704,7 +704,7 @@ public function user($user) * @param PersistingStoreInterface|object $store * @param int|null $ttl * @return $this - * @throws \Crunz\Exception\CrunzException + * @throws CrunzException */ public function preventOverlapping(?object $store = null, ?int $ttl = 30) { From c9666d973414d82a1ae9721c11c480e24d76a8c4 Mon Sep 17 00:00:00 2001 From: akhateeb Date: Thu, 21 Nov 2024 12:21:09 +0100 Subject: [PATCH 4/7] - Stop refreshing the lock's TTL, if the lock store supports expiring. --- src/Event.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Event.php b/src/Event.php index 65704c8..fe52c28 100644 --- a/src/Event.php +++ b/src/Event.php @@ -1009,7 +1009,8 @@ public function errorCallbacks() } /** - * If this event is prevented from overlapping, this method should be called regularly to refresh the lock. + * If this event is prevented from overlapping, this method should be called regularly to refresh the lock, + * UNLESS the lock store supports expiring (TTL), which means no refresh is needed. */ public function refreshLock(): void { @@ -1017,6 +1018,13 @@ public function refreshLock(): void return; } + // If the lock has remaining lifetime (i.e. the method returns a float and not NULL), that means the LockStore does support TTL [ 'MemcachedStore', 'MongoDbStore' , 'PdoStore', 'DoctrineDbalStore', 'RedisStore' ] + // @see https://symfony.com/doc/6.4/components/lock.html#available-stores + $remainingLifetime = $this->lock->getRemainingLifetime(); + if (null !== $remainingLifetime) { + return; + }; + $lock = $this->createLockObject(); $remainingLifetime = $lock->getRemainingLifetime(); From 5f2482360cdce00462a0898562485a8c934403d6 Mon Sep 17 00:00:00 2001 From: akhateeb Date: Thu, 21 Nov 2024 13:57:19 +0100 Subject: [PATCH 5/7] - Some PHP CS Fixer's fixes & workarounds. --- src/Event.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Event.php b/src/Event.php index fe52c28..8e7a052 100644 --- a/src/Event.php +++ b/src/Event.php @@ -701,9 +701,11 @@ public function user($user) * By default, the lock is acquired through file system locks. Alternatively, you can pass a symfony lock store * that will be responsible for the locking. * - * @param PersistingStoreInterface|object $store - * @param int|null $ttl + * @param PersistingStoreInterface|object|null $store A symfony lock store + * @param int|null $ttl Time To Live of the lock in seconds + * * @return $this + * * @throws CrunzException */ public function preventOverlapping(?object $store = null, ?int $ttl = 30) @@ -1019,11 +1021,11 @@ public function refreshLock(): void } // If the lock has remaining lifetime (i.e. the method returns a float and not NULL), that means the LockStore does support TTL [ 'MemcachedStore', 'MongoDbStore' , 'PdoStore', 'DoctrineDbalStore', 'RedisStore' ] - // @see https://symfony.com/doc/6.4/components/lock.html#available-stores + // @see https://symfony.com/doc/6.4/components/lock.html#available-stores for detailed information $remainingLifetime = $this->lock->getRemainingLifetime(); if (null !== $remainingLifetime) { return; - }; + } $lock = $this->createLockObject(); $remainingLifetime = $lock->getRemainingLifetime(); @@ -1103,7 +1105,8 @@ public function everySixHours(): self /** * Get the symfony lock object for the task. * - * @param int|null $ttl + * @param int|null $ttl Time To Live of the lock in seconds + * * @return Lock */ protected function createLockObject(?int $ttl = 30) From dc602181411d61044f8af8da681415f1b044f17f Mon Sep 17 00:00:00 2001 From: akhateeb Date: Mon, 30 Jun 2025 13:15:58 +0200 Subject: [PATCH 6/7] Fix type hint for TTL parameter in locking methods. --- src/Event.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Event.php b/src/Event.php index 8e7a052..000d05f 100644 --- a/src/Event.php +++ b/src/Event.php @@ -702,13 +702,13 @@ public function user($user) * that will be responsible for the locking. * * @param PersistingStoreInterface|object|null $store A symfony lock store - * @param int|null $ttl Time To Live of the lock in seconds + * @param int $ttl Time To Live of the lock in seconds * * @return $this * * @throws CrunzException */ - public function preventOverlapping(?object $store = null, ?int $ttl = 30) + public function preventOverlapping(?object $store = null, int $ttl = 30) { if (null !== $store && !($store instanceof PersistingStoreInterface)) { $expectedClass = PersistingStoreInterface::class; @@ -1105,11 +1105,11 @@ public function everySixHours(): self /** * Get the symfony lock object for the task. * - * @param int|null $ttl Time To Live of the lock in seconds + * @param int $ttl Time To Live of the lock in seconds * * @return Lock */ - protected function createLockObject(?int $ttl = 30) + protected function createLockObject(int $ttl = 30) { $this->checkLockFactory(); From 487a9c6c8a9f7a6034d0030a171d6b519dce0a06 Mon Sep 17 00:00:00 2001 From: akhateeb Date: Mon, 30 Jun 2025 13:18:47 +0200 Subject: [PATCH 7/7] Add test for expiring store with TTL in preventOverlapping method. --- tests/Unit/EventTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Unit/EventTest.php b/tests/Unit/EventTest.php index ea02e5a..497d54b 100644 --- a/tests/Unit/EventTest.php +++ b/tests/Unit/EventTest.php @@ -476,6 +476,21 @@ public function test_non_blocking_store_can_be_passed_to_prevent_overlapping(): $event->preventOverlapping($store); } + public function test_expiring_store_can_be_passed_to_prevent_overlapping_with_ttl(): void + { + // Arrange + $store = new PdoStore(''); + $ttl = 3; + + $event = $this->createEvent(); + + // Expect + $this->expectNotToPerformAssertions(); + + // Act + $event->preventOverlapping($store, $ttl); + } + /** * @param \Closure(): array{ * now: \DateTimeImmutable,