-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Lock] Added Symfony/Component/Lock/Store/MongoDbStore #27346
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@nicolas-grekas what's the deal with adding |
No need for the ext on appveyor. Just skip tests there. An |
travis env has ext-mongodb but appveyor doesn't since MongoDbSessionHandler does the same i've stripped the dev requirement for mongodb/mongodb
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not familiar with MongoDb, I'm OK with the current implementation (with few comments),
BUT I've many concerns about the reliability of MongoDb. It's wellknown for not beeing ACID, whereas Atomicity is a requirements. What's about a MongoDb cluster configured with sharding and replication?
* 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] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is the purpose of this field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed it. I found it useful to debug / see when a lock was acquired. Probably not that useful in operation.
* 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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the expiration date is computed by the application, then you should warn the developer that a clock drift between the app server and the database server may delete the lock to early
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a comment to the phpdoc and symfony-docs
. I also recommended setting the expireAfterSeconds
to a value greater than 0. The actual expiry is already handled by the application (not the DBMS's clocks). I have updated my example to 60 seconds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO this safety window should be a constructor parameter. Because the same issue exist on the save
method when 2 clients are not time synchronized
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently the recommendation is to manually create the TTL index. If I were to implement this as a constructor parameter in code we would be creating / updating the index on every execution. I'm not sure this is a good idea.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would simply set the TTL to 0, and append this "clockDrift" parameter to the expiredAt (expiredAt = now + lockTTL + clockDrift). It would both solve the purge issue by Mongo and the race condition by 2 clients.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved clock discrepancy handling to a constructor option called drift
instead of relying on a TTL index delay.
$expiry = $this->createDateTime(microtime(true) + $this->initialTtl); | ||
|
||
$filter = array( | ||
$this->options['resource_field'] => (string) $key, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a restriction on key length or key format?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, 1024 bytes including structure overhead (e.g. int / string) I've added this to symfony-docs
under the Reliability section and to the phpdoc
$update = array( | ||
'$set' => array( | ||
$this->options['resource_field'] => (string) $key, | ||
$this->options['token_field'] => $this->getToken($key), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the method $this->getToken($key)
is already called at line 108
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fixed
|
||
$key->reduceLifetime($this->initialTtl); | ||
try { | ||
$this->getCollection()->updateOne($filter, $update, $options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it atomic? on every version of mongodb? What's about réplication propagation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MongoDB is atomic on a singular statement / document only. Hence the use of a update with filter (1 statement). MongoDB ^4.0
introduces multi statement transactions however this implementation does not rely on that. It should be atomic on all versions. Replication propagation can be controlled with a statement level options called a write concern. I'm adding support for that now. By default, mongo will read and write only to the PRIMARY
node in a replica set (this is equivalent of reading and writing only to the master of a replicated RDBMS with replication slaves).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With regards to writeConcern
, readConcern
and readPreference
I've opted to leave this to the collection fallback placing the onus on the DBA to setup the collection with alternate settings if desired. The default when left unspecified is to read / write ONLY to the PRIMARY of a replica set. This is safe for the use of locks (it's like only reading / writing to a master in an RDBMS) however an alternate strategy could be left to the DBA depending on the number of nodes they have.
I have added some documentation regarding this decision to symfony-docs
* db.lock.update( | ||
* { | ||
* _id: "test", | ||
* expires_at: { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
whats i the purpose of this doc block (which is not up to date with the current implementation BTW)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was more to help with seeing how the code translated to a MongoDB statement when I was supporting multiple drivers. I have since stripped the multiple driver support (Fabian said don't support PHP ^5
/ symfony ^3
so I dropped it). I've now removed these phpdocs
$this->options['resource_field'] => (string) $key, | ||
$this->options['token_field'] => $this->getToken($key), | ||
$this->options['expiry_field'] => array( | ||
'$gte' => $this->createDateTime(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather define a now
variable at the beggining of the method, then use this variable as a reference for every computes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, I've fixed this
$key->reduceLifetime($ttl); | ||
try { | ||
$this->getCollection()->updateOne($filter, $update, $options); | ||
} catch (\MongoDB\Driver\Exception\BulkWriteException $e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
improved exception handling as mentioned above
try { | ||
$result = $this->getCollection()->deleteOne($filter); | ||
} catch (\MongoDB\Driver\Exception\BulkWriteException $e) { | ||
throw new LockConflictedException('Failed to delete lock', 0, $e); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deletion does not throw ConflictException
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed
* 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The current implementation does not purge expired locks. I'd rather use the verb MUST BE used,
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed
…figurable fields
@jderusse should be good for a second look. Thanks for you feedback so far, really helpful. |
* 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why $initialTtl
is a constructor argument, whereas every other options are passed into $options
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll move it to $options
.
I've been thinking it would be simpler to remove the drift
option in favour of asking the developer set a slightly higher lockTTL. This would both simplify the implementation and probably make more sense to the developer rather than trying to explain why drift is a separate option. I would have to document the danger of clock drift against this Store. What are your thoughts?
* | ||
* A TTL index MUST BE used on MongoDB 2.2+ to automatically clean up expired locks. | ||
* | ||
* db.lock.ensureIndex( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suggest to provide a public function createCollection
like in https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Cache/Traits/PdoTrait.php#L82 or https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php#L209 or https://github.com/symfony/symfony/pull/27456/files
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added a method to createTTLIndex()
. There's no need in MongoDB to create the collection, they're created on the fly by the database.
* | ||
* @see http://docs.mongodb.org/manual/tutorial/expire-data/ | ||
*/ | ||
public function createTTLIndex(): bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return false
means a failure or "nothing updated"?
If it's a failure, I suggest to throw an exception instead.
What's about documenting the return?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My bad, I actually dug into this further and my return type bool
was incorrect. I was reading the legacy driver documentation. The createIndex
call actually does throw an exception if it fails. I have quickly tested this to verify and updated the phpdoc accordingly.
https://docs.mongodb.com/php-library/v1.3/reference/method/MongoDBCollection-createIndex/
|
||
$this->options = array_merge(array( | ||
'collection' => 'lock', | ||
'initialTtl' => 300.0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I really wonder if options for lock should be merge with options for database connection?
See PdoTrait from cache components.
What is the opinion of other contributors?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I originally had initialTtl
as a separate constructor parameter, I brought it into $options
when you suggested drift
should be.
I personally think it belongs outside of $options
.
See also: https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Cache/Adapter/PdoAdapter.php
private $collection; | ||
|
||
/** | ||
* @param \MongoDB\Client $mongo |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a way to pass a dns for lazy connection? See PdoTrait from cache components?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A MongoDB\Client
instance is already lazy. I can turn off my database and still:
$client = new \MongoDB\Client();
$store = new \Symfony\Component\Lock\Store\MongoDbStore($client, [
'database' => 'test',
]);
only if I add something like:
$store->createTTLIndex();
does it throw an exception.
@kralos would you mind squashing all commits in one please? |
Why did you close? |
Oh, continued in #27648 thanks :) |
Sorry, I broke this Pull request when squashing... has to open a new one. |
This PR was merged into the 4.3-dev branch. Discussion ---------- [Lock] Added MongoDBStore | Q | A | ------------- | --- | Branch? | master | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes (requires `ext-mongodb` and `mongodb/mongodb` to test) | Fixed tickets | #27345 | License | MIT | Doc PR | symfony/symfony-docs#9807 **Testing caveat** In order to test this, the test environment needs `ext-mongodb` and `mongodb/mongodb`. I have both written the test and tested `Symfony\Component\Lock\Store\MongoDbStore` and it does pass in an environment with `ext-mongodb` and `mongodb/mongodb`. **Description** We should support Semaphore Locks with a MongoDB back end to allow those that already use MongoDB as a distributed storage engine. Symfony already partially supports MongoDB for session storage: `Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler` **Example** ```php $client = new MongoDb\Client(); $store = new Symfony\Component\Lock\Store\MongoDbStore( $client array( 'database' => 'my-app', ) ); $lockFactory = new Symfony\Component\Lock\Factory($store); $lock = $lockFactory->createLock('my-resource'); ``` This is a squashed pull request of #27346 Commits ------- 9c04639 #27345 Added Lock/Store/MongoDbStore
ext-mongodb
andmongodb/mongodb
to test)Testing caveat
In order to test this, the test environment needs
ext-mongodb
andmongodb/mongodb
. Travis hasext-mongodb
but Appveyor doesn't meaning I can'trequire-dev
mongodb/mongodb
. I had a look atSymfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler
since it poses the same issue and neithersymfony/symfony/src/Symfony/Component/HttpFoundation/composer.json
norsymfony/symfony/composer.json
require-dev
mongodb/mongodb
. They both skip mongo based tests in the ci environments. I have therefore chosen to omitrequire-dev
mongodb/mongodb
.I have both written the test and tested
Symfony\Component\Lock\Store\MongoDbStore
and it does pass in an environment withext-mongodb
andmongodb/mongodb
.Description
We should support Semaphore Locks with a MongoDB back end to allow those that already use MongoDB as a distributed storage engine.
Symfony already partially supports MongoDB for session storage:
Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler
Example