Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
vendor/*
.idea/
vendor/
*/logs/*
*/cache/*
composer.phar
composer.lock
44 changes: 26 additions & 18 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
{
"name": "ronnylt/redlock-php",
"description": "Redis distributed locks in PHP",
"authors": [
{
"name": "Ronny Lopez",
"email": "ronny@tangotree.io"
}
],
"require": {
"php": "~5.4",
"ext-redis": "~2.2.5"
"name": "ronnylt/redlock-php",
"description": "Redis distributed locks in PHP",
"authors": [
{
"name": "Ronny Lopez",
"email": "ronny@tangotree.io"
},

"autoload": {
"psr-4": {
"RedLock\\": "src/"
}
{
"name": "Peter Scopes",
"email": "peter.scopes@gmail.com"
}


],
"require": {
"php": "~5.4",
"ext-redis": "~2.2.5|~3.1.2"
},
"require-dev": {
"php": "~5.6",
"phpunit/phpunit": "5.4.*"
},
"autoload": {
"psr-4": {
"RedLock\\": "src/"
}
},
"autoload-dev": {
"psr-4": { "": "tests/" }
}
}
21 changes: 21 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
stopOnError="false"
stopOnFailure="false"
stopOnIncomplete="false"
stopOnSkipped="false"
beStrictAboutTestsThatDoNotTestAnything="true"
verbose="true"
bootstrap="./vendor/autoload.php">
<testsuites>
<testsuite name="RedLock Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="RedLockTestListener" file="./tests/RedLockTestListener.php"/>
</listeners>
</phpunit>
79 changes: 74 additions & 5 deletions src/RedLock.php
Original file line number Diff line number Diff line change
@@ -1,17 +1,56 @@
<?php

namespace RedLock;

/**
* Class RedLock
*
* @package RedLock
* @author Ronny Lopez <ronny@tangotree.io>
*/
class RedLock
{
/**
* @var int Seconds to delay retrying
*/
private $retryDelay;

/**
* @var int Number lock attempt retries before giving up
*/
private $retryCount;

/**
* @var float Account for Redis expires precision
*/
private $clockDriftFactor = 0.01;

/**
* @var mixed
*/
private $quorum;

/**
* @var array[]|\Redis[] Array of server information arrays: [host, port, timeout]
* @see \Redis::connect()
*/
private $servers = array();

/**
* @var array|\Redis[]
*/
private $instances = array();

function __construct(array $servers, $retryDelay = 200, $retryCount = 3)
/**
* RedLock constructor.
*
* @param array[]|\Redis[] $servers Each element should an array of host, port, timeout
* @param int $retryDelay Seconds delay between retries
* @param int $retryCount Number of times to retry
*
* @see \Redis::connect()
*/
public function __construct(array $servers, $retryDelay = 200, $retryCount = 3)
{
$this->servers = $servers;

Expand All @@ -21,6 +60,12 @@ function __construct(array $servers, $retryDelay = 200, $retryCount = 3)
$this->quorum = min(count($servers), (count($servers) / 2 + 1));
}

/**
* @param string $resource Unique identifier
* @param int $ttl Time to live (milliseconds)
*
* @return array|bool
*/
public function lock($resource, $ttl)
{
$this->initInstances();
Expand Down Expand Up @@ -70,6 +115,10 @@ public function lock($resource, $ttl)
return false;
}

/**
* @param array $lock Array returned by RedLock::lock
* @see lock()
*/
public function unlock(array $lock)
{
$this->initInstances();
Expand All @@ -81,24 +130,44 @@ public function unlock(array $lock)
}
}

/**
* Initialise the Redis servers provided in the the constructor.
*/
private function initInstances()
{
if (empty($this->instances)) {
foreach ($this->servers as $server) {
list($host, $port, $timeout) = $server;
$redis = new \Redis();
$redis->connect($host, $port, $timeout);
if (!$server instanceof \Redis) {
list($host, $port, $timeout) = $server;
$server = new \Redis();
$server->connect($host, $port, $timeout);
}

$this->instances[] = $redis;
$this->instances[] = $server;
}
}
}

/**
* @param \Redis $instance Redis instance to attempt to lock
* @param string $resource Unique identifier
* @param string $token Unique token
* @param int $ttl Time to live (milliseconds)
*
* @return mixed
*/
private function lockInstance($instance, $resource, $token, $ttl)
{
return $instance->set($resource, $token, ['NX', 'PX' => $ttl]);
}

/**
* @param \Redis $instance Redis instance to unlock
* @param string $resource Unique identifier
* @param string $token Unique token
*
* @return mixed
*/
private function unlockInstance($instance, $resource, $token)
{
$script = '
Expand Down
110 changes: 110 additions & 0 deletions tests/RedLockTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

/**
* Class RedLockTest
*
* @author Peter Scopes
*/
class RedLockTest extends PHPUnit_Framework_TestCase
{
const SERVER_PORT_A = 6389;
const SERVER_PORT_B = 6390;
const SERVER_PORT_C = 6391;

/**
* @var array[]
*/
protected $servers;

public function setUp()
{
$this->servers = [
['127.0.0.1', self::SERVER_PORT_A, 0.1],
['127.0.0.1', self::SERVER_PORT_B, 0.1],
['127.0.0.1', self::SERVER_PORT_C, 0.1],
];
}

public function testLockOk()
{
$redLock = new \RedLock\RedLock($this->servers);
$resource = 'redLock.key';
$gate = $redLock->lock($resource, 500);
$redLock->unlock($gate);

// Asserts
$this->assertInternalType('array', $gate);
$this->assertArrayHasKey('validity', $gate);
$this->assertArrayHasKey('token', $gate);
$this->assertArrayHasKey('resource', $gate);
$this->assertEquals($resource, $gate['resource']);

}

public function testBlockOk()
{
$redLock = new \RedLock\RedLock($this->servers);
$resource = 'redLock.key';

$gateA = $redLock->lock($resource, 500);
$gateB = $redLock->lock($resource, 500);
$redLock->unlock($gateA);

// Asserts
$this->assertFalse($gateB);
}

public function testTimeoutOk()
{
$redLock = new \RedLock\RedLock($this->servers);
$resource = 'redLock.key';

$gateA = $redLock->lock($resource, 500);
$gateB = $redLock->lock($resource, 500);

$this->assertInternalType('array', $gateA);
$this->assertFalse($gateB);

usleep(500000);

$gateB = $redLock->lock($resource, 500);
$redLock->unlock($gateB);
$this->assertInternalType('array', $gateB);
}

public function testMultipleOk()
{
$redLock = new \RedLock\RedLock($this->servers);
$resource = 'redLock.key';

$gateA = $redLock->lock($resource, 1000);
$gateB = $redLock->lock($resource, 1000);
$gateC = $redLock->lock($resource, 1000);
$redLock->unlock($gateA);

// Asserts
$this->assertInternalType('array', $gateA);
$this->assertFalse($gateB);
$this->assertFalse($gateC);

$gateB = $redLock->lock($resource, 1000);
$gateC = $redLock->lock($resource, 1000);
$gateA = $redLock->lock($resource, 1000);
$redLock->unlock($gateB);

// Asserts
$this->assertInternalType('array', $gateB);
$this->assertFalse($gateC);
$this->assertFalse($gateA);

$gateC = $redLock->lock($resource, 1000);
$gateA = $redLock->lock($resource, 1000);
$gateB = $redLock->lock($resource, 1000);
$redLock->unlock($gateC);

// Asserts
$this->assertInternalType('array', $gateC);
$this->assertFalse($gateA);
$this->assertFalse($gateB);
}
}
61 changes: 61 additions & 0 deletions tests/RedLockTestListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

/**
* Class RedLockTestListener
*
* @author
*/
class RedLockTestListener implements PHPUnit_Framework_TestListener
{
public function startTestSuite(PHPUnit_Framework_TestSuite $suite)
{
if('RedLock Test Suite' != $suite->getName())
{
return;
}
// Start the Redis servers
passthru(sprintf('redis-server --port %d --daemonize yes', RedLockTest::SERVER_PORT_A));
passthru(sprintf('redis-server --port %d --daemonize yes', RedLockTest::SERVER_PORT_B));
passthru(sprintf('redis-server --port %d --daemonize yes', RedLockTest::SERVER_PORT_C));
}

public function endTestSuite(PHPUnit_Framework_TestSuite $suite)
{
if('RedLock Test Suite' != $suite->getName())
{
return;
}
// Stop the Redis servers
passthru(sprintf('redis-cli -p %d shutdown', RedLockTest::SERVER_PORT_A));
passthru(sprintf('redis-cli -p %d shutdown', RedLockTest::SERVER_PORT_B));
passthru(sprintf('redis-cli -p %d shutdown', RedLockTest::SERVER_PORT_C));
}

public function startTest(PHPUnit_Framework_Test $test)
{
}

public function endTest(PHPUnit_Framework_Test $test, $time)
{
}

public function addError(PHPUnit_Framework_Test $test, Exception $e, $time)
{
}

public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time)
{
}

public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time)
{
}

public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time)
{
}

public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time)
{
}
}
Loading