-
-
Notifications
You must be signed in to change notification settings - Fork 31
Add Predis timeout #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
antriver
wants to merge
1
commit into
arvenil:master
Choose a base branch
from
antriver:predis-timeout
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,15 +16,20 @@ | |
* | ||
* @author Kamil Dziedzic <[email protected]> | ||
*/ | ||
class PredisRedisLock extends LockAbstract | ||
class PredisRedisLock extends LockAbstract implements LockExpirationInterface | ||
{ | ||
/** | ||
* Predis connection | ||
* | ||
* @var | ||
* @var Predis\Client | ||
*/ | ||
protected $client; | ||
|
||
/** | ||
* @var int Expiration time of the lock in seconds | ||
*/ | ||
protected $expiration = 0; | ||
|
||
/** | ||
* @param $client Predis\Client | ||
*/ | ||
|
@@ -35,18 +40,71 @@ public function __construct($client) | |
$this->client = $client; | ||
} | ||
|
||
/** | ||
* @param int $expiration Expiration time of the lock in seconds | ||
*/ | ||
public function setExpiration($expiration) | ||
{ | ||
$this->expiration = $expiration; | ||
} | ||
|
||
/** | ||
* @param string $name | ||
* @param bool $blocking | ||
* @return bool | ||
*/ | ||
protected function getLock($name, $blocking) | ||
{ | ||
if (!$this->client->setnx($name, serialize($this->getLockInformation()))) { | ||
return false; | ||
/** | ||
* Perform the process recommended by Redis for acquiring a lock, from here: https://redis.io/commands/setnx | ||
* We are "C4" in this example... | ||
* | ||
* 1. C4 sends SETNX lock.foo in order to acquire the lock (sets the value if it does not already exist). | ||
* 2. The crashed client C3 still holds it, so Redis will reply with 0 to C4. | ||
* 3. C4 sends GET lock.foo to check if the lock expired. | ||
* If it is not, it will sleep for some time and retry from the start. | ||
* 4. Instead, if the lock is expired because the Unix time at lock.foo is older than the current Unix time, | ||
* C4 tries to perform: | ||
* GETSET lock.foo <current Unix timestamp + lock timeout + 1> | ||
* Because of the GETSET semantic, C4 can check if the old value stored at key is still an expired timestamp | ||
* If it is, the lock was acquired. | ||
* 5. If another client, for instance C5, was faster than C4 and acquired the lock with the GETSET operation, | ||
* the C4 GETSET operation will return a non expired timestamp. | ||
* C4 will simply restart from the first step. Note that even if C4 wrote they key and set the expiry time | ||
* a few seconds in the future this is not a problem. C5's timeout will just be a few seconds later. | ||
*/ | ||
|
||
$lockValue = $this->getLockInformation(); | ||
if ($this->expiration) { | ||
// Add expiration timestamp to value stored in Redis. | ||
$lockValue['expires'] = time() + $this->expiration; | ||
} | ||
$lockValue = serialize($lockValue); | ||
|
||
return true; | ||
if ($this->client->setnx($name, $lockValue)) { | ||
return true; | ||
} | ||
|
||
// Check if the existing lock has an expiry time. If it does and it has expired, delete the lock. | ||
if ($existingValue = $this->client->get($name)) { | ||
$existingValue = unserialize($existingValue); | ||
if (!empty($existingValue['expires']) && $existingValue['expires'] <= time()) { | ||
// The existing lock has expired. We can delete it and take over. | ||
$newExistingValue = unserialize($this->client->getset($name, $lockValue)); | ||
|
||
// GETSET atomically sets key to value and returns the old value that was stored at key. | ||
// If the old value from getset does not still contain an expired timestamp | ||
// another probably acquired the lock in the meantime. | ||
if ($newExistingValue['expires'] > time()) { | ||
return false; | ||
} | ||
|
||
// Got it! | ||
return true; | ||
} | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
|
@@ -57,7 +115,7 @@ protected function getLock($name, $blocking) | |
*/ | ||
public function releaseLock($name) | ||
{ | ||
if (isset($this->locks[$name]) && $this->client->del($name)) { | ||
if (isset($this->locks[$name]) && $this->client->del(array($name))) { | ||
unset($this->locks[$name]); | ||
|
||
return true; | ||
|
@@ -76,4 +134,20 @@ public function isLocked($name) | |
{ | ||
return null !== $this->client->get($name); | ||
} | ||
|
||
/** | ||
* Forget a lock without releasing it | ||
* | ||
* @param string $name name of lock | ||
* @return bool | ||
*/ | ||
public function clearLock($name) | ||
{ | ||
if (!isset($this->locks[$name])) { | ||
return false; | ||
} | ||
|
||
unset($this->locks[$name]); | ||
return true; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,7 +21,7 @@ class ResolvedHostnameLockInformationProvider extends BasicLockInformationProvid | |
public function getLockInformation() | ||
{ | ||
$params = parent::getLockInformation(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed the parent call - |
||
$params[] = gethostbyname(gethostname()); | ||
$params['hostIp'] = gethostbyname(gethostname()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a key the same as above in BasicLockInformationProvider |
||
|
||
return $params; | ||
} | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
|
||
namespace NinjaMutex\Tests\Lock; | ||
|
||
use NinjaMutex\Lock\PredisRedisLock; | ||
use NinjaMutex\Mutex; | ||
use NinjaMutex\Tests\Mock\MockPredisClient; | ||
|
||
class PredisRedisLockTest extends \NinjaMutex\Tests\AbstractTest | ||
{ | ||
protected function createPredisClient() | ||
{ | ||
return new MockPredisClient(); | ||
} | ||
|
||
protected function createLock($predisClient) | ||
{ | ||
return new PredisRedisLock($predisClient); | ||
} | ||
|
||
public function testAcquireLock() | ||
{ | ||
$predis = $this->createPredisClient(); | ||
$lock = $this->createLock($predis); | ||
$mutex = new Mutex('very-critical-stuff', $lock); | ||
$this->assertTrue($mutex->acquireLock()); | ||
} | ||
|
||
public function testAcquireLockFails() | ||
{ | ||
$predis = $this->createPredisClient(); | ||
|
||
// Acquire lock in 1st instance - should succeed | ||
$lock = $this->createLock($predis); | ||
$mutex = new Mutex('very-critical-stuff', $lock); | ||
$this->assertTrue($mutex->acquireLock()); | ||
|
||
// Acquire lock in 2nd instance - should fail instantly because 0 timeout | ||
$lock2 = $this->createLock($predis); | ||
$mutex2 = new Mutex('very-critical-stuff', $lock2); | ||
$this->assertFalse($mutex2->acquireLock(0)); | ||
} | ||
|
||
public function testAcquireLockSucceedsAfterReleased() | ||
{ | ||
$predis = $this->createPredisClient(); | ||
|
||
// Acquire lock in 1st instance - should succeed | ||
$lock = $this->createLock($predis); | ||
$mutex = new Mutex('very-critical-stuff', $lock); | ||
$this->assertTrue($mutex->acquireLock()); | ||
|
||
$this->assertTrue($mutex->releaseLock()); | ||
|
||
// Acquire lock in 2nd instance - should succeed because 1st lock had been released | ||
$lock2 = $this->createLock($predis); | ||
$mutex2 = new Mutex('very-critical-stuff', $lock2); | ||
$this->assertTrue($mutex2->acquireLock(0)); | ||
} | ||
|
||
public function testAcquireLockSucceedsAfterTimeout() | ||
{ | ||
$predis = $this->createPredisClient(); | ||
|
||
// Acquire lock in 1st instance - should succeed | ||
$lock = $this->createLock($predis); | ||
$lock->setExpiration(2); | ||
$mutex = new Mutex('very-critical-stuff', $lock); | ||
$this->assertTrue($mutex->acquireLock()); | ||
|
||
// Acquire lock in 2nd instance - should succeed after 2 seconds | ||
$lock2 = $this->createLock($predis); | ||
$mutex2 = new Mutex('very-critical-stuff', $lock2); | ||
$this->assertTrue($mutex2->acquireLock()); | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to give these keys because now that there can be different information providers the order, length, and contents of the lock values will vary. The predis lock adds an 'expiration' key to this so the structure needs to be known.
Plus it just makes sense to give these keys now that the data can be anything a provider wants to use.