cachette 4.0.14 → 4.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/lib/CacheInstance.d.ts +15 -4
- package/dist/src/lib/CacheInstance.js +64 -50
- package/dist/src/lib/CacheInstance.js.map +1 -1
- package/dist/src/lib/LocalCache.d.ts +2 -7
- package/dist/src/lib/LocalCache.js +15 -26
- package/dist/src/lib/LocalCache.js.map +1 -1
- package/dist/src/lib/RedisCache.d.ts +1 -9
- package/dist/src/lib/RedisCache.js +3 -8
- package/dist/src/lib/RedisCache.js.map +1 -1
- package/package.json +1 -1
- package/src/lib/CacheInstance.ts +77 -49
- package/src/lib/LocalCache.ts +20 -28
- package/src/lib/RedisCache.ts +6 -15
- package/test/CacheClient_test.ts +44 -0
- package/test/CacheInstance_test.ts +58 -0
- package/test/LocalCache_test.ts +101 -0
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
export type CachableValue = any;
|
|
3
3
|
export type FetchingFunction = () => Promise<CachableValue>;
|
|
4
|
+
/**
|
|
5
|
+
* Represents a lock on a resource.
|
|
6
|
+
* The lock object must be passed to unlock() to release the lock.
|
|
7
|
+
*/
|
|
8
|
+
export interface Lock {
|
|
9
|
+
value: string | null;
|
|
10
|
+
unlock(): Promise<void>;
|
|
11
|
+
}
|
|
4
12
|
export declare abstract class CacheInstance extends EventEmitter {
|
|
13
|
+
static LOCK_RETRY_COUNT: number;
|
|
14
|
+
static LOCK_RETRY_DELAY_MS: number;
|
|
5
15
|
/**
|
|
6
16
|
* Will resolve when the cache instance connection is ready.
|
|
7
17
|
*/
|
|
@@ -76,15 +86,15 @@ export declare abstract class CacheInstance extends EventEmitter {
|
|
|
76
86
|
* @param ttlMs The time to live of the lock in ms
|
|
77
87
|
* @param retry Whether or not to retry attempts to lock
|
|
78
88
|
*
|
|
79
|
-
* @returns The lock
|
|
89
|
+
* @returns The lock object that must be passed to unlock()
|
|
80
90
|
*/
|
|
81
|
-
lock(resource: string, ttlMs: number, retry?: boolean): Promise<
|
|
91
|
+
lock(resource: string, ttlMs: number, retry?: boolean): Promise<Lock>;
|
|
82
92
|
/**
|
|
83
|
-
* Unlock a named resource
|
|
93
|
+
* Unlock a named resource acquired with lock()
|
|
84
94
|
*
|
|
85
95
|
* @param lock The lock object
|
|
86
96
|
*/
|
|
87
|
-
unlock(lock:
|
|
97
|
+
unlock(lock: Lock): Promise<void>;
|
|
88
98
|
/**
|
|
89
99
|
* Determine whether *at least one non-expired lock* starts with the given pattern.
|
|
90
100
|
*/
|
|
@@ -98,6 +108,7 @@ export declare abstract class CacheInstance extends EventEmitter {
|
|
|
98
108
|
* simultaneous requests to the same resource in parallel.
|
|
99
109
|
*/
|
|
100
110
|
private activeFetches;
|
|
111
|
+
private getValueWithErrorHandling;
|
|
101
112
|
/**
|
|
102
113
|
* Get or fetch a value
|
|
103
114
|
*
|
|
@@ -3,6 +3,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CacheInstance = void 0;
|
|
4
4
|
const node_events_1 = require("node:events");
|
|
5
5
|
class CacheInstance extends node_events_1.EventEmitter {
|
|
6
|
+
// Shared lock configuration with generic env vars
|
|
7
|
+
static LOCK_RETRY_COUNT = parseInt(process.env.CACHETTE_LOCK_RETRY_COUNT, 10) || 20;
|
|
8
|
+
static LOCK_RETRY_DELAY_MS = parseInt(process.env.CACHETTE_LOCK_RETRY_DELAY_MS, 10) || 200;
|
|
6
9
|
/**
|
|
7
10
|
* Determines if locking is supported in the cache implementation
|
|
8
11
|
*/
|
|
@@ -16,18 +19,18 @@ class CacheInstance extends node_events_1.EventEmitter {
|
|
|
16
19
|
* @param ttlMs The time to live of the lock in ms
|
|
17
20
|
* @param retry Whether or not to retry attempts to lock
|
|
18
21
|
*
|
|
19
|
-
* @returns The lock
|
|
22
|
+
* @returns The lock object that must be passed to unlock()
|
|
20
23
|
*/
|
|
21
24
|
lock(resource, ttlMs, retry) {
|
|
22
25
|
throw new Error('unsupported');
|
|
23
26
|
}
|
|
24
27
|
/**
|
|
25
|
-
* Unlock a named resource
|
|
28
|
+
* Unlock a named resource acquired with lock()
|
|
26
29
|
*
|
|
27
30
|
* @param lock The lock object
|
|
28
31
|
*/
|
|
29
|
-
unlock(lock) {
|
|
30
|
-
|
|
32
|
+
async unlock(lock) {
|
|
33
|
+
await lock.unlock();
|
|
31
34
|
}
|
|
32
35
|
/**
|
|
33
36
|
* Determine whether *at least one non-expired lock* starts with the given pattern.
|
|
@@ -48,6 +51,18 @@ class CacheInstance extends node_events_1.EventEmitter {
|
|
|
48
51
|
* simultaneous requests to the same resource in parallel.
|
|
49
52
|
*/
|
|
50
53
|
activeFetches = {};
|
|
54
|
+
async getValueWithErrorHandling(key, shouldCacheError) {
|
|
55
|
+
const cached = await this.getValue(key);
|
|
56
|
+
if (cached instanceof Error) {
|
|
57
|
+
if (shouldCacheError && shouldCacheError(cached)) {
|
|
58
|
+
throw cached;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return cached;
|
|
65
|
+
}
|
|
51
66
|
/**
|
|
52
67
|
* Get or fetch a value
|
|
53
68
|
*
|
|
@@ -63,15 +78,7 @@ class CacheInstance extends node_events_1.EventEmitter {
|
|
|
63
78
|
*/
|
|
64
79
|
async getOrFetchValue(key, ttl, fetchFunction, lockTtl, shouldCacheError) {
|
|
65
80
|
// already cached?
|
|
66
|
-
let cached = await this.
|
|
67
|
-
if (cached instanceof Error) {
|
|
68
|
-
if (shouldCacheError) {
|
|
69
|
-
throw cached;
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
cached = undefined;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
81
|
+
let cached = await this.getValueWithErrorHandling(key, shouldCacheError);
|
|
75
82
|
if (cached !== undefined) {
|
|
76
83
|
return cached;
|
|
77
84
|
}
|
|
@@ -80,54 +87,61 @@ class CacheInstance extends node_events_1.EventEmitter {
|
|
|
80
87
|
if (currentFetch) {
|
|
81
88
|
return currentFetch;
|
|
82
89
|
}
|
|
83
|
-
// I'm the one fetching
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (shouldCacheError) {
|
|
94
|
-
throw cachedValue;
|
|
90
|
+
// I'm the one fetching - immediately register the fetch promise to prevent race conditions
|
|
91
|
+
this.activeFetches[key] = (async () => {
|
|
92
|
+
let lock;
|
|
93
|
+
let lockErrorMessage;
|
|
94
|
+
try {
|
|
95
|
+
// get the lock if needed
|
|
96
|
+
if (lockTtl && this.isLockingSupported()) {
|
|
97
|
+
const lockName = `lock__${key}`;
|
|
98
|
+
try {
|
|
99
|
+
lock = await this.lock(lockName, lockTtl * 1000);
|
|
95
100
|
}
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
catch (lockError) {
|
|
102
|
+
lockErrorMessage = lockError instanceof Error ? lockError.message : String(lockError);
|
|
98
103
|
}
|
|
99
104
|
}
|
|
105
|
+
// check gain if the value has been populated while we previously checked
|
|
106
|
+
const cachedValue = await this.getValueWithErrorHandling(key, shouldCacheError);
|
|
100
107
|
if (cachedValue !== undefined) {
|
|
101
108
|
return cachedValue;
|
|
102
109
|
}
|
|
110
|
+
if (lockErrorMessage) {
|
|
111
|
+
throw new Error(`Failed to acquire lock for key ${key}. No active fetch or cached value found: ${lockErrorMessage}`);
|
|
112
|
+
}
|
|
113
|
+
// fetch!
|
|
114
|
+
let error;
|
|
115
|
+
let result;
|
|
116
|
+
try {
|
|
117
|
+
result = await fetchFunction();
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
error = err;
|
|
121
|
+
}
|
|
122
|
+
// cache! results: always, errors: only if satisfying user assertion
|
|
123
|
+
if (error && shouldCacheError && shouldCacheError(error)) {
|
|
124
|
+
await this.setValue(key, error, ttl);
|
|
125
|
+
}
|
|
126
|
+
else if (result !== undefined) {
|
|
127
|
+
await this.setValue(key, result, ttl);
|
|
128
|
+
}
|
|
129
|
+
if (error) {
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
103
133
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const fetchPromise = (this.activeFetches[key] = fetchFunction());
|
|
109
|
-
result = await fetchPromise;
|
|
110
|
-
}
|
|
111
|
-
catch (err) {
|
|
112
|
-
error = err;
|
|
113
|
-
}
|
|
114
|
-
// cache! results: always, errors: only if satisfying user assertion
|
|
115
|
-
if (error && shouldCacheError && shouldCacheError(error)) {
|
|
116
|
-
await this.setValue(key, error, ttl);
|
|
117
|
-
}
|
|
118
|
-
else if (result !== undefined) {
|
|
119
|
-
await this.setValue(key, result, ttl);
|
|
120
|
-
}
|
|
121
|
-
if (error) {
|
|
122
|
-
throw error;
|
|
134
|
+
finally {
|
|
135
|
+
if (lock) {
|
|
136
|
+
await this.unlock(lock);
|
|
137
|
+
}
|
|
123
138
|
}
|
|
124
|
-
|
|
139
|
+
})();
|
|
140
|
+
try {
|
|
141
|
+
return await this.activeFetches[key];
|
|
125
142
|
}
|
|
126
143
|
finally {
|
|
127
144
|
delete this.activeFetches[key];
|
|
128
|
-
if (lock) {
|
|
129
|
-
await this.unlock(lock);
|
|
130
|
-
}
|
|
131
145
|
}
|
|
132
146
|
}
|
|
133
147
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CacheInstance.js","sourceRoot":"","sources":["../../../src/lib/CacheInstance.ts"],"names":[],"mappings":";;;AAAA,6CAA2C;
|
|
1
|
+
{"version":3,"file":"CacheInstance.js","sourceRoot":"","sources":["../../../src/lib/CacheInstance.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { CachableValue, CacheInstance } from './CacheInstance';
|
|
1
|
+
import { CachableValue, CacheInstance, Lock } from './CacheInstance';
|
|
2
2
|
export declare class LocalCache extends CacheInstance {
|
|
3
3
|
static DEFAULT_MAX_ITEMS: number;
|
|
4
4
|
static DEFAULT_MAX_AGE: number;
|
|
5
|
-
static LOCK_ACQUIRE_TIMEOUT: number;
|
|
6
5
|
private cache;
|
|
7
6
|
/**
|
|
8
7
|
* @inheritdoc
|
|
@@ -52,11 +51,7 @@ export declare class LocalCache extends CacheInstance {
|
|
|
52
51
|
/**
|
|
53
52
|
* @inheritdoc
|
|
54
53
|
*/
|
|
55
|
-
lock(resource: string, ttlMs: number): Promise<
|
|
56
|
-
/**
|
|
57
|
-
* @inheritdoc
|
|
58
|
-
*/
|
|
59
|
-
unlock(lock: any): Promise<void>;
|
|
54
|
+
lock(resource: string, ttlMs: number, retry?: boolean): Promise<Lock>;
|
|
60
55
|
/**
|
|
61
56
|
* @inheritdoc
|
|
62
57
|
*
|
|
@@ -10,7 +10,6 @@ class LocalCache extends CacheInstance_1.CacheInstance {
|
|
|
10
10
|
static DEFAULT_MAX_ITEMS = 5000;
|
|
11
11
|
// Default maximum age for the items, in MS.
|
|
12
12
|
static DEFAULT_MAX_AGE = 30 * 60 * 1000;
|
|
13
|
-
static LOCK_ACQUIRE_TIMEOUT = 2000;
|
|
14
13
|
// See https://github.com/isaacs/node-lru-cache#options
|
|
15
14
|
// for options.
|
|
16
15
|
cache = new lru_cache_1.LRUCache({
|
|
@@ -108,36 +107,26 @@ class LocalCache extends CacheInstance_1.CacheInstance {
|
|
|
108
107
|
/**
|
|
109
108
|
* @inheritdoc
|
|
110
109
|
*/
|
|
111
|
-
async lock(resource, ttlMs) {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
while (isLocked) {
|
|
115
|
-
if (Date.now() - startTimestamp > LocalCache.LOCK_ACQUIRE_TIMEOUT) {
|
|
116
|
-
throw new Error(`Abandoning locking ${resource} , as timed out while waiting for other lock to be released.`);
|
|
117
|
-
}
|
|
110
|
+
async lock(resource, ttlMs, retry = true) {
|
|
111
|
+
const maxAttempts = retry ? LocalCache.LOCK_RETRY_COUNT : 1;
|
|
112
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
118
113
|
this.cache.purgeStale();
|
|
119
114
|
if (!this.cache.has(resource)) {
|
|
120
|
-
|
|
115
|
+
// Lock acquired
|
|
116
|
+
const lockValue = `local-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
117
|
+
this.cache.set(resource, lockValue, { ttl: ttlMs });
|
|
118
|
+
return {
|
|
119
|
+
value: lockValue,
|
|
120
|
+
unlock: async () => {
|
|
121
|
+
this.cache.delete(resource);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
121
124
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// Whatever, we just loop on waiting a bit and retrying.
|
|
125
|
-
await sleep(10);
|
|
125
|
+
if (attempt < maxAttempts - 1) {
|
|
126
|
+
await sleep(LocalCache.LOCK_RETRY_DELAY_MS);
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
|
-
|
|
129
|
-
return new Promise((resolve) => {
|
|
130
|
-
resolve(resource);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* @inheritdoc
|
|
135
|
-
*/
|
|
136
|
-
async unlock(lock) {
|
|
137
|
-
this.cache.delete(lock);
|
|
138
|
-
return new Promise((resolve) => {
|
|
139
|
-
resolve();
|
|
140
|
-
});
|
|
129
|
+
throw new Error(`Failed to acquire lock on ${resource} after ${maxAttempts} attempts`);
|
|
141
130
|
}
|
|
142
131
|
/**
|
|
143
132
|
* @inheritdoc
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LocalCache.js","sourceRoot":"","sources":["../../../src/lib/LocalCache.ts"],"names":[],"mappings":";;;AAAA,yCAAqC;AAErC,
|
|
1
|
+
{"version":3,"file":"LocalCache.js","sourceRoot":"","sources":["../../../src/lib/LocalCache.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { CachableValue, CacheInstance } from './CacheInstance';
|
|
1
|
+
import { CachableValue, CacheInstance, Lock } from './CacheInstance';
|
|
2
2
|
export declare const SIZE_THRESHOLD_WARNING_BYTES = 20000000;
|
|
3
|
-
export interface Lock {
|
|
4
|
-
value: string | null;
|
|
5
|
-
unlock(): Promise<void>;
|
|
6
|
-
}
|
|
7
3
|
/**
|
|
8
4
|
* Wrapper class for using Redis as a cache.
|
|
9
5
|
*
|
|
@@ -123,10 +119,6 @@ export declare class RedisCache extends CacheInstance {
|
|
|
123
119
|
* @inheritdoc
|
|
124
120
|
*/
|
|
125
121
|
lock(resource: string, ttlMs: number, retry?: boolean): Promise<Lock>;
|
|
126
|
-
/**
|
|
127
|
-
* @inheritdoc
|
|
128
|
-
*/
|
|
129
|
-
unlock(lock: Lock): Promise<void>;
|
|
130
122
|
/**
|
|
131
123
|
* @inheritdoc
|
|
132
124
|
*
|
|
@@ -24,8 +24,9 @@ class RedisCache extends CacheInstance_1.CacheInstance {
|
|
|
24
24
|
static ERROR_PREFIX = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-ERROR';
|
|
25
25
|
static NUMBER_PREFIX = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-NUMBER';
|
|
26
26
|
static REDIS_CONNECTION_TIMEOUT_MS = parseInt(process.env.REDIS_CONNECTION_TIMEOUT_MS, 10) || 5000;
|
|
27
|
-
|
|
28
|
-
static
|
|
27
|
+
// Backward compatible: REDLOCK_* takes precedence, then CACHETTE_LOCK_*
|
|
28
|
+
static REDLOCK_RETRY_COUNT = parseInt(process.env.REDLOCK_RETRY_COUNT, 10) || RedisCache.LOCK_RETRY_COUNT; // lib. default: 20
|
|
29
|
+
static REDLOCK_RETRY_DELAY_MS = parseInt(process.env.REDLOCK_RETRY_DELAY_MS, 10) || RedisCache.LOCK_RETRY_DELAY_MS; // lib. default: 200
|
|
29
30
|
static REDLOCK_CLOCK_DRIFT_FACTOR = parseInt(process.env.REDLOCK_CLOCK_DRIFT_FACTOR, 10) || 0.01; // lib. default: 0.01
|
|
30
31
|
static REDLOCK_JITTER_MS = parseInt(process.env.REDLOCK_JITTER_MS, 10) || 200; // lib. default: 200
|
|
31
32
|
redisClient;
|
|
@@ -341,12 +342,6 @@ class RedisCache extends CacheInstance_1.CacheInstance {
|
|
|
341
342
|
unlock: async () => internalLock.unlock(),
|
|
342
343
|
};
|
|
343
344
|
}
|
|
344
|
-
/**
|
|
345
|
-
* @inheritdoc
|
|
346
|
-
*/
|
|
347
|
-
async unlock(lock) {
|
|
348
|
-
await lock.unlock();
|
|
349
|
-
}
|
|
350
345
|
/**
|
|
351
346
|
* @inheritdoc
|
|
352
347
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RedisCache.js","sourceRoot":"","sources":["../../../src/lib/RedisCache.ts"],"names":[],"mappings":";;;AAAA,qCAA4B;AAC5B,mCAAmC;AAEnC,
|
|
1
|
+
{"version":3,"file":"RedisCache.js","sourceRoot":"","sources":["../../../src/lib/RedisCache.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/lib/CacheInstance.ts
CHANGED
|
@@ -3,7 +3,19 @@ import { EventEmitter } from 'node:events';
|
|
|
3
3
|
export type CachableValue = any;
|
|
4
4
|
export type FetchingFunction = () => Promise<CachableValue>;
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Represents a lock on a resource.
|
|
8
|
+
* The lock object must be passed to unlock() to release the lock.
|
|
9
|
+
*/
|
|
10
|
+
export interface Lock {
|
|
11
|
+
value: string | null;
|
|
12
|
+
unlock(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
|
|
6
15
|
export abstract class CacheInstance extends EventEmitter {
|
|
16
|
+
// Shared lock configuration with generic env vars
|
|
17
|
+
public static LOCK_RETRY_COUNT = parseInt(process.env.CACHETTE_LOCK_RETRY_COUNT as string, 10) || 20;
|
|
18
|
+
public static LOCK_RETRY_DELAY_MS = parseInt(process.env.CACHETTE_LOCK_RETRY_DELAY_MS as string, 10) || 200;
|
|
7
19
|
/**
|
|
8
20
|
* Will resolve when the cache instance connection is ready.
|
|
9
21
|
*/
|
|
@@ -90,19 +102,19 @@ export abstract class CacheInstance extends EventEmitter {
|
|
|
90
102
|
* @param ttlMs The time to live of the lock in ms
|
|
91
103
|
* @param retry Whether or not to retry attempts to lock
|
|
92
104
|
*
|
|
93
|
-
* @returns The lock
|
|
105
|
+
* @returns The lock object that must be passed to unlock()
|
|
94
106
|
*/
|
|
95
|
-
public lock(resource: string, ttlMs: number, retry?: boolean): Promise<
|
|
107
|
+
public lock(resource: string, ttlMs: number, retry?: boolean): Promise<Lock> {
|
|
96
108
|
throw new Error('unsupported');
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
/**
|
|
100
|
-
* Unlock a named resource
|
|
112
|
+
* Unlock a named resource acquired with lock()
|
|
101
113
|
*
|
|
102
114
|
* @param lock The lock object
|
|
103
115
|
*/
|
|
104
|
-
public unlock(lock:
|
|
105
|
-
|
|
116
|
+
public async unlock(lock: Lock): Promise<void> {
|
|
117
|
+
await lock.unlock();
|
|
106
118
|
}
|
|
107
119
|
|
|
108
120
|
/**
|
|
@@ -127,6 +139,19 @@ export abstract class CacheInstance extends EventEmitter {
|
|
|
127
139
|
*/
|
|
128
140
|
private activeFetches: { [key: string]: Promise<CachableValue> } = {};
|
|
129
141
|
|
|
142
|
+
private async getValueWithErrorHandling(key: string, shouldCacheError?: (err: Error) => boolean): Promise<CachableValue> {
|
|
143
|
+
const cached = await this.getValue(key);
|
|
144
|
+
if (cached instanceof Error) {
|
|
145
|
+
if (shouldCacheError && shouldCacheError(cached)) {
|
|
146
|
+
throw cached;
|
|
147
|
+
} else {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return cached;
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
/**
|
|
131
156
|
* Get or fetch a value
|
|
132
157
|
*
|
|
@@ -148,14 +173,7 @@ export abstract class CacheInstance extends EventEmitter {
|
|
|
148
173
|
shouldCacheError?: (err: Error) => boolean,
|
|
149
174
|
): Promise<ReturnType<F>> {
|
|
150
175
|
// already cached?
|
|
151
|
-
let cached = await this.
|
|
152
|
-
if (cached instanceof Error) {
|
|
153
|
-
if (shouldCacheError) {
|
|
154
|
-
throw cached;
|
|
155
|
-
} else {
|
|
156
|
-
cached = undefined;
|
|
157
|
-
}
|
|
158
|
-
}
|
|
176
|
+
let cached = await this.getValueWithErrorHandling(key, shouldCacheError);
|
|
159
177
|
if (cached !== undefined) {
|
|
160
178
|
return cached;
|
|
161
179
|
}
|
|
@@ -166,53 +184,63 @@ export abstract class CacheInstance extends EventEmitter {
|
|
|
166
184
|
return currentFetch;
|
|
167
185
|
}
|
|
168
186
|
|
|
169
|
-
// I'm the one fetching
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
cachedValue = undefined;
|
|
187
|
+
// I'm the one fetching - immediately register the fetch promise to prevent race conditions
|
|
188
|
+
this.activeFetches[key] = (async () => {
|
|
189
|
+
let lock: Lock | undefined;
|
|
190
|
+
let lockErrorMessage: string | undefined;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
// get the lock if needed
|
|
194
|
+
if (lockTtl && this.isLockingSupported()) {
|
|
195
|
+
const lockName = `lock__${key}`;
|
|
196
|
+
try {
|
|
197
|
+
lock = await this.lock(lockName, lockTtl * 1000);
|
|
198
|
+
} catch (lockError) {
|
|
199
|
+
lockErrorMessage = lockError instanceof Error ? lockError.message : String(lockError);
|
|
183
200
|
}
|
|
184
201
|
}
|
|
202
|
+
|
|
203
|
+
// check gain if the value has been populated while we previously checked
|
|
204
|
+
const cachedValue = await this.getValueWithErrorHandling(key, shouldCacheError);
|
|
185
205
|
if (cachedValue !== undefined) {
|
|
186
206
|
return cachedValue;
|
|
187
207
|
}
|
|
188
|
-
}
|
|
189
208
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
const fetchPromise = (this.activeFetches[key] = fetchFunction());
|
|
195
|
-
result = await fetchPromise;
|
|
196
|
-
} catch (err) {
|
|
197
|
-
error = err;
|
|
198
|
-
}
|
|
209
|
+
if (lockErrorMessage) {
|
|
210
|
+
throw new Error(`Failed to acquire lock for key ${key}. No active fetch or cached value found: ${lockErrorMessage}`);
|
|
211
|
+
}
|
|
199
212
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
213
|
+
// fetch!
|
|
214
|
+
let error: Error | undefined;
|
|
215
|
+
let result: any;
|
|
216
|
+
try {
|
|
217
|
+
result = await fetchFunction();
|
|
218
|
+
} catch (err) {
|
|
219
|
+
error = err;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// cache! results: always, errors: only if satisfying user assertion
|
|
223
|
+
if (error && shouldCacheError && shouldCacheError(error)) {
|
|
224
|
+
await this.setValue(key, error, ttl);
|
|
225
|
+
} else if (result !== undefined) {
|
|
226
|
+
await this.setValue(key, result, ttl);
|
|
227
|
+
}
|
|
206
228
|
|
|
207
|
-
|
|
208
|
-
|
|
229
|
+
if (error) {
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
return result;
|
|
233
|
+
} finally {
|
|
234
|
+
if (lock) {
|
|
235
|
+
await this.unlock(lock);
|
|
236
|
+
}
|
|
209
237
|
}
|
|
210
|
-
|
|
238
|
+
})();
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
return await this.activeFetches[key];
|
|
211
242
|
} finally {
|
|
212
243
|
delete this.activeFetches[key];
|
|
213
|
-
if (lock) {
|
|
214
|
-
await this.unlock(lock);
|
|
215
|
-
}
|
|
216
244
|
}
|
|
217
245
|
}
|
|
218
246
|
}
|
package/src/lib/LocalCache.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LRUCache } from 'lru-cache';
|
|
2
2
|
|
|
3
|
-
import { CachableValue, CacheInstance } from './CacheInstance';
|
|
3
|
+
import { CachableValue, CacheInstance, Lock } from './CacheInstance';
|
|
4
4
|
|
|
5
5
|
async function sleep(ms: number): Promise<void> {
|
|
6
6
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -11,8 +11,6 @@ export class LocalCache extends CacheInstance {
|
|
|
11
11
|
// Default maximum age for the items, in MS.
|
|
12
12
|
public static DEFAULT_MAX_AGE: number = 30 * 60 * 1000;
|
|
13
13
|
|
|
14
|
-
public static LOCK_ACQUIRE_TIMEOUT = 2000;
|
|
15
|
-
|
|
16
14
|
// See https://github.com/isaacs/node-lru-cache#options
|
|
17
15
|
// for options.
|
|
18
16
|
private cache = new LRUCache<string, any>({
|
|
@@ -123,36 +121,30 @@ export class LocalCache extends CacheInstance {
|
|
|
123
121
|
/**
|
|
124
122
|
* @inheritdoc
|
|
125
123
|
*/
|
|
126
|
-
public async lock(resource: string, ttlMs: number): Promise<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (Date.now() - startTimestamp > LocalCache.LOCK_ACQUIRE_TIMEOUT) {
|
|
131
|
-
throw new Error(`Abandoning locking ${resource} , as timed out while waiting for other lock to be released.`);
|
|
132
|
-
}
|
|
124
|
+
public async lock(resource: string, ttlMs: number, retry = true): Promise<Lock> {
|
|
125
|
+
const maxAttempts = retry ? LocalCache.LOCK_RETRY_COUNT : 1;
|
|
126
|
+
|
|
127
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
133
128
|
this.cache.purgeStale();
|
|
134
129
|
if (!this.cache.has(resource)) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
130
|
+
// Lock acquired
|
|
131
|
+
const lockValue = `local-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
132
|
+
this.cache.set(resource, lockValue, { ttl: ttlMs });
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
value: lockValue,
|
|
136
|
+
unlock: async () => {
|
|
137
|
+
this.cache.delete(resource);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (attempt < maxAttempts - 1) {
|
|
143
|
+
await sleep(LocalCache.LOCK_RETRY_DELAY_MS);
|
|
140
144
|
}
|
|
141
145
|
}
|
|
142
|
-
this.cache.set(resource, 1, { ttl: ttlMs });
|
|
143
|
-
return new Promise((resolve) => {
|
|
144
|
-
resolve(resource);
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
146
|
|
|
148
|
-
|
|
149
|
-
* @inheritdoc
|
|
150
|
-
*/
|
|
151
|
-
public async unlock(lock: any): Promise<void> {
|
|
152
|
-
this.cache.delete(lock);
|
|
153
|
-
return new Promise((resolve) => {
|
|
154
|
-
resolve();
|
|
155
|
-
});
|
|
147
|
+
throw new Error(`Failed to acquire lock on ${resource} after ${maxAttempts} attempts`);
|
|
156
148
|
}
|
|
157
149
|
|
|
158
150
|
/**
|
package/src/lib/RedisCache.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import Redis from 'ioredis';
|
|
2
2
|
import * as Redlock from 'redlock';
|
|
3
3
|
|
|
4
|
-
import { CachableValue, CacheInstance } from './CacheInstance';
|
|
4
|
+
import { CachableValue, CacheInstance, Lock } from './CacheInstance';
|
|
5
5
|
|
|
6
6
|
export const SIZE_THRESHOLD_WARNING_BYTES = 20_000_000;
|
|
7
7
|
|
|
8
|
-
export interface Lock {
|
|
9
|
-
value: string | null;
|
|
10
|
-
unlock(): Promise<void>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
8
|
/**
|
|
14
9
|
* Wrapper class for using Redis as a cache.
|
|
15
10
|
*
|
|
@@ -30,8 +25,11 @@ export class RedisCache extends CacheInstance {
|
|
|
30
25
|
public static NUMBER_PREFIX = 'f405eed4-507c-4aa5-a6d2-c1813d584b8f-NUMBER';
|
|
31
26
|
|
|
32
27
|
public static REDIS_CONNECTION_TIMEOUT_MS = parseInt(process.env.REDIS_CONNECTION_TIMEOUT_MS as string, 10) || 5000;
|
|
33
|
-
|
|
34
|
-
public static
|
|
28
|
+
// Backward compatible: REDLOCK_* takes precedence, then CACHETTE_LOCK_*
|
|
29
|
+
public static REDLOCK_RETRY_COUNT =
|
|
30
|
+
parseInt(process.env.REDLOCK_RETRY_COUNT as string, 10) || RedisCache.LOCK_RETRY_COUNT; // lib. default: 20
|
|
31
|
+
public static REDLOCK_RETRY_DELAY_MS =
|
|
32
|
+
parseInt(process.env.REDLOCK_RETRY_DELAY_MS as string, 10) || RedisCache.LOCK_RETRY_DELAY_MS; // lib. default: 200
|
|
35
33
|
public static REDLOCK_CLOCK_DRIFT_FACTOR = parseInt(process.env.REDLOCK_CLOCK_DRIFT_FACTOR as string, 10) || 0.01; // lib. default: 0.01
|
|
36
34
|
public static REDLOCK_JITTER_MS = parseInt(process.env.REDLOCK_JITTER_MS as string, 10) || 200; // lib. default: 200
|
|
37
35
|
|
|
@@ -390,13 +388,6 @@ export class RedisCache extends CacheInstance {
|
|
|
390
388
|
};
|
|
391
389
|
}
|
|
392
390
|
|
|
393
|
-
/**
|
|
394
|
-
* @inheritdoc
|
|
395
|
-
*/
|
|
396
|
-
public async unlock(lock: Lock): Promise<void> {
|
|
397
|
-
await lock.unlock();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
391
|
/**
|
|
401
392
|
* @inheritdoc
|
|
402
393
|
*
|
package/test/CacheClient_test.ts
CHANGED
|
@@ -132,6 +132,50 @@ describe('CacheClient', () => {
|
|
|
132
132
|
expect(myObj.numCalled).to.equal(2); // from cache -> NO increase
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
+
it('correctly evaluates shouldCacheError callback when retrieving cached errors', async () => {
|
|
136
|
+
const cache = new LocalCache();
|
|
137
|
+
const key = 'test-key';
|
|
138
|
+
|
|
139
|
+
// Manually cache an error with a specific property
|
|
140
|
+
const cachedError = new Error('cached error');
|
|
141
|
+
cachedError['errorType'] = 'temporary';
|
|
142
|
+
await cache.setValue(key, cachedError, 60);
|
|
143
|
+
|
|
144
|
+
// Try to retrieve with a shouldCacheError that should reject this error
|
|
145
|
+
const result = await cache.getOrFetchValue(
|
|
146
|
+
key,
|
|
147
|
+
60,
|
|
148
|
+
async () => 'fresh value',
|
|
149
|
+
undefined,
|
|
150
|
+
(err) => err['errorType'] === 'permanent', // Only cache permanent errors
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Should return 'fresh value' because the cached error doesn't match the callback criteria
|
|
154
|
+
expect(result).to.equal('fresh value');
|
|
155
|
+
|
|
156
|
+
// Now try with a callback that accepts the cached error
|
|
157
|
+
const cachedError2 = new Error('cached error 2');
|
|
158
|
+
cachedError2['errorType'] = 'permanent';
|
|
159
|
+
await cache.setValue(key, cachedError2, 60);
|
|
160
|
+
|
|
161
|
+
let didThrow = false;
|
|
162
|
+
try {
|
|
163
|
+
await cache.getOrFetchValue(
|
|
164
|
+
key,
|
|
165
|
+
60,
|
|
166
|
+
async () => 'fresh value',
|
|
167
|
+
undefined,
|
|
168
|
+
(err) => err['errorType'] === 'permanent', // Only cache permanent errors
|
|
169
|
+
);
|
|
170
|
+
} catch (err) {
|
|
171
|
+
didThrow = true;
|
|
172
|
+
expect(err.message).to.equal('cached error 2');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Should throw because the cached error matches the callback criteria
|
|
176
|
+
expect(didThrow).to.be.true;
|
|
177
|
+
});
|
|
178
|
+
|
|
135
179
|
it('protect against concurrent fetches', async () => {
|
|
136
180
|
const myObj = new MyClass();
|
|
137
181
|
const jobs: Promise<any>[] = [];
|
|
@@ -456,5 +456,63 @@ function runTests(name: string, cache: CacheInstance): void {
|
|
|
456
456
|
sinon.assert.calledTwice(lockSpy); // includes our own call above
|
|
457
457
|
sinon.assert.calledTwice(unlockSpy);
|
|
458
458
|
});
|
|
459
|
+
|
|
460
|
+
ifLockIt('returns cached value if lock acquisition fails and value was populated meanwhile', async () => {
|
|
461
|
+
const key = `key${Math.random()}`;
|
|
462
|
+
let numCalled = 0;
|
|
463
|
+
const object = {
|
|
464
|
+
fetch: async (v) => {
|
|
465
|
+
numCalled++;
|
|
466
|
+
return v;
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const fetchFunction = object.fetch.bind(object, 'newvalue');
|
|
471
|
+
|
|
472
|
+
// Restore the spy temporarily so we can stub it
|
|
473
|
+
lockSpy.restore();
|
|
474
|
+
|
|
475
|
+
const lockStub = sinon.stub(cache, 'lock').callsFake(async () => {
|
|
476
|
+
// Before failing, simulate the cache being populated
|
|
477
|
+
await cache.setValue(key, 'from-cache');
|
|
478
|
+
throw new Error('Lock acquisition failed');
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const value = await cache.getOrFetchValue(key, 10, fetchFunction, 1);
|
|
482
|
+
|
|
483
|
+
expect(value).to.eql('from-cache');
|
|
484
|
+
expect(numCalled).to.eql(0); // never called fetch, got from cache
|
|
485
|
+
|
|
486
|
+
lockStub.restore();
|
|
487
|
+
lockSpy = sinon.spy(cache, 'lock');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
ifLockIt('throws detailed error if lock fails with no activeFetch or cached value', async () => {
|
|
491
|
+
const key = `key${Math.random()}`;
|
|
492
|
+
const object = {
|
|
493
|
+
fetch: async (v) => v,
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const fetchFunction = object.fetch.bind(object, 'newvalue');
|
|
497
|
+
|
|
498
|
+
// Restore the spy temporarily so we can stub it
|
|
499
|
+
lockSpy.restore();
|
|
500
|
+
|
|
501
|
+
const lockStub = sinon.stub(cache, 'lock').rejects(new Error('Lock acquisition failed'));
|
|
502
|
+
|
|
503
|
+
let thrownError: Error | undefined;
|
|
504
|
+
try {
|
|
505
|
+
await cache.getOrFetchValue(key, 10, fetchFunction, 1);
|
|
506
|
+
} catch (err) {
|
|
507
|
+
thrownError = err as Error;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
expect(thrownError).to.exist;
|
|
511
|
+
expect(thrownError!.message).to.include(`Failed to acquire lock for key ${key}. No active fetch or cached value found: Lock acquisition failed`);
|
|
512
|
+
|
|
513
|
+
// Restore and recreate the spy
|
|
514
|
+
lockStub.restore();
|
|
515
|
+
lockSpy = sinon.spy(cache, 'lock');
|
|
516
|
+
});
|
|
459
517
|
});
|
|
460
518
|
}
|
package/test/LocalCache_test.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { expect } from 'chai';
|
|
2
|
+
import * as sinon from 'sinon';
|
|
2
3
|
|
|
3
4
|
import { LocalCache } from '../src/lib/LocalCache';
|
|
5
|
+
import { CacheInstance } from '../src/lib/CacheInstance';
|
|
4
6
|
|
|
5
7
|
describe('LocalCache', () => {
|
|
6
8
|
it('can set values', async () => {
|
|
@@ -115,6 +117,105 @@ describe('LocalCache', () => {
|
|
|
115
117
|
expect(wasSet).to.be.true;
|
|
116
118
|
expect(cacheTtl).to.equal(0);
|
|
117
119
|
});
|
|
120
|
+
|
|
121
|
+
describe('lock', () => {
|
|
122
|
+
it('returns a Lock object with value property and unlock method', async () => {
|
|
123
|
+
const cache = new LocalCache();
|
|
124
|
+
const lock = await cache.lock('test-resource', 10000);
|
|
125
|
+
|
|
126
|
+
expect(lock).to.have.property('value');
|
|
127
|
+
expect(lock.value).to.be.a('string');
|
|
128
|
+
expect(lock.value).to.match(/^local-\d+-[a-z0-9]+$/);
|
|
129
|
+
|
|
130
|
+
expect(lock).to.have.property('unlock');
|
|
131
|
+
expect(lock.unlock).to.be.a('function');
|
|
132
|
+
|
|
133
|
+
await cache.unlock(lock);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('releases lock via the returned Lock object unlock method', async () => {
|
|
137
|
+
const cache = new LocalCache();
|
|
138
|
+
const resource = `test-resource-${Math.random()}`;
|
|
139
|
+
|
|
140
|
+
const lock = await cache.lock(resource, 10000);
|
|
141
|
+
expect(await cache.hasLock(resource)).to.be.true;
|
|
142
|
+
|
|
143
|
+
await lock.unlock();
|
|
144
|
+
expect(await cache.hasLock(resource)).to.be.false;
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('releases lock via cache.unlock()', async () => {
|
|
148
|
+
const cache = new LocalCache();
|
|
149
|
+
const resource = `test-resource-${Math.random()}`;
|
|
150
|
+
|
|
151
|
+
const lock = await cache.lock(resource, 10000);
|
|
152
|
+
expect(await cache.hasLock(resource)).to.be.true;
|
|
153
|
+
|
|
154
|
+
await cache.unlock(lock);
|
|
155
|
+
expect(await cache.hasLock(resource)).to.be.false;
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('with retry = true, retries until lock is available', async () => {
|
|
159
|
+
// Fake only setTimeout to avoid conflicts with LRU cache's TTL (which uses Date)
|
|
160
|
+
const clock = sinon.useFakeTimers({ toFake: ['setTimeout'] });
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const resource = 'test-resource';
|
|
164
|
+
const cache = new LocalCache();
|
|
165
|
+
const hasSpy = sinon.spy(cache['cache'], 'has');
|
|
166
|
+
|
|
167
|
+
const firstLock = await cache.lock(resource, 10000);
|
|
168
|
+
const hasCallsAfterFirstLock = hasSpy.callCount;
|
|
169
|
+
|
|
170
|
+
let secondLockAcquired = false;
|
|
171
|
+
const lockPromise = cache.lock(resource, 10000, true).then((lock) => {
|
|
172
|
+
secondLockAcquired = true;
|
|
173
|
+
return lock;
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Advance time to trigger retry, but lock still held
|
|
177
|
+
await clock.tickAsync(CacheInstance.LOCK_RETRY_DELAY_MS);
|
|
178
|
+
expect(secondLockAcquired).to.be.false;
|
|
179
|
+
expect(hasSpy.callCount).to.be.greaterThan(hasCallsAfterFirstLock);
|
|
180
|
+
|
|
181
|
+
// Release first lock
|
|
182
|
+
await cache.unlock(firstLock);
|
|
183
|
+
|
|
184
|
+
// Advance time for next retry - should acquire now
|
|
185
|
+
await clock.tickAsync(CacheInstance.LOCK_RETRY_DELAY_MS);
|
|
186
|
+
const secondLock = await lockPromise;
|
|
187
|
+
|
|
188
|
+
expect(secondLockAcquired).to.be.true;
|
|
189
|
+
expect(secondLock).to.have.property('value');
|
|
190
|
+
|
|
191
|
+
hasSpy.restore();
|
|
192
|
+
await cache.unlock(secondLock);
|
|
193
|
+
} finally {
|
|
194
|
+
clock.restore();
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('with retry = false, throws immediately if lock is held', async () => {
|
|
199
|
+
const cache = new LocalCache();
|
|
200
|
+
const resource = `test-resource-${Math.random()}`;
|
|
201
|
+
|
|
202
|
+
const firstLock = await cache.lock(resource, 10000);
|
|
203
|
+
expect(await cache.hasLock(resource)).to.be.true;
|
|
204
|
+
|
|
205
|
+
let error: Error | undefined;
|
|
206
|
+
try {
|
|
207
|
+
await cache.lock(resource, 10000, false);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
error = err as Error;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
expect(error).to.exist;
|
|
213
|
+
expect(error!.message).to.include('Failed to acquire lock');
|
|
214
|
+
expect(error!.message).to.include('after 1 attempts');
|
|
215
|
+
|
|
216
|
+
await cache.unlock(firstLock);
|
|
217
|
+
});
|
|
218
|
+
});
|
|
118
219
|
});
|
|
119
220
|
|
|
120
221
|
function sleep(ms: number): Promise<void> {
|