1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

Client Side Caching (#2947)

* CSC POC ontop of Parser

* add csc file that weren't merged after patch

* address review comments

* nits to try and fix github

* last change from review

* Update client-side cache and improve documentation

* Add client side caching RESP3 validation

* Add documentation for RESP and unstableResp3 options

* Add comprehensive cache statistics

The `CacheStats` class provides detailed metrics like hit/miss counts,
load success/failure counts, total load time, and eviction counts.
It also offers derived metrics such as hit/miss rates, load failure rate,
and average load penalty. The design is inspired by Caffeine.

`BasicClientSideCache` now uses a `StatsCounter` to accumulate these
statistics, exposed via a new `stats()` method. The previous
`cacheHits()` and `cacheMisses()` methods have been removed.

A `recordStats` option (default: true) in `ClientSideCacheConfig`
allows disabling statistics collection.

---------

Co-authored-by: Shaya Potter <shaya@redislabs.com>
This commit is contained in:
Bobby I.
2025-05-19 15:11:47 +03:00
committed by GitHub
parent 6f961bd715
commit f01f1014cb
25 changed files with 2330 additions and 101 deletions

View File

@@ -5,11 +5,59 @@ import { RESP_TYPES } from '../RESP/decoder';
import { WatchError } from "../errors";
import { RedisSentinelConfig, SentinelFramework } from "./test-util";
import { RedisSentinelEvent, RedisSentinelType, RedisSentinelClientType, RedisNode } from "./types";
import RedisSentinel from "./index";
import { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, NumberReply } from '../RESP/types';
import { promisify } from 'node:util';
import { exec } from 'node:child_process';
import { BasicPooledClientSideCache } from '../client/cache'
import { once } from 'node:events'
const execAsync = promisify(exec);
describe('RedisSentinel', () => {
describe('initialization', () => {
describe('clientSideCache validation', () => {
const clientSideCacheConfig = { ttl: 0, maxEntries: 0 };
const options = {
name: 'mymaster',
sentinelRootNodes: [
{ host: 'localhost', port: 26379 }
]
};
it('should throw error when clientSideCache is enabled with RESP 2', () => {
assert.throws(
() => RedisSentinel.create({
...options,
clientSideCache: clientSideCacheConfig,
RESP: 2 as const,
}),
new Error('Client Side Caching is only supported with RESP3')
);
});
it('should throw error when clientSideCache is enabled with RESP undefined', () => {
assert.throws(
() => RedisSentinel.create({
...options,
clientSideCache: clientSideCacheConfig,
}),
new Error('Client Side Caching is only supported with RESP3')
);
});
it('should not throw when clientSideCache is enabled with RESP 3', () => {
assert.doesNotThrow(() =>
RedisSentinel.create({
...options,
clientSideCache: clientSideCacheConfig,
RESP: 3 as const,
})
);
});
});
});
});
[GLOBAL.SENTINEL.OPEN, GLOBAL.SENTINEL.PASSWORD].forEach(testOptions => {
const passIndex = testOptions.serverArguments.indexOf('--requirepass')+1;
let password: string | undefined = undefined;
@@ -967,6 +1015,34 @@ describe.skip('legacy tests', () => {
tracer.push("added node and waiting on added promise");
await nodeAddedPromise;
})
it('with client side caching', async function() {
this.timeout(30000);
const csc = new BasicPooledClientSideCache();
sentinel = frame.getSentinelClient({nodeClientOptions: {RESP: 3}, clientSideCache: csc, masterPoolSize: 5});
await sentinel.connect();
await sentinel.set('x', 1);
await sentinel.get('x');
await sentinel.get('x');
await sentinel.get('x');
await sentinel.get('x');
assert.equal(1, csc.cacheMisses());
assert.equal(3, csc.cacheHits());
const invalidatePromise = once(csc, 'invalidate');
await sentinel.set('x', 2);
await invalidatePromise;
await sentinel.get('x');
await sentinel.get('x');
await sentinel.get('x');
await sentinel.get('x');
assert.equal(csc.cacheMisses(), 2);
assert.equal(csc.cacheHits(), 6);
})
});
});