Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mathroc committed Oct 18, 2015
0 parents commit 146bcc4
Show file tree
Hide file tree
Showing 8 changed files with 312 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
composer.lock
/vendor/
18 changes: 18 additions & 0 deletions .travis.yml
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
72 changes: 72 additions & 0 deletions README.md
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.
23 changes: 23 additions & 0 deletions composer.json
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"
}
}
65 changes: 65 additions & 0 deletions src/RedisSimpleLock.php
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();
}
}
34 changes: 34 additions & 0 deletions src/RedisSimpleLockFactory.php
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);
}
}
22 changes: 22 additions & 0 deletions tests/RedisSimpleLockFactoryTest.php
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);
}
}
76 changes: 76 additions & 0 deletions tests/RedisSimpleLockTest.php
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();
}
}

0 comments on commit 146bcc4

Please sign in to comment.