mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 10:20:15 +01:00
- This allows use APP_CONFIG in FORCED_IN_MEMORY_CACHE_NAMESPACES - Remove the complexity of nested namespace (e.g. we no longer have to worry about the prefix of every role key)
224 lines
7.8 KiB
JavaScript
224 lines
7.8 KiB
JavaScript
const { cacheConfig } = require('./cacheConfig');
|
|
const { Keyv } = require('keyv');
|
|
const { CacheKeys, ViolationTypes, Time } = require('librechat-data-provider');
|
|
const { logFile } = require('./keyvFiles');
|
|
const keyvMongo = require('./keyvMongo');
|
|
const { standardCache, sessionCache, violationCache } = require('./cacheFactory');
|
|
|
|
const namespaces = {
|
|
[ViolationTypes.GENERAL]: new Keyv({ store: logFile, namespace: 'violations' }),
|
|
[ViolationTypes.LOGINS]: violationCache(ViolationTypes.LOGINS),
|
|
[ViolationTypes.CONCURRENT]: violationCache(ViolationTypes.CONCURRENT),
|
|
[ViolationTypes.NON_BROWSER]: violationCache(ViolationTypes.NON_BROWSER),
|
|
[ViolationTypes.MESSAGE_LIMIT]: violationCache(ViolationTypes.MESSAGE_LIMIT),
|
|
[ViolationTypes.REGISTRATIONS]: violationCache(ViolationTypes.REGISTRATIONS),
|
|
[ViolationTypes.TOKEN_BALANCE]: violationCache(ViolationTypes.TOKEN_BALANCE),
|
|
[ViolationTypes.TTS_LIMIT]: violationCache(ViolationTypes.TTS_LIMIT),
|
|
[ViolationTypes.STT_LIMIT]: violationCache(ViolationTypes.STT_LIMIT),
|
|
[ViolationTypes.CONVO_ACCESS]: violationCache(ViolationTypes.CONVO_ACCESS),
|
|
[ViolationTypes.TOOL_CALL_LIMIT]: violationCache(ViolationTypes.TOOL_CALL_LIMIT),
|
|
[ViolationTypes.FILE_UPLOAD_LIMIT]: violationCache(ViolationTypes.FILE_UPLOAD_LIMIT),
|
|
[ViolationTypes.VERIFY_EMAIL_LIMIT]: violationCache(ViolationTypes.VERIFY_EMAIL_LIMIT),
|
|
[ViolationTypes.RESET_PASSWORD_LIMIT]: violationCache(ViolationTypes.RESET_PASSWORD_LIMIT),
|
|
[ViolationTypes.ILLEGAL_MODEL_REQUEST]: violationCache(ViolationTypes.ILLEGAL_MODEL_REQUEST),
|
|
[ViolationTypes.BAN]: new Keyv({
|
|
store: keyvMongo,
|
|
namespace: CacheKeys.BANS,
|
|
ttl: cacheConfig.BAN_DURATION,
|
|
}),
|
|
|
|
[CacheKeys.OPENID_SESSION]: sessionCache(CacheKeys.OPENID_SESSION),
|
|
[CacheKeys.SAML_SESSION]: sessionCache(CacheKeys.SAML_SESSION),
|
|
|
|
[CacheKeys.ROLES]: standardCache(CacheKeys.ROLES),
|
|
[CacheKeys.APP_CONFIG]: standardCache(CacheKeys.APP_CONFIG),
|
|
[CacheKeys.CONFIG_STORE]: standardCache(CacheKeys.CONFIG_STORE),
|
|
[CacheKeys.STATIC_CONFIG]: standardCache(CacheKeys.STATIC_CONFIG),
|
|
[CacheKeys.PENDING_REQ]: standardCache(CacheKeys.PENDING_REQ),
|
|
[CacheKeys.ENCODED_DOMAINS]: new Keyv({ store: keyvMongo, namespace: CacheKeys.ENCODED_DOMAINS }),
|
|
[CacheKeys.ABORT_KEYS]: standardCache(CacheKeys.ABORT_KEYS, Time.TEN_MINUTES),
|
|
[CacheKeys.TOKEN_CONFIG]: standardCache(CacheKeys.TOKEN_CONFIG, Time.THIRTY_MINUTES),
|
|
[CacheKeys.GEN_TITLE]: standardCache(CacheKeys.GEN_TITLE, Time.TWO_MINUTES),
|
|
[CacheKeys.S3_EXPIRY_INTERVAL]: standardCache(CacheKeys.S3_EXPIRY_INTERVAL, Time.THIRTY_MINUTES),
|
|
[CacheKeys.MODEL_QUERIES]: standardCache(CacheKeys.MODEL_QUERIES),
|
|
[CacheKeys.AUDIO_RUNS]: standardCache(CacheKeys.AUDIO_RUNS, Time.TEN_MINUTES),
|
|
[CacheKeys.MESSAGES]: standardCache(CacheKeys.MESSAGES, Time.ONE_MINUTE),
|
|
[CacheKeys.FLOWS]: standardCache(CacheKeys.FLOWS, Time.ONE_MINUTE * 3),
|
|
[CacheKeys.OPENID_EXCHANGED_TOKENS]: standardCache(
|
|
CacheKeys.OPENID_EXCHANGED_TOKENS,
|
|
Time.TEN_MINUTES,
|
|
),
|
|
};
|
|
|
|
/**
|
|
* Gets all cache stores that have TTL configured
|
|
* @returns {Keyv[]}
|
|
*/
|
|
function getTTLStores() {
|
|
return Object.values(namespaces).filter(
|
|
(store) =>
|
|
store instanceof Keyv &&
|
|
parseInt(store.opts?.ttl ?? '0') > 0 &&
|
|
!store.opts?.store?.constructor?.name?.includes('Redis'), // Only include non-Redis stores
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Clears entries older than the cache's TTL
|
|
* @param {Keyv} cache
|
|
*/
|
|
async function clearExpiredFromCache(cache) {
|
|
if (!cache?.opts?.store?.entries) {
|
|
return;
|
|
}
|
|
|
|
const ttl = cache.opts.ttl;
|
|
if (!ttl) {
|
|
return;
|
|
}
|
|
|
|
const expiryTime = Date.now() - ttl;
|
|
let cleared = 0;
|
|
|
|
// Get all keys first to avoid modification during iteration
|
|
const keys = Array.from(cache.opts.store.keys());
|
|
|
|
for (const key of keys) {
|
|
try {
|
|
const raw = cache.opts.store.get(key);
|
|
if (!raw) {
|
|
continue;
|
|
}
|
|
|
|
const data = cache.opts.deserialize(raw);
|
|
// Check if the entry is older than TTL
|
|
if (data?.expires && data.expires <= expiryTime) {
|
|
const deleted = await cache.opts.store.delete(key);
|
|
if (!deleted) {
|
|
cacheConfig.DEBUG_MEMORY_CACHE &&
|
|
console.warn(`[Cache] Error deleting entry: ${key} from ${cache.opts.namespace}`);
|
|
continue;
|
|
}
|
|
cleared++;
|
|
}
|
|
} catch (error) {
|
|
cacheConfig.DEBUG_MEMORY_CACHE &&
|
|
console.log(`[Cache] Error processing entry from ${cache.opts.namespace}:`, error);
|
|
const deleted = await cache.opts.store.delete(key);
|
|
if (!deleted) {
|
|
cacheConfig.DEBUG_MEMORY_CACHE &&
|
|
console.warn(`[Cache] Error deleting entry: ${key} from ${cache.opts.namespace}`);
|
|
continue;
|
|
}
|
|
cleared++;
|
|
}
|
|
}
|
|
|
|
if (cleared > 0) {
|
|
cacheConfig.DEBUG_MEMORY_CACHE &&
|
|
console.log(
|
|
`[Cache] Cleared ${cleared} entries older than ${ttl}ms from ${cache.opts.namespace}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const auditCache = () => {
|
|
const ttlStores = getTTLStores();
|
|
console.log('[Cache] Starting audit');
|
|
|
|
ttlStores.forEach((store) => {
|
|
if (!store?.opts?.store?.entries) {
|
|
return;
|
|
}
|
|
|
|
console.log(`[Cache] ${store.opts.namespace} entries:`, {
|
|
count: store.opts.store.size,
|
|
ttl: store.opts.ttl,
|
|
keys: Array.from(store.opts.store.keys()),
|
|
entriesWithTimestamps: Array.from(store.opts.store.entries()).map(([key, value]) => ({
|
|
key,
|
|
value,
|
|
})),
|
|
});
|
|
});
|
|
};
|
|
|
|
/**
|
|
* Clears expired entries from all TTL-enabled stores
|
|
*/
|
|
async function clearAllExpiredFromCache() {
|
|
const ttlStores = getTTLStores();
|
|
await Promise.all(ttlStores.map((store) => clearExpiredFromCache(store)));
|
|
|
|
// Force garbage collection if available (Node.js with --expose-gc flag)
|
|
if (global.gc) {
|
|
global.gc();
|
|
}
|
|
}
|
|
|
|
if (!cacheConfig.USE_REDIS && !cacheConfig.CI) {
|
|
/** @type {Set<NodeJS.Timeout>} */
|
|
const cleanupIntervals = new Set();
|
|
|
|
// Clear expired entries every 30 seconds
|
|
const cleanup = setInterval(() => {
|
|
clearAllExpiredFromCache();
|
|
}, Time.THIRTY_SECONDS);
|
|
|
|
cleanupIntervals.add(cleanup);
|
|
|
|
if (cacheConfig.DEBUG_MEMORY_CACHE) {
|
|
const monitor = setInterval(() => {
|
|
const ttlStores = getTTLStores();
|
|
const memory = process.memoryUsage();
|
|
const totalSize = ttlStores.reduce((sum, store) => sum + (store.opts?.store?.size ?? 0), 0);
|
|
|
|
console.log('[Cache] Memory usage:', {
|
|
heapUsed: `${(memory.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
|
heapTotal: `${(memory.heapTotal / 1024 / 1024).toFixed(2)} MB`,
|
|
rss: `${(memory.rss / 1024 / 1024).toFixed(2)} MB`,
|
|
external: `${(memory.external / 1024 / 1024).toFixed(2)} MB`,
|
|
totalCacheEntries: totalSize,
|
|
});
|
|
|
|
auditCache();
|
|
}, Time.ONE_MINUTE);
|
|
|
|
cleanupIntervals.add(monitor);
|
|
}
|
|
|
|
const dispose = () => {
|
|
cacheConfig.DEBUG_MEMORY_CACHE && console.log('[Cache] Cleaning up and shutting down...');
|
|
cleanupIntervals.forEach((interval) => clearInterval(interval));
|
|
cleanupIntervals.clear();
|
|
|
|
// One final cleanup before exit
|
|
clearAllExpiredFromCache().then(() => {
|
|
cacheConfig.DEBUG_MEMORY_CACHE && console.log('[Cache] Final cleanup completed');
|
|
process.exit(0);
|
|
});
|
|
};
|
|
|
|
// Handle various termination signals
|
|
process.on('SIGTERM', dispose);
|
|
process.on('SIGINT', dispose);
|
|
process.on('SIGQUIT', dispose);
|
|
process.on('SIGHUP', dispose);
|
|
}
|
|
|
|
/**
|
|
* Returns the keyv cache specified by type.
|
|
* If an invalid type is passed, an error will be thrown.
|
|
*
|
|
* @param {string} key - The key for the namespace to access
|
|
* @returns {Keyv} - If a valid key is passed, returns an object containing the cache store of the specified key.
|
|
* @throws Will throw an error if an invalid key is passed.
|
|
*/
|
|
const getLogStores = (key) => {
|
|
if (!key || !namespaces[key]) {
|
|
throw new Error(`Invalid store key: ${key}`);
|
|
}
|
|
return namespaces[key];
|
|
};
|
|
|
|
module.exports = getLogStores;
|