├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Exceptions │ ├── ClosureRefreshException.php │ └── QueueWithoutOverlapRefreshException.php ├── Facades │ └── RedLock.php ├── RedLock.php ├── RedLockServiceProvider.php └── Traits │ └── QueueWithoutOverlap.php └── tests ├── QueueWithoutOverlapTest.php ├── RedLockFacadeTest.php ├── RedLockTest.php ├── TestCase.php └── fixtures ├── QueueWithoutOverlapJob.php └── QueueWithoutOverlapJobDefaultLockTime.php /.gitignore: -------------------------------------------------------------------------------- 1 | sftp-config.json 2 | vendor 3 | composer.lock 4 | 5 | \.idea/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 That's Us 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel RedLock 2 | 3 | Provides a generic locking mechanism using Redis. Implements the locking standard proposed by Redis. 4 | 5 | 6 | 7 | ### Acknowledgements 8 | 9 | This library was originally built by LibiChai based on the Redlock algorithm developed by antirez. The library was reworked by the team at That's Us, Inc. 10 | 11 | ### Installation 12 | 13 | 1. `composer require thatsus/laravel-redlock` 14 | 2. Add `ThatsUs\RedLock\RedLockServiceProvider::class,` to the `providers` array in config/app.php 15 | 3. Enjoy! 16 | 17 | 18 | ### It's Simple! 19 | 20 | Set a lock on any scalar. If the `lock()` method returns false, you did not acquire the lock. 21 | 22 | Store results of the `lock()` method. Use this value to release the lock with `unlock()`. 23 | 24 | ### Example 25 | 26 | This example sets a lock on the key "1" with a 3 second expiration time. 27 | 28 | If it acquired the lock, it does some work and releases the lock. 29 | 30 | ```php 31 | use ThatsUs\RedLock\Facades\RedLock; 32 | 33 | $product_id = 1; 34 | 35 | $lock_token = RedLock::lock($product_id, 3000); 36 | 37 | if ($lock_token) { 38 | 39 | $order->submit($product_id); 40 | 41 | RedLock::unlock($lock_token); 42 | } 43 | ``` 44 | 45 | ### Refresh 46 | 47 | Use `refreshLock()` to reacquire and extend the time of your lock. 48 | 49 | ```php 50 | use ThatsUs\RedLock\Facades\RedLock; 51 | 52 | $product_ids = [1, 2, 3, 5, 7]; 53 | 54 | $lock_token = RedLock::lock('order-submitter', 3000); 55 | 56 | while ($product_ids && $lock_token) { 57 | 58 | $order->submit(array_shift($product_ids)); 59 | 60 | $lock_token = RedLock::refreshLock($lock_token); 61 | } 62 | 63 | RedLock::unlock($lock_token); 64 | ``` 65 | 66 | ### Even Easier with Closures 67 | 68 | Use `runLocked()` for nicer syntax. The method returns the results of the closure, or else false if the lock could not be acquired. 69 | 70 | ```php 71 | use ThatsUs\RedLock\Facades\RedLock; 72 | 73 | $product_id = 1; 74 | 75 | $result = RedLock::runLocked($product_id, 3000, function () use ($order, $product_id) { 76 | $order->submit($product_id); 77 | return true; 78 | }); 79 | 80 | echo $result ? 'Worked!' : 'Lock not acquired.'; 81 | ``` 82 | 83 | ### Refresh with Closures 84 | 85 | You can easily refresh your tokens when using closures. The first parameter to your closure is `$refresh`. Simply call it when you want to refresh. If the lock cannot be refreshed, `$refresh()` will break out of the closure. 86 | 87 | ```php 88 | use ThatsUs\RedLock\Facades\RedLock; 89 | 90 | $product_ids = [1, 2, 3, 5, 7]; 91 | 92 | $result = RedLock::runLocked($product_id, 3000, function ($refresh) use ($order, $product_ids) { 93 | foreach ($product_ids as $product_id) { 94 | $refresh(); 95 | $order->submit($product_id); 96 | } 97 | return true; 98 | }); 99 | 100 | echo $result ? 'Worked!' : 'Lock lost or never acquired.'; 101 | ``` 102 | 103 | ### Lock Queue Jobs Easily 104 | 105 | If you're running jobs on a Laravel queue, you may want to avoid queuing up the same job more than once at a time. 106 | 107 | The `ThatsUs\RedLock\Traits\QueueWithoutOverlap` trait provides this functionality with very few changes to your job. Usually only two changes are necessary. 108 | 109 | 1. `use ThatsUs\RedLock\Traits\QueueWithoutOverlap` as a trait 110 | 2. Change the `handle()` method to `handleSync()` 111 | 112 | ```php 113 | use ThatsUs\RedLock\Traits\QueueWithoutOverlap; 114 | 115 | class OrderProductJob 116 | { 117 | use QueueWithoutOverlap; 118 | 119 | public function __construct($order, $product_id) 120 | { 121 | $this->order = $order; 122 | $this->product_id = $product_id; 123 | } 124 | 125 | public function handleSync() 126 | { 127 | $this->order->submit($this->product_id); 128 | } 129 | 130 | } 131 | ``` 132 | 133 | Sometimes it's also necessary to specify a `getLockKey()` method. This method must return the string that needs to be locked. 134 | 135 | This is typically unnecessary because the lock key can be generated automatically. But if the job's data is not easy to stringify, you must define the `getLockKey()` method. 136 | 137 | This trait also provides a refresh method called `refreshLock()`. If `refreshLock()` is unable to refresh the lock, an exception is thrown and the job fails. 138 | 139 | Finally, you can change the lock time-to-live from the default 300 seconds to another 140 | value using the `$lock_time` property. 141 | 142 | ```php 143 | use ThatsUs\RedLock\Traits\QueueWithoutOverlap; 144 | 145 | class OrderProductsJob 146 | { 147 | use QueueWithoutOverlap; 148 | 149 | protected $lock_time = 600; // 10 minutes in seconds 150 | 151 | public function __construct($order, array $product_ids) 152 | { 153 | $this->order = $order; 154 | $this->product_ids = $product_ids; 155 | } 156 | 157 | // We need to define getLockKey() because $product_ids is an array and the 158 | // automatic key generator can't deal with arrays. 159 | protected function getLockKey() 160 | { 161 | $product_ids = implode(',', $this->product_ids); 162 | return "OrderProductsJob:{$this->order->id}:{$product_ids}"; 163 | } 164 | 165 | public function handleSync() 166 | { 167 | foreach ($this->product_ids as $product_id) { 168 | $this->refreshLock(); 169 | $this->order->submit($product_id); 170 | } 171 | } 172 | 173 | } 174 | ``` 175 | 176 | ### Contribution 177 | 178 | If you find a bug or want to contribute to the code or documentation, you can help by submitting an [issue](https://github.com/thatsus/laravel-redlock/issues) or a [pull request](https://github.com/thatsus/laravel-redlock/pulls). 179 | 180 | ### License 181 | 182 | [MIT](http://opensource.org/licenses/MIT) 183 | 184 | 185 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "thatsus/laravel-redlock", 3 | "description": "Redis distributed locks for laravel", 4 | "license": "MIT", 5 | "keywords": ["redlock", "laravel redis lock", "redis lock"], 6 | "authors": [ 7 | { 8 | "name": "LibiChai", 9 | "email": "chaiguoxing@qq.com" 10 | }, 11 | { 12 | "name": "Daniel Kuck-Alvarez", 13 | "email": "dankuck@gmail.com" 14 | }, 15 | { 16 | "name": "Potsky", 17 | "email": "potsky@me.com" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.4.0", 22 | "illuminate/support": "^5.0", 23 | "illuminate/console": "^5.0", 24 | "predis/predis": "^1.1" 25 | }, 26 | "require-dev": { 27 | "orchestra/testbench": "~3.0", 28 | "php-mock/php-mock-mockery": "^1.1", 29 | "phpunit/phpunit": "~5.7" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "ThatsUs\\RedLock\\": "src/" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "classmap": [ 38 | "tests/" 39 | ] 40 | }, 41 | "extra": { 42 | "laravel": { 43 | "providers": [ 44 | "ThatsUs\\RedLock\\RedLockServiceProvider" 45 | ] 46 | } 47 | }, 48 | "scripts": { 49 | "test": [ 50 | "rm vendor -rf; rm composer.lock; echo 1", 51 | "composer require --dev orchestra/testbench 3.4", 52 | "phpunit | tee phpunit.4.log", 53 | "rm vendor -rf; rm composer.lock; echo 1", 54 | "composer require --dev orchestra/testbench 3.3", 55 | "phpunit | tee phpunit.3.log", 56 | "rm vendor -rf; rm composer.lock; echo 1", 57 | "composer require --dev orchestra/testbench 3.2", 58 | "phpunit | tee phpunit.2.log", 59 | "cat phpunit.*.log" 60 | ] 61 | }, 62 | "minimum-stability": "stable" 63 | } 64 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | src/ 20 | 21 | 22 | app/Http/routes.php 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Exceptions/ClosureRefreshException.php: -------------------------------------------------------------------------------- 1 | servers = $servers; 20 | $this->retryDelay = $retryDelay; 21 | $this->retryCount = $retryCount; 22 | $this->quorum = min(count($servers), (count($servers) / 2 + 1)); 23 | } 24 | 25 | public function lock($resource, $ttl) 26 | { 27 | $this->initInstances(); 28 | $token = uniqid(); 29 | $retry = $this->retryCount; 30 | do { 31 | $n = 0; 32 | $startTime = microtime(true) * 1000; 33 | foreach ($this->instances as $instance) { 34 | if ($this->lockInstance($instance, $resource, $token, $ttl)) { 35 | $n++; 36 | } 37 | } 38 | # Add 2 milliseconds to the drift to account for Redis expires 39 | # precision, which is 1 millisecond, plus 1 millisecond min drift 40 | # for small TTLs. 41 | $drift = ($ttl * $this->clockDriftFactor) + 2; 42 | $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift; 43 | if ($n >= $this->quorum && $validityTime > 0) { 44 | return [ 45 | 'validity' => $validityTime, 46 | 'resource' => $resource, 47 | 'token' => $token, 48 | 'ttl' => $ttl, 49 | ]; 50 | } else { 51 | foreach ($this->instances as $instance) { 52 | $this->unlockInstance($instance, $resource, $token); 53 | } 54 | } 55 | // Wait a random delay before to retry 56 | $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay); 57 | usleep($delay * 1000); 58 | $retry--; 59 | } while ($retry > 0); 60 | return false; 61 | } 62 | 63 | public function unlock(array $lock) 64 | { 65 | $this->initInstances(); 66 | $resource = $lock['resource']; 67 | $token = $lock['token']; 68 | foreach ($this->instances as $instance) { 69 | $this->unlockInstance($instance, $resource, $token); 70 | } 71 | } 72 | 73 | private function initInstances() 74 | { 75 | $app = app(); 76 | if (empty($this->instances)) { 77 | foreach ($this->servers as $server) { 78 | // support newer and older Laravel 5.* 79 | if (method_exists($app, 'makeWith')) { 80 | $redis = $app->makeWith(Redis::class, ['parameters' => $server]); 81 | } else { 82 | $redis = $app->make(Redis::class, [$server]); 83 | } 84 | $this->instances[] = $redis; 85 | } 86 | } 87 | } 88 | 89 | private function lockInstance($instance, $resource, $token, $ttl) 90 | { 91 | return $instance->set($resource, $token, "PX", $ttl, "NX"); 92 | //return $instance->set($resource, $token, ['NX', 'PX' => $ttl]); 93 | } 94 | 95 | private function unlockInstance($instance, $resource, $token) 96 | { 97 | $script = ' 98 | if redis.call("GET", KEYS[1]) == ARGV[1] then 99 | return redis.call("DEL", KEYS[1]) 100 | else 101 | return 0 102 | end 103 | '; 104 | return $instance->eval($script, 1, $resource, $token); 105 | //return $instance->eval($script, [$resource, $token], 1); 106 | } 107 | 108 | public function refreshLock(array $lock) 109 | { 110 | $this->unlock($lock); 111 | return $this->lock($lock['resource'], $lock['ttl']); 112 | } 113 | 114 | public function runLocked($resource, $ttl, $closure) 115 | { 116 | $lock = $this->lock($resource, $ttl); 117 | if (!$lock) { 118 | return false; 119 | } 120 | $refresh = function () use (&$lock) { 121 | $lock = $this->refreshLock($lock); 122 | if (!$lock) { 123 | throw new Exceptions\ClosureRefreshException(); 124 | } 125 | }; 126 | try { 127 | $result = $closure($refresh); 128 | } catch (Exceptions\ClosureRefreshException $e) { 129 | return false; 130 | } finally { 131 | if (is_array($lock)) { 132 | $this->unlock($lock); 133 | } 134 | } 135 | return $result; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/RedLockServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('redlock', function ($app) { 24 | return new RedLock( 25 | config('database.redis.servers') ?: [config('database.redis.default')], 26 | config('database.redis.redis_lock.retry_delay'), 27 | config('database.redis.redis_lock.retry_count') 28 | ); 29 | }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Traits/QueueWithoutOverlap.php: -------------------------------------------------------------------------------- 1 | acquireLock()) { 24 | return $this->pushCommandToQueue($queue, $command); 25 | } else { 26 | // do nothing, could not get lock 27 | return false; 28 | } 29 | } 30 | 31 | /** 32 | * Lock this job's key in redis, so no other 33 | * jobs can run with the same key. 34 | * @return bool - false if it fails to lock 35 | */ 36 | protected function acquireLock(array $lock = []) 37 | { 38 | $lock_time = isset($this->lock_time) ? $this->lock_time : 300; // in seconds; 5 minutes default 39 | $this->lock = RedLock::lock($lock['resource'] ?? $this->getLockKey(), $lock_time * 1000); 40 | return (bool)$this->lock; 41 | } 42 | 43 | /** 44 | * Unlock this job's key in redis, so other 45 | * jobs can run with the same key. 46 | * @return void 47 | */ 48 | protected function releaseLock() 49 | { 50 | if ($this->lock) { 51 | RedLock::unlock($this->lock); 52 | } 53 | } 54 | 55 | /** 56 | * Build a unique key based on the values stored in this job. 57 | * Any job with the same values is assumed to represent the same 58 | * task and so will not overlap this. 59 | * 60 | * Override this method if necessary. 61 | * 62 | * @return string 63 | */ 64 | protected function getLockKey() 65 | { 66 | $values = collect((array)$this) 67 | ->values() 68 | ->map(function ($value) { 69 | if ($value instanceof Model) { 70 | return $value->id; 71 | } else if (is_object($value) || is_array($value)) { 72 | throw new \Exception('This job cannot auto-generate a lock-key. Please define getLockKey() on ' . get_class($this) . '.'); 73 | } else { 74 | return $value; 75 | } 76 | }); 77 | return get_class($this) . ':' . $values->implode(':'); 78 | } 79 | 80 | /** 81 | * This code is copied from Illuminate\Bus\Dispatcher v5.4 82 | * @ https://github.com/laravel/framework/blob/5.4/src/Illuminate/Bus/Dispatcher.php#L163 83 | * 84 | * Push the command onto the given queue instance. 85 | * 86 | * @param \Illuminate\Contracts\Queue\Queue $queue 87 | * @param mixed $command 88 | * @return mixed 89 | */ 90 | protected function pushCommandToQueue($queue, $command) 91 | { 92 | if (isset($command->queue, $command->delay)) { 93 | return $queue->laterOn($command->queue, $command->delay, $command); 94 | } 95 | if (isset($command->queue)) { 96 | return $queue->pushOn($command->queue, $command); 97 | } 98 | if (isset($command->delay)) { 99 | return $queue->later($command->delay, $command); 100 | } 101 | return $queue->push($command); 102 | } 103 | 104 | /** 105 | * Normal jobs are called via handle. Use handleSync instead. 106 | * @return void 107 | */ 108 | public function handle() 109 | { 110 | try { 111 | $this->handleSync(); 112 | } finally { 113 | $this->releaseLock(); 114 | } 115 | } 116 | 117 | /** 118 | * Attempt to reacquire and extend the lock. 119 | * @return bool true if the lock is reacquired, false if it is not 120 | */ 121 | protected function refreshLock() 122 | { 123 | $this->releaseLock(); 124 | if (!$this->acquireLock($this->lock)) { 125 | throw new QueueWithoutOverlapRefreshException(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/QueueWithoutOverlapTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('push')->with($job)->once(); 22 | 23 | RedLock::shouldReceive('lock') 24 | ->with("ThatsUs\RedLock\Traits\QueueWithoutOverlapJob::1000:", 1000000) 25 | ->twice() 26 | ->andReturn(['resource' => 'ThatsUs\RedLock\Traits\QueueWithoutOverlapJob::1000:']); 27 | RedLock::shouldReceive('unlock') 28 | ->with(['resource' => 'ThatsUs\RedLock\Traits\QueueWithoutOverlapJob::1000:']) 29 | ->twice() 30 | ->andReturn(true); 31 | 32 | $job->queue($queue, $job); 33 | 34 | $job->handle(); 35 | 36 | $this->assertTrue($job->ran); 37 | } 38 | 39 | public function testFailToLock() 40 | { 41 | $job = new QueueWithoutOverlapJob(); 42 | 43 | $queue = Mockery::mock(); 44 | 45 | RedLock::shouldReceive('lock') 46 | ->with("ThatsUs\RedLock\Traits\QueueWithoutOverlapJob::1000:", 1000000) 47 | ->once() 48 | ->andReturn(false); 49 | 50 | $id = $job->queue($queue, $job); 51 | 52 | $this->assertFalse($id); 53 | } 54 | 55 | public function testFailToRefresh() 56 | { 57 | $job = new QueueWithoutOverlapJob(); 58 | 59 | $queue = Mockery::mock(); 60 | $queue->shouldReceive('push')->with($job)->once(); 61 | 62 | RedLock::shouldReceive('lock') 63 | ->with("ThatsUs\RedLock\Traits\QueueWithoutOverlapJob::1000:", 1000000) 64 | ->twice() 65 | ->andReturn( 66 | ['resource' => 'ThatsUs\RedLock\Traits\QueueWithoutOverlapJob::1000:'], 67 | false 68 | ); 69 | RedLock::shouldReceive('unlock') 70 | ->with(['resource' => 'ThatsUs\RedLock\Traits\QueueWithoutOverlapJob::1000:']) 71 | ->once() 72 | ->andReturn(true); 73 | 74 | $job->queue($queue, $job); 75 | 76 | $this->expectException('ThatsUs\RedLock\Exceptions\QueueWithoutOverlapRefreshException'); 77 | 78 | $job->handle(); 79 | } 80 | 81 | public function testAllOfItDefaultLockTime() 82 | { 83 | $job = new QueueWithoutOverlapJobDefaultLockTime(); 84 | 85 | $queue = Mockery::mock(); 86 | $queue->shouldReceive('push')->with($job)->once(); 87 | 88 | RedLock::shouldReceive('lock') 89 | ->with("ThatsUs\RedLock\Traits\QueueWithoutOverlapJobDefaultLockTime::", 300000) 90 | ->twice() 91 | ->andReturn(['resource' => "ThatsUs\RedLock\Traits\QueueWithoutOverlapJobDefaultLockTime::"]); 92 | RedLock::shouldReceive('unlock') 93 | ->with(['resource' => "ThatsUs\RedLock\Traits\QueueWithoutOverlapJobDefaultLockTime::"]) 94 | ->twice() 95 | ->andReturn(true); 96 | 97 | $job->queue($queue, $job); 98 | 99 | $job->handle(); 100 | 101 | $this->assertTrue($job->ran); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/RedLockFacadeTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('doodad')->once(); 16 | App::instance('redlock', $mock); 17 | 18 | RedLock::doodad(); 19 | } 20 | 21 | public function testRoot() 22 | { 23 | $this->assertTrue(RedLock::getFacadeRoot() instanceof \ThatsUs\RedLock\RedLock); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/RedLockTest.php: -------------------------------------------------------------------------------- 1 | 'host.test', 15 | 'password' => 'password', 16 | 'port' => 6379, 17 | 'database' => 0, 18 | ], 19 | ]; 20 | 21 | public function testInstanciate() 22 | { 23 | new RedLock([]); 24 | } 25 | 26 | private function assertGoodRedisMake($args) 27 | { 28 | $this->assertTrue(is_array($args)); 29 | $app = app(); 30 | if (method_exists($app, 'makeWith')) { 31 | // Laravel 5.4+ 32 | $this->assertEquals($this->servers[0], $args['parameters']); 33 | } else { 34 | // Laravel 5.0 - 5.3 35 | $this->assertEquals($this->servers[0], $args[0]); 36 | } 37 | } 38 | 39 | public function testLock() 40 | { 41 | $caught_args = null; 42 | App::bind(Redis::class, function($app, $args) use (&$caught_args) { 43 | $caught_args = $args; 44 | $predis = Mockery::mock(Redis::class); 45 | $predis->shouldReceive('set') 46 | ->with('XYZ', Mockery::any(), "PX", 300000, "NX") 47 | ->once() 48 | ->andReturn(true); 49 | return $predis; 50 | }); 51 | 52 | $redlock = new RedLock($this->servers); 53 | $lock = $redlock->lock('XYZ', 300000); 54 | 55 | $this->assertGoodRedisMake($caught_args); 56 | $this->assertEquals('XYZ', $lock['resource']); 57 | $this->assertTrue(is_numeric($lock['validity'])); 58 | $this->assertNotNull($lock['token']); 59 | } 60 | 61 | public function testUnlock() 62 | { 63 | $caught_args = null; 64 | App::bind(Redis::class, function($app, $args) use (&$caught_args) { 65 | $caught_args = $args; 66 | $predis = Mockery::mock(Redis::class); 67 | $predis->shouldReceive('eval') 68 | ->with(Mockery::any(), 1, 'XYZ', '1234') 69 | ->once() 70 | ->andReturn(true); 71 | return $predis; 72 | }); 73 | 74 | $redlock = new RedLock($this->servers); 75 | $redlock->unlock([ 76 | 'resource' => 'XYZ', 77 | 'validity' => 300000, 78 | 'token' => 1234, 79 | ]); 80 | 81 | $this->assertGoodRedisMake($caught_args); 82 | } 83 | 84 | public function testLockFail() 85 | { 86 | $caught_args = null; 87 | App::bind(Redis::class, function($app, $args) use (&$caught_args) { 88 | $caught_args = $args; 89 | $predis = Mockery::mock(Redis::class); 90 | $predis->shouldReceive('set') 91 | ->with('XYZ', Mockery::any(), "PX", 300000, "NX") 92 | ->times(3) 93 | ->andReturn(false); 94 | $predis->shouldReceive('eval') 95 | ->with(Mockery::any(), 1, 'XYZ', Mockery::any()) 96 | ->times(3) 97 | ->andReturn(true); 98 | return $predis; 99 | }); 100 | 101 | $redlock = new RedLock($this->servers); 102 | $lock = $redlock->lock('XYZ', 300000); 103 | 104 | $this->assertGoodRedisMake($caught_args); 105 | $this->assertFalse($lock); 106 | } 107 | 108 | public function testUnlockFail() 109 | { 110 | $caught_args = null; 111 | App::bind(Redis::class, function($app, $args) use (&$caught_args) { 112 | $caught_args = $args; 113 | $predis = Mockery::mock(Redis::class); 114 | $predis->shouldReceive('eval') 115 | ->with(Mockery::any(), 1, 'XYZ', '1234') 116 | ->once() 117 | ->andReturn(false); 118 | return $predis; 119 | }); 120 | 121 | $redlock = new RedLock($this->servers); 122 | $redlock->unlock([ 123 | 'resource' => 'XYZ', 124 | 'validity' => 300000, 125 | 'token' => 1234, 126 | ]); 127 | 128 | $this->assertGoodRedisMake($caught_args); 129 | } 130 | 131 | public function testRefresh() 132 | { 133 | $caught_args = null; 134 | App::bind(Redis::class, function($app, $args) use (&$caught_args) { 135 | $caught_args = $args; 136 | $predis = Mockery::mock(Redis::class); 137 | $predis->shouldReceive('eval') 138 | ->with(Mockery::any(), 1, 'XYZ', '1234') 139 | ->once() 140 | ->andReturn(true); 141 | $predis->shouldReceive('set') 142 | ->with('XYZ', Mockery::any(), "PX", 300000, "NX") 143 | ->once() 144 | ->andReturn(true); 145 | return $predis; 146 | }); 147 | 148 | $redlock = new RedLock($this->servers); 149 | $lock = $redlock->refreshLock([ 150 | 'resource' => 'XYZ', 151 | 'validity' => 300000, 152 | 'token' => 1234, 153 | 'ttl' => 300000, 154 | ]); 155 | 156 | $this->assertGoodRedisMake($caught_args); 157 | $this->assertEquals('XYZ', $lock['resource']); 158 | $this->assertTrue(is_numeric($lock['validity'])); 159 | $this->assertNotNull($lock['token']); 160 | } 161 | 162 | public function testRunLocked() 163 | { 164 | $caught_args = null; 165 | App::bind(Redis::class, function($app, $args) use (&$caught_args) { 166 | $caught_args = $args; 167 | $predis = Mockery::mock(Redis::class); 168 | $predis->shouldReceive('set') 169 | ->with('XYZ', Mockery::any(), "PX", 300000, "NX") 170 | ->once() 171 | ->andReturn(true); 172 | $predis->shouldReceive('eval') 173 | ->with(Mockery::any(), 1, 'XYZ', Mockery::any()) 174 | ->once() 175 | ->andReturn(true); 176 | return $predis; 177 | }); 178 | 179 | $redlock = new RedLock($this->servers); 180 | $results = $redlock->runLocked('XYZ', 300000, function () { 181 | return "ABC"; 182 | }); 183 | 184 | $this->assertGoodRedisMake($caught_args); 185 | $this->assertEquals('ABC', $results); 186 | } 187 | 188 | public function testRunLockedRefresh() 189 | { 190 | $caught_args = null; 191 | App::bind(Redis::class, function($app, $args) use (&$caught_args) { 192 | $caught_args = $args; 193 | $predis = Mockery::mock(Redis::class); 194 | $predis->shouldReceive('set') 195 | ->with('XYZ', Mockery::any(), "PX", 300000, "NX") 196 | ->twice() 197 | ->andReturn(true); 198 | $predis->shouldReceive('eval') 199 | ->with(Mockery::any(), 1, 'XYZ', Mockery::any()) 200 | ->twice() 201 | ->andReturn(true); 202 | return $predis; 203 | }); 204 | 205 | $redlock = new RedLock($this->servers); 206 | $results = $redlock->runLocked('XYZ', 300000, function ($refresh) { 207 | $refresh(); 208 | return "ABC"; 209 | }); 210 | 211 | $this->assertGoodRedisMake($caught_args); 212 | $this->assertEquals('ABC', $results); 213 | } 214 | 215 | public function testRunLockedRefreshFail() 216 | { 217 | $caught_args = null; 218 | App::bind(Redis::class, function($app, $args) use (&$caught_args) { 219 | $caught_args = $args; 220 | $predis = Mockery::mock(Redis::class); 221 | $predis->shouldReceive('set') 222 | ->with('XYZ', Mockery::any(), "PX", 300000, "NX") 223 | ->times(4) 224 | ->andReturn(true, false, false, false); 225 | $predis->shouldReceive('eval') 226 | ->with(Mockery::any(), 1, 'XYZ', Mockery::any()) 227 | ->times(4) 228 | ->andReturn(true); 229 | return $predis; 230 | }); 231 | 232 | $redlock = new RedLock($this->servers); 233 | $results = $redlock->runLocked('XYZ', 300000, function ($refresh) { 234 | $refresh(); 235 | return "ABC"; 236 | }); 237 | 238 | $this->assertGoodRedisMake($caught_args); 239 | $this->assertFalse($results); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | refreshLock(); 25 | $this->ran = true; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tests/fixtures/QueueWithoutOverlapJobDefaultLockTime.php: -------------------------------------------------------------------------------- 1 | refreshLock(); 24 | $this->ran = true; 25 | } 26 | } 27 | --------------------------------------------------------------------------------