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:
parent
57c76a405b
commit
c72c297dc3
@ -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
|
||||
|
||||
- |
|
||||
|
@ -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>
|
||||
|
@ -5,6 +5,7 @@ CHANGELOG
|
||||
-----
|
||||
|
||||
* added the PDO Store
|
||||
* Add a new Zookeeper Data Store for Lock Component.
|
||||
|
||||
3.4.0
|
||||
-----
|
||||
|
@ -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)));
|
||||
}
|
||||
|
150
src/Symfony/Component/Lock/Store/ZookeeperStore.php
Normal file
150
src/Symfony/Component/Lock/Store/ZookeeperStore.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
108
src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php
Normal file
108
src/Symfony/Component/Lock/Tests/Store/ZookeeperStoreTest.php
Normal 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));
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
Reference in New Issue
Block a user