layercache 1.3.3 → 1.4.0
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/README.md +42 -41
- package/dist/{chunk-BORDQ3LA.js → chunk-7KMKQ6QZ.js} +15 -1
- package/dist/{chunk-5RCAX2BQ.js → chunk-FFZCC7EQ.js} +3 -3
- package/dist/{chunk-4PPBOOXT.js → chunk-KJDFYE5T.js} +38 -26
- package/dist/cli.cjs +9 -9
- package/dist/cli.js +4 -4
- package/dist/{edge-CUHTP9Bc.d.cts → edge-D2FpRlyS.d.cts} +74 -36
- package/dist/{edge-CUHTP9Bc.d.ts → edge-D2FpRlyS.d.ts} +74 -36
- package/dist/edge.cjs +9 -9
- package/dist/edge.d.cts +1 -1
- package/dist/edge.d.ts +1 -1
- package/dist/edge.js +2 -2
- package/dist/index.cjs +787 -466
- package/dist/index.d.cts +6 -6
- package/dist/index.d.ts +6 -6
- package/dist/index.js +682 -383
- package/package.json +6 -6
- package/benchmarks/direct.ts +0 -221
- package/benchmarks/edge-utils.ts +0 -28
- package/benchmarks/edge.ts +0 -491
- package/benchmarks/http.ts +0 -99
- package/benchmarks/latency.ts +0 -45
- package/benchmarks/memory-pressure.ts +0 -144
- package/benchmarks/multi-process-fanout.ts +0 -231
- package/benchmarks/multi-process-worker.ts +0 -151
- package/benchmarks/paths.ts +0 -25
- package/benchmarks/queue-amplification-utils.ts +0 -48
- package/benchmarks/queue-amplification.ts +0 -230
- package/benchmarks/redis-latency-proxy.ts +0 -100
- package/benchmarks/redis.ts +0 -107
- package/benchmarks/scenario-utils.ts +0 -38
- package/benchmarks/server.ts +0 -157
- package/benchmarks/slow-redis-latency.ts +0 -309
- package/benchmarks/slow-redis-utils.ts +0 -29
- package/benchmarks/slow-redis.ts +0 -47
- package/benchmarks/stampede.ts +0 -26
- package/benchmarks/stats.ts +0 -46
- package/benchmarks/workload.ts +0 -77
- package/examples/express-api/index.ts +0 -31
- package/examples/nextjs-api-routes/route.ts +0 -16
package/benchmarks/edge.ts
DELETED
|
@@ -1,491 +0,0 @@
|
|
|
1
|
-
import { CacheStack, MemoryLayer, RedisInvalidationBus, RedisLayer, RedisSingleFlightCoordinator } from '../src'
|
|
2
|
-
import { type OutageResult, buildPayloadString, normalizeOutageResult } from './edge-utils'
|
|
3
|
-
import {
|
|
4
|
-
createRedisClient,
|
|
5
|
-
ensureRedisContainer,
|
|
6
|
-
pauseRedisContainer,
|
|
7
|
-
stopRedisContainer,
|
|
8
|
-
unpauseRedisContainer,
|
|
9
|
-
waitForRedisReady
|
|
10
|
-
} from './redis'
|
|
11
|
-
import { createCountedFetcher, runConcurrent, summarizeScenario } from './scenario-utils'
|
|
12
|
-
import { type DurationSummary, summarizeDurations } from './stats'
|
|
13
|
-
|
|
14
|
-
const TTL_CONCURRENCY = 40
|
|
15
|
-
const TTL_RUNS = 5
|
|
16
|
-
const TTL_MS = 1_100
|
|
17
|
-
const PAYLOAD_SAMPLES = 60
|
|
18
|
-
const PAYLOAD_SMALL_BYTES = 1_024
|
|
19
|
-
const PAYLOAD_LARGE_BYTES = 1_024 * 1_024
|
|
20
|
-
const DISTRIBUTED_CONCURRENCY = 60
|
|
21
|
-
const COMMAND_TIMEOUT_MS = 200
|
|
22
|
-
|
|
23
|
-
interface ModeSummary extends DurationSummary {
|
|
24
|
-
mode: string
|
|
25
|
-
scenario: string
|
|
26
|
-
fetchCount?: number
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface InvalidationResult {
|
|
30
|
-
scenario: string
|
|
31
|
-
success: boolean
|
|
32
|
-
latencyMs: number
|
|
33
|
-
observedVersion: number | null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
interface DistributedSingleFlightResult {
|
|
37
|
-
scenario: string
|
|
38
|
-
concurrency: number
|
|
39
|
-
latencyMs: number
|
|
40
|
-
fetchCount: number
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function sleep(ms: number): Promise<void> {
|
|
44
|
-
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function measure<TResult>(task: () => Promise<TResult>): Promise<{ durationMs: number; result: TResult }> {
|
|
48
|
-
const startedAt = process.hrtime.bigint()
|
|
49
|
-
const result = await task()
|
|
50
|
-
return {
|
|
51
|
-
durationMs: Number(process.hrtime.bigint() - startedAt) / 1_000_000,
|
|
52
|
-
result
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function requireValue<T>(value: T | null, label: string): T {
|
|
57
|
-
if (value === null) {
|
|
58
|
-
throw new Error(`${label} unexpectedly returned null`)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return value
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
|
|
65
|
-
return Promise.race([
|
|
66
|
-
promise,
|
|
67
|
-
new Promise<T>((_, reject) => {
|
|
68
|
-
setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs)
|
|
69
|
-
})
|
|
70
|
-
])
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function runTtlExpiryStampede(): Promise<ModeSummary[]> {
|
|
74
|
-
const redis = createRedisClient()
|
|
75
|
-
await redis.ping()
|
|
76
|
-
|
|
77
|
-
const memoryCache = new CacheStack([new MemoryLayer({ ttl: 1, maxSize: 1_000 })], {
|
|
78
|
-
stampedePrevention: true
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
const layeredCache = new CacheStack(
|
|
82
|
-
[
|
|
83
|
-
new MemoryLayer({ ttl: 1, maxSize: 1_000 }),
|
|
84
|
-
new RedisLayer({ client: redis, ttl: 1, prefix: 'layercache-bench:edge:ttl:' })
|
|
85
|
-
],
|
|
86
|
-
{
|
|
87
|
-
stampedePrevention: true,
|
|
88
|
-
singleFlightCoordinator: new RedisSingleFlightCoordinator({
|
|
89
|
-
client: redis,
|
|
90
|
-
prefix: 'layercache-bench:edge:ttl:sf:'
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
const results: ModeSummary[] = []
|
|
97
|
-
|
|
98
|
-
for (const { mode, cache } of [
|
|
99
|
-
{ mode: 'memory', cache: memoryCache },
|
|
100
|
-
{ mode: 'layered', cache: layeredCache }
|
|
101
|
-
]) {
|
|
102
|
-
const samples: number[] = []
|
|
103
|
-
let fetchCount = 0
|
|
104
|
-
|
|
105
|
-
for (let index = 0; index < TTL_RUNS; index += 1) {
|
|
106
|
-
await cache.clear()
|
|
107
|
-
if (mode === 'layered') {
|
|
108
|
-
await redis.flushdb()
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
await cache.get(`ttl:key:${index}`, async () => ({ version: 1 }), { ttl: 1 })
|
|
112
|
-
await sleep(TTL_MS)
|
|
113
|
-
|
|
114
|
-
const fetcher = createCountedFetcher(async () => ({ version: 2 }))
|
|
115
|
-
const { durationMs } = await measure(() =>
|
|
116
|
-
runConcurrent(TTL_CONCURRENCY, async () =>
|
|
117
|
-
requireValue(await cache.get(`ttl:key:${index}`, () => fetcher.run(), { ttl: 1 }), `${mode} ttl`)
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
samples.push(durationMs)
|
|
122
|
-
fetchCount += fetcher.getCount()
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
results.push({
|
|
126
|
-
...summarizeScenario('ttl-expiry-stampede', samples, fetchCount),
|
|
127
|
-
mode,
|
|
128
|
-
scenario: 'ttl-expiry-stampede'
|
|
129
|
-
})
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return results
|
|
133
|
-
} finally {
|
|
134
|
-
await Promise.all([memoryCache.disconnect(), layeredCache.disconnect()])
|
|
135
|
-
await redis.quit()
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async function runPayloadSizeVariation(): Promise<ModeSummary[]> {
|
|
140
|
-
const redis = createRedisClient()
|
|
141
|
-
await redis.ping()
|
|
142
|
-
|
|
143
|
-
const memoryCache = new CacheStack([new MemoryLayer({ ttl: 60, maxSize: 100 })])
|
|
144
|
-
const redisCache = new CacheStack([
|
|
145
|
-
new RedisLayer({ client: redis, ttl: 300, prefix: 'layercache-bench:edge:payload:' })
|
|
146
|
-
])
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
const results: ModeSummary[] = []
|
|
150
|
-
|
|
151
|
-
for (const scenario of [
|
|
152
|
-
{ mode: 'memory-1kb', cache: memoryCache, bytes: PAYLOAD_SMALL_BYTES },
|
|
153
|
-
{ mode: 'memory-1mb', cache: memoryCache, bytes: PAYLOAD_LARGE_BYTES },
|
|
154
|
-
{ mode: 'redis-1kb', cache: redisCache, bytes: PAYLOAD_SMALL_BYTES },
|
|
155
|
-
{ mode: 'redis-1mb', cache: redisCache, bytes: PAYLOAD_LARGE_BYTES }
|
|
156
|
-
]) {
|
|
157
|
-
await scenario.cache.clear()
|
|
158
|
-
if (scenario.mode.startsWith('redis')) {
|
|
159
|
-
await redis.flushdb()
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
await scenario.cache.set(
|
|
163
|
-
`payload:${scenario.mode}`,
|
|
164
|
-
{ size: scenario.bytes, payload: buildPayloadString(scenario.bytes) },
|
|
165
|
-
{ ttl: 60 }
|
|
166
|
-
)
|
|
167
|
-
|
|
168
|
-
const samples: number[] = []
|
|
169
|
-
for (let index = 0; index < PAYLOAD_SAMPLES; index += 1) {
|
|
170
|
-
const { durationMs } = await measure(async () => {
|
|
171
|
-
requireValue(await scenario.cache.get(`payload:${scenario.mode}`), `${scenario.mode} payload`)
|
|
172
|
-
})
|
|
173
|
-
samples.push(durationMs)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
results.push({
|
|
177
|
-
...summarizeDurations('payload-warm-hit', samples),
|
|
178
|
-
mode: scenario.mode,
|
|
179
|
-
scenario: 'payload-warm-hit'
|
|
180
|
-
})
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return results
|
|
184
|
-
} finally {
|
|
185
|
-
await Promise.all([memoryCache.disconnect(), redisCache.disconnect()])
|
|
186
|
-
await redis.quit()
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async function runRedisOutageScenario(): Promise<OutageResult[]> {
|
|
191
|
-
const strictRedis = createRedisClient()
|
|
192
|
-
const gracefulRedis = createRedisClient()
|
|
193
|
-
await Promise.all([strictRedis.ping(), gracefulRedis.ping()])
|
|
194
|
-
|
|
195
|
-
const strictCache = new CacheStack(
|
|
196
|
-
[
|
|
197
|
-
new MemoryLayer({ ttl: 60, maxSize: 500 }),
|
|
198
|
-
new RedisLayer({
|
|
199
|
-
client: strictRedis,
|
|
200
|
-
ttl: 300,
|
|
201
|
-
prefix: 'layercache-bench:edge:strict:',
|
|
202
|
-
commandTimeoutMs: COMMAND_TIMEOUT_MS
|
|
203
|
-
})
|
|
204
|
-
],
|
|
205
|
-
{
|
|
206
|
-
stampedePrevention: true,
|
|
207
|
-
singleFlightCoordinator: new RedisSingleFlightCoordinator({
|
|
208
|
-
client: strictRedis,
|
|
209
|
-
prefix: 'layercache-bench:edge:strict:sf:',
|
|
210
|
-
commandTimeoutMs: COMMAND_TIMEOUT_MS
|
|
211
|
-
})
|
|
212
|
-
}
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
const gracefulCache = new CacheStack(
|
|
216
|
-
[
|
|
217
|
-
new MemoryLayer({ ttl: 60, maxSize: 500 }),
|
|
218
|
-
new RedisLayer({
|
|
219
|
-
client: gracefulRedis,
|
|
220
|
-
ttl: 300,
|
|
221
|
-
prefix: 'layercache-bench:edge:graceful:',
|
|
222
|
-
commandTimeoutMs: COMMAND_TIMEOUT_MS
|
|
223
|
-
})
|
|
224
|
-
],
|
|
225
|
-
{
|
|
226
|
-
stampedePrevention: true,
|
|
227
|
-
gracefulDegradation: { retryAfterMs: 10_000 },
|
|
228
|
-
singleFlightCoordinator: new RedisSingleFlightCoordinator({
|
|
229
|
-
client: gracefulRedis,
|
|
230
|
-
prefix: 'layercache-bench:edge:graceful:sf:',
|
|
231
|
-
commandTimeoutMs: COMMAND_TIMEOUT_MS
|
|
232
|
-
})
|
|
233
|
-
}
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
try {
|
|
237
|
-
await strictCache.clear()
|
|
238
|
-
await gracefulCache.clear()
|
|
239
|
-
await strictRedis.flushdb()
|
|
240
|
-
|
|
241
|
-
await strictCache.get('outage:warm', async () => ({ version: 'warm-strict' }), { ttl: 60 })
|
|
242
|
-
await gracefulCache.get('outage:warm', async () => ({ version: 'warm-graceful' }), { ttl: 60 })
|
|
243
|
-
|
|
244
|
-
await pauseRedisContainer()
|
|
245
|
-
|
|
246
|
-
const hotResults = await Promise.all([
|
|
247
|
-
measure(async () => {
|
|
248
|
-
requireValue(await strictCache.get('outage:warm'), 'strict hot')
|
|
249
|
-
})
|
|
250
|
-
.then(({ durationMs }) => normalizeOutageResult('strict-hot-hit', true, durationMs))
|
|
251
|
-
.catch((error) =>
|
|
252
|
-
normalizeOutageResult('strict-hot-hit', false, 0, error instanceof Error ? error.message : String(error))
|
|
253
|
-
),
|
|
254
|
-
measure(async () => {
|
|
255
|
-
requireValue(await gracefulCache.get('outage:warm'), 'graceful hot')
|
|
256
|
-
})
|
|
257
|
-
.then(({ durationMs }) => normalizeOutageResult('graceful-hot-hit', true, durationMs))
|
|
258
|
-
.catch((error) =>
|
|
259
|
-
normalizeOutageResult('graceful-hot-hit', false, 0, error instanceof Error ? error.message : String(error))
|
|
260
|
-
)
|
|
261
|
-
])
|
|
262
|
-
|
|
263
|
-
const coldStrict = await measure(() =>
|
|
264
|
-
withTimeout(
|
|
265
|
-
strictCache.get('outage:cold:strict', async () => ({ version: 'strict-cold' }), { ttl: 60 }),
|
|
266
|
-
2_000,
|
|
267
|
-
'strict cold miss'
|
|
268
|
-
)
|
|
269
|
-
)
|
|
270
|
-
.then(({ durationMs }) => normalizeOutageResult('strict-cold-miss', true, durationMs))
|
|
271
|
-
.catch((error) =>
|
|
272
|
-
normalizeOutageResult('strict-cold-miss', false, 0, error instanceof Error ? error.message : String(error))
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
const coldGraceful = await measure(() =>
|
|
276
|
-
withTimeout(
|
|
277
|
-
gracefulCache.get('outage:cold:graceful', async () => ({ version: 'graceful-cold' }), { ttl: 60 }),
|
|
278
|
-
2_000,
|
|
279
|
-
'graceful cold miss'
|
|
280
|
-
)
|
|
281
|
-
)
|
|
282
|
-
.then(({ durationMs }) => normalizeOutageResult('graceful-cold-miss', true, durationMs))
|
|
283
|
-
.catch((error) =>
|
|
284
|
-
normalizeOutageResult('graceful-cold-miss', false, 0, error instanceof Error ? error.message : String(error))
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
return [...hotResults, coldStrict, coldGraceful]
|
|
288
|
-
} finally {
|
|
289
|
-
await unpauseRedisContainer()
|
|
290
|
-
await waitForRedisReady()
|
|
291
|
-
await Promise.all([strictCache.disconnect(), gracefulCache.disconnect()])
|
|
292
|
-
await Promise.all([strictRedis.quit(), gracefulRedis.quit()])
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
async function runMultiInstanceInvalidation(): Promise<InvalidationResult> {
|
|
297
|
-
const publisher = createRedisClient()
|
|
298
|
-
const subscriberA = createRedisClient()
|
|
299
|
-
const subscriberB = createRedisClient()
|
|
300
|
-
const dataA = createRedisClient()
|
|
301
|
-
const dataB = createRedisClient()
|
|
302
|
-
|
|
303
|
-
await Promise.all([publisher.ping(), subscriberA.ping(), subscriberB.ping(), dataA.ping(), dataB.ping()])
|
|
304
|
-
|
|
305
|
-
const busA = new RedisInvalidationBus({
|
|
306
|
-
publisher,
|
|
307
|
-
subscriber: subscriberA,
|
|
308
|
-
channel: 'layercache-bench:edge:invalidation'
|
|
309
|
-
})
|
|
310
|
-
const busB = new RedisInvalidationBus({
|
|
311
|
-
publisher,
|
|
312
|
-
subscriber: subscriberB,
|
|
313
|
-
channel: 'layercache-bench:edge:invalidation'
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
const cacheA = new CacheStack(
|
|
317
|
-
[
|
|
318
|
-
new MemoryLayer({ ttl: 60, maxSize: 100 }),
|
|
319
|
-
new RedisLayer({ client: dataA, ttl: 300, prefix: 'layercache-bench:edge:invalidation:' })
|
|
320
|
-
],
|
|
321
|
-
{
|
|
322
|
-
invalidationBus: busA,
|
|
323
|
-
broadcastL1Invalidation: true
|
|
324
|
-
}
|
|
325
|
-
)
|
|
326
|
-
const cacheB = new CacheStack(
|
|
327
|
-
[
|
|
328
|
-
new MemoryLayer({ ttl: 60, maxSize: 100 }),
|
|
329
|
-
new RedisLayer({ client: dataB, ttl: 300, prefix: 'layercache-bench:edge:invalidation:' })
|
|
330
|
-
],
|
|
331
|
-
{
|
|
332
|
-
invalidationBus: busB,
|
|
333
|
-
broadcastL1Invalidation: true
|
|
334
|
-
}
|
|
335
|
-
)
|
|
336
|
-
|
|
337
|
-
try {
|
|
338
|
-
await cacheA.clear()
|
|
339
|
-
await dataA.flushdb()
|
|
340
|
-
|
|
341
|
-
await cacheA.get('shared:key', async () => ({ version: 1 }), { ttl: 60 })
|
|
342
|
-
await cacheB.get('shared:key', async () => ({ version: 1 }), { ttl: 60 })
|
|
343
|
-
|
|
344
|
-
await cacheA.delete('shared:key')
|
|
345
|
-
await cacheA.get('shared:key', async () => ({ version: 2 }), { ttl: 60 })
|
|
346
|
-
|
|
347
|
-
const startedAt = performance.now()
|
|
348
|
-
let observedVersion: number | null = null
|
|
349
|
-
for (let attempt = 0; attempt < 40; attempt += 1) {
|
|
350
|
-
const value = await cacheB.get<{ version: number }>('shared:key')
|
|
351
|
-
observedVersion = value?.version ?? null
|
|
352
|
-
if (observedVersion === 2) {
|
|
353
|
-
return {
|
|
354
|
-
scenario: 'multi-instance-invalidation',
|
|
355
|
-
success: true,
|
|
356
|
-
latencyMs: Number((performance.now() - startedAt).toFixed(3)),
|
|
357
|
-
observedVersion
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
await sleep(25)
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return {
|
|
365
|
-
scenario: 'multi-instance-invalidation',
|
|
366
|
-
success: false,
|
|
367
|
-
latencyMs: Number((performance.now() - startedAt).toFixed(3)),
|
|
368
|
-
observedVersion
|
|
369
|
-
}
|
|
370
|
-
} finally {
|
|
371
|
-
await Promise.all([cacheA.disconnect(), cacheB.disconnect()])
|
|
372
|
-
await Promise.all([publisher.quit(), subscriberA.quit(), subscriberB.quit(), dataA.quit(), dataB.quit()])
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
async function runDistributedSingleFlight(): Promise<DistributedSingleFlightResult> {
|
|
377
|
-
const redisA = createRedisClient()
|
|
378
|
-
const redisB = createRedisClient()
|
|
379
|
-
const coordinatorClient = createRedisClient()
|
|
380
|
-
|
|
381
|
-
await Promise.all([redisA.ping(), redisB.ping(), coordinatorClient.ping()])
|
|
382
|
-
|
|
383
|
-
const coordinator = new RedisSingleFlightCoordinator({
|
|
384
|
-
client: coordinatorClient,
|
|
385
|
-
prefix: 'layercache-bench:edge:distributed:sf:'
|
|
386
|
-
})
|
|
387
|
-
|
|
388
|
-
const cacheA = new CacheStack(
|
|
389
|
-
[
|
|
390
|
-
new MemoryLayer({ ttl: 60, maxSize: 100 }),
|
|
391
|
-
new RedisLayer({ client: redisA, ttl: 300, prefix: 'layercache-bench:edge:distributed:' })
|
|
392
|
-
],
|
|
393
|
-
{
|
|
394
|
-
stampedePrevention: true,
|
|
395
|
-
singleFlightCoordinator: coordinator
|
|
396
|
-
}
|
|
397
|
-
)
|
|
398
|
-
const cacheB = new CacheStack(
|
|
399
|
-
[
|
|
400
|
-
new MemoryLayer({ ttl: 60, maxSize: 100 }),
|
|
401
|
-
new RedisLayer({ client: redisB, ttl: 300, prefix: 'layercache-bench:edge:distributed:' })
|
|
402
|
-
],
|
|
403
|
-
{
|
|
404
|
-
stampedePrevention: true,
|
|
405
|
-
singleFlightCoordinator: coordinator
|
|
406
|
-
}
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
try {
|
|
410
|
-
await cacheA.clear()
|
|
411
|
-
await redisA.flushdb()
|
|
412
|
-
|
|
413
|
-
let fetchCount = 0
|
|
414
|
-
const fetchUser = async () => {
|
|
415
|
-
fetchCount += 1
|
|
416
|
-
await sleep(25)
|
|
417
|
-
return { id: 1 }
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
const startedAt = performance.now()
|
|
421
|
-
await runConcurrent(DISTRIBUTED_CONCURRENCY, (index) =>
|
|
422
|
-
(index % 2 === 0 ? cacheA : cacheB)
|
|
423
|
-
.get('distributed:user:1', fetchUser)
|
|
424
|
-
.then((value) => requireValue(value, 'distributed'))
|
|
425
|
-
)
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
scenario: 'distributed-single-flight',
|
|
429
|
-
concurrency: DISTRIBUTED_CONCURRENCY,
|
|
430
|
-
latencyMs: Number((performance.now() - startedAt).toFixed(3)),
|
|
431
|
-
fetchCount
|
|
432
|
-
}
|
|
433
|
-
} finally {
|
|
434
|
-
await Promise.all([cacheA.disconnect(), cacheB.disconnect()])
|
|
435
|
-
await Promise.all([redisA.quit(), redisB.quit(), coordinatorClient.quit()])
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
async function main(): Promise<void> {
|
|
440
|
-
await ensureRedisContainer()
|
|
441
|
-
await waitForRedisReady()
|
|
442
|
-
|
|
443
|
-
try {
|
|
444
|
-
const ttlResults = await runTtlExpiryStampede()
|
|
445
|
-
const payloadResults = await runPayloadSizeVariation()
|
|
446
|
-
const outageResults = await runRedisOutageScenario()
|
|
447
|
-
const invalidationResult = await runMultiInstanceInvalidation()
|
|
448
|
-
const distributedResult = await runDistributedSingleFlight()
|
|
449
|
-
|
|
450
|
-
console.table([
|
|
451
|
-
...ttlResults.map((result) => ({
|
|
452
|
-
mode: result.mode,
|
|
453
|
-
scenario: result.scenario,
|
|
454
|
-
avgMs: result.avgMs,
|
|
455
|
-
p95Ms: result.p95Ms,
|
|
456
|
-
fetchCount: result.fetchCount
|
|
457
|
-
})),
|
|
458
|
-
...payloadResults.map((result) => ({
|
|
459
|
-
mode: result.mode,
|
|
460
|
-
scenario: result.scenario,
|
|
461
|
-
avgMs: result.avgMs,
|
|
462
|
-
p95Ms: result.p95Ms
|
|
463
|
-
})),
|
|
464
|
-
...outageResults,
|
|
465
|
-
invalidationResult,
|
|
466
|
-
distributedResult
|
|
467
|
-
])
|
|
468
|
-
console.log(
|
|
469
|
-
JSON.stringify(
|
|
470
|
-
{
|
|
471
|
-
type: 'edge-benchmark',
|
|
472
|
-
ttlResults,
|
|
473
|
-
payloadResults,
|
|
474
|
-
outageResults,
|
|
475
|
-
invalidationResult,
|
|
476
|
-
distributedResult
|
|
477
|
-
},
|
|
478
|
-
null,
|
|
479
|
-
2
|
|
480
|
-
)
|
|
481
|
-
)
|
|
482
|
-
} finally {
|
|
483
|
-
await unpauseRedisContainer().catch(() => {})
|
|
484
|
-
await stopRedisContainer()
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
void main().catch((error) => {
|
|
489
|
-
console.error(error)
|
|
490
|
-
process.exitCode = 1
|
|
491
|
-
})
|
package/benchmarks/http.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import autocannon, { type Result } from 'autocannon'
|
|
2
|
-
import { createRedisClient, ensureRedisContainer, stopRedisContainer, waitForRedisReady } from './redis'
|
|
3
|
-
import { startBenchmarkServer } from './server'
|
|
4
|
-
|
|
5
|
-
interface HttpBenchmarkSummary {
|
|
6
|
-
route: string
|
|
7
|
-
coldStartMs: number
|
|
8
|
-
averageLatencyMs: number
|
|
9
|
-
p97_5LatencyMs: number
|
|
10
|
-
maxLatencyMs: number
|
|
11
|
-
requestsPerSec: number
|
|
12
|
-
throughputBytesPerSec: number
|
|
13
|
-
errors: number
|
|
14
|
-
timeouts: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function singleRequestLatency(url: string): Promise<number> {
|
|
18
|
-
const startedAt = process.hrtime.bigint()
|
|
19
|
-
const response = await fetch(url)
|
|
20
|
-
await response.arrayBuffer()
|
|
21
|
-
|
|
22
|
-
return Number(process.hrtime.bigint() - startedAt) / 1_000_000
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function runAutocannon(url: string): Promise<Result> {
|
|
26
|
-
return new Promise((resolve, reject) => {
|
|
27
|
-
const instance = autocannon(
|
|
28
|
-
{
|
|
29
|
-
url,
|
|
30
|
-
connections: 40,
|
|
31
|
-
duration: 8,
|
|
32
|
-
pipelining: 1
|
|
33
|
-
},
|
|
34
|
-
(error: Error | null, result: Result) => {
|
|
35
|
-
if (error) {
|
|
36
|
-
reject(error)
|
|
37
|
-
return
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
resolve(result)
|
|
41
|
-
}
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
instance.on('error', reject)
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function summarizeHttpRoute(route: string, coldStartMs: number, result: Result): HttpBenchmarkSummary {
|
|
49
|
-
return {
|
|
50
|
-
route,
|
|
51
|
-
coldStartMs: Number(coldStartMs.toFixed(3)),
|
|
52
|
-
averageLatencyMs: Number(result.latency.average.toFixed(3)),
|
|
53
|
-
p97_5LatencyMs: Number(result.latency.p97_5.toFixed(3)),
|
|
54
|
-
maxLatencyMs: Number(result.latency.max.toFixed(3)),
|
|
55
|
-
requestsPerSec: Number(result.requests.average.toFixed(3)),
|
|
56
|
-
throughputBytesPerSec: Number(result.throughput.average.toFixed(3)),
|
|
57
|
-
errors: result.errors,
|
|
58
|
-
timeouts: result.timeouts
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async function main(): Promise<void> {
|
|
63
|
-
await ensureRedisContainer()
|
|
64
|
-
|
|
65
|
-
await waitForRedisReady()
|
|
66
|
-
const redis = createRedisClient()
|
|
67
|
-
await redis.ping()
|
|
68
|
-
|
|
69
|
-
const server = await startBenchmarkServer(redis)
|
|
70
|
-
const baseUrl = `http://127.0.0.1:${server.port}`
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
const routes = ['/nocache', '/memory', '/layered']
|
|
74
|
-
const results: HttpBenchmarkSummary[] = []
|
|
75
|
-
|
|
76
|
-
for (const route of routes) {
|
|
77
|
-
await server.reset()
|
|
78
|
-
if (route !== '/nocache') {
|
|
79
|
-
await server.warm()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const coldStartMs = await singleRequestLatency(`${baseUrl}${route}`)
|
|
83
|
-
const result = await runAutocannon(`${baseUrl}${route}`)
|
|
84
|
-
results.push(summarizeHttpRoute(route, coldStartMs, result))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
console.table(results)
|
|
88
|
-
console.log(JSON.stringify({ type: 'http-benchmark', results }, null, 2))
|
|
89
|
-
} finally {
|
|
90
|
-
await server.close()
|
|
91
|
-
await redis.quit()
|
|
92
|
-
await stopRedisContainer()
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
void main().catch((error) => {
|
|
97
|
-
console.error(error)
|
|
98
|
-
process.exitCode = 1
|
|
99
|
-
})
|
package/benchmarks/latency.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { performance } from 'node:perf_hooks'
|
|
2
|
-
import Redis from 'ioredis-mock'
|
|
3
|
-
import { CacheStack, MemoryLayer, RedisLayer } from '../src'
|
|
4
|
-
|
|
5
|
-
async function main(): Promise<void> {
|
|
6
|
-
const iterations = 5_000
|
|
7
|
-
const redis = new Redis()
|
|
8
|
-
const cache = new CacheStack([
|
|
9
|
-
new MemoryLayer({ ttl: 60, maxSize: 10_000 }),
|
|
10
|
-
new RedisLayer({ client: redis, ttl: 300 })
|
|
11
|
-
])
|
|
12
|
-
|
|
13
|
-
await cache.set('bench:key', { ok: true })
|
|
14
|
-
|
|
15
|
-
const memoryStart = performance.now()
|
|
16
|
-
for (let index = 0; index < iterations; index += 1) {
|
|
17
|
-
await cache.get('bench:key')
|
|
18
|
-
}
|
|
19
|
-
const memoryElapsed = performance.now() - memoryStart
|
|
20
|
-
|
|
21
|
-
await redis.del('bench:key')
|
|
22
|
-
await redis.set('bench:key', JSON.stringify({ ok: true }))
|
|
23
|
-
|
|
24
|
-
const redisOnlyStart = performance.now()
|
|
25
|
-
for (let index = 0; index < iterations; index += 1) {
|
|
26
|
-
await cache.get('bench:key')
|
|
27
|
-
await cache.delete('bench:key')
|
|
28
|
-
await redis.set('bench:key', JSON.stringify({ ok: true }))
|
|
29
|
-
}
|
|
30
|
-
const redisOnlyElapsed = performance.now() - redisOnlyStart
|
|
31
|
-
|
|
32
|
-
const noCacheStart = performance.now()
|
|
33
|
-
for (let index = 0; index < iterations; index += 1) {
|
|
34
|
-
await new Promise((resolve) => setTimeout(resolve, 1))
|
|
35
|
-
}
|
|
36
|
-
const noCacheElapsed = performance.now() - noCacheStart
|
|
37
|
-
|
|
38
|
-
console.table({
|
|
39
|
-
l1MemoryAvgMs: memoryElapsed / iterations,
|
|
40
|
-
l2RedisAvgMs: redisOnlyElapsed / iterations,
|
|
41
|
-
noCacheAvgMs: noCacheElapsed / iterations
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
void main()
|