mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-27 04:07:05 +00:00
🕯️ fix: Decay Violation Scores With a Configurable TTL (#15153)
This commit is contained in:
parent
c52ba4efdb
commit
b6e3cf46d2
5 changed files with 116 additions and 3 deletions
|
|
@ -677,6 +677,9 @@ BAN_VIOLATIONS=true
|
|||
BAN_DURATION=1000 * 60 * 60 * 2
|
||||
BAN_INTERVAL=20
|
||||
|
||||
# Violation scores expire after this long (in ms) without new violations; 0 = never expire
|
||||
VIOLATION_SCORE_TTL=1000 * 60 * 60
|
||||
|
||||
LOGIN_VIOLATION_SCORE=1
|
||||
REGISTRATION_VIOLATION_SCORE=1
|
||||
CONCURRENT_VIOLATION_SCORE=1
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ describe('cacheConfig', () => {
|
|||
delete process.env.REDIS_CLUSTER_SAFE_DELETE;
|
||||
delete process.env.REDIS_PING_INTERVAL;
|
||||
delete process.env.FORCED_IN_MEMORY_CACHE_NAMESPACES;
|
||||
delete process.env.VIOLATION_SCORE_TTL;
|
||||
|
||||
// Clear module cache
|
||||
jest.resetModules();
|
||||
|
|
@ -263,4 +264,39 @@ describe('cacheConfig', () => {
|
|||
expect(cacheConfig.FORCED_IN_MEMORY_CACHE_NAMESPACES).toEqual(['CONFIG_STORE', 'APP_CONFIG']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VIOLATION_SCORE_TTL configuration', () => {
|
||||
test('should default to one hour when not set', async () => {
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.VIOLATION_SCORE_TTL).toBe(3600000);
|
||||
});
|
||||
|
||||
test('should evaluate math expressions from the environment', async () => {
|
||||
process.env.VIOLATION_SCORE_TTL = '1000 * 60 * 60 * 24';
|
||||
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.VIOLATION_SCORE_TTL).toBe(86400000);
|
||||
});
|
||||
|
||||
test('should disable expiry when set to 0', async () => {
|
||||
process.env.VIOLATION_SCORE_TTL = '0';
|
||||
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.VIOLATION_SCORE_TTL).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should disable expiry for negative values', async () => {
|
||||
process.env.VIOLATION_SCORE_TTL = '-1000';
|
||||
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.VIOLATION_SCORE_TTL).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should fall back to the default on invalid input', async () => {
|
||||
process.env.VIOLATION_SCORE_TTL = 'not-a-duration';
|
||||
|
||||
const { cacheConfig } = await import('../cacheConfig');
|
||||
expect(cacheConfig.VIOLATION_SCORE_TTL).toBe(3600000);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
56
packages/api/src/cache/__tests__/cacheFactory/violationCache.spec.ts
vendored
Normal file
56
packages/api/src/cache/__tests__/cacheFactory/violationCache.spec.ts
vendored
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
describe('violationCache TTL defaults', () => {
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
delete process.env.USE_REDIS;
|
||||
delete process.env.REDIS_URI;
|
||||
delete process.env.VIOLATION_SCORE_TTL;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
test('applies the default violation score TTL when none is given', async () => {
|
||||
const { violationCache } = await import('../../cacheFactory');
|
||||
const cache = violationCache('logins');
|
||||
|
||||
expect(cache.opts.ttl).toBe(3600000);
|
||||
expect(cache.opts.namespace).toBe('violations:logins');
|
||||
});
|
||||
|
||||
test('an explicit TTL overrides the default', async () => {
|
||||
const { violationCache } = await import('../../cacheFactory');
|
||||
const cache = violationCache('logins', 60000);
|
||||
|
||||
expect(cache.opts.ttl).toBe(60000);
|
||||
});
|
||||
|
||||
test('honors VIOLATION_SCORE_TTL from the environment', async () => {
|
||||
process.env.VIOLATION_SCORE_TTL = '1000 * 60 * 5';
|
||||
|
||||
const { violationCache } = await import('../../cacheFactory');
|
||||
expect(violationCache('concurrent').opts.ttl).toBe(300000);
|
||||
});
|
||||
|
||||
test('VIOLATION_SCORE_TTL=0 disables expiry', async () => {
|
||||
process.env.VIOLATION_SCORE_TTL = '0';
|
||||
|
||||
const { violationCache } = await import('../../cacheFactory');
|
||||
expect(violationCache('concurrent').opts.ttl).toBeUndefined();
|
||||
});
|
||||
|
||||
test('expires violation entries once the TTL elapses', async () => {
|
||||
const { violationCache } = await import('../../cacheFactory');
|
||||
const cache = violationCache('expiry-check', 500);
|
||||
|
||||
await cache.set('user-1', 3);
|
||||
await expect(cache.get('user-1')).resolves.toBe(3);
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 800));
|
||||
await expect(cache.get('user-1')).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
16
packages/api/src/cache/cacheConfig.ts
vendored
16
packages/api/src/cache/cacheConfig.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
import { readFileSync, existsSync } from 'fs';
|
||||
import { logger } from '@librechat/data-schemas';
|
||||
import { CacheKeys } from 'librechat-data-provider';
|
||||
import { Time, CacheKeys } from 'librechat-data-provider';
|
||||
import { math, isEnabled } from '~/utils';
|
||||
|
||||
// To ensure that different deployments do not interfere with each other's cache, we use a prefix for the Redis keys.
|
||||
|
|
@ -48,6 +48,12 @@ if (FORCED_IN_MEMORY_CACHE_NAMESPACES.length > 0) {
|
|||
}
|
||||
}
|
||||
|
||||
// Violation scores expire after this long without new violations; every violation write
|
||||
// restarts the countdown. Non-positive values disable expiry, restoring the legacy
|
||||
// accumulate-forever behavior.
|
||||
const VIOLATION_SCORE_TTL_MS = math(process.env.VIOLATION_SCORE_TTL, Time.ONE_HOUR);
|
||||
const VIOLATION_SCORE_TTL = VIOLATION_SCORE_TTL_MS > 0 ? VIOLATION_SCORE_TTL_MS : undefined;
|
||||
|
||||
/** Helper function to safely read Redis CA certificate from file
|
||||
* @returns {string|null} The contents of the CA certificate file, or null if not set or on error
|
||||
*/
|
||||
|
|
@ -105,6 +111,13 @@ const cacheConfig: {
|
|||
CI: boolean;
|
||||
DEBUG_MEMORY_CACHE: boolean;
|
||||
BAN_DURATION: number; // 2 hours
|
||||
/**
|
||||
* TTL in ms for violation scores: a score expires after this long without new violations
|
||||
* (each violation write restarts the countdown). `undefined` — from a non-positive
|
||||
* setting — disables expiry so scores accumulate forever.
|
||||
* @default 3600000 (1 hour)
|
||||
*/
|
||||
VIOLATION_SCORE_TTL: number | undefined;
|
||||
/**
|
||||
* Number of keys to delete in each batch during Redis DEL operations.
|
||||
* In cluster mode, keys are deleted individually in parallel chunks to avoid CROSSSLOT errors.
|
||||
|
|
@ -176,6 +189,7 @@ const cacheConfig: {
|
|||
DEBUG_MEMORY_CACHE: isEnabled(process.env.DEBUG_MEMORY_CACHE),
|
||||
|
||||
BAN_DURATION: math(process.env.BAN_DURATION, 7200000), // 2 hours
|
||||
VIOLATION_SCORE_TTL,
|
||||
|
||||
/**
|
||||
* Number of keys to delete in each batch during Redis DEL operations.
|
||||
|
|
|
|||
8
packages/api/src/cache/cacheFactory.ts
vendored
8
packages/api/src/cache/cacheFactory.ts
vendored
|
|
@ -116,10 +116,14 @@ export const tokenConfigCache = (): Keyv =>
|
|||
* Creates a cache instance for storing violation data.
|
||||
* Uses a file-based fallback store if Redis is not enabled.
|
||||
* @param namespace - The cache namespace for violations.
|
||||
* @param ttl - Time to live for cache entries.
|
||||
* @param ttl - Time to live for cache entries. Defaults to `cacheConfig.VIOLATION_SCORE_TTL`
|
||||
* so violation scores decay instead of accumulating forever; each write restarts the countdown.
|
||||
* @returns Cache instance for violations.
|
||||
*/
|
||||
export const violationCache = (namespace: string, ttl?: number): Keyv => {
|
||||
export const violationCache = (
|
||||
namespace: string,
|
||||
ttl: number | undefined = cacheConfig.VIOLATION_SCORE_TTL,
|
||||
): Keyv => {
|
||||
return standardCache(`violations:${namespace}`, ttl, violationFile);
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue