Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8692ca3

Browse filesBrowse files
author
Joe Bennett
committed
#27345 Added MongoDbStore
1 parent 766a82b commit 8692ca3
Copy full SHA for 8692ca3

File tree

6 files changed

+362
-2
lines changed
Filter options

6 files changed

+362
-2
lines changed

‎phpunit.xml.dist

Copy file name to clipboardExpand all lines: phpunit.xml.dist
+1Lines 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
</php>
2324

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

‎src/Symfony/Component/Lock/StoreInterface.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Lock/StoreInterface.php
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Lock;
1313

1414
use Symfony\Component\Lock\Exception\LockConflictedException;
15+
use Symfony\Component\Lock\Exception\LockExpiredException;
1516
use Symfony\Component\Lock\Exception\NotSupportedException;
1617

1718
/**
@@ -25,6 +26,7 @@ interface StoreInterface
2526
* Stores the resource if it's not locked by someone else.
2627
*
2728
* @throws LockConflictedException
29+
* @throws LockExpiredException
2830
*/
2931
public function save(Key $key);
3032

@@ -34,6 +36,7 @@ public function save(Key $key);
3436
* If the store does not support this feature it should throw a NotSupportedException.
3537
*
3638
* @throws LockConflictedException
39+
* @throws LockExpiredException
3740
* @throws NotSupportedException
3841
*/
3942
public function waitAndSave(Key $key);
@@ -46,6 +49,7 @@ public function waitAndSave(Key $key);
4649
* @param float $ttl amount of second to keep the lock in the store
4750
*
4851
* @throws LockConflictedException
52+
* @throws LockExpiredException
4953
* @throws NotSupportedException
5054
*/
5155
public function putOffExpiration(Key $key, $ttl);
+65Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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\Store\MongoDbStore;
15+
16+
/**
17+
* @author Joe Bennett <joe@assimtech.com>
18+
*/
19+
class MongoDbStoreTest extends AbstractStoreTest
20+
{
21+
use ExpiringStoreTestTrait;
22+
23+
public static function setupBeforeClass()
24+
{
25+
try {
26+
if (!class_exists(\MongoDB\Client::class)) {
27+
throw new \RuntimeException('The mongodb/mongodb package is required.');
28+
}
29+
$client = self::getMongoConnection();
30+
$client->listDatabases();
31+
} catch (\Exception $e) {
32+
self::markTestSkipped($e->getMessage());
33+
}
34+
}
35+
36+
protected static function getMongoConnection(): \MongoDB\Client
37+
{
38+
return new \MongoDB\Client('mongodb://'.getenv('MONGODB_HOST'));
39+
}
40+
41+
/**
42+
* {@inheritdoc}
43+
*/
44+
protected function getClockDelay()
45+
{
46+
return 250000;
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
public function getStore()
53+
{
54+
return new MongoDbStore(self::getMongoConnection(), array(
55+
'database' => 'test',
56+
));
57+
}
58+
59+
public function testCreateIndex()
60+
{
61+
$store = $this->getStore();
62+
63+
$this->assertEquals($store->createTTLIndex(), 'expires_at_1');
64+
}
65+
}

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.