Skip to content

Commit 146bcc4

Browse files
committed
first commit
0 parents  commit 146bcc4

8 files changed

+312
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
composer.lock
2+
/vendor/

.travis.yml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
language: php
2+
3+
sudo: false
4+
5+
services:
6+
- redis-server
7+
8+
php:
9+
- 5.5
10+
- 5.6
11+
- 7.0
12+
- hhvm
13+
14+
before_script:
15+
- composer install -n
16+
17+
script:
18+
- vendor/bin/phpunit tests

README.md

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# php-lock-redis
2+
3+
[![Build Status](https://travis-ci.org/texthtml/php-lock-redis.svg?branch=master)](https://travis-ci.org/texthtml/php-lock-redis)
4+
[![Latest Stable Version](https://poser.pugx.org/texthtml/php-lock-redis/v/stable.svg)](https://packagist.org/packages/texthtml/php-lock-redis)
5+
[![License](https://poser.pugx.org/texthtml/php-lock-redis/license.svg)](http://www.gnu.org/licenses/agpl-3.0.html)
6+
[![Total Downloads](https://poser.pugx.org/texthtml/php-lock-redis/downloads.svg)](https://packagist.org/packages/texthtml/php-lock-redis)
7+
[![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)
8+
9+
[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.
10+
11+
## Installation
12+
13+
With Composer :
14+
15+
```bash
16+
composer require texthtml/php-lock-redis
17+
```
18+
19+
## Usage
20+
21+
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.
22+
23+
### Locking a ressource
24+
25+
```php
26+
use TH\RedisLock\RedisSimpleLockFactory;
27+
28+
$redisClient = new \Predis\Client;
29+
$factory = new RedisSimpleLockFactory($redisClient);
30+
$lock = $factory->create('lock identifier');
31+
32+
$lock->acquire();
33+
34+
// other processes that try to acquire a lock on 'lock identifier' will fail
35+
36+
// do some stuff
37+
38+
$lock->release();
39+
40+
// other processes can now acquire a lock on 'lock identifier'
41+
```
42+
43+
### Auto release
44+
45+
`$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.
46+
47+
```php
48+
use TH\RedisLock\RedisSimpleLockFactory;
49+
50+
function batch() {
51+
$redisClient = new \Predis\Client;
52+
$factory = new RedisSimpleLockFactory($redisClient);
53+
$lock = $factory->create('lock identifier');
54+
$lock->acquire();
55+
56+
// lot of stuff
57+
}
58+
59+
batch();
60+
61+
// the lock will be released here even if $lock->release() is not called in batch()
62+
```
63+
64+
## Limitations
65+
66+
### Validity time
67+
68+
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.
69+
70+
### Mutual exclusion
71+
72+
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.

composer.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "texthtml/php-lock-redis",
3+
"description" : "redis lock",
4+
"license": "aGPLv3",
5+
"type": "library",
6+
"autoload": {
7+
"psr-4": { "TH\\RedisLock\\": "src" }
8+
},
9+
"authors": [
10+
{
11+
"name": "Mathieu Rochette",
12+
"email": "[email protected]"
13+
}
14+
],
15+
"require-dev": {
16+
"phpunit/phpunit": "~4.0"
17+
},
18+
"require": {
19+
"texthtml/php-lock": "~2.0",
20+
"predis/predis": "~1.0",
21+
"psr/log": "~1.0"
22+
}
23+
}

src/RedisSimpleLock.php

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace TH\RedisLock;
4+
5+
use Exception;
6+
use Predis\Client;
7+
use Predis\Response\Status;
8+
use Psr\Log\LoggerInterface;
9+
use Psr\Log\NullLogger;
10+
use TH\Lock\Lock;
11+
12+
class RedisSimpleLock implements Lock
13+
{
14+
private $identifier;
15+
private $client;
16+
private $ttl;
17+
private $logger;
18+
private $id;
19+
20+
/**
21+
* Create new RedisSimpleLock
22+
*
23+
* @param string $identifier the redis lock key
24+
* @param Client $client the Predis client
25+
* @param integer $ttl lock time-to-live in milliseconds
26+
* @param LoggerInterface|null $logger
27+
*/
28+
public function __construct($identifier, Client $client, $ttl = 10000, LoggerInterface $logger = null)
29+
{
30+
$this->identifier = $identifier;
31+
$this->client = $client;
32+
$this->ttl = $ttl;
33+
$this->logger = $logger ?: new NullLogger;
34+
$this->id = mt_rand();
35+
}
36+
37+
public function acquire()
38+
{
39+
$log_data = ["identifier" => $this->identifier];
40+
$response = $this->client->set($this->identifier, $this->id, "PX", $this->ttl, "NX");
41+
if (!$response instanceof Status || $response->getPayload() !== "OK") {
42+
$this->logger->debug("could not acquire lock on {identifier}", $log_data);
43+
44+
throw new Exception("Could not acquire lock on " . $this->identifier);
45+
}
46+
$this->logger->debug("lock acquired on {identifier}", $log_data);
47+
}
48+
49+
public function release()
50+
{
51+
$script = <<<LUA
52+
if redis.call("get", KEYS[1]) == ARGV[1] then
53+
return redis.call("del", KEYS[1])
54+
end
55+
LUA;
56+
if ($this->client->eval($script, 1, $this->identifier, $this->id)) {
57+
$this->logger->debug("lock released on {identifier}", ["identifier" => $this->identifier]);
58+
}
59+
}
60+
61+
public function __destruct()
62+
{
63+
$this->release();
64+
}
65+
}

src/RedisSimpleLockFactory.php

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace TH\RedisLock;
4+
5+
use Predis\Client;
6+
use Psr\Log\LoggerInterface;
7+
use Psr\Log\NullLogger;
8+
9+
class RedisSimpleLockFactory
10+
{
11+
private $client;
12+
private $defaultTtl;
13+
private $logger;
14+
15+
public function __construct(Client $client, $defaultTtl = 10000, LoggerInterface $logger = null)
16+
{
17+
$this->client = $client;
18+
$this->defaultTtl = $defaultTtl;
19+
$this->logger = $logger ?: new NullLogger;
20+
}
21+
22+
/**
23+
* Create a new RedisSimpleLock
24+
*
25+
* @param string $identifier the redis lock key
26+
* @param integer $ttl lock time-to-live in milliseconds
27+
*
28+
* @return RedisSimpleLock
29+
*/
30+
public function create($identifier, $ttl = null)
31+
{
32+
return new RedisSimpleLock($identifier, $this->client, $ttl ?: $this->defaultTtl, $this->logger);
33+
}
34+
}

tests/RedisSimpleLockFactoryTest.php

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
use TH\RedisLock\RedisSimpleLock;
4+
use TH\RedisLock\RedisSimpleLockFactory;
5+
6+
class RedisSimpleLockFactoryTest extends PHPUnit_Framework_TestCase
7+
{
8+
private $redisClient;
9+
10+
protected function setUp()
11+
{
12+
$this->redisClient = new \Predis\Client(getenv('REDIS_URI'));
13+
$this->redisClient->flushdb();
14+
}
15+
16+
public function testCreateLock()
17+
{
18+
$factory = new RedisSimpleLockFactory($this->redisClient, 50);
19+
$lock = $factory->create('lock identifier');
20+
$this->assertInstanceOf(RedisSimpleLock::class, $lock);
21+
}
22+
}

tests/RedisSimpleLockTest.php

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
use TH\RedisLock\RedisSimpleLock;
4+
5+
class RedisSimpleLockTest extends PHPUnit_Framework_TestCase
6+
{
7+
private $redisClient;
8+
9+
protected function setUp()
10+
{
11+
$this->redisClient = new \Predis\Client(getenv("REDIS_URI"));
12+
$this->redisClient->flushdb();
13+
}
14+
15+
public function testLock()
16+
{
17+
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50);
18+
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient);
19+
20+
$lock1->acquire();
21+
22+
// Only the second acquire is supposed to fail
23+
$this->setExpectedException("Exception");
24+
$lock2->acquire();
25+
}
26+
27+
public function testLockTtl()
28+
{
29+
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50);
30+
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient);
31+
32+
$lock1->acquire();
33+
usleep(100000);
34+
35+
// first lock sould have been released
36+
$lock2->acquire();
37+
}
38+
39+
public function testLockSafeRelease()
40+
{
41+
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50);
42+
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient);
43+
44+
$lock1->acquire();
45+
usleep(100000);
46+
$lock2->acquire();
47+
$lock1->release();
48+
49+
// lock should still exists
50+
$this->assertTrue($this->redisClient->exists("lock identifier"), "Lock should not have been released");
51+
}
52+
53+
public function testLockRelease()
54+
{
55+
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50);
56+
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient);
57+
58+
$lock1->acquire();
59+
$lock1->release();
60+
61+
// first lock sould have been released
62+
$lock2->acquire();
63+
}
64+
65+
public function testLockAutoRelease()
66+
{
67+
$lock1 = new RedisSimpleLock("lock identifier", $this->redisClient, 50);
68+
$lock2 = new RedisSimpleLock("lock identifier", $this->redisClient);
69+
70+
$lock1->acquire();
71+
unset($lock1);
72+
73+
// first lock sould have been released
74+
$lock2->acquire();
75+
}
76+
}

0 commit comments

Comments
 (0)