-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 146bcc4
Showing
8 changed files
with
312 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
composer.lock | ||
/vendor/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
language: php | ||
|
||
sudo: false | ||
|
||
services: | ||
- redis-server | ||
|
||
php: | ||
- 5.5 | ||
- 5.6 | ||
- 7.0 | ||
- hhvm | ||
|
||
before_script: | ||
- composer install -n | ||
|
||
script: | ||
- vendor/bin/phpunit tests |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# php-lock-redis | ||
|
||
[![Build Status](https://travis-ci.org/texthtml/php-lock-redis.svg?branch=master)](https://travis-ci.org/texthtml/php-lock-redis) | ||
[![Latest Stable Version](https://poser.pugx.org/texthtml/php-lock-redis/v/stable.svg)](https://packagist.org/packages/texthtml/php-lock-redis) | ||
[![License](https://poser.pugx.org/texthtml/php-lock-redis/license.svg)](http://www.gnu.org/licenses/agpl-3.0.html) | ||
[![Total Downloads](https://poser.pugx.org/texthtml/php-lock-redis/downloads.svg)](https://packagist.org/packages/texthtml/php-lock-redis) | ||
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/texthtml/php-lock-redis/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/texthtml/php-lock-redis/?branch=master) | ||
|
||
[php-lock-redis](https://packagist.org/packages/texthtml/php-lock-redis) is an extension for [php-lock](https://packagist.org/packages/texthtml/php-lock) that makes locking on resources easy on distributed system using Redis. It can be used instead of file base locking to lock operations on a distributed system. | ||
|
||
## Installation | ||
|
||
With Composer : | ||
|
||
```bash | ||
composer require texthtml/php-lock-redis | ||
``` | ||
|
||
## Usage | ||
|
||
You can create an object that represent a lock on a resource. 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. | ||
|
||
### Locking a ressource | ||
|
||
```php | ||
use TH\RedisLock\RedisSimpleLockFactory; | ||
|
||
$redisClient = new \Predis\Client; | ||
$factory = new RedisSimpleLockFactory($redisClient); | ||
$lock = $factory->create('lock identifier'); | ||
|
||
$lock->acquire(); | ||
|
||
// other processes that try to acquire a lock on 'lock identifier' will fail | ||
|
||
// do some stuff | ||
|
||
$lock->release(); | ||
|
||
// other processes can now acquire a lock on 'lock identifier' | ||
``` | ||
|
||
### Auto release | ||
|
||
`$lock->release()` is called automatically when the lock is destroyed so you don't need to manually release it at the end of a script or if it goes out of scope. | ||
|
||
```php | ||
use TH\RedisLock\RedisSimpleLockFactory; | ||
|
||
function batch() { | ||
$redisClient = new \Predis\Client; | ||
$factory = new RedisSimpleLockFactory($redisClient); | ||
$lock = $factory->create('lock identifier'); | ||
$lock->acquire(); | ||
|
||
// lot of stuff | ||
} | ||
|
||
batch(); | ||
|
||
// the lock will be released here even if $lock->release() is not called in batch() | ||
``` | ||
|
||
## Limitations | ||
|
||
### Validity time | ||
|
||
If a client crashes before releasing the lock (or forget to release it), no other clients would be able to acquire the lock again. To avoid Deadlock, `RedisSimpleLock` locks have a validity time at witch the lock key will expire. But be careful, if the operation is too long, another client might acquire the lock too. | ||
|
||
### Mutual exclusion | ||
|
||
Because `RedisSimpleLock` does not implements the [RedLock algorithm](http://redis.io/topics/distlock), it have a limitation : with a master slave replication, a race condition can occurs when the master crashes before the lock key is transmitted to the slave. In this case a second client could acquire the same lock. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
{ | ||
"name": "texthtml/php-lock-redis", | ||
"description" : "redis lock", | ||
"license": "aGPLv3", | ||
"type": "library", | ||
"autoload": { | ||
"psr-4": { "TH\\RedisLock\\": "src" } | ||
}, | ||
"authors": [ | ||
{ | ||
"name": "Mathieu Rochette", | ||
"email": "[email protected]" | ||
} | ||
], | ||
"require-dev": { | ||
"phpunit/phpunit": "~4.0" | ||
}, | ||
"require": { | ||
"texthtml/php-lock": "~2.0", | ||
"predis/predis": "~1.0", | ||
"psr/log": "~1.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
<?php | ||
|
||
namespace TH\RedisLock; | ||
|
||
use Exception; | ||
use Predis\Client; | ||
use Predis\Response\Status; | ||
use Psr\Log\LoggerInterface; | ||
use Psr\Log\NullLogger; | ||
use TH\Lock\Lock; | ||
|
||
class RedisSimpleLock implements Lock | ||
{ | ||
private $identifier; | ||
private $client; | ||
private $ttl; | ||
private $logger; | ||
private $id; | ||
|
||
/** | ||
* Create new RedisSimpleLock | ||
* | ||
* @param string $identifier the redis lock key | ||
* @param Client $client the Predis client | ||
* @param integer $ttl lock time-to-live in milliseconds | ||
* @param LoggerInterface|null $logger | ||
*/ | ||
public function __construct($identifier, Client $client, $ttl = 10000, LoggerInterface $logger = null) | ||
{ | ||
$this->identifier = $identifier; | ||
$this->client = $client; | ||
$this->ttl = $ttl; | ||
$this->logger = $logger ?: new NullLogger; | ||
$this->id = mt_rand(); | ||
} | ||
|
||
public function acquire() | ||
{ | ||
$log_data = ["identifier" => $this->identifier]; | ||
$response = $this->client->set($this->identifier, $this->id, "PX", $this->ttl, "NX"); | ||
if (!$response instanceof Status || $response->getPayload() !== "OK") { | ||
$this->logger->debug("could not acquire lock on {identifier}", $log_data); | ||
|
||
throw new Exception("Could not acquire lock on " . $this->identifier); | ||
} | ||
$this->logger->debug("lock acquired on {identifier}", $log_data); | ||
} | ||
|
||
public function release() | ||
{ | ||
$script = <<<LUA | ||
if redis.call("get", KEYS[1]) == ARGV[1] then | ||
return redis.call("del", KEYS[1]) | ||
end | ||
LUA; | ||
if ($this->client->eval($script, 1, $this->identifier, $this->id)) { | ||
$this->logger->debug("lock released on {identifier}", ["identifier" => $this->identifier]); | ||
} | ||
} | ||
|
||
public function __destruct() | ||
{ | ||
$this->release(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
<?php | ||
|
||
namespace TH\RedisLock; | ||
|
||
use Predis\Client; | ||
use Psr\Log\LoggerInterface; | ||
use Psr\Log\NullLogger; | ||
|
||
class RedisSimpleLockFactory | ||
{ | ||
private $client; | ||
private $defaultTtl; | ||
private $logger; | ||
|
||
public function __construct(Client $client, $defaultTtl = 10000, LoggerInterface $logger = null) | ||
{ | ||
$this->client = $client; | ||
$this->defaultTtl = $defaultTtl; | ||
$this->logger = $logger ?: new NullLogger; | ||
} | ||
|
||
/** | ||
* Create a new RedisSimpleLock | ||
* | ||
* @param string $identifier the redis lock key | ||
* @param integer $ttl lock time-to-live in milliseconds | ||
* | ||
* @return RedisSimpleLock | ||
*/ | ||
public function create($identifier, $ttl = null) | ||
{ | ||
return new RedisSimpleLock($identifier, $this->client, $ttl ?: $this->defaultTtl, $this->logger); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?php | ||
|
||
use TH\RedisLock\RedisSimpleLock; | ||
use TH\RedisLock\RedisSimpleLockFactory; | ||
|
||
class RedisSimpleLockFactoryTest extends PHPUnit_Framework_TestCase | ||
{ | ||
private $redisClient; | ||
|
||
protected function setUp() | ||
{ | ||
$this->redisClient = new \Predis\Client(getenv('REDIS_URI')); | ||
$this->redisClient->flushdb(); | ||
} | ||
|
||
public function testCreateLock() | ||
{ | ||
$factory = new RedisSimpleLockFactory($this->redisClient, 50); | ||
$lock = $factory->create('lock identifier'); | ||
$this->assertInstanceOf(RedisSimpleLock::class, $lock); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
|
||
use TH\RedisLock\RedisSimpleLock; | ||
|
||
class RedisSimpleLockTest extends PHPUnit_Framework_TestCase | ||
{ | ||
private $redisClient; | ||
|
||
protected function setUp() | ||
{ | ||
$this->redisClient = new \Predis\Client(getenv("REDIS_URI")); | ||
$this->redisClient->flushdb(); | ||
} | ||
|
||
public function testLock() | ||
{ | ||
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50); | ||
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient); | ||
|
||
$lock1->acquire(); | ||
|
||
// Only the second acquire is supposed to fail | ||
$this->setExpectedException("Exception"); | ||
$lock2->acquire(); | ||
} | ||
|
||
public function testLockTtl() | ||
{ | ||
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50); | ||
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient); | ||
|
||
$lock1->acquire(); | ||
usleep(100000); | ||
|
||
// first lock sould have been released | ||
$lock2->acquire(); | ||
} | ||
|
||
public function testLockSafeRelease() | ||
{ | ||
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50); | ||
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient); | ||
|
||
$lock1->acquire(); | ||
usleep(100000); | ||
$lock2->acquire(); | ||
$lock1->release(); | ||
|
||
// lock should still exists | ||
$this->assertTrue($this->redisClient->exists("lock identifier"), "Lock should not have been released"); | ||
} | ||
|
||
public function testLockRelease() | ||
{ | ||
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50); | ||
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient); | ||
|
||
$lock1->acquire(); | ||
$lock1->release(); | ||
|
||
// first lock sould have been released | ||
$lock2->acquire(); | ||
} | ||
|
||
public function testLockAutoRelease() | ||
{ | ||
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50); | ||
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient); | ||
|
||
$lock1->acquire(); | ||
unset($lock1); | ||
|
||
// first lock sould have been released | ||
$lock2->acquire(); | ||
} | ||
} |