Improve cache-clear behavior (#17662)

## 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',
}
```
This commit is contained in:
Charles Bochet
2026-02-03 15:13:23 +01:00
committed by GitHub
parent 9b063af7ab
commit 10ba8b9a97
@@ -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<string, any>,
options?: Record<string, string>,
): Promise<void> {
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 <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 <pattern>',
description: 'Pattern to flush specific cache keys (e.g., engine:*)',
description:
'Pattern within the namespace (default *). Keys matched are <namespace>:<pattern>.',
})
parsePattern(val: string): string {
return val;