diff --git a/.gitignore b/.gitignore index dc8bf6e..742ddf7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -vendor/* +.idea/ +vendor/ */logs/* */cache/* composer.phar +composer.lock diff --git a/composer.json b/composer.json index a591c12..0461884 100644 --- a/composer.json +++ b/composer.json @@ -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/" } + } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..87f70c3 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,21 @@ + + + + + ./tests/ + + + + + + \ No newline at end of file diff --git a/src/RedLock.php b/src/RedLock.php index 5f6770a..9672e41 100644 --- a/src/RedLock.php +++ b/src/RedLock.php @@ -1,17 +1,56 @@ + */ 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; @@ -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(); @@ -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(); @@ -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 = ' diff --git a/tests/RedLockTest.php b/tests/RedLockTest.php new file mode 100644 index 0000000..966e214 --- /dev/null +++ b/tests/RedLockTest.php @@ -0,0 +1,110 @@ +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); + } +} \ No newline at end of file diff --git a/tests/RedLockTestListener.php b/tests/RedLockTestListener.php new file mode 100644 index 0000000..bae8eee --- /dev/null +++ b/tests/RedLockTestListener.php @@ -0,0 +1,61 @@ +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) + { + } +} \ No newline at end of file diff --git a/tests/test1.php b/tests/test1.php deleted file mode 100644 index 9582ae1..0000000 --- a/tests/test1.php +++ /dev/null @@ -1,21 +0,0 @@ -lock('test', 10000); - - if ($lock) { - print_r($lock); - } else { - print "Lock not acquired\n"; - } -}