Skip to content

Commit

Permalink
add LockSet
Browse files Browse the repository at this point in the history
closes #4
  • Loading branch information
mathroc committed Jan 22, 2016
1 parent 88cb956 commit 2e8748d
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 12 deletions.
28 changes: 17 additions & 11 deletions README.md
Expand Up @@ -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

Expand All @@ -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);
Expand Down Expand Up @@ -81,15 +80,15 @@ 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();

// process 1 does stuff
```

```
```php
$lock = $factory->create('protected resource', 'process 2');

$lock->acquire();
Expand All @@ -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

Expand Down
112 changes: 112 additions & 0 deletions spec/TH/Lock/LockSetSpec.php
@@ -0,0 +1,112 @@
<?php

namespace spec\TH\Lock;

use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use TH\Lock\Lock;
use TH\Lock\LockSet;
use TH\Lock\RuntimeException;
use VirtualFileSystem\FileSystem;

class LockSetSpec extends ObjectBehavior
{
public function let(Lock $lock1)
{
$this->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();
}
}
14 changes: 14 additions & 0 deletions src/AggregationException.php
@@ -0,0 +1,14 @@
<?php

namespace TH\Lock;

class AggregationException extends RuntimeException
{
private $exceptions;

public function __construct(array $exceptions, $message = "", $code = 0)
{
parent::__construct($message, $code);
$this->exceptions = $exceptions;
}
}
1 change: 0 additions & 1 deletion src/FileLock.php
Expand Up @@ -4,7 +4,6 @@

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use TH\Lock\RuntimeException;

class FileLock implements Lock
{
Expand Down
67 changes: 67 additions & 0 deletions src/LockSet.php
@@ -0,0 +1,67 @@
<?php

namespace TH\Lock;

use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

class LockSet implements Lock
{
private $locks = [];

private $logger;

/**
* @param Lock[] $locks array of Lock
* @param LoggerInterface|null $logger
*/
public function __construct(
array $locks,
LoggerInterface $logger = null
) {
if (empty($locks)) {
throw new RuntimeException("Lock set cannot be empty");
}
$this->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();
}
}

0 comments on commit 2e8748d

Please sign in to comment.