🔒 fix: Prevent Race Condition in RedisJobStore (#11764)
Some checks failed
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Has been cancelled
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Has been cancelled
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Has been cancelled

* 🔧 fix: Optimize job update logic in RedisJobStore

- Refactored the updateJob method to use a Lua script for atomic updates, ensuring that jobs are only updated if they exist in Redis.
- Removed redundant existence check and streamlined the serialization process for better performance and clarity.

* 🔧 test: Add race condition tests for RedisJobStore

- Introduced tests to verify behavior of updateJob after deleteJob, ensuring no job hash is recreated post-deletion.
- Added checks for orphan keys when concurrent deleteJob and updateJob operations occur, enhancing reliability in job management.

* 🔧 test: Refactor Redis client readiness checks in violationCache tests

- Introduced a new helper function `waitForRedisClients` to streamline the readiness checks for Redis clients in the violationCache integration tests.
- Removed redundant Redis client readiness checks from individual test cases, improving code clarity and maintainability.

* 🔧 fix: Update RedisJobStore to use hset instead of hmset

- Replaced instances of `hmset` with `hset` in the RedisJobStore implementation to align with the latest Redis command updates.
- Updated Lua script in the eval method to reflect the change, ensuring consistent job handling in both cluster and non-cluster modes.
This commit is contained in:
Danny Avila 2026-02-12 18:47:57 -05:00 committed by GitHub
parent b8c31e7314
commit e142ab72da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 96 additions and 43 deletions

View file

@ -20,6 +20,24 @@ interface ViolationData {
};
}
/** Waits for both Redis clients (ioredis + keyv/node-redis) to be ready */
async function waitForRedisClients() {
const redisClients = await import('../../redisClients');
const { ioredisClient, keyvRedisClientReady } = redisClients;
if (ioredisClient && ioredisClient.status !== 'ready') {
await new Promise<void>((resolve) => {
ioredisClient.once('ready', resolve);
});
}
if (keyvRedisClientReady) {
await keyvRedisClientReady;
}
return redisClients;
}
describe('violationCache', () => {
let originalEnv: NodeJS.ProcessEnv;
@ -45,17 +63,9 @@ describe('violationCache', () => {
test('should create violation cache with Redis when USE_REDIS is true', async () => {
const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients;
await waitForRedisClients();
const cache = cacheFactory.violationCache('test-violations', 60000); // 60 second TTL
// Wait for Redis connection to be ready
if (ioredisClient && ioredisClient.status !== 'ready') {
await new Promise<void>((resolve) => {
ioredisClient.once('ready', resolve);
});
}
// Verify it returns a Keyv instance
expect(cache).toBeDefined();
expect(cache.constructor.name).toBe('Keyv');
@ -112,18 +122,10 @@ describe('violationCache', () => {
test('should respect namespace prefixing', async () => {
const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients;
await waitForRedisClients();
const cache1 = cacheFactory.violationCache('namespace1');
const cache2 = cacheFactory.violationCache('namespace2');
// Wait for Redis connection to be ready
if (ioredisClient && ioredisClient.status !== 'ready') {
await new Promise<void>((resolve) => {
ioredisClient.once('ready', resolve);
});
}
const testKey = 'shared-key';
const value1: ViolationData = { namespace: 1 };
const value2: ViolationData = { namespace: 2 };
@ -146,18 +148,10 @@ describe('violationCache', () => {
test('should respect TTL settings', async () => {
const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients;
await waitForRedisClients();
const ttl = 1000; // 1 second TTL
const cache = cacheFactory.violationCache('ttl-test', ttl);
// Wait for Redis connection to be ready
if (ioredisClient && ioredisClient.status !== 'ready') {
await new Promise<void>((resolve) => {
ioredisClient.once('ready', resolve);
});
}
const testKey = 'ttl-key';
const testValue: ViolationData = { data: 'expires soon' };
@ -178,17 +172,9 @@ describe('violationCache', () => {
test('should handle complex violation data structures', async () => {
const cacheFactory = await import('../../cacheFactory');
const redisClients = await import('../../redisClients');
const { ioredisClient } = redisClients;
await waitForRedisClients();
const cache = cacheFactory.violationCache('complex-violations');
// Wait for Redis connection to be ready
if (ioredisClient && ioredisClient.status !== 'ready') {
await new Promise<void>((resolve) => {
ioredisClient.once('ready', resolve);
});
}
const complexData: ViolationData = {
userId: 'user123',
violations: [