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:
+69
-17
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user