From c72c297dc3f7288b0e08a3f46a9512a6da809ea1 Mon Sep 17 00:00:00 2001 From: Ganesh Chandrasekaran Date: Sun, 8 Jul 2018 03:07:36 +0200 Subject: [PATCH] Add new Zookeeper Data Store. Add functional test for Zookeeper Data Store. Modify Store Factory to support initialization of Zookeeper Data Store. --- .travis.yml | 3 + phpunit.xml.dist | 1 + src/Symfony/Component/Lock/CHANGELOG.md | 1 + .../Component/Lock/Store/StoreFactory.php | 7 +- .../Component/Lock/Store/ZookeeperStore.php | 150 ++++++++++++++++++ src/Symfony/Component/Lock/StoreInterface.php | 5 + .../Lock/Tests/Store/ZookeeperStoreTest.php | 108 +++++++++++++ src/Symfony/Component/Lock/phpunit.xml.dist | 1 + 8 files changed, 274 insertions(+), 2 deletions(-) create mode 100644 src/Symfony/Component/Lock/Store/ZookeeperStore.php create mode 100644 src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php diff --git a/.travis.yml b/.travis.yml index bbed5895245fb..f6bbb97d6127b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,8 @@ addons: - ldap-utils - slapd - librabbitmq-dev + - zookeeperd + - libzookeeper-mt-dev env: global: @@ -161,6 +163,7 @@ before_install: tfold ext.mongodb tpecl mongodb-1.5.0 mongodb.so $INI tfold ext.amqp tpecl amqp-1.9.3 amqp.so $INI tfold ext.igbinary tpecl igbinary-2.0.6 igbinary.so $INI + tfold ext.zookeeper tpecl zookeeper-0.5.0 zookeeper.so $INI done - | diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7b808fd674501..9717aa56f31c5 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,6 +19,7 @@ + diff --git a/src/Symfony/Component/Lock/CHANGELOG.md b/src/Symfony/Component/Lock/CHANGELOG.md index 32fd243b8f363..8632b50e495b7 100644 --- a/src/Symfony/Component/Lock/CHANGELOG.md +++ b/src/Symfony/Component/Lock/CHANGELOG.md @@ -5,6 +5,7 @@ CHANGELOG ----- * added the PDO Store + * Add a new Zookeeper Data Store for Lock Component. 3.4.0 ----- diff --git a/src/Symfony/Component/Lock/Store/StoreFactory.php b/src/Symfony/Component/Lock/Store/StoreFactory.php index cbbcb685c3626..701bf2b8bbb50 100644 --- a/src/Symfony/Component/Lock/Store/StoreFactory.php +++ b/src/Symfony/Component/Lock/Store/StoreFactory.php @@ -22,9 +22,9 @@ class StoreFactory { /** - * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached $connection + * @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached|\Zookeeper $connection * - * @return RedisStore|MemcachedStore + * @return RedisStore|MemcachedStore|ZookeeperStore */ public static function createStore($connection) { @@ -34,6 +34,9 @@ public static function createStore($connection) if ($connection instanceof \Memcached) { return new MemcachedStore($connection); } + if ($connection instanceof \Zookeeper) { + return new ZookeeperStore($connection); + } throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection))); } diff --git a/src/Symfony/Component/Lock/Store/ZookeeperStore.php b/src/Symfony/Component/Lock/Store/ZookeeperStore.php new file mode 100644 index 0000000000000..2304dd47a0881 --- /dev/null +++ b/src/Symfony/Component/Lock/Store/ZookeeperStore.php @@ -0,0 +1,150 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Lock\Store; + +use Symfony\Component\Lock\Exception\LockAcquiringException; +use Symfony\Component\Lock\Exception\LockConflictedException; +use Symfony\Component\Lock\Exception\LockReleasingException; +use Symfony\Component\Lock\Exception\NotSupportedException; +use Symfony\Component\Lock\Key; +use Symfony\Component\Lock\StoreInterface; + +/** + * ZookeeperStore is a StoreInterface implementation using Zookeeper as store engine. + * + * @author Ganesh Chandrasekaran + */ +class ZookeeperStore implements StoreInterface +{ + private $zookeeper; + + public function __construct(\Zookeeper $zookeeper) + { + $this->zookeeper = $zookeeper; + } + + /** + * {@inheritdoc} + */ + public function save(Key $key) + { + if ($this->exists($key)) { + return; + } + + $resource = $this->getKeyResource($key); + $token = $this->getUniqueToken($key); + + $this->createNewLock($resource, $token); + } + + /** + * {@inheritdoc} + */ + public function delete(Key $key) + { + if (!$this->exists($key)) { + return; + } + $resource = $this->getKeyResource($key); + try { + $this->zookeeper->delete($resource); + } catch (\ZookeeperException $exception) { + // For Zookeeper Ephemeral Nodes, the node will be deleted upon session death. But, if we want to unlock + // the lock before proceeding further in the session, the client should be aware of this + throw new LockReleasingException($exception); + } + } + + /** + * {@inheritdoc} + */ + public function exists(Key $key): bool + { + $resource = $this->getKeyResource($key); + try { + return $this->zookeeper->get($resource) === $this->getUniqueToken($key); + } catch (\ZookeeperException $ex) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function waitAndSave(Key $key) + { + throw new NotSupportedException(); + } + + /** + * {@inheritdoc} + */ + public function putOffExpiration(Key $key, $ttl) + { + throw new NotSupportedException(); + } + + /** + * Creates a zookeeper node. + * + * @param string $node The node which needs to be created + * @param string $value The value to be assigned to a zookeeper node + * + * @throws LockConflictedException + * @throws LockAcquiringException + */ + private function createNewLock(string $node, string $value) + { + // Default Node Permissions + $acl = array(array('perms' => \Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone')); + // This ensures that the nodes are deleted when the client session to zookeeper server ends. + $type = \Zookeeper::EPHEMERAL; + + try { + $this->zookeeper->create($node, $value, $acl, $type); + } catch (\ZookeeperException $ex) { + if (\Zookeeper::NODEEXISTS === $ex->getCode()) { + throw new LockConflictedException($ex); + } + + throw new LockAcquiringException($ex); + } + } + + private function getKeyResource(Key $key): string + { + // Since we do not support storing locks as multi-level nodes, we convert them to be stored at root level. + // For example: foo/bar will become /foo-bar and /foo/bar will become /-foo-bar + $resource = (string) $key; + + if (false !== \strpos($resource, '/')) { + $resource = \strtr($resource, array('/' => '-')).'-'.sha1($resource); + } + + if ('' === $resource) { + $resource = sha1($resource); + } + + return '/'.$resource; + } + + private function getUniqueToken(Key $key): string + { + if (!$key->hasState(self::class)) { + $token = base64_encode(random_bytes(32)); + $key->setState(self::class, $token); + } + + return $key->getState(self::class); + } +} diff --git a/src/Symfony/Component/Lock/StoreInterface.php b/src/Symfony/Component/Lock/StoreInterface.php index 985c4476d7da6..d3d446292648d 100644 --- a/src/Symfony/Component/Lock/StoreInterface.php +++ b/src/Symfony/Component/Lock/StoreInterface.php @@ -11,7 +11,9 @@ namespace Symfony\Component\Lock; +use Symfony\Component\Lock\Exception\LockAcquiringException; use Symfony\Component\Lock\Exception\LockConflictedException; +use Symfony\Component\Lock\Exception\LockReleasingException; use Symfony\Component\Lock\Exception\NotSupportedException; /** @@ -24,6 +26,7 @@ interface StoreInterface /** * Stores the resource if it's not locked by someone else. * + * @throws LockAcquiringException * @throws LockConflictedException */ public function save(Key $key); @@ -52,6 +55,8 @@ public function putOffExpiration(Key $key, $ttl); /** * Removes a resource from the storage. + * + * @throws LockReleasingException */ public function delete(Key $key); diff --git a/src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php b/src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php new file mode 100644 index 0000000000000..73be28bcca3ea --- /dev/null +++ b/src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php @@ -0,0 +1,108 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Lock\Tests\Store; + +use Symfony\Component\Lock\Key; +use Symfony\Component\Lock\Store\StoreFactory; +use Symfony\Component\Lock\Store\ZookeeperStore; + +/** + * @author Ganesh Chandrasekaran + * + * @requires extension zookeeper + */ +class ZookeeperStoreTest extends AbstractStoreTest +{ + public function getStore(): ZookeeperStore + { + $zookeeper_server = getenv('ZOOKEEPER_HOST').':2181'; + + $zookeeper = new \Zookeeper(implode(',', array($zookeeper_server))); + + return StoreFactory::createStore($zookeeper); + } + + public function testSaveSucceedsWhenPathContainsMoreThanOneNode() + { + $store = $this->getStore(); + $resource = '/baseNode/lockNode'; + $key = new Key($resource); + + $store->save($key); + $this->assertTrue($store->exists($key)); + + $store->delete($key); + $this->assertFalse($store->exists($key)); + } + + public function testSaveSucceedsWhenPathContainsOneNode() + { + $store = $this->getStore(); + $resource = '/baseNode'; + $key = new Key($resource); + + $store->save($key); + $this->assertTrue($store->exists($key)); + + $store->delete($key); + $this->assertFalse($store->exists($key)); + } + + public function testSaveSucceedsWhenPathsContainSameFirstNode() + { + $store = $this->getStore(); + $resource = 'foo/bar'; + $key = new Key($resource); + + $store->save($key); + $this->assertTrue($store->exists($key)); + + $resource2 = 'foo'; + $key2 = new Key($resource2); + + $this->assertFalse($store->exists($key2)); + $store->save($key2); + $this->assertTrue($store->exists($key2)); + + $store->delete($key2); + $this->assertFalse($store->exists($key2)); + + $store->delete($key); + $this->assertFalse($store->exists($key)); + } + + public function testRootPathIsLockable() + { + $store = $this->getStore(); + $resource = '/'; + $key = new Key($resource); + + $store->save($key); + $this->assertTrue($store->exists($key)); + + $store->delete($key); + $this->assertFalse($store->exists($key)); + } + + public function testEmptyStringIsLockable() + { + $store = $this->getStore(); + $resource = ''; + $key = new Key($resource); + + $store->save($key); + $this->assertTrue($store->exists($key)); + + $store->delete($key); + $this->assertFalse($store->exists($key)); + } +} diff --git a/src/Symfony/Component/Lock/phpunit.xml.dist b/src/Symfony/Component/Lock/phpunit.xml.dist index be3ca21576fdd..df5fc03ed2be6 100644 --- a/src/Symfony/Component/Lock/phpunit.xml.dist +++ b/src/Symfony/Component/Lock/phpunit.xml.dist @@ -12,6 +12,7 @@ +