From 157fcd798c1481039e1609b7f979338e5eb976bb Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 23 May 2018 15:35:19 +1000 Subject: [PATCH 01/29] #27345 Added Symfony/Component/Lock/Store/MongoDbStore --- composer.json | 1 + phpunit.xml.dist | 1 + .../Component/Lock/Store/MongoDbStore.php | 296 ++++++++++++++++++ .../Lock/Tests/Store/MongoDbClientTest.php | 55 ++++ src/Symfony/Component/Lock/composer.json | 3 +- src/Symfony/Component/Lock/phpunit.xml.dist | 1 + 6 files changed, 356 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/Lock/Store/MongoDbStore.php create mode 100644 src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php diff --git a/composer.json b/composer.json index 3a861887ce60d..d47f2e22ae804 100644 --- a/composer.json +++ b/composer.json @@ -92,6 +92,7 @@ "doctrine/dbal": "~2.4", "doctrine/orm": "~2.4,>=2.4.5", "doctrine/doctrine-bundle": "~1.4", + "mongodb/mongodb": "~1.0", "monolog/monolog": "~1.11", "ocramius/proxy-manager": "~0.4|~1.0|~2.0", "predis/predis": "~1.0", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 59ec7725254f3..ac2fb16aaedf3 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,6 +19,7 @@ + diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php new file mode 100644 index 0000000000000..113647460ef79 --- /dev/null +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -0,0 +1,296 @@ + + * + * 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\InvalidArgumentException; +use Symfony\Component\Lock\Exception\LockConflictedException; +use Symfony\Component\Lock\Exception\LockExpiredException; +use Symfony\Component\Lock\Key; +use Symfony\Component\Lock\StoreInterface; + +/** + * MongoDbStore is a StoreInterface implementation using MongoDB as store engine. + * + * @author Joe Bennett + */ +class MongoDbStore implements StoreInterface +{ + private $mongo; + private $options; + private $initialTtl; + private $collection; + + /** + * @param \MongoDB\Client $mongo + * @param array $options + * List of available options: + * * database: The name of the database [required] + * * collection: The name of the collection [default: lock] + * * resource_field: The field name for storing the lock id [default: _id] MUST be uniquely indexed if you chage it + * * token_field: The field name for storing the lock token [default: token] + * * aquired_field: The field name for storing the acquisition timestamp [default: aquired_at] + * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. + * + * It is strongly recommended to put an index on the `expiry_field` for + * garbage-collection. Alternatively it's possible to automatically expire + * the locks in the database as described below: + * + * A TTL collections can be used on MongoDB 2.2+ to cleanup expired locks + * automatically. Such an index can for example look like this: + * + * db..ensureIndex( + * { "": 1 }, + * { "expireAfterSeconds": 0 } + * ) + * + * More details on: http://docs.mongodb.org/manual/tutorial/expire-data/ + * + * @param float $initialTtl The expiration delay of locks in seconds + */ + public function __construct(\MongoDB\Client $mongo, array $options = [], float $initialTtl = 300.0) + { + if (!isset($options['database'])) { + throw new \InvalidArgumentException( + 'You must provide the "database" option for MongoDBStore' + ); + } + + $this->mongo = $mongo; + + $this->options = array_merge([ + 'collection' => 'lock', + 'resource_field' => '_id', + 'token_field' => 'token', + 'aquired_field' => 'aquired_at', + 'expiry_field' => 'expires_at', + ], $options); + + $this->initialTtl = $initialTtl; + } + + /** + * {@inheritdoc} + * + * db.lock.update( + * { + * _id: "test", + * expires_at: { + * $lte : new Date() + * } + * }, + * { + * _id: "test", + * token: {# unique token #}, + * aquired: new Date(), + * expires_at: new Date({# now + ttl #}) + * }, + * { + * upsert: 1 + * } + * ); + */ + public function save(Key $key) + { + $expiry = $this->createDateTime(microtime(true) + $this->initialTtl); + + $filter = [ + $this->options['resource_field'] => (string)$key, + '$or' => [ + [ + $this->options['token_field'] => $this->getToken($key), + ], + [ + $this->options['expiry_field'] => [ + '$lte' => $this->createDateTime(), + ], + ], + ], + ]; + + $update = [ + '$set' => [ + $this->options['resource_field'] => (string)$key, + $this->options['token_field'] => $this->getToken($key), + $this->options['aquired_field'] => $this->createDateTime(), + $this->options['expiry_field'] => $expiry, + ], + ]; + + $options = [ + 'upsert' => true, + ]; + + $key->reduceLifetime($this->initialTtl); + try { + $this->getCollection()->updateOne($filter, $update, $options); + } catch (\MongoDB\Driver\Exception\BulkWriteException $e) { + throw new LockConflictedException('Failed to aquire lock', 0, $e); + } + + if ($key->isExpired()) { + throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key)); + } + } + + public function waitAndSave(Key $key) + { + throw new InvalidArgumentException(sprintf( + 'The store "%s" does not supports blocking locks.', + __CLASS__ + )); + } + + /** + * {@inheritdoc} + * + * db.lock.update( + * { + * _id: "test", + * token: {# unique token #}, + * expires_at: { + * $gte : new Date() + * } + * }, + * { + * _id: "test", + * expires_at: new Date({# now + ttl #}) + * }, + * { + * upsert: 1 + * } + * ); + */ + public function putOffExpiration(Key $key, $ttl) + { + $expiry = $this->createDateTime(microtime(true) + $ttl); + + $filter = [ + $this->options['resource_field'] => (string)$key, + $this->options['token_field'] => $this->getToken($key), + $this->options['expiry_field'] => [ + '$gte' => $this->createDateTime(), + ], + ]; + + $update = [ + '$set' =>[ + $this->options['resource_field'] => (string)$key, + $this->options['expiry_field'] => $expiry, + ], + ]; + + $options = [ + 'upsert' => true, + ]; + + $key->reduceLifetime($ttl); + try { + $this->getCollection()->updateOne($filter, $update, $options); + } catch (\MongoDB\Driver\Exception\BulkWriteException $e) { + throw new LockConflictedException('Failed to put off the expiration of the lock', 0, $e); + } + + if ($key->isExpired()) { + throw new LockExpiredException(sprintf( + 'Failed to put off the expiration of the "%s" lock within the specified time.', + $key + )); + } + } + + /** + * {@inheritdoc} + * + * db.lock.remove({ + * _id: "test" + * }); + */ + public function delete(Key $key) + { + $filter = [ + $this->options['resource_field'] => (string)$key, + $this->options['token_field'] => $this->getToken($key), + ]; + + try { + $result = $this->getCollection()->deleteOne($filter); + } catch (\MongoDB\Driver\Exception\BulkWriteException $e) { + throw new LockConflictedException('Failed to delete lock', 0, $e); + } + } + + /** + * {@inheritdoc} + * + * db.lock.find({ + * _id: "test", + * expires_at: { + * $gte : new Date() + * } + * }); + */ + public function exists(Key $key) + { + $filter = [ + $this->options['resource_field'] => (string)$key, + $this->options['expiry_field'] => [ + '$gte' => $this->createDateTime(), + ], + ]; + + $key->reduceLifetime($this->initialTtl); + $doc = $this->getCollection()->findOne($filter); + + return $doc && $doc['token'] === $this->getToken($key); + } + + private function getCollection(): \MongoDB\Collection + { + if (null === $this->collection) { + $this->collection = $this->mongo->selectCollection( + $this->options['database'], + $this->options['collection'] + ); + } + + return $this->collection; + } + + /** + * @param float $seconds Seconds since 1970-01-01T00:00:00.000Z supporting millisecond precision. Defaults to now. + * + * @return \MongoDB\BSON\UTCDateTime + */ + private function createDateTime(float $seconds = null): \MongoDB\BSON\UTCDateTime + { + if (null === $seconds) { + $seconds = microtime(true); + } + + $milliseconds = $seconds * 1000; + + return new \MongoDB\BSON\UTCDateTime($milliseconds); + } + + /** + * Retrieves an unique token for the given key. + */ + private function getToken(Key $key): string + { + if (!$key->hasState(__CLASS__)) { + $token = base64_encode(random_bytes(32)); + $key->setState(__CLASS__, $token); + } + + return $key->getState(__CLASS__); + } +} diff --git a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php new file mode 100644 index 0000000000000..a0279cd23142b --- /dev/null +++ b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php @@ -0,0 +1,55 @@ + + * + * 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\Store\MongoDbStore; + +/** + * @author Joe Bennett + */ +class MongoDbClientTest extends AbstractStoreTest +{ + use ExpiringStoreTestTrait; + + public static function setupBeforeClass() + { + try { + $client = self::getMongoConnection(); + $client->listDatabases(); + } catch (\Exception $e) { + self::markTestSkipped($e->getMessage()); + } + } + + protected static function getMongoConnection(): \MongoDB\Client + { + return new \MongoDB\Client('mongodb://'.getenv('MONGODB_HOST')); + } + + /** + * {@inheritdoc} + */ + protected function getClockDelay() + { + return 250000; + } + + /** + * {@inheritdoc} + */ + public function getStore() + { + return new MongoDbStore(self::getMongoConnection(), [ + 'database' => 'test', + ]); + } +} diff --git a/src/Symfony/Component/Lock/composer.json b/src/Symfony/Component/Lock/composer.json index 8aaf0eab94d25..f0ea282aafdd3 100644 --- a/src/Symfony/Component/Lock/composer.json +++ b/src/Symfony/Component/Lock/composer.json @@ -20,7 +20,8 @@ "psr/log": "~1.0" }, "require-dev": { - "predis/predis": "~1.0" + "predis/predis": "~1.0", + "mongodb/mongodb": "~1.0" }, "autoload": { "psr-4": { "Symfony\\Component\\Lock\\": "" }, diff --git a/src/Symfony/Component/Lock/phpunit.xml.dist b/src/Symfony/Component/Lock/phpunit.xml.dist index be3ca21576fdd..23acd9f0e00e8 100644 --- a/src/Symfony/Component/Lock/phpunit.xml.dist +++ b/src/Symfony/Component/Lock/phpunit.xml.dist @@ -12,6 +12,7 @@ + From e73f663b62d4816c68e5d8e3aa39febdde17c8a6 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 23 May 2018 15:43:50 +1000 Subject: [PATCH 02/29] #27345 Fixed typo --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 113647460ef79..4a842bdaa9f00 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -37,7 +37,7 @@ class MongoDbStore implements StoreInterface * * collection: The name of the collection [default: lock] * * resource_field: The field name for storing the lock id [default: _id] MUST be uniquely indexed if you chage it * * token_field: The field name for storing the lock token [default: token] - * * aquired_field: The field name for storing the acquisition timestamp [default: aquired_at] + * * acquired_field: The field name for storing the acquisition timestamp [default: acquired_at] * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. * * It is strongly recommended to put an index on the `expiry_field` for @@ -70,7 +70,7 @@ public function __construct(\MongoDB\Client $mongo, array $options = [], float $ 'collection' => 'lock', 'resource_field' => '_id', 'token_field' => 'token', - 'aquired_field' => 'aquired_at', + 'acquired_field' => 'acquired_at', 'expiry_field' => 'expires_at', ], $options); @@ -90,7 +90,7 @@ public function __construct(\MongoDB\Client $mongo, array $options = [], float $ * { * _id: "test", * token: {# unique token #}, - * aquired: new Date(), + * acquired: new Date(), * expires_at: new Date({# now + ttl #}) * }, * { @@ -120,7 +120,7 @@ public function save(Key $key) '$set' => [ $this->options['resource_field'] => (string)$key, $this->options['token_field'] => $this->getToken($key), - $this->options['aquired_field'] => $this->createDateTime(), + $this->options['acquired_field'] => $this->createDateTime(), $this->options['expiry_field'] => $expiry, ], ]; @@ -133,7 +133,7 @@ public function save(Key $key) try { $this->getCollection()->updateOne($filter, $update, $options); } catch (\MongoDB\Driver\Exception\BulkWriteException $e) { - throw new LockConflictedException('Failed to aquire lock', 0, $e); + throw new LockConflictedException('Failed to acquire lock', 0, $e); } if ($key->isExpired()) { From 38384124a7832c8adc6c6818028613efac953744 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 23 May 2018 15:46:39 +1000 Subject: [PATCH 03/29] #27345 fixed coding standards --- .../Component/Lock/Store/MongoDbStore.php | 101 +++++++++--------- .../Lock/Tests/Store/MongoDbClientTest.php | 4 +- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 4a842bdaa9f00..9bc8543c3330f 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -31,14 +31,14 @@ class MongoDbStore implements StoreInterface /** * @param \MongoDB\Client $mongo - * @param array $options - * List of available options: - * * database: The name of the database [required] - * * collection: The name of the collection [default: lock] - * * resource_field: The field name for storing the lock id [default: _id] MUST be uniquely indexed if you chage it - * * token_field: The field name for storing the lock token [default: token] - * * acquired_field: The field name for storing the acquisition timestamp [default: acquired_at] - * * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. + * @param array $options + * + * database: The name of the database [required] + * collection: The name of the collection [default: lock] + * resource_field: The field name for storing the lock id [default: _id] MUST be uniquely indexed if you chage it + * token_field: The field name for storing the lock token [default: token] + * acquired_field: The field name for storing the acquisition timestamp [default: acquired_at] + * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. * * It is strongly recommended to put an index on the `expiry_field` for * garbage-collection. Alternatively it's possible to automatically expire @@ -53,10 +53,9 @@ class MongoDbStore implements StoreInterface * ) * * More details on: http://docs.mongodb.org/manual/tutorial/expire-data/ - * * @param float $initialTtl The expiration delay of locks in seconds */ - public function __construct(\MongoDB\Client $mongo, array $options = [], float $initialTtl = 300.0) + public function __construct(\MongoDB\Client $mongo, array $options = array(), float $initialTtl = 300.0) { if (!isset($options['database'])) { throw new \InvalidArgumentException( @@ -66,13 +65,13 @@ public function __construct(\MongoDB\Client $mongo, array $options = [], float $ $this->mongo = $mongo; - $this->options = array_merge([ + $this->options = array_merge(array( 'collection' => 'lock', 'resource_field' => '_id', 'token_field' => 'token', 'acquired_field' => 'acquired_at', 'expiry_field' => 'expires_at', - ], $options); + ), $options); $this->initialTtl = $initialTtl; } @@ -102,32 +101,32 @@ public function save(Key $key) { $expiry = $this->createDateTime(microtime(true) + $this->initialTtl); - $filter = [ - $this->options['resource_field'] => (string)$key, - '$or' => [ - [ + $filter = array( + $this->options['resource_field'] => (string) $key, + '$or' => array( + array( $this->options['token_field'] => $this->getToken($key), - ], - [ - $this->options['expiry_field'] => [ + ), + array( + $this->options['expiry_field'] => array( '$lte' => $this->createDateTime(), - ], - ], - ], - ]; - - $update = [ - '$set' => [ - $this->options['resource_field'] => (string)$key, + ), + ), + ), + ); + + $update = array( + '$set' => array( + $this->options['resource_field'] => (string) $key, $this->options['token_field'] => $this->getToken($key), $this->options['acquired_field'] => $this->createDateTime(), $this->options['expiry_field'] => $expiry, - ], - ]; + ), + ); - $options = [ + $options = array( 'upsert' => true, - ]; + ); $key->reduceLifetime($this->initialTtl); try { @@ -173,24 +172,24 @@ public function putOffExpiration(Key $key, $ttl) { $expiry = $this->createDateTime(microtime(true) + $ttl); - $filter = [ - $this->options['resource_field'] => (string)$key, + $filter = array( + $this->options['resource_field'] => (string) $key, $this->options['token_field'] => $this->getToken($key), - $this->options['expiry_field'] => [ + $this->options['expiry_field'] => array( '$gte' => $this->createDateTime(), - ], - ]; + ), + ); - $update = [ - '$set' =>[ - $this->options['resource_field'] => (string)$key, + $update = array( + '$set' => array( + $this->options['resource_field'] => (string) $key, $this->options['expiry_field'] => $expiry, - ], - ]; + ), + ); - $options = [ + $options = array( 'upsert' => true, - ]; + ); $key->reduceLifetime($ttl); try { @@ -216,10 +215,10 @@ public function putOffExpiration(Key $key, $ttl) */ public function delete(Key $key) { - $filter = [ - $this->options['resource_field'] => (string)$key, + $filter = array( + $this->options['resource_field'] => (string) $key, $this->options['token_field'] => $this->getToken($key), - ]; + ); try { $result = $this->getCollection()->deleteOne($filter); @@ -240,12 +239,12 @@ public function delete(Key $key) */ public function exists(Key $key) { - $filter = [ - $this->options['resource_field'] => (string)$key, - $this->options['expiry_field'] => [ + $filter = array( + $this->options['resource_field'] => (string) $key, + $this->options['expiry_field'] => array( '$gte' => $this->createDateTime(), - ], - ]; + ), + ); $key->reduceLifetime($this->initialTtl); $doc = $this->getCollection()->findOne($filter); diff --git a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php index a0279cd23142b..e74aead98c571 100644 --- a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php @@ -48,8 +48,8 @@ protected function getClockDelay() */ public function getStore() { - return new MongoDbStore(self::getMongoConnection(), [ + return new MongoDbStore(self::getMongoConnection(), array( 'database' => 'test', - ]); + )); } } From 1a660e6e66bb012107cbaa62240e604cd6024c84 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 23 May 2018 17:12:41 +1000 Subject: [PATCH 04/29] #27345 Removed dev requirement for mongodb/mongodb travis env has ext-mongodb but appveyor doesn't since MongoDbSessionHandler does the same i've stripped the dev requirement for mongodb/mongodb --- composer.json | 1 - src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d47f2e22ae804..3a861887ce60d 100644 --- a/composer.json +++ b/composer.json @@ -92,7 +92,6 @@ "doctrine/dbal": "~2.4", "doctrine/orm": "~2.4,>=2.4.5", "doctrine/doctrine-bundle": "~1.4", - "mongodb/mongodb": "~1.0", "monolog/monolog": "~1.11", "ocramius/proxy-manager": "~0.4|~1.0|~2.0", "predis/predis": "~1.0", diff --git a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php index e74aead98c571..f319047ef5cc9 100644 --- a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php @@ -23,6 +23,9 @@ class MongoDbClientTest extends AbstractStoreTest public static function setupBeforeClass() { try { + if (!class_exists(\MongoDB\Client::class)) { + throw new \RuntimeException('The mongodb/mongodb package is required.'); + } $client = self::getMongoConnection(); $client->listDatabases(); } catch (\Exception $e) { From d65fddf8f485c52eef3973c94de0d609b874f5ae Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Thu, 24 May 2018 16:08:57 +1000 Subject: [PATCH 05/29] #27345 Removed unnessasary reduceLifetime --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 9bc8543c3330f..105dbad48c305 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -246,7 +246,6 @@ public function exists(Key $key) ), ); - $key->reduceLifetime($this->initialTtl); $doc = $this->getCollection()->findOne($filter); return $doc && $doc['token'] === $this->getToken($key); From f3cc220b70d9aed671b76a7f78e10f98a360bd5a Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Thu, 31 May 2018 21:10:43 +1000 Subject: [PATCH 06/29] #27345 Improved documentation, exception handling, removed configurable fields --- .../Component/Lock/Store/MongoDbStore.php | 150 +++++++----------- src/Symfony/Component/Lock/StoreInterface.php | 4 + 2 files changed, 57 insertions(+), 97 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 105dbad48c305..d05acef203e3f 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -14,6 +14,8 @@ use Symfony\Component\Lock\Exception\InvalidArgumentException; use Symfony\Component\Lock\Exception\LockConflictedException; use Symfony\Component\Lock\Exception\LockExpiredException; +use Symfony\Component\Lock\Exception\LockStorageException; +use Symfony\Component\Lock\Exception\NotSupportedException; use Symfony\Component\Lock\Key; use Symfony\Component\Lock\StoreInterface; @@ -31,34 +33,36 @@ class MongoDbStore implements StoreInterface /** * @param \MongoDB\Client $mongo - * @param array $options + * @param array $options * - * database: The name of the database [required] - * collection: The name of the collection [default: lock] - * resource_field: The field name for storing the lock id [default: _id] MUST be uniquely indexed if you chage it - * token_field: The field name for storing the lock token [default: token] - * acquired_field: The field name for storing the acquisition timestamp [default: acquired_at] - * expiry_field: The field name for storing the expiry-timestamp [default: expires_at]. + * database: The name of the database [required] + * collection: The name of the collection [default: lock] * - * It is strongly recommended to put an index on the `expiry_field` for - * garbage-collection. Alternatively it's possible to automatically expire - * the locks in the database as described below: + * A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks. + * Please be aware any clock drift between the application and mongo servers could + * cause locks to be released prematurely. To account for any drift; + * expireAfterSeconds can be set to a value higher than 0. The logical expiry of + * locks is handled by the application so setting a higher ``expireAfterSeconds`` + * has no effect other than keeping stale data for longer. * - * A TTL collections can be used on MongoDB 2.2+ to cleanup expired locks - * automatically. Such an index can for example look like this: - * - * db..ensureIndex( - * { "": 1 }, - * { "expireAfterSeconds": 0 } + * db.lock.ensureIndex( + * { "expires_at": 1 }, + * { "expireAfterSeconds": 60 } * ) * - * More details on: http://docs.mongodb.org/manual/tutorial/expire-data/ + * @see http://docs.mongodb.org/manual/tutorial/expire-data/ + * + * Please note, the Symfony\Component\Lock\Key's $resource + * must not exceed 1024 bytes including structual overhead. + * + * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit + * * @param float $initialTtl The expiration delay of locks in seconds */ - public function __construct(\MongoDB\Client $mongo, array $options = array(), float $initialTtl = 300.0) + public function __construct(\MongoDB\Client $mongo, array $options, float $initialTtl = 300.0) { if (!isset($options['database'])) { - throw new \InvalidArgumentException( + throw new InvalidArgumentException( 'You must provide the "database" option for MongoDBStore' ); } @@ -67,10 +71,6 @@ public function __construct(\MongoDB\Client $mongo, array $options = array(), fl $this->options = array_merge(array( 'collection' => 'lock', - 'resource_field' => '_id', - 'token_field' => 'token', - 'acquired_field' => 'acquired_at', - 'expiry_field' => 'expires_at', ), $options); $this->initialTtl = $initialTtl; @@ -78,38 +78,22 @@ public function __construct(\MongoDB\Client $mongo, array $options = array(), fl /** * {@inheritdoc} - * - * db.lock.update( - * { - * _id: "test", - * expires_at: { - * $lte : new Date() - * } - * }, - * { - * _id: "test", - * token: {# unique token #}, - * acquired: new Date(), - * expires_at: new Date({# now + ttl #}) - * }, - * { - * upsert: 1 - * } - * ); */ public function save(Key $key) { - $expiry = $this->createDateTime(microtime(true) + $this->initialTtl); + $now = microtime(true); + $expiry = $this->createDateTime($now + $this->initialTtl); + $token = $this->getToken($key); $filter = array( - $this->options['resource_field'] => (string) $key, + '_id' => (string) $key, '$or' => array( array( - $this->options['token_field'] => $this->getToken($key), + 'token' => $token, ), array( - $this->options['expiry_field'] => array( - '$lte' => $this->createDateTime(), + 'expires_at' => array( + '$lte' => $this->createDateTime($now), ), ), ), @@ -117,10 +101,9 @@ public function save(Key $key) $update = array( '$set' => array( - $this->options['resource_field'] => (string) $key, - $this->options['token_field'] => $this->getToken($key), - $this->options['acquired_field'] => $this->createDateTime(), - $this->options['expiry_field'] => $expiry, + '_id' => (string) $key, + 'token' => $token, + 'expires_at' => $expiry, ), ); @@ -131,8 +114,10 @@ public function save(Key $key) $key->reduceLifetime($this->initialTtl); try { $this->getCollection()->updateOne($filter, $update, $options); - } catch (\MongoDB\Driver\Exception\BulkWriteException $e) { + } catch (\MongoDB\Driver\Exception\WriteException $e) { throw new LockConflictedException('Failed to acquire lock', 0, $e); + } catch (\Exception $e) { + throw new LockStorageException($e->getMessage(), 0, $e); } if ($key->isExpired()) { @@ -142,7 +127,7 @@ public function save(Key $key) public function waitAndSave(Key $key) { - throw new InvalidArgumentException(sprintf( + throw new NotSupportedException(sprintf( 'The store "%s" does not supports blocking locks.', __CLASS__ )); @@ -150,40 +135,24 @@ public function waitAndSave(Key $key) /** * {@inheritdoc} - * - * db.lock.update( - * { - * _id: "test", - * token: {# unique token #}, - * expires_at: { - * $gte : new Date() - * } - * }, - * { - * _id: "test", - * expires_at: new Date({# now + ttl #}) - * }, - * { - * upsert: 1 - * } - * ); */ public function putOffExpiration(Key $key, $ttl) { - $expiry = $this->createDateTime(microtime(true) + $ttl); + $now = microtime(true); + $expiry = $this->createDateTime($now + $ttl); $filter = array( - $this->options['resource_field'] => (string) $key, - $this->options['token_field'] => $this->getToken($key), - $this->options['expiry_field'] => array( - '$gte' => $this->createDateTime(), + '_id' => (string) $key, + 'token' => $this->getToken($key), + 'expires_at' => array( + '$gte' => $this->createDateTime($now), ), ); $update = array( '$set' => array( - $this->options['resource_field'] => (string) $key, - $this->options['expiry_field'] => $expiry, + '_id' => (string) $key, + 'expires_at' => $expiry, ), ); @@ -194,8 +163,10 @@ public function putOffExpiration(Key $key, $ttl) $key->reduceLifetime($ttl); try { $this->getCollection()->updateOne($filter, $update, $options); - } catch (\MongoDB\Driver\Exception\BulkWriteException $e) { + } catch (\MongoDB\Driver\Exception\WriteException $e) { throw new LockConflictedException('Failed to put off the expiration of the lock', 0, $e); + } catch (\Exception $e) { + throw new LockStorageException($e->getMessage(), 0, $e); } if ($key->isExpired()) { @@ -208,40 +179,25 @@ public function putOffExpiration(Key $key, $ttl) /** * {@inheritdoc} - * - * db.lock.remove({ - * _id: "test" - * }); */ public function delete(Key $key) { $filter = array( - $this->options['resource_field'] => (string) $key, - $this->options['token_field'] => $this->getToken($key), + '_id' => (string) $key, + 'token' => $this->getToken($key), ); - try { - $result = $this->getCollection()->deleteOne($filter); - } catch (\MongoDB\Driver\Exception\BulkWriteException $e) { - throw new LockConflictedException('Failed to delete lock', 0, $e); - } + $this->getCollection()->deleteOne($filter); } /** * {@inheritdoc} - * - * db.lock.find({ - * _id: "test", - * expires_at: { - * $gte : new Date() - * } - * }); */ public function exists(Key $key) { $filter = array( - $this->options['resource_field'] => (string) $key, - $this->options['expiry_field'] => array( + '_id' => (string) $key, + 'expires_at' => array( '$gte' => $this->createDateTime(), ), ); diff --git a/src/Symfony/Component/Lock/StoreInterface.php b/src/Symfony/Component/Lock/StoreInterface.php index 985c4476d7da6..369338eecc5cc 100644 --- a/src/Symfony/Component/Lock/StoreInterface.php +++ b/src/Symfony/Component/Lock/StoreInterface.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Lock; use Symfony\Component\Lock\Exception\LockConflictedException; +use Symfony\Component\Lock\Exception\LockExpiredException; use Symfony\Component\Lock\Exception\NotSupportedException; /** @@ -25,6 +26,7 @@ interface StoreInterface * Stores the resource if it's not locked by someone else. * * @throws LockConflictedException + * @throws LockExpiredException */ public function save(Key $key); @@ -34,6 +36,7 @@ public function save(Key $key); * If the store does not support this feature it should throw a NotSupportedException. * * @throws LockConflictedException + * @throws LockExpiredException * @throws NotSupportedException */ public function waitAndSave(Key $key); @@ -46,6 +49,7 @@ public function waitAndSave(Key $key); * @param float $ttl amount of second to keep the lock in the store * * @throws LockConflictedException + * @throws LockExpiredException * @throws NotSupportedException */ public function putOffExpiration(Key $key, $ttl); From ec8d2171e68bff6b00b0713ac5135e7fe75a2d81 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Thu, 31 May 2018 21:14:54 +1000 Subject: [PATCH 07/29] #27345 fixed typo and @param indentation --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index d05acef203e3f..917454e1a8c99 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -33,7 +33,7 @@ class MongoDbStore implements StoreInterface /** * @param \MongoDB\Client $mongo - * @param array $options + * @param array $options * * database: The name of the database [required] * collection: The name of the collection [default: lock] @@ -53,11 +53,10 @@ class MongoDbStore implements StoreInterface * @see http://docs.mongodb.org/manual/tutorial/expire-data/ * * Please note, the Symfony\Component\Lock\Key's $resource - * must not exceed 1024 bytes including structual overhead. - * + * must not exceed 1024 bytes including structural overhead. * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit * - * @param float $initialTtl The expiration delay of locks in seconds + * @param float $initialTtl The expiration delay of locks in seconds */ public function __construct(\MongoDB\Client $mongo, array $options, float $initialTtl = 300.0) { From a1a3be73f56f2821aa175472f9ba16c135e4fb3e Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Thu, 31 May 2018 21:18:53 +1000 Subject: [PATCH 08/29] #27345 fixed @param indentation --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 917454e1a8c99..1f801e64513a3 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -56,7 +56,7 @@ class MongoDbStore implements StoreInterface * must not exceed 1024 bytes including structural overhead. * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit * - * @param float $initialTtl The expiration delay of locks in seconds + * @param float $initialTtl The expiration delay of locks in seconds */ public function __construct(\MongoDB\Client $mongo, array $options, float $initialTtl = 300.0) { From 1b150baa988f293197405c683723a09cbbd1b60d Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Thu, 31 May 2018 23:03:59 +1000 Subject: [PATCH 09/29] #27345 moved clock discrepancy handling to a constructor option 'drift' --- .../Component/Lock/Store/MongoDbStore.php | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 1f801e64513a3..757df423ccd02 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -35,23 +35,28 @@ class MongoDbStore implements StoreInterface * @param \MongoDB\Client $mongo * @param array $options * - * database: The name of the database [required] - * collection: The name of the collection [default: lock] + * database: The name of the database [required] + * collection: The name of the collection [default: lock] + * drift: Seconds to extend expiries to account for clock discrepancies [default: 0.0] + * + * Please be aware, MongoDbStore relies on clock syncronisation between + * the application nodes and mongodb servers. The drift option can be used + * to extend expiries to account for clock discrepancies. * * A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks. - * Please be aware any clock drift between the application and mongo servers could - * cause locks to be released prematurely. To account for any drift; - * expireAfterSeconds can be set to a value higher than 0. The logical expiry of - * locks is handled by the application so setting a higher ``expireAfterSeconds`` - * has no effect other than keeping stale data for longer. + * * * db.lock.ensureIndex( * { "expires_at": 1 }, - * { "expireAfterSeconds": 60 } + * { "expireAfterSeconds": 0 } * ) * * @see http://docs.mongodb.org/manual/tutorial/expire-data/ * + * writeConcern, readConcern and readPreference are not specified by MongoDbStore + * meaning the collection's settings will take effect. + * @see https://docs.mongodb.com/manual/applications/replication/ + * * Please note, the Symfony\Component\Lock\Key's $resource * must not exceed 1024 bytes including structural overhead. * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit @@ -70,8 +75,15 @@ public function __construct(\MongoDB\Client $mongo, array $options, float $initi $this->options = array_merge(array( 'collection' => 'lock', + 'drift' => 0.0, ), $options); + if (!is_numeric($this->options['drift'])) { + throw new InvalidArgumentException( + 'The "drift" option must be numeric (number of seconds)' + ); + } + $this->initialTtl = $initialTtl; } @@ -81,7 +93,7 @@ public function __construct(\MongoDB\Client $mongo, array $options, float $initi public function save(Key $key) { $now = microtime(true); - $expiry = $this->createDateTime($now + $this->initialTtl); + $expiry = $this->createDateTime($now + $this->initialTtl + $this->options['drift']); $token = $this->getToken($key); $filter = array( @@ -138,7 +150,7 @@ public function waitAndSave(Key $key) public function putOffExpiration(Key $key, $ttl) { $now = microtime(true); - $expiry = $this->createDateTime($now + $ttl); + $expiry = $this->createDateTime($now + $ttl + $this->options['drift']); $filter = array( '_id' => (string) $key, @@ -186,7 +198,9 @@ public function delete(Key $key) 'token' => $this->getToken($key), ); - $this->getCollection()->deleteOne($filter); + $options = array(); + + $this->getCollection()->deleteOne($filter, $options); } /** @@ -196,6 +210,7 @@ public function exists(Key $key) { $filter = array( '_id' => (string) $key, + 'token' => $this->getToken($key), 'expires_at' => array( '$gte' => $this->createDateTime(), ), @@ -203,7 +218,7 @@ public function exists(Key $key) $doc = $this->getCollection()->findOne($filter); - return $doc && $doc['token'] === $this->getToken($key); + return null !== $doc; } private function getCollection(): \MongoDB\Collection From 73bb802085fbbff0c797bb1c9ce6b09c2b942ca5 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Thu, 31 May 2018 23:07:30 +1000 Subject: [PATCH 10/29] #27345 fixed phpdoc spacing --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 757df423ccd02..eb4e964c08f82 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -45,7 +45,6 @@ class MongoDbStore implements StoreInterface * * A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks. * - * * db.lock.ensureIndex( * { "expires_at": 1 }, * { "expireAfterSeconds": 0 } From 5e86904b6e2e64feabb042ec4bd1712a46bf0558 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Fri, 1 Jun 2018 09:30:09 +1000 Subject: [PATCH 11/29] #27345 removed drift option in favour or recommending setting TTL higher --- .../Component/Lock/Store/MongoDbStore.php | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index eb4e964c08f82..f59d79294d4ae 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -28,20 +28,20 @@ class MongoDbStore implements StoreInterface { private $mongo; private $options; - private $initialTtl; private $collection; /** * @param \MongoDB\Client $mongo * @param array $options * - * database: The name of the database [required] - * collection: The name of the collection [default: lock] - * drift: Seconds to extend expiries to account for clock discrepancies [default: 0.0] + * database: The name of the database [required] + * collection: The name of the collection [default: lock] + * initialTtl: The expiration delay of locks in seconds [default: 300.0] * - * Please be aware, MongoDbStore relies on clock syncronisation between - * the application nodes and mongodb servers. The drift option can be used - * to extend expiries to account for clock discrepancies. + * CAUTION: This store relies on all client and server nodes to have + * synchronized clocks for lock expiry to occur at the correct time. + * To ensure locks don't expire prematurely; the ttl's should be set with enough + * extra time to account for any clock drift between nodes. * * A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks. * @@ -59,10 +59,8 @@ class MongoDbStore implements StoreInterface * Please note, the Symfony\Component\Lock\Key's $resource * must not exceed 1024 bytes including structural overhead. * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit - * - * @param float $initialTtl The expiration delay of locks in seconds */ - public function __construct(\MongoDB\Client $mongo, array $options, float $initialTtl = 300.0) + public function __construct(\MongoDB\Client $mongo, array $options) { if (!isset($options['database'])) { throw new InvalidArgumentException( @@ -74,16 +72,8 @@ public function __construct(\MongoDB\Client $mongo, array $options, float $initi $this->options = array_merge(array( 'collection' => 'lock', - 'drift' => 0.0, + 'initialTtl' => 300.0, ), $options); - - if (!is_numeric($this->options['drift'])) { - throw new InvalidArgumentException( - 'The "drift" option must be numeric (number of seconds)' - ); - } - - $this->initialTtl = $initialTtl; } /** @@ -91,9 +81,8 @@ public function __construct(\MongoDB\Client $mongo, array $options, float $initi */ public function save(Key $key) { - $now = microtime(true); - $expiry = $this->createDateTime($now + $this->initialTtl + $this->options['drift']); $token = $this->getToken($key); + $now = microtime(true); $filter = array( '_id' => (string) $key, @@ -113,7 +102,7 @@ public function save(Key $key) '$set' => array( '_id' => (string) $key, 'token' => $token, - 'expires_at' => $expiry, + 'expires_at' => $this->createDateTime($now + $this->options['initialTtl']), ), ); @@ -121,7 +110,7 @@ public function save(Key $key) 'upsert' => true, ); - $key->reduceLifetime($this->initialTtl); + $key->reduceLifetime($this->options['initialTtl']); try { $this->getCollection()->updateOne($filter, $update, $options); } catch (\MongoDB\Driver\Exception\WriteException $e) { @@ -149,7 +138,6 @@ public function waitAndSave(Key $key) public function putOffExpiration(Key $key, $ttl) { $now = microtime(true); - $expiry = $this->createDateTime($now + $ttl + $this->options['drift']); $filter = array( '_id' => (string) $key, @@ -162,7 +150,7 @@ public function putOffExpiration(Key $key, $ttl) $update = array( '$set' => array( '_id' => (string) $key, - 'expires_at' => $expiry, + 'expires_at' => $this->createDateTime($now + $ttl), ), ); From 8572c21f0f170d2d91303cb62f677bbedcfff547 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 08:32:22 +1000 Subject: [PATCH 12/29] #27345 Added public createTTLIndex method --- .../Component/Lock/Store/MongoDbStore.php | 36 ++++++++++++++----- .../Lock/Tests/Store/MongoDbClientTest.php | 7 ++++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index f59d79294d4ae..16a6c3e9a9e01 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -43,15 +43,6 @@ class MongoDbStore implements StoreInterface * To ensure locks don't expire prematurely; the ttl's should be set with enough * extra time to account for any clock drift between nodes. * - * A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks. - * - * db.lock.ensureIndex( - * { "expires_at": 1 }, - * { "expireAfterSeconds": 0 } - * ) - * - * @see http://docs.mongodb.org/manual/tutorial/expire-data/ - * * writeConcern, readConcern and readPreference are not specified by MongoDbStore * meaning the collection's settings will take effect. * @see https://docs.mongodb.com/manual/applications/replication/ @@ -76,6 +67,33 @@ public function __construct(\MongoDB\Client $mongo, array $options) ), $options); } + /** + * Create a TTL index to automatically remove expired locks. + * + * This should be called once during database setup. + * + * Alternatively the TTL index can be created manually: + * + * db.lock.ensureIndex( + * { "expires_at": 1 }, + * { "expireAfterSeconds": 0 } + * ) + * + * A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks. + * + * @see http://docs.mongodb.org/manual/tutorial/expire-data/ + */ + public function createTTLIndex(): bool + { + $keys = array( + 'expires_at' => 1, + ); + $options = array( + 'expireAfterSeconds' => 0, + ); + return $this->getCollection()->createIndex($keys, $options); + } + /** * {@inheritdoc} */ diff --git a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php index f319047ef5cc9..f21b234d6b704 100644 --- a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php @@ -55,4 +55,11 @@ public function getStore() 'database' => 'test', )); } + + public function testCreateIndex() + { + $store = $this->getStore(); + + $this->assertTrue($store->createTTLIndex()); + } } From dbdad36750bd8ea218bbbe57e63d40dd65af6216 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 08:35:34 +1000 Subject: [PATCH 13/29] #27345 Fixed coding standards --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 16a6c3e9a9e01..aa64585e40589 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -45,10 +45,12 @@ class MongoDbStore implements StoreInterface * * writeConcern, readConcern and readPreference are not specified by MongoDbStore * meaning the collection's settings will take effect. + * * @see https://docs.mongodb.com/manual/applications/replication/ * * Please note, the Symfony\Component\Lock\Key's $resource * must not exceed 1024 bytes including structural overhead. + * * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit */ public function __construct(\MongoDB\Client $mongo, array $options) From 84c4d1546700478d2477afd162ebb1d9fde38988 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 08:37:06 +1000 Subject: [PATCH 14/29] #27345 Fixed coding standards --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index aa64585e40589..5fb109fd72118 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -45,12 +45,10 @@ class MongoDbStore implements StoreInterface * * writeConcern, readConcern and readPreference are not specified by MongoDbStore * meaning the collection's settings will take effect. - * * @see https://docs.mongodb.com/manual/applications/replication/ * * Please note, the Symfony\Component\Lock\Key's $resource * must not exceed 1024 bytes including structural overhead. - * * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit */ public function __construct(\MongoDB\Client $mongo, array $options) @@ -90,9 +88,11 @@ public function createTTLIndex(): bool $keys = array( 'expires_at' => 1, ); + $options = array( 'expireAfterSeconds' => 0, ); + return $this->getCollection()->createIndex($keys, $options); } From d577191e501fe35062604af99270553cfda02b7e Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 08:38:00 +1000 Subject: [PATCH 15/29] #27345 Fixed coding standards --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 5fb109fd72118..d17de26150460 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -45,10 +45,12 @@ class MongoDbStore implements StoreInterface * * writeConcern, readConcern and readPreference are not specified by MongoDbStore * meaning the collection's settings will take effect. + * * @see https://docs.mongodb.com/manual/applications/replication/ * * Please note, the Symfony\Component\Lock\Key's $resource * must not exceed 1024 bytes including structural overhead. + * * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit */ public function __construct(\MongoDB\Client $mongo, array $options) From 2ce68f5aa22f06bac71f12b6607e97494be81575 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 08:42:37 +1000 Subject: [PATCH 16/29] #27345 Fixed coding standards --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index d17de26150460..e0f6bed9f9f02 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -50,7 +50,6 @@ class MongoDbStore implements StoreInterface * * Please note, the Symfony\Component\Lock\Key's $resource * must not exceed 1024 bytes including structural overhead. - * * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit */ public function __construct(\MongoDB\Client $mongo, array $options) From 5446b1128ab53260741bbf31b99f881ae2b4e256 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 09:34:05 +1000 Subject: [PATCH 17/29] #27345 Improved documentation --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index e0f6bed9f9f02..037b1c837cd59 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -40,16 +40,19 @@ class MongoDbStore implements StoreInterface * * CAUTION: This store relies on all client and server nodes to have * synchronized clocks for lock expiry to occur at the correct time. - * To ensure locks don't expire prematurely; the ttl's should be set with enough - * extra time to account for any clock drift between nodes. + * To ensure locks don't expire prematurely; the lock TTL should be set + * with enough extra time to account for any clock drift between nodes. * - * writeConcern, readConcern and readPreference are not specified by MongoDbStore - * meaning the collection's settings will take effect. + * @see self::createTTLIndex() For more info on creating a TTL index + * + * writeConcern, readConcern and readPreference are not specified by + * MongoDbStore meaning the collection's settings will take effect. * * @see https://docs.mongodb.com/manual/applications/replication/ * * Please note, the Symfony\Component\Lock\Key's $resource * must not exceed 1024 bytes including structural overhead. + * * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit */ public function __construct(\MongoDB\Client $mongo, array $options) From a9b85d609c66f5cdce12dd301264af44d99fd79b Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 09:35:13 +1000 Subject: [PATCH 18/29] #27345 Fixed spacing for fabbot --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 037b1c837cd59..fc760a05c6a01 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -47,12 +47,10 @@ class MongoDbStore implements StoreInterface * * writeConcern, readConcern and readPreference are not specified by * MongoDbStore meaning the collection's settings will take effect. - * * @see https://docs.mongodb.com/manual/applications/replication/ * * Please note, the Symfony\Component\Lock\Key's $resource * must not exceed 1024 bytes including structural overhead. - * * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit */ public function __construct(\MongoDB\Client $mongo, array $options) From 6fdc602e3c377b45ec53090d4271892ab77c405c Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 09:43:14 +1000 Subject: [PATCH 19/29] #27345 reordered documentation in order of importance --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index fc760a05c6a01..079404b6dfb6b 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -20,7 +20,8 @@ use Symfony\Component\Lock\StoreInterface; /** - * MongoDbStore is a StoreInterface implementation using MongoDB as store engine. + * MongoDbStore is a StoreInterface implementation using MongoDB as a storage + * engine. * * @author Joe Bennett */ @@ -42,16 +43,17 @@ class MongoDbStore implements StoreInterface * synchronized clocks for lock expiry to occur at the correct time. * To ensure locks don't expire prematurely; the lock TTL should be set * with enough extra time to account for any clock drift between nodes. + * @see self::createTTLIndex() * - * @see self::createTTLIndex() For more info on creating a TTL index + * CAUTION: The locked resouce name is indexed in the ``_id`` field of the + * lock collection. + * An indexed field's value in MongoDB can be a maximum of 1024 bytes in + * length inclusive of structural overhead. + * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit * * writeConcern, readConcern and readPreference are not specified by * MongoDbStore meaning the collection's settings will take effect. * @see https://docs.mongodb.com/manual/applications/replication/ - * - * Please note, the Symfony\Component\Lock\Key's $resource - * must not exceed 1024 bytes including structural overhead. - * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit */ public function __construct(\MongoDB\Client $mongo, array $options) { From 9d80a0d9195ddfb1771cad7db564632873107aff Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 09:44:35 +1000 Subject: [PATCH 20/29] #27345 Fixed spacing for fabbot --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 079404b6dfb6b..a0267f903b9b7 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -39,17 +39,18 @@ class MongoDbStore implements StoreInterface * collection: The name of the collection [default: lock] * initialTtl: The expiration delay of locks in seconds [default: 300.0] * + * CAUTION: The locked resouce name is indexed in the _id field of the + * lock collection. + * An indexed field's value in MongoDB can be a maximum of 1024 bytes in + * length inclusive of structural overhead. + * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit + * * CAUTION: This store relies on all client and server nodes to have * synchronized clocks for lock expiry to occur at the correct time. * To ensure locks don't expire prematurely; the lock TTL should be set * with enough extra time to account for any clock drift between nodes. - * @see self::createTTLIndex() * - * CAUTION: The locked resouce name is indexed in the ``_id`` field of the - * lock collection. - * An indexed field's value in MongoDB can be a maximum of 1024 bytes in - * length inclusive of structural overhead. - * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit + * @see self::createTTLIndex() * * writeConcern, readConcern and readPreference are not specified by * MongoDbStore meaning the collection's settings will take effect. From 1ac702ad76c2221e170c9efd9eaf387c8305a4e3 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 09:47:26 +1000 Subject: [PATCH 21/29] #27345 Fixed spacing for fabbot --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index a0267f903b9b7..1d92709c21552 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -43,13 +43,13 @@ class MongoDbStore implements StoreInterface * lock collection. * An indexed field's value in MongoDB can be a maximum of 1024 bytes in * length inclusive of structural overhead. + * * @see https://docs.mongodb.com/manual/reference/limits/#Index-Key-Limit * * CAUTION: This store relies on all client and server nodes to have * synchronized clocks for lock expiry to occur at the correct time. * To ensure locks don't expire prematurely; the lock TTL should be set * with enough extra time to account for any clock drift between nodes. - * * @see self::createTTLIndex() * * writeConcern, readConcern and readPreference are not specified by From 4ba5b41b0bed71b434bdc712e3e34ebdd7c48279 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 15:15:49 +1000 Subject: [PATCH 22/29] #27345 Updated return type and exception handling on createTTLIndex --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 1d92709c21552..197e67c06bcaf 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -87,8 +87,13 @@ public function __construct(\MongoDB\Client $mongo, array $options) * A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks. * * @see http://docs.mongodb.org/manual/tutorial/expire-data/ + * + * @return string The name of the created index as a string. + * + * @throws \MongoDB\Exception\UnsupportedException + * @throws \MongoDB\Exception\InvalidArgumentException */ - public function createTTLIndex(): bool + public function createTTLIndex(): string { $keys = array( 'expires_at' => 1, From 58ef8737976aac0664f74b4c548d44727a7abba9 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 15:17:13 +1000 Subject: [PATCH 23/29] #27345 Updated exception handling on createTTLIndex --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 197e67c06bcaf..58282d8183c7a 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -88,10 +88,11 @@ public function __construct(\MongoDB\Client $mongo, array $options) * * @see http://docs.mongodb.org/manual/tutorial/expire-data/ * - * @return string The name of the created index as a string. + * @return string The name of the created index as a string * - * @throws \MongoDB\Exception\UnsupportedException - * @throws \MongoDB\Exception\InvalidArgumentException + * @throws \MongoDB\Exception\UnsupportedException if options are not supported by the selected server + * @throws \MongoDB\Exception\InvalidArgumentException for parameter/option parsing errors + * @throws \MongoDB\Exception\DriverRuntimeException for other driver errors (e.g. connection errors) */ public function createTTLIndex(): string { From 36c3af4e54315969d5f640173795c42cecf67561 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 15:18:08 +1000 Subject: [PATCH 24/29] #27345 Fixed spacing for fabbot --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 58282d8183c7a..f11128a788ba6 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -90,9 +90,9 @@ public function __construct(\MongoDB\Client $mongo, array $options) * * @return string The name of the created index as a string * - * @throws \MongoDB\Exception\UnsupportedException if options are not supported by the selected server + * @throws \MongoDB\Exception\UnsupportedException if options are not supported by the selected server * @throws \MongoDB\Exception\InvalidArgumentException for parameter/option parsing errors - * @throws \MongoDB\Exception\DriverRuntimeException for other driver errors (e.g. connection errors) + * @throws \MongoDB\Exception\DriverRuntimeException for other driver errors (e.g. connection errors) */ public function createTTLIndex(): string { From 168f194f0fe9309d9741880760e068ed042b1c71 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 19:25:37 +1000 Subject: [PATCH 25/29] #27345 Removed initialTtl from array and fixed unit test --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 14 +++++++++----- .../Lock/Tests/Store/MongoDbClientTest.php | 2 +- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index f11128a788ba6..7ae370bcc26db 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -29,6 +29,8 @@ class MongoDbStore implements StoreInterface { private $mongo; private $options; + private $initialTtl; + private $collection; /** @@ -37,7 +39,8 @@ class MongoDbStore implements StoreInterface * * database: The name of the database [required] * collection: The name of the collection [default: lock] - * initialTtl: The expiration delay of locks in seconds [default: 300.0] + * + * @param float $initialTtl the expiration delay of locks in seconds * * CAUTION: The locked resouce name is indexed in the _id field of the * lock collection. @@ -56,7 +59,7 @@ class MongoDbStore implements StoreInterface * MongoDbStore meaning the collection's settings will take effect. * @see https://docs.mongodb.com/manual/applications/replication/ */ - public function __construct(\MongoDB\Client $mongo, array $options) + public function __construct(\MongoDB\Client $mongo, array $options, float $initialTtl = 300.0) { if (!isset($options['database'])) { throw new InvalidArgumentException( @@ -68,8 +71,9 @@ public function __construct(\MongoDB\Client $mongo, array $options) $this->options = array_merge(array( 'collection' => 'lock', - 'initialTtl' => 300.0, ), $options); + + $this->initialTtl = $initialTtl; } /** @@ -133,7 +137,7 @@ public function save(Key $key) '$set' => array( '_id' => (string) $key, 'token' => $token, - 'expires_at' => $this->createDateTime($now + $this->options['initialTtl']), + 'expires_at' => $this->createDateTime($now + $this->initialTtl), ), ); @@ -141,7 +145,7 @@ public function save(Key $key) 'upsert' => true, ); - $key->reduceLifetime($this->options['initialTtl']); + $key->reduceLifetime($this->initialTtl); try { $this->getCollection()->updateOne($filter, $update, $options); } catch (\MongoDB\Driver\Exception\WriteException $e) { diff --git a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php index f21b234d6b704..a4b4cc481dae5 100644 --- a/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php +++ b/src/Symfony/Component/Lock/Tests/Store/MongoDbClientTest.php @@ -60,6 +60,6 @@ public function testCreateIndex() { $store = $this->getStore(); - $this->assertTrue($store->createTTLIndex()); + $this->assertEquals($store->createTTLIndex(), 'expires_at_1'); } } From abc72f81f29760672d97094746360388c37cc1b3 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 19:29:27 +1000 Subject: [PATCH 26/29] #27345 phpdoc adjusted for fabbot --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 7ae370bcc26db..91e018e7331f8 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -35,12 +35,12 @@ class MongoDbStore implements StoreInterface /** * @param \MongoDB\Client $mongo - * @param array $options + * @param array $options See below + * @param float $initialTtl The expiration delay of locks in seconds * - * database: The name of the database [required] - * collection: The name of the collection [default: lock] - * - * @param float $initialTtl the expiration delay of locks in seconds + * Options: + * database: The name of the database [required] + * collection: The name of the collection [default: lock] * * CAUTION: The locked resouce name is indexed in the _id field of the * lock collection. From e062bf28e540717f4cf10f2fc70d49d0e813e821 Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Wed, 6 Jun 2018 19:30:06 +1000 Subject: [PATCH 27/29] #27345 phpdoc adjusted for fabbot --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 91e018e7331f8..a8c59f34fb21c 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -35,8 +35,8 @@ class MongoDbStore implements StoreInterface /** * @param \MongoDB\Client $mongo - * @param array $options See below - * @param float $initialTtl The expiration delay of locks in seconds + * @param array $options See below + * @param float $initialTtl The expiration delay of locks in seconds * * Options: * database: The name of the database [required] From f2e23b4c54e6ef5400979c7070fbf1c08c6d5a0d Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Thu, 14 Jun 2018 10:02:10 +0000 Subject: [PATCH 28/29] #27345 trigger ci retry --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index a8c59f34fb21c..25638bfe3b73d 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -146,6 +146,7 @@ public function save(Key $key) ); $key->reduceLifetime($this->initialTtl); + try { $this->getCollection()->updateOne($filter, $update, $options); } catch (\MongoDB\Driver\Exception\WriteException $e) { From 97e571196d5c30425cbf14453b724fca12c1bcab Mon Sep 17 00:00:00 2001 From: Joe Bennett Date: Thu, 14 Jun 2018 11:13:39 +0000 Subject: [PATCH 29/29] #27345 trigger ci retry --- src/Symfony/Component/Lock/Store/MongoDbStore.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Symfony/Component/Lock/Store/MongoDbStore.php b/src/Symfony/Component/Lock/Store/MongoDbStore.php index 25638bfe3b73d..95794e76ee0a7 100644 --- a/src/Symfony/Component/Lock/Store/MongoDbStore.php +++ b/src/Symfony/Component/Lock/Store/MongoDbStore.php @@ -195,6 +195,7 @@ public function putOffExpiration(Key $key, $ttl) ); $key->reduceLifetime($ttl); + try { $this->getCollection()->updateOne($filter, $update, $options); } catch (\MongoDB\Driver\Exception\WriteException $e) {