Skip to content

PHPORM-99 Enable TTL index to auto-purge of expired cache and lock items #2891

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 7 commits into from
Apr 23, 2024

Conversation

GromNaN
Copy link
Member

@GromNaN GromNaN commented Apr 22, 2024

Fix PHPORM-99
Complement to #2877

By using an UTCDateTime for the expiration field of cache and lock collections, we can create a TTL index.

// Create TTL index on cache collection
Cache::driver('mongodb')->createTTLIndex();

// Create TTL index on lock collection
Cache::driver('mongodb')->lock('')->createTTLIndex();

Once TTL index have been created (using a migration), the lock_lottery value can be set to [0, 0] in order to disable random pruning of expired locks.

Also, using a UTCDateTime is more precise than the int timestamp in seconds. So we can use updateOne as discussed here.

@GromNaN GromNaN added this to the 4.3 milestone Apr 22, 2024
@GromNaN GromNaN requested review from jmikola and alcaeus April 22, 2024 09:58
@GromNaN GromNaN requested a review from a team as a code owner April 22, 2024 09:58
@GromNaN GromNaN force-pushed the PHPORM-99-ttl branch 2 times, most recently from 665930a to aa7bff1 Compare April 22, 2024 11:29
Copy link
Member

@alcaeus alcaeus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Not sure about what prompted the change for the return value of the acquire method, but it looks like it achieves the same result.

@GromNaN
Copy link
Member Author

GromNaN commented Apr 22, 2024

LGTM. Not sure about what prompted the change for the return value of the acquire method, but it looks like it achieves the same result.

I think it saves the server from having to return the modified document, which means fewer operations. It's probably negligible, but in any case unnecessary.

@alcaeus
Copy link
Member

alcaeus commented Apr 22, 2024

I think it saves the server from having to return the modified document, which means fewer operations. It's probably negligible, but in any case unnecessary.

I'm not an expert, but IIRC update does have a few advantages over findAndModify, good call on that!

@@ -107,6 +107,17 @@ public function forceRelease(): void
]);
}

/** Creates a TTL index that automatically deletes expired objects. */
public function createTTLIndex(): void
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally didn't suggest using a TTL index in #2877 because it seemed redundant with the $lottery constructor argument that remained in place.

Given that these are just public methods on the MongoLock and MongoStore classes, how do you expect users to interact with them? My understanding is that both classes would typically be used while handling a request, and this method seems more like schema setup. It makes sense for MongoDB, of course, but doesn't fit very well into whatever common API Laravel intended for these classes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an additional feature that should be documented as I have described. The methods will probably be called in a migration. I think that for applications with a significant volume of traffic, using TTL indexes will have a positive impact on performance without having to search for the correct lottery value.

$lockTimeout = $this->seconds > 0 ? $this->seconds : $this->defaultTimeoutInSeconds;

return $this->currentTime() + $lockTimeout;
return new UTCDateTime(Carbon::now()->addSeconds($additionalSeconds));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What allows UTCDateTime to be constructed from a Carbon instance? Is this relying on Carbon's string representation?

I looked at the Addition and Subtraction docs for Carbon and the strings in the examples don't look correct.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Carbon class extends DateTime, and the UTCDateTime constructor accepts any DateTimeInterface. There is no string transformation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I missed Carbon's inheritance. SGTM!

$result = $this->collection->findOneAndUpdate(
[
'_id' => $this->prefix . $key,
'expiration' => ['$gte' => $this->currentTime()],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By removing this criteria and the preceding call to forgetIfExpired(), you're potentially incrementing an expired document. Is that a concern?

Since the TTL index is optional, I wouldn't have expected this to change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed the previous forgetIfExpired in order to avoid useless command. A nominal usage must be a single command.

Even if the expired item is incremented here, it is deleted just after.

@@ -74,11 +74,11 @@ public function acquire(): bool
],
);

if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
$this->collection->deleteMany(['expiration' => ['$lte' => $this->currentTime()]]);
if (! empty($this->lottery[0]) && random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does anything enforce that $this->lottery is a tuple? This only checks that the first element exists and is non-empty, but there is no check for a second element or both being integers.

Perhaps that should be done in the constructor.

I assume you intentionally decided not to make $lottery nullable as a means to disable it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seemed to me that the "zero chances on whatever" value was explicit enough to say that we wanted to disable this mechanism.

}

return $result['owner'] === $this->owner;
return $result->getModifiedCount() > 0 || $result->getUpsertedCount() > 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't this logic problematic in the original PR? Are you now relying on the UTCDateTime always changing due to sub-second precision?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem with the previous logic. I said to myself that it was impossible to have the same lock with the same owner acquired in the same microsecond. But I've reverted to that, so you don't have to worry.

*/
public function __construct(
private readonly Collection $collection,
string $name,
int $seconds,
?string $owner = null,
private readonly array $lottery = [2, 100],
private readonly int $defaultTimeoutInSeconds = 86400,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed entirely? It looks like it previously served a purpose if $seconds was zero. Now, specifying zero for $seconds would cause locks to immediately expire.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since $defaultTimeoutInSeconds was only a default value, I moved the processing to MongoStore::lock. It is not possible to disable lock expiration.

Copy link
Member

@jmikola jmikola left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I trust there will be a separate PR for documentation (since this was a follow-up on an original PR).

@GromNaN GromNaN merged commit 3de2876 into mongodb:4.3 Apr 23, 2024
23 checks passed
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants