Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c44b211

Browse files
author
Joe Bennett
committed
#27345 Added Component\Lock\Store\MongoDbStore
1 parent 5a0cad2 commit c44b211

File tree

5 files changed

+391
-2
lines changed

5 files changed

+391
-2
lines changed

phpunit.xml.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
<env name="LDAP_PORT" value="3389" />
2020
<env name="REDIS_HOST" value="localhost" />
2121
<env name="MEMCACHED_HOST" value="localhost" />
22+
<env name="MONGODB_HOST" value="localhost" />
2223
<env name="ZOOKEEPER_HOST" value="localhost" />
2324
</php>
2425

Lines changed: 322 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,322 @@
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 MongoDB\BSON\UTCDateTime;
15+
use MongoDB\Client;
16+
use MongoDB\Collection;
17+
use MongoDB\Driver\Exception\WriteException;
18+
use MongoDB\Exception\DriverRuntimeException;
19+
use MongoDB\Exception\InvalidArgumentException as MongoInvalidArgumentException;
20+
use MongoDB\Exception\UnsupportedException;
21+
use Symfony\Component\Lock\Exception\InvalidArgumentException;
22+
use Symfony\Component\Lock\Exception\LockConflictedException;
23+
use Symfony\Component\Lock\Exception\LockExpiredException;
24+
use Symfony\Component\Lock\Exception\LockStorageException;
25+
use Symfony\Component\Lock\Exception\NotSupportedException;
26+
use Symfony\Component\Lock\Key;
27+
use Symfony\Component\Lock\StoreInterface;
28+
29+
/**
30+
* MongoDbStore is a StoreInterface implementation using MongoDB as a storage
31+
* engine.
32+
*
33+
* @author Joe Bennett <[email protected]>
34+
*/
35+
class MongoDbStore implements StoreInterface
36+
{
37+
private $mongo;
38+
private $options;
39+
private $initialTtl;
40+
41+
private $collection;
42+
43+
/**
44+
* @param Client $mongo
45+
* @param array $options See below
46+
* @param float $initialTtl The expiration delay of locks in seconds
47+
*
48+
* @throws InvalidArgumentException if required options are not provided
49+
*
50+
* Options:
51+
* database: The name of the database [required]
52+
* collection: The name of the collection [default: lock]
53+
*
54+
* CAUTION: The locked resource name is indexed in the _id field of the
55+
* lock collection. An indexed field's value in MongoDB can be a maximum
56+
* of 1024 bytes in length inclusive of structural overhead.
57+
*
58+
* @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit
59+
*
60+
* CAUTION: This store relies on all client and server nodes to have
61+
* synchronized clocks for lock expiry to occur at the correct time.
62+
* To ensure locks don't expire prematurely; the lock TTL should be set
63+
* with enough extra time to account for any clock drift between nodes.
64+
* @see self::createTtlIndex()
65+
*
66+
* writeConcern, readConcern and readPreference are not specified by
67+
* MongoDbStore meaning the collection's settings will take effect.
68+
* @see https://docs.mongodb.com/manual/applications/replication/
69+
*/
70+
public function __construct(Client $mongo, array $options, float $initialTtl = 300.0)
71+
{
72+
if (!isset($options['database'])) {
73+
throw new InvalidArgumentException(
74+
'You must provide the "database" option for MongoDBStore'
75+
);
76+
}
77+
78+
$this->mongo = $mongo;
79+
80+
$this->options = array_merge(array(
81+
'collection' => 'lock',
82+
), $options);
83+
84+
$this->initialTtl = $initialTtl;
85+
}
86+
87+
/**
88+
* Create a TTL index to automatically remove expired locks.
89+
*
90+
* This should be called once during database setup.
91+
*
92+
* Alternatively the TTL index can be created manually:
93+
*
94+
* db.lock.ensureIndex(
95+
* { "expires_at": 1 },
96+
* { "expireAfterSeconds": 0 }
97+
* )
98+
*
99+
* Please note, expires_at is based on the application server. If the
100+
* database time differs; a lock could be cleaned up before it has expired.
101+
* Set a positive expireAfterSeconds to account for any time drift between
102+
* application and database server.
103+
*
104+
* A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks.
105+
*
106+
* @see http://docs.mongodb.org/manual/tutorial/expire-data/
107+
*
108+
* @return string The name of the created index
109+
*
110+
* @throws UnsupportedException if options are not supported by the selected server
111+
* @throws MongoInvalidArgumentException for parameter/option parsing errors
112+
* @throws DriverRuntimeException for other driver errors (e.g. connection errors)
113+
*/
114+
public function createTtlIndex(): string
115+
{
116+
$keys = array(
117+
'expires_at' => 1,
118+
);
119+
120+
$options = array(
121+
'expireAfterSeconds' => 0,
122+
);
123+
124+
return $this->getCollection()->createIndex($keys, $options);
125+
}
126+
127+
/**
128+
* {@inheritdoc}
129+
*
130+
* @throws LockExpiredException when save is called on an expired lock
131+
*/
132+
public function save(Key $key)
133+
{
134+
$token = $this->getUniqueToken($key);
135+
$now = microtime(true);
136+
137+
$filter = array(
138+
'_id' => (string) $key,
139+
'$or' => array(
140+
array(
141+
'token' => $token,
142+
),
143+
array(
144+
'expires_at' => array(
145+
'$lte' => $this->createDateTime($now),
146+
),
147+
),
148+
),
149+
);
150+
151+
$update = array(
152+
'$set' => array(
153+
'_id' => (string) $key,
154+
'token' => $token,
155+
'expires_at' => $this->createDateTime($now + $this->initialTtl),
156+
),
157+
);
158+
159+
$options = array(
160+
'upsert' => true,
161+
);
162+
163+
$key->reduceLifetime($this->initialTtl);
164+
165+
try {
166+
$this->getCollection()->updateOne($filter, $update, $options);
167+
} catch (WriteException $e) {
168+
throw new LockConflictedException('Failed to acquire lock', 0, $e);
169+
}
170+
171+
if ($key->isExpired()) {
172+
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
173+
}
174+
}
175+
176+
/**
177+
* {@inheritdoc}
178+
*/
179+
public function waitAndSave(Key $key)
180+
{
181+
throw new NotSupportedException(sprintf(
182+
'The store "%s" does not supports blocking locks.',
183+
__CLASS__
184+
));
185+
}
186+
187+
/**
188+
* {@inheritdoc}
189+
*
190+
* @throws LockStorageException
191+
* @throws LockExpiredException
192+
*/
193+
public function putOffExpiration(Key $key, $ttl)
194+
{
195+
$now = microtime(true);
196+
197+
$filter = array(
198+
'_id' => (string) $key,
199+
'token' => $this->getUniqueToken($key),
200+
'expires_at' => array(
201+
'$gte' => $this->createDateTime($now),
202+
),
203+
);
204+
205+
$update = array(
206+
'$set' => array(
207+
'_id' => (string) $key,
208+
'expires_at' => $this->createDateTime($now + $ttl),
209+
),
210+
);
211+
212+
$options = array(
213+
'upsert' => true,
214+
);
215+
216+
$key->reduceLifetime($ttl);
217+
218+
try {
219+
$this->getCollection()->updateOne($filter, $update, $options);
220+
} catch (WriteException $e) {
221+
$writeErrors = $e->getWriteResult()->getWriteErrors();
222+
if (1 === \count($writeErrors)) {
223+
$code = $writeErrors[0]->getCode();
224+
} else {
225+
$code = $e->getCode();
226+
}
227+
// Mongo error E11000 - DuplicateKey
228+
if (11000 === $code) {
229+
throw new LockConflictedException('Failed to put off the expiration of the lock', 0, $e);
230+
}
231+
throw new LockStorageException($e->getMessage(), 0, $e);
232+
}
233+
234+
if ($key->isExpired()) {
235+
throw new LockExpiredException(sprintf(
236+
'Failed to put off the expiration of the "%s" lock within the specified time.',
237+
$key
238+
));
239+
}
240+
}
241+
242+
/**
243+
* {@inheritdoc}
244+
*/
245+
public function delete(Key $key)
246+
{
247+
$filter = array(
248+
'_id' => (string) $key,
249+
'token' => $this->getUniqueToken($key),
250+
);
251+
252+
$options = array();
253+
254+
$this->getCollection()->deleteOne($filter, $options);
255+
}
256+
257+
/**
258+
* {@inheritdoc}
259+
*/
260+
public function exists(Key $key)
261+
{
262+
$filter = array(
263+
'_id' => (string) $key,
264+
'token' => $this->getUniqueToken($key),
265+
'expires_at' => array(
266+
'$gte' => $this->createDateTime(),
267+
),
268+
);
269+
270+
$doc = $this->getCollection()->findOne($filter);
271+
272+
return null !== $doc;
273+
}
274+
275+
/**
276+
* @return Collection
277+
*/
278+
private function getCollection(): Collection
279+
{
280+
if (null === $this->collection) {
281+
$this->collection = $this->mongo->selectCollection(
282+
$this->options['database'],
283+
$this->options['collection']
284+
);
285+
}
286+
287+
return $this->collection;
288+
}
289+
290+
/**
291+
* @param float $seconds Seconds since 1970-01-01T00:00:00.000Z supporting millisecond precision. Defaults to now.
292+
*
293+
* @return UTCDateTime
294+
*/
295+
private function createDateTime(float $seconds = null): UTCDateTime
296+
{
297+
if (null === $seconds) {
298+
$seconds = microtime(true);
299+
}
300+
301+
$milliseconds = $seconds * 1000;
302+
303+
return new UTCDateTime($milliseconds);
304+
}
305+
306+
/**
307+
* Retrieves an unique token for the given key namespaced to this store.
308+
*
309+
* @param Key lock state container
310+
*
311+
* @return string token
312+
*/
313+
private function getUniqueToken(Key $key): string
314+
{
315+
if (!$key->hasState(__CLASS__)) {
316+
$token = base64_encode(random_bytes(32));
317+
$key->setState(__CLASS__, $token);
318+
}
319+
320+
return $key->getState(__CLASS__);
321+
}
322+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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 MongoDB\Client;
15+
use Symfony\Component\Lock\Store\MongoDbStore;
16+
use Symfony\Component\Lock\StoreInterface;
17+
18+
/**
19+
* @author Joe Bennett <[email protected]>
20+
* @requires extension mongodb
21+
*/
22+
class MongoDbStoreTest extends AbstractStoreTest
23+
{
24+
use ExpiringStoreTestTrait;
25+
26+
public static function setupBeforeClass()
27+
{
28+
$client = self::getMongoConnection();
29+
$client->listDatabases();
30+
}
31+
32+
/**
33+
* @return Client
34+
*/
35+
private static function getMongoConnection(): Client
36+
{
37+
return new Client('mongodb://'.getenv('MONGODB_HOST'));
38+
}
39+
40+
/**
41+
* @return int
42+
*/
43+
protected function getClockDelay()
44+
{
45+
return 250000;
46+
}
47+
48+
/**
49+
* {@inheritdoc}
50+
*/
51+
public function getStore(): StoreInterface
52+
{
53+
return new MongoDbStore(self::getMongoConnection(), array(
54+
'database' => 'test',
55+
));
56+
}
57+
58+
public function testCreateIndex()
59+
{
60+
$store = $this->getStore();
61+
62+
$this->assertEquals($store->createTtlIndex(), 'expires_at_1');
63+
}
64+
}

0 commit comments

Comments
 (0)