-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[Lock] Added MongoDBStore #27648
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
[Lock] Added MongoDBStore #27648
Conversation
6c017c4
to
c871857
Compare
Removed composer |
By removing the dependency all the tests are flagged |
f290cc6
to
987548d
Compare
Ah ok, i've reinstated |
Personally, when this is contributed like here, I'm OK with adding this to the component directly, provided the implementation is top quality (double care for reviewers + author). |
Could you please rebase? We prefer not merging PRs when they contain merge commits. |
af48098
to
8692ca3
Compare
304839f
to
0b4abbb
Compare
try { | ||
$this->getCollection()->updateOne($filter, $update, $options); | ||
} catch (WriteException $e) { | ||
throw new LockConflictedException('Failed to acquire 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.
I think you forgot to handle the lock conflicted case like in the the putoffExpiration
function exception handling. This is just wrapping all exceptions as LockConflictedException
.
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.
yeah thinking about this some more, you are correct. I have added handling for duplicate key detection on save which could only be caused by an existing lock owned by someone else (based on the $or
).
$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.
So you are doing an updateOne
instead of insertOne
here because you want to update the expiration time of a resource on same key when called multiple times correct?
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, if for some reason the user doesn't create a TTL Index (auto deleting index). A lock that gets left behind (expired) can be picked up and re-used for a different thread. The (or token equals
) behavior is to allow successive calls to save to just work. For the later, see AbstractStoreTest::testSaveTwice
, this is the intended behavior of all lock stores.
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.
Okay - that makes sense. Can we make the implementation such that if users don't call the ttl
we set a default index which can be overridden with what user specifies? We don't want to leave the possibility of being able to leave documents behind in a collection and let it grow unknowingly.
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.
Please don't get mixed up between:
a lock ttl i.e. public function __construct(Client $mongo, array $options, float $initialTtl = 300.0)
and a MongoDb TTL Index i.e. https://docs.mongodb.com/manual/core/index-ttl/
A MongoDb TTL Index is a database index which causes documents to be DELETED by the DBMS. Adding a MongoDB TTL index is a Database administration task. The MongoDbStore should NOT do this. It does however provide a method MongoDbStore::createTtlIndex
allowing a programmer to automate the creation of this index in their deployment workflow. I personally would never use this method as it should only be called once when the database is created.
The responsibility of creating a MongoDB TTL Index is mentioned both on the constructor's phpdoc of this store and in the symfony documentation: https://github.com/symfony/symfony-docs/pull/9807/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.
Right, I was't confused between the two, sorry for using the wrong terminology there :)
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.
so when you say:
if users don't call the
ttl
we set a default index
Are you talking about us automatically calling MongoDbStore::createTtlIndex
?
Because this won't work on mongodb prior to version 2.2
. Also we can't assume the application servers and DB servers are clock synced. Creating a TTL index when they are out can cause locks to be removed before they expire. In the worst case scenario, a database server could be on a different timezone to an application server.
If the user is running their locks across a stack spanning multiple data centers in different geographic regions they may decide to set a relatively high expireAfterSeconds
. Someone running everything in 1 data center and running NTP might decide to set expireAfterSeconds = 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.
Yes that's what I suggesting to automatically call MongoDbStore::createTtlIndex
. Okay, I am not sure if this is okay or not. Will let the core members of this component and symfony decide on this one :)
Have suggested on the PR how to clean up stale 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.
This has been added as the option gcProbability
and I will update symfony/symfony-docs#9807 to mention increasing $initialTtl / $ttl
to account for clock drift.
$token = $this->getUniqueToken($key); | ||
$now = microtime(true); | ||
|
||
$filter = array( |
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 am not sure why you need the $or
in the filter. Can this blocked be refactored as below for better readability?
$filter = array(
'_id' => (string) $key,
'token' => $token,
'expires_at' => array(
'$lte' => $this->createDateTime($now),
),
);
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.
No;
We might want to pickup a leftover document from a previously expired instantiation of Key
(same resource / _id
) with a different token
.
OR
We can overwrite our instantiation of Key
's lock (AbstractStoreTest::testSaveTwice
).
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.
Okay - thanks for mentioning that. IMO every store should clean up the key as well as the associated data with it on a release or expiration. Because, if a lock is only ever used once, it doesn't make sense to leave it present in the collection forever. I believe all the other stores also do this.
And we should also not override the AbstractStoreTest::testSaveTwice
.
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 also don't understand the design:
We might want to pickup a leftover document from a previous instantiation of Key (same resource / _id) with a different token
How will the new process know that the lock resource is free to be acquired in a document with the key id already exists?
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.
IMO every store should clean up the key as well as the associated data with it on a release or expiration
We do this on the MongoDBStore via the usage of a MongoDB TTL Index (see the phpdoc on the __construct
and also https://github.com/symfony/symfony-docs/pull/9807/files
The implementation however is designed to be resilient in the event someone doesn't create a MongoDb TTL Index. It will begin to recycle documents ONLY if they are expired or they are owned by the current thread (this is why we have an $or
in the query).
e.g.
$k1 = new \Symfony\Component\Lock\Key('my-resource');
$k2 = new \Symfony\Component\Lock\Key('my-resource'); // This has a different "token" to k1
$store->save($k1);
$store->save($k1);
// This is ok, we are re-saving k1 (scenario enforced by `AbstractStoreTest::testSaveTwice`)
$store->save($k2);
// This is NOT ok, k1 has the lock
sleep($lockTtl + 1);
// $k1 has now expired
$store->save($k2);
// This is ok, re-cycling the document created by k1 IF the developer ignored the recommendation to create a MongoDb TTL Index
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.
Okay this is making more sense to me, thanks for the explanation. Let me know if I am understanding this wrong:
In your above case, it is assumed that the client created a TTL index on the collection. There is a possibility that user doesn't explicitly call this. So will you run into the following scenario?
// TTL is not set on the collection
- $store->save($k1); // it somehow creates the document with an expiration time in future, by default it is 300 seconds
- // the client forgets to call release and the thread finishes processing
- // the collection still has `k1` with the original $token because the expiration value has no effect as TTL index was never created on Mongo DB through Database administration or by the client during DB setup once initially
- $store->save($k2); // this should never work because the token associated with `_id` never expires because no TTL Index was created
Or does not setting a TTL Index on the collection mean that, the document itself won't be deleted but the data ($token
in our case) associated with _id
(lock resource
in our case) will get deleted but the document itself remains with the _id
?
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 scenario above does NOT assume a TTL index was created. The PHP will behave the same regardless of if the TTL Index exists. The only difference is if a document is left behind because PHP crashed and didn't get to remove the lock, the lock / document would persist forever (or until the next time another lock was acquired with the same resource id).
Think of a TTL Index like a cron that runs on an SQL database periodically:
DELETE FROM lock WHERE expires_at < (NOW() + :expireAfterSeconds);
It simply cleans up expired locks / documents
Assuming a TTL index is NOT created...
the collection still has
k1
with the original $token because the expiration value has no effect as TTL index was never created on Mongo DB through Database administration or by the client during DB setup once initially
This is true, a document
(a.k.a row
in RDBMS world) will remain in the lock
collection (a.k.a. table
in RDBMS world) indefinitely.
$store->save($k2); // this should never work because the token associated with
_id
never expires because no TTL Index was created
This will work as soon as the expires_at <= $now
($k2 will overwritten into $k1
's expired document), MongoDbStore::save
will:
$filter = array(
'_id' => (string) $key,
'$or' => array(
array(
'token' => $token,
),
array(
'expires_at' => array(
'$lte' => $this->createDateTime($now),
),
),
),
);
$update = array(
'$set' => array(
'_id' => (string) $key,
'token' => $token,
'expires_at' => $this->createDateTime($now + $this->initialTtl),
),
);
$options = array(
'upsert' => true,
);
If however you tried to $store->save($k2);
BEFORE expires_at <= $now
(e.g. before $k1
has expired), the upsert would effectively attempt to INSERT
which would cause a Mongo error E11000 - DuplicateKey
.
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.
Okay makes sense now.
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getStore() |
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.
Can you add return type as StoreInterface
please?
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.
sure
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.
@jderusse since AbstractStoreTest
doesn't yet specify the return type should we go ahead an upgrade all missing return types from Symfony\Component\Lock
? What's the policy on doing this at the moment?
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 suggest only making changes for the stores implemented in this PR. We can open a separate issue/PR to fix in other places.
), | ||
array( | ||
'expires_at' => array( | ||
'$lte' => $this->createDateTime($now), |
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 do you need the
$lte
- why not
$this->createDateTime($now + $this->initialTtl)
instead?
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.
Sorry, I don't really follow... This is a filter looking for locks that have expired.
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 am not familiar with mongodb so just wanted to understand what was happening 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.
UPDATE OR INSERT lock
SET
id = :key,
token = :token,
expires_at = :newExpiry
WHERE
id = :key
AND (
token = :key
OR expires_at <= :now
)
:key = (string)$key;
:token = $this->getUniqueToken($key);
:newExpiry = $this->createDateTime($now + $this->initialTtl);
:now = new \DateTime('now');
Does this explain it? (Assuming UPDATE OR INSERT
is a real thing and it's ACID compliant being 1 statement).
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.
First mongo will attempt to locate an existing document WHERE <conditions>
and if it finds one it will be updated with SET <fields>
.
Otherwise it will attempt to INSERT (id, token, expires_at) VALUES (<fields>)
The INSERT could fail if there's a duplicate key exception on id
.
In MongoDb world _id
is a PRIMARY 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 logic makes more sense now. Thanks for explaining.
/** | ||
* {@inheritdoc} | ||
* | ||
* @throws LockStorageException if failed to save to storage (fallback exception) |
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.
From the documentation, it seems like LockStorageException
should only be used when getting an error trying to manipulate an already existing resource in the store.
/cc - @jderusse in case he has better insight between the usage of LockStorageException
or LockAcquiringException
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 just looking at Lock::acquire
and Lock::refresh
; both have a catch (\Exception $e)
re-throwing as LockAcquiringException
. I have therefore just removed my } catch (Exception $e) {
's in MongoDbStore::save
and MongoDbStore::putOffExpiration
to allow Lock
to maintain exception consistency.
* {@inheritdoc} | ||
* | ||
* @throws LockStorageException if failed to save to storage (fallback exception) | ||
* @throws LockExpiredException when save is called on an expired lock |
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 lock.php
already does this check so I believe this can be removed.
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 have removed it from my implementation, @jderusse do you think we should do the same to all other stores? They all currently seem to check 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.
actually I can't because ExpiringStoreTestTrait::testAbortAfterExpiration
expects the store to check this. I have re-instated it however if Jérémy wants to move responsibility of this check from the Stores up to Lock
we could (in a separate ticket) change all stores and the test.
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.
Okay makes sense. We can take care of this in separate ticket. I believe we will also revisit in future on the store interface to clearly specify contract depending on what kind of store we are implementing.
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.
See #28779
0b4abbb
to
c44b211
Compare
$token = $this->getUniqueToken($key); | ||
$now = microtime(true); | ||
|
||
$filter = array( |
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.
Okay - thanks for mentioning that. IMO every store should clean up the key as well as the associated data with it on a release or expiration. Because, if a lock is only ever used once, it doesn't make sense to leave it present in the collection forever. I believe all the other stores also do this.
And we should also not override the AbstractStoreTest::testSaveTwice
.
* {@inheritdoc} | ||
* | ||
* @throws LockStorageException if failed to save to storage (fallback exception) | ||
* @throws LockExpiredException when save is called on an expired lock |
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.
Okay makes sense. We can take care of this in separate ticket. I believe we will also revisit in future on the store interface to clearly specify contract depending on what kind of store we are implementing.
$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.
Okay - that makes sense. Can we make the implementation such that if users don't call the ttl
we set a default index which can be overridden with what user specifies? We don't want to leave the possibility of being able to leave documents behind in a collection and let it grow unknowingly.
$token = $this->getUniqueToken($key); | ||
$now = microtime(true); | ||
|
||
$filter = array( |
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 also don't understand the design:
We might want to pickup a leftover document from a previous instantiation of Key (same resource / _id) with a different token
How will the new process know that the lock resource is free to be acquired in a document with the key id already exists?
/** | ||
* @return int | ||
*/ | ||
protected function getClockDelay() |
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.
getClockDelay(): int
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.
added
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function exists(Key $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.
exists(Key $key): 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.
added, initially I wasn't sure if I should be doing this kind of stuff since the parent doesn't do it... but it seems to work.
), | ||
array( | ||
'expires_at' => array( | ||
'$lte' => $this->createDateTime($now), |
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 am not familiar with mongodb so just wanted to understand what was happening here. :)
c44b211
to
8a51f9f
Compare
@GaneshChandrasekaran I've responded to all of your comments, can you please resolve every discussion you are happy with? |
8a51f9f
to
6999955
Compare
* | ||
* @author Joe Bennett <joe@assimtech.com> | ||
*/ | ||
class MongoDbStore implements StoreInterface |
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.
Starting a thread here:
Thanks for your patience and answering all my questions @kralos
The only concern that I have left is, this implementation leaves with a possibility of leaving behind stale locks in the DB if an application encounters a crash or some other failure or if the version of mongodb is older than2.2
. This can be bad if you have a high throughput application that requires a lock only once to avoid duplicate processing or something.
I am thinking maybe it should also do something similar toPDOStore
which seems to clean up stale locks at random intervals
https://github.com/symfony/symfony/blob/master/src/Symfony/Component/Lock/Store/PdoStore.php#L143I've had a look at
PdoStore
's implementation and I have some concerns with it:
- The expiry time is using the DBMS's clock for cleanup, what if the DBMS clock differs from the application server? It would be possible to use a timestamp generated from PHP for the
WHERE
clause. Shouldn't we avoid this where possible?- What about clock drift? If there are multiple servers spanning geographic regions should the developer be allowed to specify an acceptable drift (seconds) allowing cleanup to happen slightly after expiry? e.g.
MongoDbStore::createTtlIndex(int $expireAfterSeconds = 0)
The$expireAfterSeconds
allows the developer / sysadmin to specify acceptable delay between the expiry and deletion.I did also have a concern about people who run servers on different timezones however I'm not sure if we care about these people... :-p
This is a good question. Looks like @jderusse is solving for the problem by expecting clients to give a higher TTL from the beginning and it also assumes that the client and server locks are synchronized. I don't particularly see any problem with you generating a timestamp from PHP side to solve this problem.
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.
Assuming we are going to suggest a higher TTL to account for clock drift I have implemented automatic mongodb TTL Index creation. I did this using a probability modifier like php session / PDO store however due to the incompatibility with MongoDB prior to 2.2 I have also implemented mongodb version detection and reverted to a delete expired documents for older servers. I have still allowed for a developer to set their probability to 0.0
(effectively disabling the automatic TTL creation) in case they wish to manually manage the creation of their TTL Index. Let me know what you think?
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.
This seems fine to me. Thanks!
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 of PdoStore always use the SGDB internal clock. This would avoid collision when 2 servers are not time synced. I suggest to the the same here.
From an other hand, the documentation recommend to take clock drift into account when diffining the TTL (https://github.com/symfony/symfony-docs/pull/9875/files#diff-1dc6462c0bc00fcdc7cba99d2f13e400R499)
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 equivalent of SQL: NOW()
wasn't introduced until MongoDB 2.6 for UPDATE
: https://jira.mongodb.org/browse/SERVER-10911
There is no equivalent of DATE_ADD()
for an update.
I looked into doing it this way first because I have concerns about PHP $ttl
expiry not being strictly adhered to (due to clock drift) however it's not possible in MongoDB. In light of this I think it's down to setting a higher $initialTtl / $ttl
if a developer needs to account for high drift.
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.
@jderusse is the current implementation ok with you since it's not possible to do all time from the DB's clock? I'll be sure to document this on symfony/symfony-docs#9807
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.
Given mongoDb don't provide methods to manipulate dates on upsert. I'm fine with using the APP date, and warning the user in the documentation.
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.
Looks good to me :)
Pinging @jderusse for his review from here.
* | ||
* @author Joe Bennett <joe@assimtech.com> | ||
*/ | ||
class MongoDbStore implements StoreInterface |
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.
This seems fine to me. Thanks!
/** | ||
* @return bool | ||
*/ | ||
private function isDuplicateKeyException( |
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.
these can be on single line
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.
updated
* Create a TTL index to automatically remove expired locks. | ||
* | ||
* If the createTtlIndexProbability option is set higher than 0.0 | ||
* (defaults to 0.001); this will be called automatically on self::save(). |
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.
there will be a chance to be called...
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.
updated
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.
Few comments, the big issue is the use of application's clock instead of Database's one
* Options: | ||
* 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] |
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.
This option is used both for creating index and removing manually expired locks (depends on the version).
What's about gcProbablity
like in PdoStore?
Moreover, I wonder if ti make send to create several time the TTL index...
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 changed to gcProbability.
Re creating the TTL Index several times, in Mongo most actions are usually idempotent. There used to be a method called ensureIndex
however it was deprecated in favour of just calling createIndex
see: https://docs.mongodb.com/manual/reference/method/db.collection.ensureIndex/
try { | ||
$store->waitAndSave($key); | ||
$this->fail('The store shouldn\'t support waitAndSave'); | ||
} catch (NotSupportedException $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.
use https://phpunit.de/manual/6.5/en/appendixes.annotations.html#appendixes.annotations.expectedException to assert that an exception is thorwn
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.
updated
'_id' => (string) $key, | ||
'token' => $this->getUniqueToken($key), | ||
'expires_at' => 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.
You have to define whether equals
means locked or available. This statement conflict with $lte
in save
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
'_id' => (string) $key, | ||
'token' => $this->getUniqueToken($key), | ||
'expires_at' => array( | ||
'$gte' => $this->createDateTime($now), |
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.
should be token = $token OR expires_at <= now()
Look at PdoStore for consistency
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 have consolidated save
and putOffExpiration
into a common method since they are now the same DB statement
* | ||
* @author Joe Bennett <joe@assimtech.com> | ||
*/ | ||
class MongoDbStore implements StoreInterface |
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 of PdoStore always use the SGDB internal clock. This would avoid collision when 2 servers are not time synced. I suggest to the the same here.
From an other hand, the documentation recommend to take clock drift into account when diffining the TTL (https://github.com/symfony/symfony-docs/pull/9875/files#diff-1dc6462c0bc00fcdc7cba99d2f13e400R499)
*/ | ||
public function waitAndSave(Key $key) | ||
{ | ||
throw new NotSupportedException(sprintf( |
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.
use a single line for consistency
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
/** | ||
* @return bool | ||
*/ | ||
private function isDuplicateKeyException( |
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.
Move after public methods
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.
moved
faa6e1c
to
692155e
Compare
@jderusse How can we move this one forward? |
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.
Few minor comment.
Thank's for the reminder @fabpot
* | ||
* @see http://docs.mongodb.org/manual/tutorial/expire-data/ | ||
* | ||
* @return string The name of the created index |
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.
no needed to return it IMHO
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.
removed the return
/** | ||
* @return Manager | ||
*/ | ||
private function getManager(): Manager |
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.
Can be removed. This method is called once in the class: inside the getDatabaseVersion
inside a if cached missed
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.
removed
* @throws MongoInvalidArgumentException for parameter/option parsing errors | ||
* @throws DriverRuntimeException for other driver errors (e.g. connection errors) | ||
*/ | ||
public function createTtlIndex(int $expireAfterSeconds = 0): string |
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.
This method should be either
- public: must be called by user (and disabled the GC logic when version >= 2.2)
- private: automatically called by GC
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 intent was to make default behaviour to create a TTL Index with a gcProbablity
default of 0.001
. This is to prevent orphan locks for anyone who uses this store without reading docs or changing any config.
If someone doesn't want this to happen (and they read the docs), I left the createTtlIndex
method public
allowing someone to configure gcProbablity = 0.0
and hook createTtlIndex
in their deployment or database set up scripts.
See constructor docs:
* .... If you prefer to create your TTL Index
* manually you can set gcProbablity to 0.0 and optionally leverage
* self::createTtlIndex(int $expireAfterSeconds = 0).
It's my opinion setting TTL index automatically is dangerous (if someone has differing clocks between DBMS and App) as well as an unnecessary expense. I would like to make a recommendation in symfony/symfony-docs#9807 to set gcProbablity = 0.0
and manually create the TTLIndex.
If you disagree this this approach, which way do you think is the better way to go?
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 forgot the part about GC=0 and creting the index manually. Sound's good to me
'expireAfterSeconds' => $expireAfterSeconds, | ||
); | ||
|
||
return $this->getCollection()->createIndex($keys, $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.
What if the method is called several time (like you do in GC)? any impact on existing data or performances?
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.
createIndex
is idempotent in MongoDb https://docs.mongodb.com/manual/indexes/#create-an-index
The db.collection.createIndex method only creates an index if an index of the same specification does not already exist.
There used to be an alias called ensureIndex
which was deprecated in favour of createIndex
(IMO they should have deprecated createIndex
).
https://docs.mongodb.com/manual/reference/method/db.collection.ensureIndex/
* | ||
* @author Joe Bennett <joe@assimtech.com> | ||
*/ | ||
class MongoDbStore implements StoreInterface |
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.
Given mongoDb don't provide methods to manipulate dates on upsert. I'm fine with using the APP date, and warning the user in the documentation.
a017cea
to
1f4d601
Compare
Tests are broken. |
02e826e
to
9c04639
Compare
@fabpot
|
Thank you @kralos. |
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
…ennett) This PR was merged into the master branch. Discussion ---------- #27345 Added MongoDbStore Lock Store documentation Added support for MongoDbStore Lock Store, See symfony/symfony#27648 Commits ------- a641baa #27345 Added docs for Symfony\Component\Lock\Store\MongoDbStore
This PR was submitted for the 4.4 branch but it was squashed and merged into the 5.1-dev branch instead (closes #31889). Discussion ---------- [Lock] add mongodb store | Q | A | ------------- | --- | Branch? | 4.4 | 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 | Original Doc PR | symfony/symfony-docs#9807 | Remove from 4.3 Doc PR | symfony/symfony-docs#11686 | Add to 4.4 Doc PR | symfony/symfony-docs#11735 Looks like I messed up `kralos:27345-lock-mongodb` with a force push (trying to fix ci issues) right before it was merged to `master` (`4.3.0`). see #27648 **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'); ``` Commits ------- a6bfa59 [Lock] add mongodb store
ext-mongodb
andmongodb/mongodb
to test)Testing caveat
In order to test this, the test environment needs
ext-mongodb
andmongodb/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
This is a squashed pull request of #27346