Skip to content

Commit 2b109ef

Browse files
committed
Implement optimized lock and cache
1 parent d46dcdd commit 2b109ef

File tree

8 files changed

+573
-9
lines changed

8 files changed

+573
-9
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
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

src/Cache/MongoLock.php

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
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+
'owner' => [
56+
'$cond' => [
57+
'if' => $condition,
58+
'then' => $this->owner,
59+
'else' => '$owner',
60+
],
61+
],
62+
'expiration' => [
63+
'$cond' => [
64+
'if' => $condition,
65+
'then' => $this->expiresAt(),
66+
'else' => '$expiration',
67+
],
68+
],
69+
],
70+
],
71+
],
72+
[
73+
'upsert' => true,
74+
'returnDocument' => FindOneAndUpdate::RETURN_DOCUMENT_AFTER,
75+
],
76+
);
77+
78+
if (random_int(1, $this->lottery[1]) <= $this->lottery[0]) {
79+
$this->collection->deleteMany(['expiration' => ['$lte' => $this->currentTime()]]);
80+
}
81+
82+
return $result->owner === $this->owner;
83+
}
84+
85+
/**
86+
* Get the UNIX timestamp indicating when the lock should expire.
87+
*
88+
* @return int
89+
*/
90+
protected function expiresAt()
91+
{
92+
$lockTimeout = $this->seconds > 0 ? $this->seconds : $this->defaultTimeoutInSeconds;
93+
94+
return $this->currentTime() + $lockTimeout;
95+
}
96+
97+
/**
98+
* Release the lock.
99+
*
100+
* @return bool
101+
*/
102+
#[Override]
103+
public function release()
104+
{
105+
$result = $this->collection
106+
->deleteMany([
107+
'key' => $this->name,
108+
'owner' => $this->owner,
109+
]);
110+
111+
return $result->getDeletedCount() > 0;
112+
}
113+
114+
/**
115+
* Releases this lock in disregard of ownership.
116+
*
117+
* @return void
118+
*/
119+
#[Override]
120+
public function forceRelease(): void
121+
{
122+
$this->collection->deleteMany([
123+
'key' => $this->name,
124+
]);
125+
}
126+
127+
/**
128+
* Returns the owner value written into the driver for this lock.
129+
*/
130+
#[Override]
131+
protected function getCurrentOwner(): ?string
132+
{
133+
return $this->collection->findOne([
134+
'key' => $this->name,
135+
'expiration' => ['$gte' => $this->currentTime()],
136+
])?->owner;
137+
}
138+
}

src/Cache/MongoStore.php

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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' => ['$eq' => $key]],
34+
['$set' => ['value' => $value, 'expiration' => $expiration]],
35+
['upsert' => true],
36+
);
37+
38+
return $result->getUpsertedCount() > 0 || $result->getModifiedCount() > 0;
39+
}
40+
41+
public function add($key, $value, $seconds): bool
42+
{
43+
$key = $this->prefix . $key;
44+
$value = $this->serialize($value);
45+
$expiration = $this->getTime() + $seconds;
46+
$collection = $this->table()->raw(null);
47+
assert($collection instanceof Collection);
48+
49+
$result = $collection->updateOne(
50+
['key' => ['$eq' => $key]],
51+
[
52+
[
53+
'$set' => [
54+
'value' => [
55+
'$cond' => [
56+
'if' => ['$lte' => ['$expiration', $this->getTime()]],
57+
'then' => $value,
58+
'else' => '$value',
59+
],
60+
],
61+
'expiration' => [
62+
'$cond' => [
63+
'if' => ['$lte' => ['$expiration', $this->getTime()]],
64+
'then' => $expiration,
65+
'else' => '$expiration',
66+
],
67+
],
68+
],
69+
],
70+
],
71+
['upsert' => true],
72+
);
73+
74+
return $result->getUpsertedCount() > 0 || $result->getModifiedCount() > 0;
75+
}
76+
77+
public function lock($name, $seconds = 0, $owner = null)
78+
{
79+
assert($this->connection instanceof Connection);
80+
81+
return new MongoLock(
82+
($this->lockConnection ?? $this->connection)->getCollection($this->lockTable),
83+
$this->prefix . $name,
84+
$seconds,
85+
$owner,
86+
$this->lockLottery,
87+
$this->defaultLockTimeoutInSeconds,
88+
);
89+
}
90+
}

src/MongoDBServiceProvider.php

Lines changed: 28 additions & 0 deletions
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)