Add new Zookeeper Data Store. Add functional test for Zookeeper Data Store. Modify Store Factory to support initialization of Zookeeper Data Store.

This commit is contained in:
Ganesh Chandrasekaran 2018-07-08 03:07:36 +02:00
parent 57c76a405b
commit c72c297dc3
8 changed files with 274 additions and 2 deletions

View File

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

View File

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

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* added the PDO Store
* Add a new Zookeeper Data Store for Lock Component.
3.4.0
-----

View File

@ -22,9 +22,9 @@ use Symfony\Component\Lock\Exception\InvalidArgumentException;
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 @@ class StoreFactory
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)));
}

View File

@ -0,0 +1,150 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <gchandrasekaran@wayfair.com>
*/
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);
}
}

View File

@ -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 @@ interface StoreInterface
/**
* Removes a resource from the storage.
*
* @throws LockReleasingException
*/
public function delete(Key $key);

View File

@ -0,0 +1,108 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <gchandrasekaran@wayfair.com>
*
* @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));
}
}

View File

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