-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
Add Zookeeper data store for Lock Component #27920
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
Add Zookeeper data store for Lock Component #27920
Conversation
@@ -1,5 +1,9 @@ | |||
CHANGELOG | |||
========= | |||
4.2.0 |
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 am not sure if this correct. Can someone confirm what would go here?
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.
This looks good. But you should add a blank line between the current lines 2 and 3. :)
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.
Thanks. Done.
* | ||
* @author Ganesh Chandrasekaran <gchandrasekaran@wayfair.com> | ||
* | ||
* @requires extension zookeeper |
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 need help with this - Does this make the tests skip if the environment doesn't have a Zookeeper extension installed?
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.
Probably needs to be a class level annotation, not a generic comment in the file
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.
This was in wrong place by mistake. Moved.
9ff64c8
to
2e9e12e
Compare
/** | ||
* A base node within which all the locks for PHP application are managed in the Zookeeper environment. | ||
*/ | ||
const LOCK_NODE = '/PHP_LOCK_NODE'; |
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.
Can you add a modifier? I believe this version of Symfony supports it.
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.
Made it private.
} | ||
|
||
/** | ||
* @param \Symfony\Component\Lock\Key $key Key |
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.
@param Key $key
, but as it doesn't add anything, it can be removed
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.
Replaced with @inheritdoc
instead of removing it.
* | ||
* @param \Symfony\Component\Lock\Key $key Key | ||
* | ||
* @return bool |
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.
param and return annotations can be removed
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.
Replaced with @inheritdoc
instead of removing it.
/** | ||
* Retrieve an unique token for the given key. | ||
* | ||
* @param \Symfony\Component\Lock\Key $key The resource key to be stored in the storage |
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.
please import the class
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.
It is already imported. Removed the fully qualified path.
* | ||
* @author Ganesh Chandrasekaran <gchandrasekaran@wayfair.com> | ||
* | ||
* @requires extension zookeeper |
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.
Probably needs to be a class level annotation, not a generic comment in the file
try { | ||
$zookeeper = new Zookeeper(implode(',', array($zookeeper_server))); | ||
// The session creation happens asynchronously, so we can wait for a second to make sure it is established. | ||
sleep(1); |
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.
perhaps a small while loop and check every 10ms is sufficient? Should probably try to avoid long sleeps.
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.
Have added a while loop of 10 ms
each but, still keeping the max sleep time to 1 second
{ | ||
$zookeeper_server = getenv('ZOOKEEPER_HOST').':2181'; | ||
|
||
try { |
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.
Does everything really have to be in a try here?
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.
Wrapping the logic to get state in this as well in case it throws an exception.
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.
"Since version 0.3.0, this method emits ZookeeperException and it's derivatives."
For __construct
: "This method emits PHP error/warning when parameters count or types are wrong or could not init instance." The test should fail when this occurs, not be skipped, otherwise we'll never know if we made a mistake somewhere or if the connection can't be made. However, both cases are a legit reason to fail a test.
For getState
: "This method emits PHP error/warning when it fails to get state of zookeeper connection. " I would very much like to know if this fails, because it might be a setup issue, I rather not miss this if something broke.
To me it sounds like the whole try/catch should be removed and the markTestAsSkipped
for the connection underneath the while loop, should trigger a failure instead. If there would be a setup error in travis, we would never be notified of this as everything will always be marked as skipped. This means the test suite will probably just keep reporting that everything is okay.
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 agree with you on the first 2 points and have removed the try and catch
blocks.
However, I feel like connection errors should not be raised as failures because environments can be flaky for many reasons and we should probably have some other way to be monitoring those. I think that's what @jderusse also wanted to do with Redis, Memcached and other stores.
Having said that, how can I install zookeeper extension and run zookeeper server on the Travis?
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've got experience with php extensions and travis, so I can't help you on that. The result I'm aiming for, is that we can distinguish connection failures (faulty travis setup), from errors, so we know when the tests fail. Environments shouldn't be flaky, because it shouldn't be flaky in production either. If environments are flaky, that should be fixed asap.
} | ||
|
||
/** | ||
* @expectedException \Symfony\Component\Lock\Exception\LockConflictedException |
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.
Please use $this->expectException(LockConflictedException::class)
right before it's supposed to be thrown. Right now if getStore()
throws it, this test would pass, while we probably expect it on the second save($key)
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.
This should be fine as is. If getStore
throws an then it would fail saying that Failed asserting that exception of type "WhateverException" matches expected exception "\Symfony\Component\Lock\Exception\LockConflictedException"
. getStore
will never throw a LockConflictedException
.
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.
"getStore will never throw a LockConflictedException", not in this pull request. Tests should be robust, thus if someone throws an exception in another PR at an earlier point and this test does not fail, the test is wrong.
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.
Removed this test as per @jderusse 's suggestion that I shouldn't be modifying the behavior from the parent class for this.
{ | ||
$store = $this->getStore(); | ||
|
||
$resource = uniqid(__METHOD__, true); |
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.
base64 encode on random bytes sounds like a better idea here.
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.
Right. Fixed.
6f8bb93
to
0501a86
Compare
use Symfony\Component\Lock\Key; | ||
use Symfony\Component\Lock\StoreInterface; | ||
use Zookeeper; | ||
use Throwable; |
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.
Symfony does not "import" classes without namespace. Use directly \Throwable
when needed
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.
Do you mean they don't get autoloaded without namespace? These should be in global namespace and it works fine when I test locally. I don't know if it's a standard but I prefer declaring them at top so I don't have to add \
for every usage. If I am missing something can you please elaborate on this? Thanks!
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.
In PHP you have two options when working with namespace:
- importing the namespace (what you have done)
- using it directly (ex: new \Foo\Bar)
In Symfony, we never import global class in the global namespace (like Throwable, DateTime, SplFileObject, Exception ...)
So you have to update your code to remove the use Throwable
, then use directly \Throwable
Thanks
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.
Thanks @lyrixx ! I will update the code.
{ | ||
$zookeeper_server = getenv('ZOOKEEPER_HOST').':2181'; | ||
|
||
try { |
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.
"Since version 0.3.0, this method emits ZookeeperException and it's derivatives."
For __construct
: "This method emits PHP error/warning when parameters count or types are wrong or could not init instance." The test should fail when this occurs, not be skipped, otherwise we'll never know if we made a mistake somewhere or if the connection can't be made. However, both cases are a legit reason to fail a test.
For getState
: "This method emits PHP error/warning when it fails to get state of zookeeper connection. " I would very much like to know if this fails, because it might be a setup issue, I rather not miss this if something broke.
To me it sounds like the whole try/catch should be removed and the markTestAsSkipped
for the connection underneath the while loop, should trigger a failure instead. If there would be a setup error in travis, we would never be notified of this as everything will always be marked as skipped. This means the test suite will probably just keep reporting that everything is okay.
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.
As @iltar , I think this Store could be part of a dedicated bundle/bridge (the same question goes for the MongoDbStore and DbalStore ping @nicolas-grekas)
Aside few comments, I've some concerns regarding the reliability of this store: The lock is automatically released if the client session goes away
/** | ||
* @var string A prefix for the locking resource to identify the process | ||
*/ | ||
private $prefix; |
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.
Looks like a duplicate of const LOCK_NODE
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.
It's not a duplicate, but rather a prefix for every lock node. For example: const LOCK_NODE = PHP_LOCK_NODE
, while prefix can be ORDER
, so for an ORDER ID 1234
, the lock would be created as /PHP_LOCK_NODE/ORDER_1234
I see that the prefix
is not really required and the client can pass this in as the resource
itself. So removed it. I also removed the const LOCK_NODE
as we can let the client decide how they want to organize the nodes.
* @param string $lockPrefix Prefix for a locking resource | ||
*/ | ||
public function __construct( | ||
Zookeeper $zookeeper, |
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.
no need to use multiline declaration
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.
Fixed.
): string { | ||
set_error_handler(array(self::class, 'errorHandlerForCreateNode')); | ||
try { | ||
// If the node exists then update the value of the node. |
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 don't get this comment
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.
Bad copy paste. Removed.
continue; | ||
} | ||
|
||
$nodeCreated = $this->zookeeper->create($subpath, null, $acl); |
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.
Who is in charge of cleaning all this intermediate path?
Wouldn't it be easier to provide a node $node
without /
?
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.
It would be easier, but I also wanted to provide the functionality of having intermediate nodes if anyone wanted to organize those. Ideally it would be preferred to have just /node
for the sake of simplicity, but I can specify this in the documentation as a recommendation if that works for you.
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.
IMHO the client shouldn't have to know how stores works internally, and doesn't need to manipulate it. And I'm not sure that organizing "ephemeral nodes" worth it.
Moreover, the service asking a Lock may not (shouldn't?) be aware of the store used. It may not be aware using keys with slashes have a special meaning.
The issue with this feature are:
- it add a lot of complexity to the method
- it requires to create intermediate nodes that you can't delete theme afterward. what if a jobs locks
/foo/{id}/bar
and iterate over billions ofids
?
Do you have a use case in mind?
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 was following the recipe from here: https://zookeeper.apache.org/doc/r3.1.2/recipes.html#sc_recipes_Locks
But that protocol was recommending ephermeal + sequence
nodes. Since in our case we don't use sequence nodes, I think it is fine to not support multi-level nodes.
Changed the implementation to support single ephemeral node. In this case, I am validating the input during lock creation. Also added a test case for this.
|
||
// If a node needs to be created with the path "/node1/node2/node3", | ||
// this loop will create "/node1" and "/node1/node2" before creation of deepest level node. | ||
for ($numberOfParts = count($parts); $numberOfParts > 1; $numberOfParts = count($parts)) { |
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.
while (count($parts)>1)
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.
Fixed.
|
||
$nodeCreated = $this->zookeeper->create($node, $value, $acl, $type); | ||
if (!$nodeCreated) { | ||
throw new LockAcquiringException('Unable to create node for '.$subpath); |
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.
SHouldn't it be a LockConflictException
?
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.
If you see the error handler, if another node was created due to a race condition, it would throw a LockConflictedException
. If it reaches here, it means it failed to acquire the lock for some other reason.
throw new LockAcquiringException('Unable to create node for '.$subpath); | ||
} | ||
|
||
return $nodeCreated; |
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.
note used
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.
Removed.
*/ | ||
public function putOffExpiration(Key $key, $ttl) | ||
{ | ||
} |
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.
Add a comment saying why there is nothing to do
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.
Added.
return false; | ||
} | ||
|
||
return $this->zookeeper->get($lockNodePath) === $this->getToken($key); |
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.
what if the node is removed (by a conurrent request) just before this line?
IMHO you can remove the this->zookeeper->exists
and wrap this this->zookeeper->get
in a try/catch block
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.
The lock can only be removed by the original owner and no other concurrent request can delete it unless someone does it via command line or write a script to delete the nodes.
/** | ||
* @expectedException \Symfony\Component\Lock\Exception\LockConflictedException | ||
*/ | ||
public function testSaveTwice() |
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.
The original testSaveTwice
asserts that the store is able to save the same Key twice. This is the expected behavior of a Store.
If the test does not pass, I suggest to fix the implementation of the store instead of changing the behavior of the test...
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.
You are right, thanks for highlighting that. Removed this function from here.
{ | ||
$zookeeper_server = getenv('ZOOKEEPER_HOST').':2181'; | ||
|
||
$zookeeper = new Zookeeper(implode(',', array($zookeeper_server))); |
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.
How does Zookeeper handle integrity when using a cluster of servers?
Are you sure that creating an existing node would raise an exception even if 2 concurrent requests ask the creation at exactly the same time on 2 distinguished servers?
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.
Great question - Zookeeper always has only 1 leader and accepts writes through that leader and uses quorum, so even if it due to some race condition 2 process try to create a node, the second one will raise a warning and eventuallyLockConflictedException
will be thrown.
e77ed23
to
cbe31ce
Compare
2bb178a
to
479bfa3
Compare
5726c1f
to
185895a
Compare
Personally, when this is contributed like here, I'm OK with adding this to the component directly, provided the implementation is top quality (double care for reviewers + author). |
I prefer to have it in the core too |
4d3f89f
to
0ccdde7
Compare
e49b6cd
to
ca868cf
Compare
Thanks @nicolas-grekas for the comments, they were really helpful. On your question
I had tried debugging that but couldn't find root cause and gave up previously. Today however, I was able to find the root cause for that. I was setting the |
67755c9
to
b0bbf57
Compare
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.
Lgtm with one minor comment. @jderusse, ok also?
b0bbf57
to
c58b04b
Compare
Thank you very much @jderusse and @nicolas-grekas for your valuable reviews!!!! This is very exciting 🎉 |
c58b04b
to
3270165
Compare
…Store. Modify Store Factory to support initialization of Zookeeper Data Store.
3270165
to
c72c297
Compare
Thank you @GaneshChandrasekaran. |
…andrasekaran) This PR was merged into the 4.2-dev branch. Discussion ---------- Add Zookeeper data store for Lock Component | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | Not applicable | License | MIT | Doc PR | symfony/symfony-docs#10043 This change adds a new feature to the Lock Component to give the capability to store locks in Zookeeper Data Store. The corresponding documentation PR should describe how this works. The change here also adds a functional test to make sure all the basic functionality of the lock using this data store works. Requirements for this to work are having a PHP-Zookeeper extension available to use this. Commits ------- c72c297 Add new Zookeeper Data Store. Add functional test for Zookeeper Data Store. Modify Store Factory to support initialization of Zookeeper Data Store.
PR welcome! |
…k component (Ganesh Chandrasekaran) This PR was merged into the master branch. Discussion ---------- Add documentation for using Zookeeper data store for lock component This is a documentation update for new Feature of the Lock Component. This will be linked to the PR which will be created in https://github.com/symfony/symfony Corresponding code PR: symfony/symfony#27920 Commits ------- 2ba70f0 Add docs for Zookeeper Data Store.
This PR was merged into the 4.2-dev branch. Discussion ---------- [Lock] Wrap release exception | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #27920 (comment) | License | MIT | Doc PR | NA Commits ------- c37f9e9 Wrap release exception
This change adds a new feature to the Lock Component to give the capability to store locks in Zookeeper Data Store. The corresponding documentation PR should describe how this works.
The change here also adds a functional test to make sure all the basic functionality of the lock using this data store works.
Requirements for this to work are having a PHP-Zookeeper extension available to use this.