Skip to content

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

Conversation

GaneshChandrasekaran-zz
Copy link
Contributor

@GaneshChandrasekaran-zz GaneshChandrasekaran-zz commented Jul 10, 2018

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.

@@ -1,5 +1,9 @@
CHANGELOG
=========
4.2.0
Copy link
Contributor Author

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?

Copy link
Member

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. :)

Copy link
Contributor Author

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
Copy link
Contributor Author

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?

Copy link
Contributor

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

Copy link
Contributor Author

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.

/**
* A base node within which all the locks for PHP application are managed in the Zookeeper environment.
*/
const LOCK_NODE = '/PHP_LOCK_NODE';
Copy link
Contributor

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.

Copy link
Contributor Author

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
Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please import the class

Copy link
Contributor Author

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
Copy link
Contributor

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);
Copy link
Contributor

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.

Copy link
Contributor Author

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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

@linaori linaori Jul 12, 2018

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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
Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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);
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. Fixed.

@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch from 6f8bb93 to 0501a86 Compare July 12, 2018 07:05
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
use Zookeeper;
use Throwable;
Copy link
Member

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

Copy link
Contributor Author

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!

Copy link
Member

@lyrixx lyrixx Jul 16, 2018

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:

  1. importing the namespace (what you have done)
  2. 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

Copy link
Contributor Author

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 {
Copy link
Contributor

@linaori linaori Jul 12, 2018

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.

Copy link
Member

@jderusse jderusse left a 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;
Copy link
Member

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

Copy link
Contributor Author

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,
Copy link
Member

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

Copy link
Contributor Author

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.
Copy link
Member

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

Copy link
Contributor Author

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);
Copy link
Member

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 /?

Copy link
Contributor Author

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.

Copy link
Member

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 of ids?

Do you have a use case in mind?

Copy link
Contributor Author

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)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while (count($parts)>1)

Copy link
Contributor Author

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);
Copy link
Member

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?

Copy link
Contributor Author

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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note used

Copy link
Contributor Author

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)
{
}
Copy link
Member

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

Copy link
Contributor Author

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);
Copy link
Member

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

Copy link
Contributor Author

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()
Copy link
Member

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...

Copy link
Contributor Author

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)));
Copy link
Member

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?

Copy link
Contributor Author

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.

@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch from e77ed23 to cbe31ce Compare July 14, 2018 23:26
@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch 3 times, most recently from 2bb178a to 479bfa3 Compare July 22, 2018 09:37
@GaneshChandrasekaran-zz GaneshChandrasekaran-zz changed the title Add Zookeeper data store for Lock Component Add Zookeeper data store as a Bridge for Lock Component Jul 22, 2018
@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch 3 times, most recently from 5726c1f to 185895a Compare July 22, 2018 11:24
@nicolas-grekas nicolas-grekas added this to the next milestone Jul 23, 2018
@nicolas-grekas
Copy link
Member

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
Copy link
Member

lyrixx commented Jul 25, 2018

I prefer to have it in the core too

@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch 2 times, most recently from 4d3f89f to 0ccdde7 Compare July 31, 2018 22:04
@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch 2 times, most recently from e49b6cd to ca868cf Compare September 19, 2018 15:26
@GaneshChandrasekaran-zz GaneshChandrasekaran-zz changed the title WIP: Add Zookeeper data store for Lock Component Add Zookeeper data store for Lock Component Sep 19, 2018
@GaneshChandrasekaran-zz
Copy link
Contributor Author

GaneshChandrasekaran-zz commented Sep 19, 2018

Thanks @nicolas-grekas for the comments, they were really helpful.

On your question

Tests are green but display "ZOO_ERROR@getaddrs@599: getaddrinfo: No such file or directory" messages, is it legit or does it hint an issue

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 ZOOKEEPER_HOST env variable only in 1 phpunit.xml file. I put it in both places and those errors are gone now. Looks like the config is read from either of those files depending on the deps. I am also not skipping my tests anymore as they should always run correctly.

Copy link
Member

@nicolas-grekas nicolas-grekas left a 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?

@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch from b0bbf57 to c58b04b Compare September 19, 2018 18:17
@GaneshChandrasekaran-zz
Copy link
Contributor Author

Thank you very much @jderusse and @nicolas-grekas for your valuable reviews!!!! This is very exciting 🎉

@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch from c58b04b to 3270165 Compare September 19, 2018 20:45
…Store. Modify Store Factory to support initialization of Zookeeper Data Store.
@GaneshChandrasekaran-zz GaneshChandrasekaran-zz force-pushed the gchandrasekaran_addZookeeperDataStore branch from 3270165 to c72c297 Compare September 20, 2018 07:05
@nicolas-grekas
Copy link
Member

Thank you @GaneshChandrasekaran.

@nicolas-grekas nicolas-grekas merged commit c72c297 into symfony:master Sep 21, 2018
nicolas-grekas added a commit that referenced this pull request Sep 21, 2018
…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.
@nicolas-grekas
Copy link
Member

I suggest to open a dedicated PR with something like [...] #27920 (comment)

PR welcome!

@GaneshChandrasekaran-zz GaneshChandrasekaran-zz deleted the gchandrasekaran_addZookeeperDataStore branch September 21, 2018 12:23
javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request Sep 21, 2018
…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.
fabpot added a commit that referenced this pull request Sep 22, 2018
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
@nicolas-grekas nicolas-grekas modified the milestones: next, 4.2 Nov 1, 2018
This was referenced Nov 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants