8000 Add new Zookeeper Data Store. Add functional test for Zookeeper Data … · symfony/symfony@ca868cf · GitHub
[go: up one dir, main page]

Skip to content

Commit ca868cf

Browse files
Add new Zookeeper Data Store. Add functional test for Zookeeper Data Store. Modify Store Factory to support initialization of Zookeeper Data Store.
1 parent 57c76a4 commit ca868cf

File tree

7 files changed

+279
-2
lines changed

7 files changed

+279
-2
lines changed

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ addons:
1313
- ldap-utils
1414
- slapd
1515
- librabbitmq-dev
16+
- zookeeperd
1617

1718
env:
1819
global:
@@ -156,11 +157,15 @@ before_install:
156157
sudo apt-get update -q
157158
sudo apt-get install libsodium-dev -y
158159
160+
#install zookeeper dependency
161+
sudo apt-get install libzookeeper-mt-dev -y
162+
159163
tfold ext.apcu tpecl apcu-5.1.6 apcu.so $INI
160164
tfold ext.libsodium tpecl libsodium sodium.so $INI
161165
tfold ext.mongodb tpecl mongodb-1.5.0 mongodb.so $INI
162166
tfold ext.amqp tpecl amqp-1.9.3 amqp.so $INI
163167
tfold ext.igbinary tpecl igbinary-2.0.6 igbinary.so $INI
168+
tfold ext.zookeeper tpecl zookeeper-0.5.0 zookeeper.so $INI
164169
done
165170
166171
- |

src/Symfony/Component/Lock/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CHANGELOG
55
-----
66

77
* added the PDO Store
8+
* Add a new Zookeeper Data Store for Lock Component.
89

910
3.4.0
1011
-----

src/Symfony/Component/Lock/Store/StoreFactory.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@
2222
class StoreFactory
2323
{
2424
/**
25-
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached $connection
25+
* @param \Redis|\RedisArray|\RedisCluster|\Predis\Client|\Memcached|\Zookeeper $connection
2626
*
27-
* @return RedisStore|MemcachedStore
27+
* @return RedisStore|MemcachedStore|ZookeeperStore
2828
*/
2929
public static function createStore($connection)
3030
{
@@ -34,6 +34,9 @@ public static function createStore($connection)
3434
if ($connection instanceof \Memcached) {
3535
return new MemcachedStore($connection);
3636
}
37+
if ($connection instanceof \Zookeeper) {
38+
return new ZookeeperStore($connection);
39+
}
3740

3841
throw new InvalidArgumentException(sprintf('Unsupported Connection: %s.', \get_class($connection)));
3942
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Lock\Store;
13+
14+
use Symfony\Component\Lock\Exception\LockAcquiringException;
15+
use Symfony\Component\Lock\Exception\LockConflictedException;
16+
use Symfony\Component\Lock\Exception\LockReleasingException;
17+
use Symfony\Component\Lock\Exception\NotSupportedException;
18+
use Symfony\Component\Lock\Key;
19+
use Symfony\Component\Lock\StoreInterface;
20+
21+
/**
22+
* ZookeeperStore is a StoreInterface implementation using Zookeeper as store engine.
23+
*
24+
* @author Ganesh Chandrasekaran <gchandrasekaran@wayfair.com>
25+
*/
26+
class ZookeeperStore implements StoreInterface
27+
{
28+
private $zookeeper;
29+
30+
public function __construct(\Zookeeper $zookeeper)
31+
{
32+
$this->zookeeper = $zookeeper;
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*/
38+
public function save(Key $key)
39+
{
40+
if ($this->exists($key)) {
41+
return;
42+
}
43+
44+
$resource = $this->getKeyResource($key);
45+
$token = $this->getUniqueToken($key);
46+
47+
$this->createNewLock($resource, $token);
48+
}
49+
50+
/**
51+
* {@inheritdoc}
52+
*/
53+
public function delete(Key $key)
54+
{
55+
if (!$this->exists($key)) {
56+
return;
57+
}
58+
$resource = $this->getKeyResource($key);
59+
try {
60+
$this->zookeeper->delete($resource);
61+
} catch (\ZookeeperException $exception) {
62+
// For Zookeeper Ephemeral Nodes, the node will be deleted upon session death. But, if we want to unlock
63+
// the lock before proceeding further in the session, the client should be aware of this
64+
throw new LockReleasingException($exception);
65+
}
66+
}
67+
68+
/**
69+
* {@inheritdoc}
70+
*/
71+
public function exists(Key $key): bool
72+
{
73+
$resource = $this->getKeyResource($key);
74+
try {
75+
return $this->zookeeper->get($resource) === $this->getUniqueToken($key);
76+
} catch (\ZookeeperException $ex) {
77+
return false;
78+
}
79+
}
80+
81+
/**
82+
* {@inheritdoc}
83+
*/
84+
public function waitAndSave(Key $key)
85+
{
86+
throw new NotSupportedException();
87+
}
88+
89+
/**
90+
* {@inheritdoc}
91+
*/
92+
public function putOffExpiration(Key $key, $ttl)
93+
{
94+
throw new NotSupportedException();
95+
}
96+
97+
/**
98+
* Creates a zookeeper node.
99+
*
100+
* @param string $node The node which needs to be created
101+
* @param string $value The value to be assigned to a zookeeper node
102+
*
103+
* @throws LockConflictedException
104+
* @throws LockAcquiringException
105+
*/
106+
private function createNewLock(string $node, string $value)
107+
{
108+
// Default Node Permissions
109+
$acl = array(array('perms' => \Zookeeper::PERM_ALL, 'scheme' => 'world', 'id' => 'anyone'));
110+
// This ensures that the nodes are deleted when the client session to zookeeper server ends.
111+
$type = \Zookeeper::EPHEMERAL;
112+
113+
try {
114+
$this->zookeeper->create($node, $value, $acl, $type);
115+
} catch (\ZookeeperException $ex) {
116+
if (\Zookeeper::NODEEXISTS === $ex->getCode()) {
117+
throw new LockConflictedException($ex);
118+
}
119+
120+
throw new LockAcquiringException($ex);
121+
}
122+
}
123+
124+
private function getKeyResource(Key $key): string
125+
{
126+
// Since we do not support storing locks as multi-level nodes, we convert them to be stored at root level.
127+
// For example: foo/bar will become /foo-bar and /foo/bar will become /-foo-bar
128+
$resource = (string) $key;
129+
130+
if (false !== \strpos($resource, '/')) {
131+
$resource = \strtr($resource, array('/' => '-')).'-'.sha1($resource);
132+
}
133+
134+
if ('' === $resource) {
135+
$resource = sha1($resource);
136+
}
137+
138+
return '/'.$resource;
139+
}
140+
141+
private function getUniqueToken(Key $key): string
142+
{
143+
if (!$key->hasState(self::class)) {
144+
$token = base64_encode(random_bytes(32));
145+
$key->setState(self::class, $token);
146+
}
147+
148+
return $key->getState(self::class);
149+
}
150+
}

src/Symfony/Component/Lock/StoreInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111

1212
namespace Symfony\Component\Lock;
1313

14+
use Symfony\Component\Lock\Exception\LockAcquiringException;
1415
use Symfony\Component\Lock\Exception\LockConflictedException;
16+
use Symfony\Component\Lock\Exception\LockReleasingException;
1517
use Symfony\Component\Lock\Exception\NotSupportedException;
1618

1719
/**
@@ -24,6 +26,7 @@ interface StoreInterface
2426
/**
2527
* Stores the resource if it's not locked by someone else.
2628
*
29+
* @throws LockAcquiringException
2730
* @throws LockConflictedException
2831
*/
2932
public function save(Key $key);
@@ -52,6 +55,8 @@ public function putOffExpiration(Key $key, $ttl);
5255

5356
/**
5457
* Removes a resource from the storage.
58+
*
59+
* @throws LockReleasingException
5560
*/
5661
public function delete(Key $key);
5762

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Lock\Tests\Store;
13+
14+
use Symfony\Component\Lock\Key;
15+
use Symfony\Component\Lock\Store\StoreFactory;
16+
use Symfony\Component\Lock\Store\ZookeeperStore;
17+
18+
/**
19+
* @author Ganesh Chandrasekaran <gchandrasekaran@wayfair.com>
20+
*
21+
* @requires extension zookeeper
22+
*/
23+
class ZookeeperStoreTest extends AbstractStoreTest
24+
{
25+
public function getStore(): ZookeeperStore
26+
{
27+
$zookeeper_server = getenv('ZOOKEEPER_HOSTT').':2181';
28+
29+
try {
30+
$zookeeper = new \Zookeeper(implode(',', array($zookeeper_server)));
31+
} catch (\ZookeeperConnectionException $ex) {
32+
self::markTestSkipped($ex->getMessage());
33+
}
34+
35+
return StoreFactory::createStore($zookeeper);
36+
}
37+
38+
public function testSaveSucceedsWhenPathContainsMoreThanOneNode()
39+
{
40+
$store = $this->getStore();
41+
$resource = '/baseNode/lockNode';
42+
$key = new Key($resource);
43+
44+
$store->save($key);
45+
$this->assertTrue($store->exists($key));
46+
47+
$store->delete($key);
48+
$this->assertFalse($store->exists($key));
49+
}
50+
51+
public function testSaveSucceedsWhenPathContainsOneNode()
52+
{
53+
$store = $this->getStore();
54+
$resource = '/baseNode';
55+
$key = new Key($resource);
56+
57+
$store->save($key);
58+
$this->assertTrue($store->exists($key));
59+
60+
$store->delete($key);
61+
$this->assertFalse($store->exists($key));
62+
}
63+
64+
public function testSaveSucceedsWhenPathsContainSameFirstNode()
65+
{
66+
$store = $this->getStore();
67+
$resource = 'foo/bar';
68+
$key = new Key($resource);
69+
70+
$store->save($key);
71+
$this->assertTrue($store->exists($key));
72+
73+
$resource2 = 'foo';
74+
$key2 = new Key($resource2);
75+
76+
$this->assertFalse($store->exists($key2));
77+
$store->save($key2);
78+
$this->assertTrue($store->exists($key2));
79+
80+
$store->delete($key2);
81+
$this->assertFalse($store->exists($key2));
82+
83+
$store->delete($key);
84+
$this->assertFalse($store->exists($key));
85+
}
86+
87+
public function testRootPathIsLockable()
88+
{
89+
$store = $this->getStore();
90+
$resource = '/';
91+
$key = new Key($resource);
92+
93+
$store->save($key);
94+
$this->assertTrue($store->exists($key));
95+
96+
$store->delete($key);
97+
$this->assertFalse($store->exists($key));
98+
}
99+
100+
public function testEmptyStringIsLockable()
101+
{
102+
$store = $this->getStore();
103+
$resource = '';
104+
$key = new Key($resource);
105+
106+
$store->save($key);
107+
$this->assertTrue($store->exists($key));
108+
109+
$store->delete($key);
110+
$this->assertFalse($store->exists($key));
111+
}
112+
}

src/Symfony/Component/Lock/phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<ini name="error_reporting" value="-1" />
1313
<env name="REDIS_HOST" value="localhost" />
1414
<env name="MEMCACHED_HOST" value="localhost" />
15+
<env name="ZOOKEEPER_HOST" value="localhost" />
1516
</php>
1617

1718
<testsuites>

0 commit comments

Comments
 (0)
0