Skip to content

Commit

Permalink
Add new Zookeeper Data Store. Add functional test for Zookeeper Data …
Browse files Browse the repository at this point in the history
…Store. Modify Store Factory to support initialization of Zookeeper Data Store.
  • Loading branch information
GaneshChandrasekaran-zz committed Sep 20, 2018
1 parent 57c76a4 commit c72c297
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ addons:
- ldap-utils
- slapd
- librabbitmq-dev
- zookeeperd
- libzookeeper-mt-dev

env:
global:
Expand Down Expand Up @@ -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
- |
Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<env name="LDAP_PORT" value="3389" />
<env name="REDIS_HOST" value="localhost" />
<env name="MEMCACHED_HOST" value="localhost" />
<env name="ZOOKEEPER_HOST" value="localhost" />
</php>

<testsuites>
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/Lock/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CHANGELOG
-----

* added the PDO Store
* Add a new Zookeeper Data Store for Lock Component.

3.4.0
-----
Expand Down
7 changes: 5 additions & 2 deletions src/Symfony/Component/Lock/Store/StoreFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand All @@ -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)));
}
Expand Down
150 changes: 150 additions & 0 deletions src/Symfony/Component/Lock/Store/ZookeeperStore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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 <[email protected]>
*/
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);
}
}
5 changes: 5 additions & 0 deletions src/Symfony/Component/Lock/StoreInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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);
Expand Down Expand Up @@ -52,6 +55,8 @@ public function putOffExpiration(Key $key, $ttl);

/**
* Removes a resource from the storage.
*
* @throws LockReleasingException
*/
public function delete(Key $key);

Expand Down
108 changes: 108 additions & 0 deletions src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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 <[email protected]>
*
* @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));
}
}
1 change: 1 addition & 0 deletions src/Symfony/Component/Lock/phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ini name="error_reporting" value="-1" />
<env name="REDIS_HOST" value="localhost" />
<env name="MEMCACHED_HOST" value="localhost" />
<env name="ZOOKEEPER_HOST" value="localhost" />
</php>

<testsuites>
Expand Down

0 comments on commit c72c297

Please sign in to comment.