Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork9.7k
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 ourterms of service andprivacy 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
Uh oh!
There was an error while loading.Please reload this page.
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 to2e9e12eCompareUh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
| /** | ||
| * A base node within which all the locks for PHP application are managed in the Zookeeper environment. | ||
| */ | ||
| constLOCK_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 =newZookeeper(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 of10 ms each but, still keeping the max sleep time to1 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.
ForgetState:"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 themarkTestAsSkipped 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 thetry 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 environmentsare 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 ifgetStore() throws it, this test would pass, while we probably expect it on the secondsave($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. IfgetStore throws an then it would fail saying thatFailed asserting that exception of type "WhateverException" matches expected exception "\Symfony\Component\Lock\Exception\LockConflictedException".getStore will never throw aLockConflictedException.
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 to0501a86Compare| useSymfony\Component\Lock\Key; | ||
| useSymfony\Component\Lock\StoreInterface; | ||
| useZookeeper; | ||
| useThrowable; |
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 theuse 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.
ForgetState:"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 themarkTestAsSkipped 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.
jderusse left a 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.
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 ofconst 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 beORDER, so for anORDER ID 1234, the lock would be created as/PHP_LOCK_NODE/ORDER_1234 I see that theprefix is not really required and the client can pass this in as theresource itself. So removed it. I also removed theconst LOCK_NODE as we can let the client decide how they want to organize the nodes.
| * @param string $lockPrefix Prefix for a locking resource | ||
| */ | ||
| publicfunction__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}/barand 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 recommendingephermeal + 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) { | ||
| thrownewLockAcquiringException('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 aLockConflictException?
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 aLockConflictedException. If it reaches here, it means it failed to acquire the lock for some other reason.
| thrownewLockAcquiringException('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.
| */ | ||
| publicfunctionputOffExpiration(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.
Uh oh!
There was an error while loading.Please reload this page.
| returnfalse; | ||
| } | ||
| 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 thethis->zookeeper->exists and wrap thisthis->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 | ||
| */ | ||
| publicfunctiontestSaveTwice() |
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 originaltestSaveTwice 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 =newZookeeper(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 tocbe31ceCompare2bb178a to479bfa3Compare5726c1f to185895aComparenicolas-grekas commentedJul 25, 2018
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). |
lyrixx commentedJul 25, 2018
I prefer to have it in the core too |
4d3f89f to0ccdde7Comparee49b6cd toca868cfCompareGaneshChandrasekaran-zz commentedSep 19, 2018 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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 tob0bbf57Compare
nicolas-grekas left a 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.
Lgtm with one minor comment.@jderusse, ok also?
b0bbf57 toc58b04bCompareGaneshChandrasekaran-zz commentedSep 19, 2018
Thank you very much@jderusse and@nicolas-grekas for your valuable reviews!!!! This is very exciting 🎉 |
Uh oh!
There was an error while loading.Please reload this page.
c58b04b to3270165CompareUh oh!
There was an error while loading.Please reload this page.
…Store. Modify Store Factory to support initialization of Zookeeper Data Store.
3270165 toc72c297Comparenicolas-grekas commentedSep 21, 2018
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#10043This 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.
nicolas-grekas commentedSep 21, 2018
PR welcome! |
…k component (Ganesh Chandrasekaran)This PR was merged into the master branch.Discussion----------Add documentation for using Zookeeper data store for lock componentThis is a documentation update for new Feature of the Lock Component. This will be linked to the PR which will be created inhttps://github.com/symfony/symfonyCorresponding code PR:symfony/symfony#27920Commits-------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 | NACommits-------c37f9e9 Wrap release exception
Uh oh!
There was an error while loading.Please reload this page.
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.