Skip to content

Commit d0978a8

Browse files
authored
PHPORM-99 Implement optimized lock and cache (#2877)
Fix fo PHPORM-99 In theory, we can use DatabaseStore and DatabaseLock. But various issues prove the changing nature of their implementation, based on new features in the query builder, make this feature unstable for MongoDB users. fix #2718, fix #2609 By introducing dedicated drivers, we can optimize the implementation to use the mongodb library directly instead of the subset of features provided by Laravel query builder. Usage: # config/cache.php return [ 'stores' => [ 'mongodb' => [ 'driver' => 'mongodb', 'connection' => 'mongodb', 'collection' => 'cache', 'lock_connection' => 'mongodb', 'lock_collection' => 'cache_locks', 'lock_lottery' => [2, 100], 'lock_timeout' => '86400', ] ] ] Cache: // Store any value into the cache. The value is serialized in MongoDB Cache::set('foo', [1, 2, 3]); // Read the value dump(Cache::get('foo')); // Clear the cache Cache::flush(); Lock: // Get an unique lock. It's very important to keep this object in memory // so that the lock can be released. $lock = Cache::lock('foo'); $lock->block(10); // Wait 10 seconds before throwing an exception if the lock isn't released // Any time-consuming task sleep(5); // Release the lock $lock->release();
1 parent 6b9511f commit d0978a8

8 files changed

+795
-1
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file.
66
* New aggregation pipeline builder by @GromNaN in [#2738](https://github.com/mongodb/laravel-mongodb/pull/2738)
77
* Drop support for Composer 1.x by @GromNaN in [#2784](https://github.com/mongodb/laravel-mongodb/pull/2784)
88
* Fix `artisan query:retry` command by @GromNaN in [#2838](https://github.com/mongodb/laravel-mongodb/pull/2838)
9+
* Add `mongodb` cache and lock drivers by @GromNaN in [#2877](https://github.com/mongodb/laravel-mongodb/pull/2877)
910

1011
## [4.2.0] - 2024-03-14
1112

composer.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@
2525
"php": "^8.1",
2626
"ext-mongodb": "^1.15",
2727
"composer-runtime-api": "^2.0.0",
28-
"illuminate/support": "^10.0|^11",
28+
"illuminate/cache": "^10.36|^11",
2929
"illuminate/container": "^10.0|^11",
3030
"illuminate/database": "^10.30|^11",
3131
"illuminate/events": "^10.0|^11",
32+
"illuminate/support": "^10.0|^11",
3233
"mongodb/mongodb": "^1.15"
3334
},
3435
"require-dev": {

src/Cache/MongoLock.php

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
namespace MongoDB\Laravel\Cache;
4+
5+
use Illuminate\Cache\Lock;
6+
use MongoDB\Laravel\Collection;
7+
use MongoDB\Operation\FindOneAndUpdate;
8+
use Override;
9+
10+
use function random_int;
11+
12+
final class MongoLock extends Lock
13+
{
14+
/**
15+
* Create a new lock instance.
16+
*
17+
* @param Collection $collection The MongoDB collection
18+
* @param string $name Name of the lock
19+
* @param int $seconds Time-to-live of the lock in seconds
20+
* @param string|null $owner A unique string that identifies the owner. Random if not set
21+
* @param array $lottery The prune probability odds
22+
* @param int $defaultTimeoutInSeconds The default number of seconds that a lock should be held
23+
*/
24+
public function __construct(
25+
private readonly Collection $collection,
26+
string $name,
27+
int $seconds,
28+
?string $owner = null,
29+
private readonly array $lottery = [2, 100],
30+
private readonly int $defaultTimeoutInSeconds = 86400,
31+
) {
32+
parent::__construct($name, $seconds, $owner);
33+
}
34+
35+
/**
36+
* Attempt to acquire the lock.
37+
*/
38+
public function acquire(): bool
39+
{
40+
// The lock can be acquired if: it doesn't exist, it has expired,
41+
// or it is already owned by the same lock instance.
42+
$isExpiredOrAlreadyOwned = [
43+
'$or' => [
44+
['$lte' => ['$expiration', $this->currentTime()]],
45+
['$eq' => ['$owner', $this->owner]],
46+
],
47+
];
48+
$result = $this->collection->findOneAndUpdate(
49+
['_id' => $this->name],
50+
[
51+
[
52+
'$set' => [
53+
'owner' => [
54+
'$cond' => [
55+
'if' => $isExpiredOrAlreadyOwned,
56+
'then' => $this->owner,
57+
'else' => '$owner',
58+
],
59+
],
60+
'expiration' => [
61+
'$cond' => [
62+
'if' => $isExpiredOrAlreadyOwned,
63+
'then' => $this->expiresAt(),
64+
'else' => '$expiration',
65+
],
66+
],
67+
],
68+
],
69+
],
70+
[
71+
'upsert' => true,
72+
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
73+
'projection' => ['owner' => 1],
74+
],
75+
);
76+
77+
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
78+
$this->collection->deleteMany(['expiration' => ['$lte' => $this->currentTime()]]);
79+
}
80+
81+
return $result['owner'] === $this->owner;
82+
}
83+
84+
/**
85+
* Release the lock.
86+
*/
87+
#[Override]
88+
public function release(): bool
89+
{
90+
$result = $this->collection
91+
->deleteOne([
92+
'_id' => $this->name,
93+
'owner' => $this->owner,
94+
]);
95+
96+
return $result->getDeletedCount() > 0;
97+
}
98+
99+
/**
100+
* Releases this lock in disregard of ownership.
101+
*/
102+
#[Override]
103+
public function forceRelease(): void
104+
{
105+
$this->collection->deleteOne([
106+
'_id' => $this->name,
107+
]);
108+
}
109+
110+
/**
111+
* Returns the owner value written into the driver for this lock.
112+
*/
113+
#[Override]
114+
protected function getCurrentOwner(): ?string
115+
{
116+
return $this->collection->findOne(
117+
[
118+
'_id' => $this->name,
119+
'expiration' => ['$gte' => $this->currentTime()],
120+
],
121+
['projection' => ['owner' => 1]],
122+
)['owner'] ?? null;
123+
}
124+
125+
/**
126+
* Get the UNIX timestamp indicating when the lock should expire.
127+
*/
128+
private function expiresAt(): int
129+
{
130+
$lockTimeout = $this->seconds > 0 ? $this->seconds : $this->defaultTimeoutInSeconds;
131+
132+
return $this->currentTime() + $lockTimeout;
133+
}
134+
}

0 commit comments

Comments
 (0)