Skip to content

Commit

Permalink
symfony#27345 Added automatic TTL Index enforcement / expired lock cl…
Browse files Browse the repository at this point in the history
…eanup on save based on a probability option
  • Loading branch information
Joe Bennett committed Oct 15, 2018
1 parent 7ff2727 commit 47aa4dd
Showing 1 changed file with 114 additions and 15 deletions.
129 changes: 114 additions & 15 deletions src/Symfony/Component/Lock/Store/MongoDbStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@

namespace Symfony\Component\Lock\Store;

use BadMethodCallException;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Client;
use MongoDB\Collection;
use MongoDB\Driver\Command;
use MongoDB\Driver\Exception\WriteException;
use MongoDB\Driver\Manager;
use MongoDB\Exception\DriverRuntimeException;
use MongoDB\Exception\InvalidArgumentException as MongoInvalidArgumentException;
use MongoDB\Exception\UnsupportedException;
Expand All @@ -40,6 +43,8 @@ class MongoDbStore implements StoreInterface
private $initialTtl;

private $collection;
private $databaseVersion;
private $manager;

/**
* @param Client $mongo
Expand All @@ -49,8 +54,9 @@ class MongoDbStore implements StoreInterface
* @throws InvalidArgumentException if required options are not provided
*
* 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]
* createTtlIndexProbability: Should a TTL Index be created expressed as a probability from 0.0 to 1.0 [default: 0.001]
*
* CAUTION: The locked resource name is indexed in the _id field of the
* lock collection. An indexed field's value in MongoDB can be a maximum
Expand All @@ -62,33 +68,58 @@ 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(int $expireAfterSeconds = 0)
*
* If createTtlIndexProbability is set to a value greater than 0.0
* there will be a chance this store will attempt to create a TTL index on
* self::save(). If however the server version is less than MongoDB 2.2
* indicating TTL Indexes are unsupported, a db.lock.remove() query will
* be used to cleanup old locks instead.
*
* 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/
*/
public function __construct(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->initialTtl = $initialTtl;

$this->options = array_merge(array(
'collection' => 'lock',
'createTtlIndexProbability' => 0.001,
), $options);

$this->initialTtl = $initialTtl;
if (!isset($this->options['database'])) {
throw new InvalidArgumentException(
'You must provide the "database" option for MongoDBStore'
);
}

if ($this->options['createTtlIndexProbability'] < 0.0 || $this->options['createTtlIndexProbability'] > 1.0) {
throw new InvalidArgumentException(sprintf(
'"%s" createTtlIndexProbability must be a float from 0.0 to 1.0, "%f" given.',
__METHOD__,
$this->options['createTtlIndexProbability']
));
}

if ($this->initialTtl <= 0) {
throw new InvalidArgumentException(sprintf(
'%s() expects a strictly positive TTL. Got %d.',
__METHOD__,
$this->initialTtl
));
}
}

/**
* Create a TTL index to automatically remove expired locks.
*
* This should be called once during database setup.
* If the createTtlIndexProbability option is set higher than 0.0
* (defaults to 0.001); this will be called automatically on self::save().
*
* Otherwise; this should be called once during database setup.
*
* Alternatively the TTL index can be created manually:
*
Expand All @@ -99,15 +130,13 @@ public function __construct(Client $mongo, array $options, float $initialTtl = 3
*
* Please note, expires_at is based on the application server. If the
* database time differs; a lock could be cleaned up before it has expired.
* Set a positive expireAfterSeconds to account for any time drift between
* application and database server.
* 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.
*
* 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/
*
* @param int $expireAfterSeconds Number of seconds after expiry to wait before deleting
*
* @return string The name of the created index
*
* @throws UnsupportedException if options are not supported by the selected server
Expand All @@ -116,6 +145,10 @@ public function __construct(Client $mongo, array $options, float $initialTtl = 3
*/
public function createTtlIndex(int $expireAfterSeconds = 0): string
{
if ($this->getDatabaseVersion() < '2.2') {
throw new BadMethodCallException('Database version does not support TTL Indexes');
}

$keys = array(
'expires_at' => 1,
);
Expand Down Expand Up @@ -177,6 +210,37 @@ public function save(Key $key)
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}

if ($this->options['createTtlIndexProbability'] > 0.0
&& (
1.0 === $this->options['createTtlIndexProbability']
|| (random_int(0, PHP_INT_MAX) / PHP_INT_MAX) <= $this->options['createTtlIndexProbability']
)
) {
if ($this->getDatabaseVersion() < '2.2') {
$this->removeExpiredLocks();
} else {
$this->createTtlIndex();
}
}
}

/**
* Remove expired locks from the collection. This is a fallback for MongoDB
* servers prior to version 2.2. For versions 2.2+ self::createTtlIndex is
* called instead based on the createTtlIndexProbability options.
*/
private function removeExpiredLocks()
{
$now = microtime(true);

$filter = array(
'expires_at' => array(
'$lt' => $this->createDateTime($now),
),
);

$this->getCollection()->deleteMany($filter);
}

/**
Expand All @@ -190,6 +254,9 @@ public function waitAndSave(Key $key)
));
}

/**
* @return bool
*/
private function isDuplicateKeyException(
WriteException $e
): bool {
Expand Down Expand Up @@ -284,6 +351,38 @@ public function exists(Key $key): bool
return null !== $doc;
}

/**
* @return Manager
*/
private function getManager(): Manager
{
if (null === $this->manager) {
$this->manager = $this->mongo->getManager();
}

return $this->manager;
}

/**
* @return string
*/
private function getDatabaseVersion(): string
{
if (null === $this->databaseVersion) {
$command = new Command(array(
"buildinfo" => 1,
));
$cursor = $this->getManager()->executeReadCommand(
$this->options['database'],
$command
);
$buildInfo = $cursor->toArray()[0];
$this->databaseVersion = $buildInfo->version;
}

return $this->databaseVersion;
}

/**
* @return Collection
*/
Expand Down

0 comments on commit 47aa4dd

Please sign in to comment.