Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 0 additions & 34 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/caching/caching.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ describe('CachingService', () => {
clear: jest.fn().mockResolvedValue(undefined),
};
metrics = { updateCacheHitRate: jest.fn() };
(cacheManager as any).store = {
keys: jest.fn().mockResolvedValue(['cache:test:1', 'cache:test:2']),
};
service = new CachingService(
cacheManager as never,
metrics as unknown as MetricsCollectionService,
Expand Down Expand Up @@ -51,6 +54,15 @@ describe('CachingService', () => {
});
});

describe('deleteByPattern', () => {
it('uses store.keys to delete matching keys when client scan is unavailable', async () => {
await service.deleteByPattern('cache:test:*');
expect((cacheManager as any).store.keys).toHaveBeenCalledWith('cache:test:*');
expect(cacheManager.del).toHaveBeenCalledWith('cache:test:1');
expect(cacheManager.del).toHaveBeenCalledWith('cache:test:2');
});
});

describe('hit rate metrics', () => {
it('calculates hit rate and publishes to metrics', async () => {
cacheManager.get.mockResolvedValueOnce('cached').mockResolvedValueOnce(undefined);
Expand Down
33 changes: 33 additions & 0 deletions src/caching/caching.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,39 @@ export class CachingService {
}
}

async deleteByPattern(pattern: string): Promise<void> {
try {
const store = (this.cacheManager as any).store;

if (store && store.client && typeof store.client.scan === 'function') {
let cursor = '0';
do {
const result = await store.client.scan(cursor, 'MATCH', pattern, 'COUNT', 100);
cursor = result[0];
const keys = result[1];
if (keys && keys.length > 0) {
await store.client.del(...keys);
}
} while (cursor !== '0');
return;
}

if (store && typeof store.keys === 'function') {
const keys = await store.keys(pattern);
if (keys && keys.length > 0) {
await this.deleteMany(keys);
}
return;
}

this.logger.warn(
`Pattern deletion not supported by current cache store for pattern: ${pattern}`,
);
} catch (error: any) {
this.logger.error(`Failed to delete by pattern ${pattern}: ${error.message}`, error.stack);
}
}

getStats(): CacheStats {
const total = this.hits + this.misses;
return {
Expand Down
5 changes: 2 additions & 3 deletions src/recommendations/recommendation-engine.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,8 @@ export class RecommendationEngineService {

/** Invalidate cached recommendations for a user (e.g., after a new enrollment). */
async invalidate(userId: string): Promise<void> {
// Pattern-style deletion: remove all limit variants by trying the common ones
const keys = [5, 10, 20, 50].map((l) => `recommendations:${userId}:${l}`);
await this.caching.deleteMany(keys);
// Delete all variants (any limit) matching the namespace prefix
await this.caching.deleteByPattern(`recommendations:${userId}:*`);
}

private async computeRecommendations(
Expand Down
10 changes: 3 additions & 7 deletions src/recommendations/recommendation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ describe('RecommendationEngineService', () => {
caching = {
getOrSet: jest.fn((_, factory) => factory()),
deleteMany: jest.fn(),
deleteByPattern: jest.fn(),
};

const module: TestingModule = await Test.createTestingModule({
Expand Down Expand Up @@ -84,14 +85,9 @@ describe('RecommendationEngineService', () => {
);
});

it('invalidate deletes cache keys for common limits', async () => {
it('invalidate deletes cache keys by pattern', async () => {
await service.invalidate('user-1');
expect(caching.deleteMany).toHaveBeenCalledWith([
'recommendations:user-1:5',
'recommendations:user-1:10',
'recommendations:user-1:20',
'recommendations:user-1:50',
]);
expect(caching.deleteByPattern).toHaveBeenCalledWith('recommendations:user-1:*');
});
});

Expand Down
2 changes: 1 addition & 1 deletion tsconfig.build.tsbuildinfo

Large diffs are not rendered by default.

Loading