Skip to content

Commit 2e8748d

Browse files
committed
add LockSet
closes #4
1 parent 88cb956 commit 2e8748d

File tree

5 files changed

+210
-12
lines changed

5 files changed

+210
-12
lines changed

README.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ composer require texthtml/php-lock
1818

1919
## Usage
2020

21-
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.
21+
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.
2222

2323
### Locking a file exclusively
2424

@@ -41,7 +41,6 @@ $lock->release();
4141
### Sharing a lock on a file
4242

4343
```php
44-
4544
use TH\Lock\FileLock;
4645

4746
$lock = new FileLock('/path/to/file', FileLock::SHARED);
@@ -81,15 +80,15 @@ batch();
8180

8281
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.
8382

84-
```
83+
```php
8584
$lock = $factory->create('protected resource', 'process 1');
8685

8786
$lock->acquire();
8887

8988
// process 1 does stuff
9089
```
9190

92-
```
91+
```php
9392
$lock = $factory->create('protected resource', 'process 2');
9493

9594
$lock->acquire();
@@ -106,23 +105,30 @@ When process 1 is running and we start process 2, an Exception will be thrown: "
106105
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.
107106

108107
```php
109-
110108
use TH\Lock\FileFactory;
111109

112110
$factory = new FileFactory('/path/to/lock_dir/');
113111
$lock = $factory->create('resource identifier');
114112
```
115113

116-
## API
114+
### Aggregating locks
117115

118-
There are two methods you can use on a `FileLock`:
116+
If you want to simplify acquiring multiple locks at once, you can use the `\TH\Lock\LockSet`:
119117

120-
* `\TH\Lock\FileLock::acquire()` used to acquire a lock on the file
121-
* `\TH\Lock\FileLock::release()` used to release a lock on the file
118+
```php
119+
use TH\Lock\LockSet;
120+
121+
$superLock = new LockSet([$lock1, $lock2, $lock3]);
122+
// You can make a set with any types of locks (eg: FileLock, RedisSimpleLock or another nested LockSet)
123+
124+
$superLock->acquire();
125+
126+
// all locks will be released when $superLock is destroyed or when `$superLock->release()` is called
127+
```
122128

123-
And one on a `FileFactory`:
129+
It will try to acquire all locks, if it fails it will release the lock that have been acquired to avoid locking other processes.
124130

125-
* `\TH\Lock\FileFactory::create($resource, $exclusive = FileLock::EXCLUSIVE, $blocking = FileLock::NON_BLOCKING)` used to create a `FileLock` for `$resource`
131+
note: `Lock` put inside a `LockSet` should not be used manually anymore
126132

127133
## Notes
128134

spec/TH/Lock/LockSetSpec.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
namespace spec\TH\Lock;
4+
5+
use PhpSpec\ObjectBehavior;
6+
use Prophecy\Argument;
7+
use TH\Lock\Lock;
8+
use TH\Lock\LockSet;
9+
use TH\Lock\RuntimeException;
10+
use VirtualFileSystem\FileSystem;
11+
12+
class LockSetSpec extends ObjectBehavior
13+
{
14+
public function let(Lock $lock1)
15+
{
16+
$this->beConstructedWith([$lock1]);
17+
}
18+
19+
public function letgo(Lock $lock1, Lock $lock2)
20+
{
21+
foreach (array_filter([$lock1, $lock2]) as $lock) {
22+
$o = $lock->getWrappedObject()->getProphecy();
23+
$r = new \ReflectionObject($o);
24+
$p = $r->getProperty('methodProphecies');
25+
$p->setAccessible(true);
26+
$p->setValue($o, []);
27+
}
28+
}
29+
30+
public function it_is_initializable(Lock $lock1)
31+
{
32+
$this->shouldHaveType(LockSet::class);
33+
$this->shouldImplement(Lock::class);
34+
}
35+
36+
public function it_should_not_be_empty()
37+
{
38+
$this->beConstructedWith([]);
39+
$this->shouldThrow(new RuntimeException("Lock set cannot be empty"))->duringInstantiation();
40+
}
41+
42+
public function it_should_acquire_a_lock(Lock $lock1)
43+
{
44+
$lock1->acquire()->shouldBeCalled();
45+
$this->acquire();
46+
}
47+
48+
public function it_should_acquire_all_locks(Lock $lock1, Lock $lock2)
49+
{
50+
$this->beConstructedWith([$lock1, $lock2]);
51+
$this->acquire();
52+
$lock1->acquire()->shouldHaveBeenCalled();
53+
$lock2->acquire()->shouldHaveBeenCalled();
54+
$lock1->release()->shouldBeCalled();
55+
$lock2->release()->shouldBeCalled();
56+
}
57+
58+
public function it_should_fail_to_acquire_if_one_lock_fail(Lock $lock1, Lock $lock2)
59+
{
60+
$this->beConstructedWith([$lock1, $lock2]);
61+
$lock1->acquire()->shouldBeCalled();
62+
$lock1->release()->shouldBeCalled();
63+
$lock2->acquire()->shouldBeCalled();
64+
$lock2->acquire()->willThrow(new RuntimeException);
65+
$this->shouldThrow(RuntimeException::class)->duringAcquire();
66+
}
67+
68+
public function it_should_stop_trying_to_acquire_on_failure(Lock $lock1, Lock $lock2)
69+
{
70+
$this->beConstructedWith([$lock1, $lock2]);
71+
$lock1->acquire()->shouldBeCalled();
72+
$lock1->acquire()->willThrow(new RuntimeException);
73+
$lock2->acquire()->shouldNotBeCalled();
74+
$this->shouldThrow(RuntimeException::class)->duringAcquire();
75+
}
76+
77+
public function it_should_release_acquired_lock_on_acquire_failure(Lock $lock1, Lock $lock2)
78+
{
79+
$this->beConstructedWith([$lock1, $lock2]);
80+
$lock1->acquire()->shouldBeCalled();
81+
$lock1->release()->shouldBeCalled();
82+
$lock2->acquire()->willThrow(new RuntimeException);
83+
$this->shouldThrow(RuntimeException::class)->duringAcquire();
84+
}
85+
86+
public function it_should_not_release_not_acquired_lock_on_acquire_failure(Lock $lock1, Lock $lock2)
87+
{
88+
$this->beConstructedWith([$lock1, $lock2]);
89+
$lock1->acquire()->shouldBeCalled();
90+
$lock1->acquire()->willThrow(new RuntimeException);
91+
$lock1->release()->shouldNotBeCalled();
92+
$lock2->release()->shouldNotBeCalled();
93+
$this->shouldThrow(RuntimeException::class)->duringAcquire();
94+
}
95+
96+
public function it_should_release_all_locks(Lock $lock1, Lock $lock2)
97+
{
98+
$this->beConstructedWith([$lock1, $lock2]);
99+
$lock1->release()->shouldBeCalled();
100+
$lock2->release()->shouldBeCalled();
101+
$this->release();
102+
}
103+
104+
public function it_should_release_all_locks_even_if_one_failed(Lock $lock1, Lock $lock2)
105+
{
106+
$this->beConstructedWith([$lock1, $lock2]);
107+
$lock1->release()->shouldBeCalled();
108+
$lock1->release()->willThrow(new RuntimeException);
109+
$lock2->release()->shouldBeCalled();
110+
$this->shouldThrow(RuntimeException::class)->duringRelease();
111+
}
112+
}

src/AggregationException.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
namespace TH\Lock;
4+
5+
class AggregationException extends RuntimeException
6+
{
7+
private $exceptions;
8+
9+
public function __construct(array $exceptions, $message = "", $code = 0)
10+
{
11+
parent::__construct($message, $code);
12+
$this->exceptions = $exceptions;
13+
}
14+
}

src/FileLock.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Psr\Log\LoggerInterface;
66
use Psr\Log\NullLogger;
7-
use TH\Lock\RuntimeException;
87

98
class FileLock implements Lock
109
{

src/LockSet.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
namespace TH\Lock;
4+
5+
use Psr\Log\LoggerInterface;
6+
use Psr\Log\NullLogger;
7+
8+
class LockSet implements Lock
9+
{
10+
private $locks = [];
11+
12+
private $logger;
13+
14+
/**
15+
* @param Lock[] $locks array of Lock
16+
* @param LoggerInterface|null $logger
17+
*/
18+
public function __construct(
19+
array $locks,
20+
LoggerInterface $logger = null
21+
) {
22+
if (empty($locks)) {
23+
throw new RuntimeException("Lock set cannot be empty");
24+
}
25+
$this->locks = $locks;
26+
$this->logger = $logger ?: new NullLogger;
27+
}
28+
29+
/**
30+
* @inherit
31+
*/
32+
public function acquire()
33+
{
34+
$acquiredLocks = [];
35+
try {
36+
foreach ($this->locks as $lock) {
37+
$lock->acquire();
38+
$acquiredLocks[] = $lock;
39+
}
40+
} catch (RuntimeException $e) {
41+
foreach ($acquiredLocks as $lock) {
42+
$lock->release();
43+
}
44+
throw $e;
45+
}
46+
}
47+
48+
public function release()
49+
{
50+
$exceptions = [];
51+
foreach ($this->locks as $lock) {
52+
try {
53+
$lock->release();
54+
} catch (RuntimeException $e) {
55+
$exceptions[] = $e;
56+
}
57+
}
58+
if (!empty($exceptions)) {
59+
throw new AggregationException($exceptions, "Some locks were not released");
60+
}
61+
}
62+
63+
public function __destruct()
64+
{
65+
$this->release();
66+
}
67+
}

0 commit comments

Comments
 (0)