Skip to content

Commit 55c5327

Browse files
committed
Implement optimized lock and cache
1 parent d46dcdd commit 55c5327

8 files changed

+544
-9
lines changed

Diff for: CHANGELOG.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ 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-
* Implement `MongoDB\Laravel\Query\Builder::insertOrIgnore()` to ignore duplicate values
9+
* Implement `MongoDB\Laravel\Query\Builder::insertOrIgnore()` to ignore duplicate values by @GromNaN in [#2877](https://github.com/mongodb/laravel-mongodb/pull/2877)
10+
* Add `mongodb` cache and lock drivers by @GromNaN in [#2877](https://github.com/mongodb/laravel-mongodb/pull/2877)
1011

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

Diff for: src/Cache/MongoLock.php

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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
19+
* @param int $seconds
20+
* @param string|null $owner
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 Collection $collection,
26+
string $name,
27+
int $seconds,
28+
?string $owner = null,
29+
private array $lottery = [2, 100],
30+
private int $defaultTimeoutInSeconds = 86400,
31+
) {
32+
parent::__construct($name, $seconds, $owner);
33+
}
34+
35+
/**
36+
* Attempt to acquire the lock.
37+
*
38+
* @return bool
39+
*/
40+
public function acquire()
41+
{
42+
// The lock can be acquired if: it doesn't exist, it has expired,
43+
// or it is already owned by the same lock instance.
44+
$condition = [
45+
'$or' => [
46+
['$lte' => ['$expiration', $this->currentTime()]],
47+
['$eq' => ['$owner', $this->owner]],
48+
],
49+
];
50+
$result = $this->collection->findOneAndUpdate(
51+
['key' => ['$eq' => $this->name]],
52+
[
53+
[
54+
'$set' => [
55+
'key' => [
56+
'$cond' => [
57+
'if' => $condition,
58+
'then' => $this->name,
59+
'else' => '$key',
60+
],
61+
],
62+
'owner' => [
63+
'$cond' => [
64+
'if' => $condition,
65+
'then' => $this->owner,
66+
'else' => '$owner',
67+
],
68+
],
69+
'expiration' => [
70+
'$cond' => [
71+
'if' => $condition,
72+
'then' => $this->expiresAt(),
73+
'else' => '$expiration',
74+
],
75+
],
76+
],
77+
],
78+
],
79+
[
80+
'upsert' => true,
81+
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
82+
],
83+
);
84+
85+
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
86+
$this->collection->deleteMany(['expiration' => ['$lte' => $this->currentTime()]]);
87+
}
88+
89+
return $result->owner === $this->owner;
90+
}
91+
92+
/**
93+
* Get the UNIX timestamp indicating when the lock should expire.
94+
*
95+
* @return int
96+
*/
97+
protected function expiresAt()
98+
{
99+
$lockTimeout = $this->seconds > 0 ? $this->seconds : $this->defaultTimeoutInSeconds;
100+
101+
return $this->currentTime() + $lockTimeout;
102+
}
103+
104+
/**
105+
* Release the lock.
106+
*
107+
* @return bool
108+
*/
109+
#[Override]
110+
public function release()
111+
{
112+
$result = $this->collection
113+
->deleteMany([
114+
'key' => $this->name,
115+
'owner' => $this->owner,
116+
]);
117+
118+
return $result->getDeletedCount() > 0;
119+
}
120+
121+
/**
122+
* Releases this lock in disregard of ownership.
123+
*
124+
* @return void
125+
*/
126+
#[Override]
127+
public function forceRelease(): void
128+
{
129+
$this->collection->deleteMany([
130+
'key' => $this->name,
131+
]);
132+
}
133+
134+
/**
135+
* Returns the owner value written into the driver for this lock.
136+
*/
137+
#[Override]
138+
protected function getCurrentOwner(): ?string
139+
{
140+
return $this->collection->findOne([
141+
'key' => $this->name,
142+
'expiration' => ['$gte' => $this->currentTime()],
143+
])?->owner;
144+
}
145+
}

Diff for: src/Cache/MongoStore.php

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
namespace MongoDB\Laravel\Cache;
4+
5+
use Illuminate\Cache\DatabaseStore;
6+
use MongoDB\Laravel\Collection;
7+
use MongoDB\Laravel\Connection;
8+
9+
use function assert;
10+
11+
class MongoStore extends DatabaseStore
12+
{
13+
public function __construct(
14+
Connection $connection,
15+
string $table,
16+
string $prefix = '',
17+
string $lockTable = 'cache_locks',
18+
array $lockLottery = [2, 100],
19+
int $defaultLockTimeoutInSeconds = 86400,
20+
) {
21+
parent::__construct($connection, $table, $prefix, $lockTable, $lockLottery, $defaultLockTimeoutInSeconds);
22+
}
23+
24+
public function put($key, $value, $seconds): bool
25+
{
26+
$key = $this->prefix . $key;
27+
$value = $this->serialize($value);
28+
$expiration = $this->getTime() + $seconds;
29+
$collection = $this->table()->raw(null);
30+
assert($collection instanceof Collection);
31+
32+
$result = $collection->updateOne(
33+
['key' => $key],
34+
['$set' => ['value' => $value, 'expiration' => $expiration]],
35+
['upsert' => true],
36+
);
37+
38+
return $result->getUpsertedCount() > 0;
39+
}
40+
41+
public function lock($name, $seconds = 0, $owner = null)
42+
{
43+
assert($this->connection instanceof Connection);
44+
45+
return new MongoLock(
46+
($this->lockConnection ?? $this->connection)->getCollection($this->lockTable),
47+
$this->prefix . $name,
48+
$seconds,
49+
$owner,
50+
$this->lockLottery,
51+
$this->defaultLockTimeoutInSeconds,
52+
);
53+
}
54+
}

Diff for: src/MongoDBServiceProvider.php

+28
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
namespace MongoDB\Laravel;
66

7+
use Illuminate\Cache\CacheManager;
8+
use Illuminate\Cache\Repository;
9+
use Illuminate\Foundation\Application;
710
use Illuminate\Support\ServiceProvider;
11+
use InvalidArgumentException;
12+
use MongoDB\Laravel\Cache\MongoStore;
813
use MongoDB\Laravel\Eloquent\Model;
914
use MongoDB\Laravel\Queue\MongoConnector;
1015

@@ -40,5 +45,28 @@ public function register()
4045
return new MongoConnector($this->app['db']);
4146
});
4247
});
48+
49+
// Add cache store.
50+
$this->app->resolving('cache', function (CacheManager $cache) {
51+
$cache->extend('mongodb', function (Application $app, array $config): Repository {
52+
$connection = $app['db']->connection($config['connection'] ?? null);
53+
54+
$store = new MongoStore(
55+
$connection,
56+
$config['collection'] ?? $config['table'] ?? throw new InvalidArgumentException('Missing "collection" name for MongoDB cache'),
57+
$this->getPrefix($config),
58+
$config['lock_table'] ?? 'cache_locks',
59+
$config['lock_lottery'] ?? [2, 100],
60+
$config['lock_timeout'] ?? 86400,
61+
);
62+
63+
return $this->repository(
64+
$store->setLockConnection(
65+
$app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null),
66+
),
67+
$config,
68+
);
69+
});
70+
});
4371
}
4472
}

0 commit comments

Comments
 (0)