🗃️ refactor: Separate Tool Cache Namespace for Blue/Green Deployments (#11738)
Some checks are pending
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

* 🔧 refactor: Introduce TOOL_CACHE for isolated caching of tools

- Added TOOL_CACHE key to CacheKeys enum for managing tool-related cache.
- Updated various services and controllers to utilize TOOL_CACHE instead of CONFIG_STORE for better separation of concerns in caching logic.
- Enhanced .env.example with comments on using in-memory cache for blue/green deployments.

* 🔧 refactor: Update cache configuration for in-memory storage handling

- Enhanced the handling of `FORCED_IN_MEMORY_CACHE_NAMESPACES` in `cacheConfig.ts` to default to `CONFIG_STORE` and `APP_CONFIG`, ensuring safer blue/green deployments.
- Updated `.env.example` with clearer comments regarding the usage of in-memory cache namespaces.
- Improved unit tests to validate the new default behavior and handling of empty strings for cache namespaces.
This commit is contained in:
Danny Avila 2026-02-11 22:20:43 -05:00 committed by GitHub
parent c7531dd029
commit 5b67e48fe1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 284 additions and 18 deletions

View file

@ -1,10 +1,92 @@
const { ToolCacheKeys } = require('../getCachedTools');
const { CacheKeys } = require('librechat-data-provider');
jest.mock('~/cache/getLogStores');
const getLogStores = require('~/cache/getLogStores');
const mockCache = { get: jest.fn(), set: jest.fn(), delete: jest.fn() };
getLogStores.mockReturnValue(mockCache);
const {
ToolCacheKeys,
getCachedTools,
setCachedTools,
getMCPServerTools,
invalidateCachedTools,
} = require('../getCachedTools');
describe('getCachedTools', () => {
beforeEach(() => {
jest.clearAllMocks();
getLogStores.mockReturnValue(mockCache);
});
describe('getCachedTools - Cache Isolation Security', () => {
describe('ToolCacheKeys.MCP_SERVER', () => {
it('should generate cache keys that include userId', () => {
const key = ToolCacheKeys.MCP_SERVER('user123', 'github');
expect(key).toBe('tools:mcp:user123:github');
});
});
describe('TOOL_CACHE namespace usage', () => {
it('getCachedTools should use TOOL_CACHE namespace', async () => {
mockCache.get.mockResolvedValue(null);
await getCachedTools();
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
});
it('getCachedTools with MCP server options should use TOOL_CACHE namespace', async () => {
mockCache.get.mockResolvedValue({ tool1: {} });
await getCachedTools({ userId: 'user1', serverName: 'github' });
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
expect(mockCache.get).toHaveBeenCalledWith(ToolCacheKeys.MCP_SERVER('user1', 'github'));
});
it('setCachedTools should use TOOL_CACHE namespace', async () => {
mockCache.set.mockResolvedValue(true);
const tools = { tool1: { type: 'function' } };
await setCachedTools(tools);
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
expect(mockCache.set).toHaveBeenCalledWith(ToolCacheKeys.GLOBAL, tools, expect.any(Number));
});
it('setCachedTools with MCP server options should use TOOL_CACHE namespace', async () => {
mockCache.set.mockResolvedValue(true);
const tools = { tool1: { type: 'function' } };
await setCachedTools(tools, { userId: 'user1', serverName: 'github' });
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
expect(mockCache.set).toHaveBeenCalledWith(
ToolCacheKeys.MCP_SERVER('user1', 'github'),
tools,
expect.any(Number),
);
});
it('invalidateCachedTools should use TOOL_CACHE namespace', async () => {
mockCache.delete.mockResolvedValue(true);
await invalidateCachedTools({ invalidateGlobal: true });
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
expect(mockCache.delete).toHaveBeenCalledWith(ToolCacheKeys.GLOBAL);
});
it('getMCPServerTools should use TOOL_CACHE namespace', async () => {
mockCache.get.mockResolvedValue(null);
await getMCPServerTools('user1', 'github');
expect(getLogStores).toHaveBeenCalledWith(CacheKeys.TOOL_CACHE);
expect(mockCache.get).toHaveBeenCalledWith(ToolCacheKeys.MCP_SERVER('user1', 'github'));
});
it('should NOT use CONFIG_STORE namespace', async () => {
mockCache.get.mockResolvedValue(null);
await getCachedTools();
await getMCPServerTools('user1', 'github');
mockCache.set.mockResolvedValue(true);
await setCachedTools({ tool1: {} });
mockCache.delete.mockResolvedValue(true);
await invalidateCachedTools({ invalidateGlobal: true });
const allCalls = getLogStores.mock.calls.flat();
expect(allCalls).not.toContain(CacheKeys.CONFIG_STORE);
expect(allCalls.every((key) => key === CacheKeys.TOOL_CACHE)).toBe(true);
});
});
});