Skip to content

Commit 4ee950c

Browse files
committed
PHPORM-99 Implement optimized lock and cache
1 parent 8355c30 commit 4ee950c

8 files changed

+803
-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

+3-1
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@
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": {
36+
"laravel/framework": "10.*",
3537
"mongodb/builder": "^0.2",
3638
"phpunit/phpunit": "^10.3",
3739
"orchestra/testbench": "^8.0|^9.0",

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;
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)