Skip to content

Commit 60afbf0

Browse files
committed
feat: add support for custom lock manager instance
1 parent 6db5883 commit 60afbf0

15 files changed

Lines changed: 171 additions & 22 deletions

File tree

docs/content/docs/options.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,32 @@ The maximum amount of time (in milliseconds) that the in-memory lock for [stampe
121121

122122
This is usually not needed, but can provide an extra layer of protection against theoretical deadlocks.
123123

124+
### `lockManager`
125+
126+
Default: built-in in-memory lock manager.
127+
128+
Levels: `global`
129+
130+
A custom lock manager instance.
131+
132+
This lock manager is used for stampede protection and file driver writes.
133+
134+
`LockManager` must return a lock compatible with `MutexInterface` from `async-mutex`. Bentocache exports `LockHandle` and `LockReleaser` as aliases for `MutexInterface` and `MutexInterface.Releaser`.
135+
136+
```ts
137+
import { Mutex, withTimeout } from 'async-mutex'
138+
139+
const lockManager = {
140+
getOrCreateForKey: (key, timeout) => withTimeout(new Mutex(), timeout ?? Infinity),
141+
release: (key, releaser) => releaser(),
142+
}
143+
144+
const bento = new BentoCache({
145+
lockManager,
146+
// ...
147+
})
148+
```
149+
124150
### `onFactoryError`
125151

126152
Default: `undefined`

docs/content/docs/stampede_protection.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,11 @@ Well, in truth, this is not really a problem. Indeed, there will be more than on
5454
Given the same scenario with the 10k users. Imagine that your application is running in cluster mode with PM2 and you have 10 instances of your app. Also, imagine that the 10k requests are distributed equally across the 10 instances.
5555

5656
This results in 1k requests per instance. And so, it will lead to **10 queries to the database instead of 10k**, with the help of our protection.
57+
58+
## Shared locks
59+
60+
BentoCache allows you to use custom lock manager so you can implement **shared** locks .
61+
62+
For example, you can implement lock manager which uses patterns like [Redis distributed locks](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/) where locks are created in L2 storage and shared across all application instances and don’t have process instance limitations.
63+
64+
In multi-instance applications scenario, this will lead to **1 query to database instead of 10k**.

packages/bentocache/src/bento_cache.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@ export class BentoCache<KnownCaches extends Record<string, BentoStore>> implemen
6969
l1Driver: entry.l1?.factory({
7070
prefix: driverItemOptions.prefix,
7171
logger: driverItemOptions.logger,
72+
lockManager: driverItemOptions.lockManager,
7273
...entry.l1.options,
7374
}),
7475
l2Driver: entry.l2?.factory({
7576
prefix: driverItemOptions.prefix,
7677
logger: driverItemOptions.logger,
78+
lockManager: driverItemOptions.lockManager,
7779
...entry.l2.options,
7880
}),
7981
busDriver: entry.bus?.factory(entry.bus?.options),

packages/bentocache/src/bento_cache_options.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,17 @@ import { ms } from '@julr/utils/string/ms'
33
import { noopLogger } from '@julr/utils/logger'
44

55
import { Logger } from './logger.js'
6+
import { Locks } from './cache/locks.js'
67
import { resolveTtl } from './helpers.js'
78
import type { FactoryError } from './errors.js'
89
import { JsonSerializer } from './serializers/json.js'
9-
import type { CacheSerializer, Duration, Emitter, RawBentoCacheOptions } from './types/main.js'
10+
import type {
11+
CacheSerializer,
12+
Duration,
13+
Emitter,
14+
LockManager,
15+
RawBentoCacheOptions,
16+
} from './types/main.js'
1017

1118
const defaultSerializer = new JsonSerializer()
1219

@@ -68,6 +75,11 @@ export class BentoCacheOptions {
6875
*/
6976
lockTimeout?: Duration = null
7077

78+
/**
79+
* The lock manager used throughout the library
80+
*/
81+
lockManager: LockManager
82+
7183
/**
7284
* Duration for the circuit breaker to stay open
7385
* if l2 cache fails
@@ -90,6 +102,7 @@ export class BentoCacheOptions {
90102
this.hardTimeout = this.#options.hardTimeout
91103
this.suppressL2Errors = this.#options.suppressL2Errors
92104
this.lockTimeout = this.#options.lockTimeout
105+
this.lockManager = this.#options.lockManager ?? new Locks()
93106
this.grace = this.#options.grace!
94107
this.graceBackoff = this.#options.graceBackoff!
95108

packages/bentocache/src/cache/factory_runner.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import pTimeout from 'p-timeout'
22
import { tryAsync } from '@julr/utils/functions'
3-
import type { MutexInterface } from 'async-mutex'
43

54
import { errors } from '../errors.js'
6-
import type { Locks } from './locks.js'
75
import type { CacheStack } from './cache_stack.js'
86
import { cacheOperation } from '../tracing_channels.js'
97
import type { GetSetFactory } from '../types/helpers.js'
8+
import type { LockManager, LockReleaser } from '../types/main.js'
109
import type { GetCacheValueReturn } from '../types/internals/index.js'
1110
import type { CacheOperationMessage } from '../types/tracing_channels.js'
1211
import type { CacheEntryOptions } from './cache_entry/cache_entry_options.js'
@@ -15,7 +14,7 @@ interface RunFactoryParameters {
1514
key: string
1615
factory: GetSetFactory
1716
options: CacheEntryOptions
18-
lockReleaser: MutexInterface.Releaser
17+
lockReleaser: LockReleaser
1918
isBackground?: boolean
2019
gracedValue?: GetCacheValueReturn
2120
}
@@ -24,11 +23,11 @@ interface RunFactoryParameters {
2423
* Factory Runner is responsible for executing factories
2524
*/
2625
export class FactoryRunner {
27-
#locks: Locks
26+
#locks: LockManager
2827
#stack: CacheStack
2928
#skipSymbol = Symbol('bentocache.skip')
3029

31-
constructor(stack: CacheStack, locks: Locks) {
30+
constructor(stack: CacheStack, locks: LockManager) {
3231
this.#stack = stack
3332
this.#locks = locks
3433
}
@@ -119,7 +118,7 @@ export class FactoryRunner {
119118
factory: GetSetFactory,
120119
gracedValue: GetCacheValueReturn | undefined,
121120
options: CacheEntryOptions,
122-
lockReleaser: MutexInterface.Releaser,
121+
lockReleaser: LockReleaser,
123122
) {
124123
const hasGracedValue = !!gracedValue
125124
const timeout = options.factoryTimeout(hasGracedValue)

packages/bentocache/src/cache/get_set/single_tier_handler.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import type { MutexInterface } from 'async-mutex'
2-
3-
import { Locks } from '../locks.js'
41
import { errors } from '../../errors.js'
52
import type { CacheStack } from '../cache_stack.js'
63
import { FactoryRunner } from '../factory_runner.js'
74
import type { Factory } from '../../types/helpers.js'
85
import type { CacheEvent } from '../../types/events.js'
96
import { cacheEvents } from '../../events/cache_events.js'
107
import { cacheOperation } from '../../tracing_channels.js'
8+
import type { LockManager, LockReleaser } from '../../types/main.js'
119
import type { GetCacheValueReturn } from '../../types/internals/index.js'
1210
import type { CacheOperationMessage } from '../../types/tracing_channels.js'
1311
import type { CacheEntryOptions } from '../cache_entry/cache_entry_options.js'
@@ -16,10 +14,11 @@ export class SingleTierHandler {
1614
/**
1715
* A map that will hold active locks for each key
1816
*/
19-
#locks = new Locks()
17+
#locks: LockManager
2018
#factoryRunner: FactoryRunner
2119

2220
constructor(protected stack: CacheStack) {
21+
this.#locks = this.stack.options.lockManager
2322
this.#factoryRunner = new FactoryRunner(this.stack, this.#locks)
2423
}
2524

@@ -140,7 +139,7 @@ export class SingleTierHandler {
140139
* If nothing is found in the remote cache, or if forceFresh is true,
141140
* we try to acquire a lock to run the factory
142141
*/
143-
let releaser: MutexInterface.Releaser
142+
let releaser: LockReleaser
144143
try {
145144
releaser = await this.#acquireLock(key, !!remoteItem, options)
146145
} catch (err) {

packages/bentocache/src/cache/get_set/two_tier_handler.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import type { MutexInterface } from 'async-mutex'
2-
3-
import { Locks } from '../locks.js'
41
import { errors } from '../../errors.js'
52
import type { CacheStack } from '../cache_stack.js'
63
import { FactoryRunner } from '../factory_runner.js'
74
import type { Factory } from '../../types/helpers.js'
85
import type { CacheEvent } from '../../types/events.js'
96
import { cacheEvents } from '../../events/cache_events.js'
107
import { cacheOperation } from '../../tracing_channels.js'
8+
import type { LockManager, LockReleaser } from '../../types/main.js'
119
import type { GetCacheValueReturn } from '../../types/internals/index.js'
1210
import type { CacheOperationMessage } from '../../types/tracing_channels.js'
1311
import type { CacheEntryOptions } from '../cache_entry/cache_entry_options.js'
@@ -16,10 +14,11 @@ export class TwoTierHandler {
1614
/**
1715
* A map that will hold active locks for each key
1816
*/
19-
#locks = new Locks()
17+
#locks: LockManager
2018
#factoryRunner: FactoryRunner
2119

2220
constructor(protected stack: CacheStack) {
21+
this.#locks = this.stack.options.lockManager
2322
this.#factoryRunner = new FactoryRunner(this.stack, this.#locks)
2423
}
2524

@@ -130,7 +129,7 @@ export class TwoTierHandler {
130129
*
131130
* We acquire a lock to prevent a cache stampede.
132131
*/
133-
let releaser: MutexInterface.Releaser
132+
let releaser: LockReleaser
134133
try {
135134
this.logger.trace({ key, cache: this.stack.name, opId: options.id }, 'acquiring lock...')
136135
releaser = await this.#acquireLock(key, !!localItem, options)

packages/bentocache/src/cache/locks.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { is } from '@julr/utils/is'
22
import { Mutex, withTimeout, type MutexInterface } from 'async-mutex'
33

4-
export class Locks {
4+
import type { LockHandle, LockManager, LockReleaser } from '../types/main.js'
5+
6+
export class Locks implements LockManager {
57
/**
68
* A map that will hold active locks for each key
79
*/
@@ -13,7 +15,7 @@ export class Locks {
1315
* @param key Key to get or create a lock for
1416
* @param timeout Time to wait to acquire the lock
1517
*/
16-
getOrCreateForKey(key: string, timeout?: number) {
18+
getOrCreateForKey(key: string, timeout?: number): LockHandle {
1719
let lock = this.#locks.get(key)
1820
if (!lock) {
1921
lock = new Mutex()
@@ -23,7 +25,7 @@ export class Locks {
2325
return is.number(timeout) ? withTimeout(lock, timeout) : lock
2426
}
2527

26-
release(key: string, releaser: MutexInterface.Releaser) {
28+
release(key: string, releaser: LockReleaser) {
2729
releaser()
2830
this.#locks.delete(key)
2931
}

packages/bentocache/src/drivers/file/file.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010
CreateDriverResult,
1111
DriverCommonInternalOptions,
1212
FileConfig,
13+
LockManager,
1314
} from '../../types/main.js'
1415

1516
/**
@@ -42,13 +43,14 @@ export class FileDriver extends BaseDriver implements CacheDriver {
4243
* Worker thread that will clean up the expired files
4344
*/
4445
#cleanerWorker?: Worker
45-
#locks = new Locks()
46+
#locks: LockManager
4647

4748
declare config: FileConfig & DriverCommonInternalOptions
4849

49-
constructor(config: FileConfig, isNamespace: boolean = false) {
50+
constructor(config: FileConfig & DriverCommonInternalOptions, isNamespace: boolean = false) {
5051
super(config)
5152

53+
this.#locks = config.lockManager ?? new Locks()
5254
this.#directory = this.#sanitizePath(join(config.directory, config.prefix || ''))
5355

5456
/**

packages/bentocache/src/types/options/drivers_options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type {
1010

1111
import type { Logger } from '../../logger.js'
1212
import type { Duration } from '../helpers.js'
13+
import type { LockManager } from './options.js'
1314

1415
/**
1516
* Options that are common to all drivers
@@ -24,6 +25,7 @@ export type DriverCommonOptions = {
2425

2526
export type DriverCommonInternalOptions = {
2627
logger?: Logger
28+
lockManager?: LockManager
2729
}
2830

2931
/**

0 commit comments

Comments
 (0)