Skip to content

Commit 4101c16

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 503ce0c commit 4101c16

File tree

6 files changed

+270
-2
lines changed

6 files changed

+270
-2
lines changed

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

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
}

Store/ZookeeperStore.php

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 <[email protected]>
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 <[email protected]>
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+
}

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

Tests/Store/ZookeeperStoreTest.php

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
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 <[email protected]>
20+
*
21+
* @requires extension zookeeper
22+
*/
23+
class ZookeeperStoreTest extends AbstractStoreTest
24+
{
25+
public function getStore(): ZookeeperStore
26+
{
27+
$zookeeper_server = getenv('ZOOKEEPER_HOST').':2181';
28+
29+
$zookeeper = new \Zookeeper(implode(',', array($zookeeper_server)));
30+
31+
return StoreFactory::createStore($zookeeper);
32+
}
33+
34+
public function testSaveSucceedsWhenPathContainsMoreThanOneNode()
35+
{
36+
$store = $this->getStore();
37+
$resource = '/baseNode/lockNode';
38+
$key = new Key($resource);
39+
40+
$store->save($key);
41+
$this->assertTrue($store->exists($key));
42+
43+
$store->delete($key);
44+
$this->assertFalse($store->exists($key));
45+
}
46+
47+
public function testSaveSucceedsWhenPathContainsOneNode()
48+
{
49+
$store = $this->getStore();
50+
$resource = '/baseNode';
51+
$key = new Key($resource);
52+
53+
$store->save($key);
54+
$this->assertTrue($store->exists($key));
55+
56+
$store->delete($key);
57+
$this->assertFalse($store->exists($key));
58+
}
59+
60+
public function testSaveSucceedsWhenPathsContainSameFirstNode()
61+
{
62+
$store = $this->getStore();
63+
$resource = 'foo/bar';
64+
$key = new Key($resource);
65+
66+
$store->save($key);
67+
$this->assertTrue($store->exists($key));
68+
69+
$resource2 = 'foo';
70+
$key2 = new Key($resource2);
71+
72+
$this->assertFalse($store->exists($key2));
73+
$store->save($key2);
74+
$this->assertTrue($store->exists($key2));
75+
76+
$store->delete($key2);
77+
$this->assertFalse($store->exists($key2));
78+
79+
$store->delete($key);
80+
$this->assertFalse($store->exists($key));
81+
}
82+
83+
public function testRootPathIsLockable()
84+
{
85+
$store = $this->getStore();
86+
$resource = '/';
87+
$key = new Key($resource);
88+
89+
$store->save($key);
90+
$this->assertTrue($store->exists($key));
91+
92+
$store->delete($key);
93+
$this->assertFalse($store->exists($key));
94+
}
95+
96+
public function testEmptyStringIsLockable()
97+
{
98+
$store = $this->getStore();
99+
$resource = '';
100+
$key = new Key($resource);
101+
102+
$store->save($key);
103+
$this->assertTrue($store->exists($key));
104+
105+
$store->delete($key);
106+
$this->assertFalse($store->exists($key));
107+
}
108+
}

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)