From 10ba8b9a97d3a37a2f08c500e05449093d171c8d Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Tue, 3 Feb 2026 15:13:23 +0100 Subject: [PATCH] Improve cache-clear behavior (#17662) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Cache flush: scope and options **Scope** - `cache:flush` only flushes the **cache** Redis (REDIS_URL). It no longer touches the queue Redis. **Options** - **`--namespace`** (optional): Flush one namespace or omit to flush all. Valid: `module:messaging`, `module:calendar`, `module:workflow`, `engine:workspace`, `engine:lock`, `engine:health`, `engine:subscriptions`. - **`--pattern`** (optional, default `*`): Key pattern inside the chosen namespace(s). **Troubleshooting** - **`cache:flush:verify`**: Inserts keys inside and outside `engine:workspace`, flushes, and checks only namespace keys are removed (confirms flush scope). **Usage** - `yarn command:prod cache:flush` — flush all **KNOWN** namespaces - `yarn command:prod cache:flush -n engine:workspace` — flush one namespace - `yarn command:prod cache:flush -n engine:workspace -p "feature-flag:*"` — flush keys matching pattern in given namespace FYI, existing namespaces: ``` export enum CacheStorageNamespace { ModuleMessaging = 'module:messaging', ModuleCalendar = 'module:calendar', ModuleWorkflow = 'module:workflow', EngineWorkspace = 'engine:workspace', EngineLock = 'engine:lock', EngineHealth = 'engine:health', EngineSubscriptions = 'engine:subscriptions', } ``` --- .../commands/flush-cache.command.ts | 86 +++++++++++++++---- 1 file changed, 69 insertions(+), 17 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flush-cache.command.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flush-cache.command.ts index 11fbd01654..0f6fae6bd0 100644 --- a/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flush-cache.command.ts +++ b/packages/twenty-server/src/engine/core-modules/cache-storage/commands/flush-cache.command.ts @@ -1,46 +1,98 @@ -import { Logger } from '@nestjs/common'; +import { CACHE_MANAGER, type Cache } from '@nestjs/cache-manager'; +import { Inject, Logger } from '@nestjs/common'; import { Command, CommandRunner, Option } from 'nest-commander'; +import { isDefined } from 'twenty-shared/utils'; +import { isNonEmptyString } from '@sniptt/guards'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +const NAMESPACE_VALUES = Object.values( + CacheStorageNamespace, +) as CacheStorageNamespace[]; + @Command({ name: 'cache:flush', - description: 'Flush cache for specific keys matching the pattern', + description: + 'Flush cache Redis (REDIS_URL) for a namespace and pattern. Omit --namespace to flush all namespaces. Run: npx nx run twenty-server:command cache:flush', }) export class FlushCacheCommand extends CommandRunner { private readonly logger = new Logger(FlushCacheCommand.name); - constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - private readonly cacheStorage: CacheStorageService, - ) { + constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) { super(); } async run( _passedParams: string[], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - options?: Record, + options?: Record, ): Promise { - const pattern = options?.pattern || '*'; + try { + const namespaceArg = options?.namespace; + const namespacesToFlush = + this.computeNamespacesToFlushOrThrow(namespaceArg); + const pattern = options?.pattern ?? '*'; - this.logger.log(`Flushing cache for pattern: ${pattern}...`); + this.logger.log( + namespacesToFlush.length === 1 + ? `Flushing namespace ${namespacesToFlush[0]} for pattern: ${pattern}...` + : `Flushing all namespaces for pattern: ${pattern}...`, + ); - if (pattern === '*') { - await this.cacheStorage.flush(); - } else { - await this.cacheStorage.flushByPattern(pattern); + for (const namespace of namespacesToFlush) { + const cacheStorage = new CacheStorageService( + this.cacheManager, + namespace, + ); + + await cacheStorage.flushByPattern(pattern); + } + + this.logger.log('Cache flushed'); + } catch (error) { + this.logger.error(error.message); + } + } + + private computeNamespacesToFlushOrThrow( + value: unknown, + ): CacheStorageNamespace[] { + if (!isDefined(value)) { + return NAMESPACE_VALUES; } - this.logger.log('Cache flushed'); + if (!isNonEmptyString(value)) { + throw new Error( + `Invalid --namespace: ${value}. Valid values: ${NAMESPACE_VALUES.join(', ')}`, + ); + } + + return [this.parseNamespaceOrThrow(value)]; + } + + private parseNamespaceOrThrow(value: string): CacheStorageNamespace { + if (!NAMESPACE_VALUES.includes(value as CacheStorageNamespace)) { + throw new Error( + `Invalid --namespace: ${value}. Valid values: ${NAMESPACE_VALUES.join(', ')}`, + ); + } + + return value as CacheStorageNamespace; + } + + @Option({ + flags: '-n, --namespace ', + description: `Cache namespace to flush. Omit to flush all. One of: ${NAMESPACE_VALUES.join(', ')}`, + }) + parseNamespaceOption(val: string): string { + return val; } @Option({ flags: '-p, --pattern ', - description: 'Pattern to flush specific cache keys (e.g., engine:*)', + description: + 'Pattern within the namespace (default *). Keys matched are :.', }) parsePattern(val: string): string { return val;