From 2e8748df0c47aee4601c242e90b3476c73597522 Mon Sep 17 00:00:00 2001 From: Mathieu Rochette Date: Fri, 22 Jan 2016 22:34:53 +0100 Subject: [PATCH] add LockSet closes #4 --- README.md | 28 +++++---- spec/TH/Lock/LockSetSpec.php | 112 +++++++++++++++++++++++++++++++++++ src/AggregationException.php | 14 +++++ src/FileLock.php | 1 - src/LockSet.php | 67 +++++++++++++++++++++ 5 files changed, 210 insertions(+), 12 deletions(-) create mode 100644 spec/TH/Lock/LockSetSpec.php create mode 100644 src/AggregationException.php create mode 100644 src/LockSet.php diff --git a/README.md b/README.md index 64504ca..6aa4edc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ composer require texthtml/php-lock ## Usage -You can create an object that represent a lock on a file. You can then try to acquire that lock by calling `$lock->acquire()`. If the lock fail it will throw an `Exception` (useful for CLI tools built with [Symfony Console Components documentation](http://symfony.com/doc/current/components/console/introduction.html)). If the lock is acquired the program can continue. +You can create an object that represent a lock on a file. You can then try to acquire that lock by calling `$lock->acquire()`. If the lock fail it will throw a `\TH\Lock\Exception` (useful for CLI tools built with [Symfony Console Components documentation](http://symfony.com/doc/current/components/console/introduction.html)). If the lock is acquired the program can continue. ### Locking a file exclusively @@ -41,7 +41,6 @@ $lock->release(); ### Sharing a lock on a file ```php - use TH\Lock\FileLock; $lock = new FileLock('/path/to/file', FileLock::SHARED); @@ -81,7 +80,7 @@ batch(); When you don't want some crontabs to overlap you can make a lock on the same file in each crontab. The `TH\Lock\LockFactory` can ease the process and provide more helpful message in case of overlap. -``` +```php $lock = $factory->create('protected resource', 'process 1'); $lock->acquire(); @@ -89,7 +88,7 @@ $lock->acquire(); // process 1 does stuff ``` -``` +```php $lock = $factory->create('protected resource', 'process 2'); $lock->acquire(); @@ -106,23 +105,30 @@ When process 1 is running and we start process 2, an Exception will be thrown: " The only `LockFactory` available at the moment is the `TH\Lock\FileFactory`. This factory autmatically create lock files for your resources in the specified folder. ```php - use TH\Lock\FileFactory; $factory = new FileFactory('/path/to/lock_dir/'); $lock = $factory->create('resource identifier'); ``` -## API +### Aggregating locks -There are two methods you can use on a `FileLock`: +If you want to simplify acquiring multiple locks at once, you can use the `\TH\Lock\LockSet`: -* `\TH\Lock\FileLock::acquire()` used to acquire a lock on the file -* `\TH\Lock\FileLock::release()` used to release a lock on the file +```php +use TH\Lock\LockSet; + +$superLock = new LockSet([$lock1, $lock2, $lock3]); +// You can make a set with any types of locks (eg: FileLock, RedisSimpleLock or another nested LockSet) + +$superLock->acquire(); + +// all locks will be released when $superLock is destroyed or when `$superLock->release()` is called +``` -And one on a `FileFactory`: +It will try to acquire all locks, if it fails it will release the lock that have been acquired to avoid locking other processes. -* `\TH\Lock\FileFactory::create($resource, $exclusive = FileLock::EXCLUSIVE, $blocking = FileLock::NON_BLOCKING)` used to create a `FileLock` for `$resource` +note: `Lock` put inside a `LockSet` should not be used manually anymore ## Notes diff --git a/spec/TH/Lock/LockSetSpec.php b/spec/TH/Lock/LockSetSpec.php new file mode 100644 index 0000000..e35d2f7 --- /dev/null +++ b/spec/TH/Lock/LockSetSpec.php @@ -0,0 +1,112 @@ +beConstructedWith([$lock1]); + } + + public function letgo(Lock $lock1, Lock $lock2) + { + foreach (array_filter([$lock1, $lock2]) as $lock) { + $o = $lock->getWrappedObject()->getProphecy(); + $r = new \ReflectionObject($o); + $p = $r->getProperty('methodProphecies'); + $p->setAccessible(true); + $p->setValue($o, []); + } + } + + public function it_is_initializable(Lock $lock1) + { + $this->shouldHaveType(LockSet::class); + $this->shouldImplement(Lock::class); + } + + public function it_should_not_be_empty() + { + $this->beConstructedWith([]); + $this->shouldThrow(new RuntimeException("Lock set cannot be empty"))->duringInstantiation(); + } + + public function it_should_acquire_a_lock(Lock $lock1) + { + $lock1->acquire()->shouldBeCalled(); + $this->acquire(); + } + + public function it_should_acquire_all_locks(Lock $lock1, Lock $lock2) + { + $this->beConstructedWith([$lock1, $lock2]); + $this->acquire(); + $lock1->acquire()->shouldHaveBeenCalled(); + $lock2->acquire()->shouldHaveBeenCalled(); + $lock1->release()->shouldBeCalled(); + $lock2->release()->shouldBeCalled(); + } + + public function it_should_fail_to_acquire_if_one_lock_fail(Lock $lock1, Lock $lock2) + { + $this->beConstructedWith([$lock1, $lock2]); + $lock1->acquire()->shouldBeCalled(); + $lock1->release()->shouldBeCalled(); + $lock2->acquire()->shouldBeCalled(); + $lock2->acquire()->willThrow(new RuntimeException); + $this->shouldThrow(RuntimeException::class)->duringAcquire(); + } + + public function it_should_stop_trying_to_acquire_on_failure(Lock $lock1, Lock $lock2) + { + $this->beConstructedWith([$lock1, $lock2]); + $lock1->acquire()->shouldBeCalled(); + $lock1->acquire()->willThrow(new RuntimeException); + $lock2->acquire()->shouldNotBeCalled(); + $this->shouldThrow(RuntimeException::class)->duringAcquire(); + } + + public function it_should_release_acquired_lock_on_acquire_failure(Lock $lock1, Lock $lock2) + { + $this->beConstructedWith([$lock1, $lock2]); + $lock1->acquire()->shouldBeCalled(); + $lock1->release()->shouldBeCalled(); + $lock2->acquire()->willThrow(new RuntimeException); + $this->shouldThrow(RuntimeException::class)->duringAcquire(); + } + + public function it_should_not_release_not_acquired_lock_on_acquire_failure(Lock $lock1, Lock $lock2) + { + $this->beConstructedWith([$lock1, $lock2]); + $lock1->acquire()->shouldBeCalled(); + $lock1->acquire()->willThrow(new RuntimeException); + $lock1->release()->shouldNotBeCalled(); + $lock2->release()->shouldNotBeCalled(); + $this->shouldThrow(RuntimeException::class)->duringAcquire(); + } + + public function it_should_release_all_locks(Lock $lock1, Lock $lock2) + { + $this->beConstructedWith([$lock1, $lock2]); + $lock1->release()->shouldBeCalled(); + $lock2->release()->shouldBeCalled(); + $this->release(); + } + + public function it_should_release_all_locks_even_if_one_failed(Lock $lock1, Lock $lock2) + { + $this->beConstructedWith([$lock1, $lock2]); + $lock1->release()->shouldBeCalled(); + $lock1->release()->willThrow(new RuntimeException); + $lock2->release()->shouldBeCalled(); + $this->shouldThrow(RuntimeException::class)->duringRelease(); + } +} diff --git a/src/AggregationException.php b/src/AggregationException.php new file mode 100644 index 0000000..07ab800 --- /dev/null +++ b/src/AggregationException.php @@ -0,0 +1,14 @@ +exceptions = $exceptions; + } +} diff --git a/src/FileLock.php b/src/FileLock.php index 9b45999..f9a3549 100644 --- a/src/FileLock.php +++ b/src/FileLock.php @@ -4,7 +4,6 @@ use Psr\Log\LoggerInterface; use Psr\Log\NullLogger; -use TH\Lock\RuntimeException; class FileLock implements Lock { diff --git a/src/LockSet.php b/src/LockSet.php new file mode 100644 index 0000000..09fdb34 --- /dev/null +++ b/src/LockSet.php @@ -0,0 +1,67 @@ +locks = $locks; + $this->logger = $logger ?: new NullLogger; + } + + /** + * @inherit + */ + public function acquire() + { + $acquiredLocks = []; + try { + foreach ($this->locks as $lock) { + $lock->acquire(); + $acquiredLocks[] = $lock; + } + } catch (RuntimeException $e) { + foreach ($acquiredLocks as $lock) { + $lock->release(); + } + throw $e; + } + } + + public function release() + { + $exceptions = []; + foreach ($this->locks as $lock) { + try { + $lock->release(); + } catch (RuntimeException $e) { + $exceptions[] = $e; + } + } + if (!empty($exceptions)) { + throw new AggregationException($exceptions, "Some locks were not released"); + } + } + + public function __destruct() + { + $this->release(); + } +}