Skip to content

Commit 0103df1

Browse files
Alaka-ibrStellar-privacy
authored andcommitted
fix(cache): add structured debug log for each cache eviction (#737)
Emit a debug-level structured log on every in-memory cache eviction with the fields required by the acceptance criteria: - cache_key: the evicted entry's key - reason: 'ttl_expired' or 'capacity_overflow' - cache_size_after: map size read after the delete call - evicted_at: ISO 8601 timestamp at eviction time The stale-on-read path in getCachedCreatorList is a TTL expiry variant and now logs with reason 'ttl_expired' instead of the previous 'stale'. The delete is performed before the log call so cache_size_after reflects the post-eviction size as specified.
1 parent dea4063 commit 0103df1

2 files changed

Lines changed: 195 additions & 15 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import {
2+
getCachedCreatorList,
3+
setCachedCreatorList,
4+
resetCreatorListCache,
5+
} from './creators.cache';
6+
import { logger } from '../../utils/logger.utils';
7+
8+
jest.mock('../../utils/logger.utils', () => ({
9+
logger: {
10+
debug: jest.fn(),
11+
info: jest.fn(),
12+
warn: jest.fn(),
13+
error: jest.fn(),
14+
isLevelEnabled: jest.fn().mockReturnValue(true),
15+
},
16+
}));
17+
18+
jest.mock('../../constants/creator-public-cache.constants', () => ({
19+
CREATOR_PUBLIC_ROUTE_CACHE_MAX_AGE_SECONDS: { publicRead: 60 },
20+
}));
21+
22+
const mockLogger = logger as unknown as {
23+
debug: jest.Mock;
24+
};
25+
26+
const BASE_QUERY = {
27+
limit: 20,
28+
offset: 0,
29+
sort: 'createdAt' as const,
30+
order: 'desc' as const,
31+
include: [] as never[],
32+
};
33+
34+
function findEvictionCalls(): Record<string, unknown>[] {
35+
return mockLogger.debug.mock.calls
36+
.map((args: unknown[]) => args[0] as Record<string, unknown>)
37+
.filter(
38+
(obj: Record<string, unknown>) =>
39+
obj.event === 'creator_list_cache_eviction'
40+
);
41+
}
42+
43+
describe('cache eviction structured log (#737)', () => {
44+
beforeEach(() => {
45+
jest.clearAllMocks();
46+
resetCreatorListCache();
47+
});
48+
49+
describe('TTL expiry', () => {
50+
it('emits a debug log with reason ttl_expired when a stale entry is read', () => {
51+
setCachedCreatorList(BASE_QUERY as any, [], 0);
52+
53+
jest.spyOn(Date, 'now').mockReturnValue(Date.now() + 120_000);
54+
55+
getCachedCreatorList(BASE_QUERY as any);
56+
57+
const evictions = findEvictionCalls();
58+
const ttlEviction = evictions.find(
59+
(e) => e.reason === 'ttl_expired'
60+
);
61+
62+
expect(ttlEviction).toBeDefined();
63+
expect(ttlEviction).toHaveProperty('cache_key');
64+
expect(ttlEviction).toHaveProperty('reason', 'ttl_expired');
65+
expect(ttlEviction).toHaveProperty('cache_size_after');
66+
expect(ttlEviction).toHaveProperty('evicted_at');
67+
68+
jest.restoreAllMocks();
69+
});
70+
71+
it('cache_size_after reflects the size after eviction', () => {
72+
setCachedCreatorList(BASE_QUERY as any, [], 0);
73+
74+
jest.spyOn(Date, 'now').mockReturnValue(Date.now() + 120_000);
75+
76+
getCachedCreatorList(BASE_QUERY as any);
77+
78+
const evictions = findEvictionCalls();
79+
const ttlEviction = evictions.find(
80+
(e) => e.reason === 'ttl_expired'
81+
);
82+
83+
expect(ttlEviction!.cache_size_after).toBe(0);
84+
85+
jest.restoreAllMocks();
86+
});
87+
88+
it('evicted_at is a valid ISO 8601 timestamp', () => {
89+
setCachedCreatorList(BASE_QUERY as any, [], 0);
90+
91+
jest.spyOn(Date, 'now').mockReturnValue(Date.now() + 120_000);
92+
93+
getCachedCreatorList(BASE_QUERY as any);
94+
95+
const evictions = findEvictionCalls();
96+
const ttlEviction = evictions.find(
97+
(e) => e.reason === 'ttl_expired'
98+
);
99+
const parsed = new Date(ttlEviction!.evicted_at as string);
100+
101+
expect(parsed.toISOString()).toBe(ttlEviction!.evicted_at);
102+
103+
jest.restoreAllMocks();
104+
});
105+
106+
it('log level is debug', () => {
107+
setCachedCreatorList(BASE_QUERY as any, [], 0);
108+
109+
jest.spyOn(Date, 'now').mockReturnValue(Date.now() + 120_000);
110+
111+
getCachedCreatorList(BASE_QUERY as any);
112+
113+
const evictions = findEvictionCalls();
114+
115+
expect(evictions.length).toBeGreaterThanOrEqual(1);
116+
expect(mockLogger.debug).toHaveBeenCalled();
117+
118+
jest.restoreAllMocks();
119+
});
120+
});
121+
122+
describe('capacity overflow', () => {
123+
it('emits a debug log with reason capacity_overflow when the cache exceeds max entries', () => {
124+
for (let i = 0; i < 252; i++) {
125+
const query = { ...BASE_QUERY, offset: i };
126+
setCachedCreatorList(query as any, [], 0);
127+
}
128+
129+
const evictions = findEvictionCalls();
130+
const overflowEviction = evictions.find(
131+
(e) => e.reason === 'capacity_overflow'
132+
);
133+
134+
expect(overflowEviction).toBeDefined();
135+
expect(overflowEviction).toHaveProperty('cache_key');
136+
expect(overflowEviction).toHaveProperty(
137+
'reason',
138+
'capacity_overflow'
139+
);
140+
expect(overflowEviction).toHaveProperty('cache_size_after');
141+
expect(overflowEviction).toHaveProperty('evicted_at');
142+
});
143+
144+
it('cache_size_after reflects the size after the overflow eviction', () => {
145+
for (let i = 0; i < 252; i++) {
146+
const query = { ...BASE_QUERY, offset: i };
147+
setCachedCreatorList(query as any, [], 0);
148+
}
149+
150+
const evictions = findEvictionCalls();
151+
const overflowEvictions = evictions.filter(
152+
(e) => e.reason === 'capacity_overflow'
153+
);
154+
155+
for (const eviction of overflowEvictions) {
156+
expect(
157+
typeof eviction.cache_size_after === 'number'
158+
).toBe(true);
159+
expect(
160+
(eviction.cache_size_after as number) <= 250
161+
).toBe(true);
162+
}
163+
});
164+
165+
it('evicted_at is a valid ISO 8601 timestamp on overflow eviction', () => {
166+
for (let i = 0; i < 252; i++) {
167+
const query = { ...BASE_QUERY, offset: i };
168+
setCachedCreatorList(query as any, [], 0);
169+
}
170+
171+
const evictions = findEvictionCalls();
172+
const overflowEviction = evictions.find(
173+
(e) => e.reason === 'capacity_overflow'
174+
);
175+
const parsed = new Date(overflowEviction!.evicted_at as string);
176+
177+
expect(parsed.toISOString()).toBe(overflowEviction!.evicted_at);
178+
});
179+
});
180+
});

src/modules/creators/creators.cache.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,15 @@ function getCreatorListCacheTtlMs(): number {
2929
function pruneCreatorListCache(now: number): void {
3030
for (const [cacheKey, entry] of creatorListCache.entries()) {
3131
if (entry.expiresAt <= now) {
32+
creatorListCache.delete(cacheKey);
3233
logger.debug({
3334
msg: 'Creator list cache eviction',
3435
event: 'creator_list_cache_eviction',
35-
cacheKey,
36-
reason: 'expired',
37-
expiresAt: entry.expiresAt,
38-
now,
36+
cache_key: cacheKey,
37+
reason: 'ttl_expired',
38+
cache_size_after: creatorListCache.size,
39+
evicted_at: new Date(now).toISOString(),
3940
});
40-
creatorListCache.delete(cacheKey);
4141
}
4242
}
4343

@@ -51,15 +51,15 @@ function pruneCreatorListCache(now: number): void {
5151
.slice(0, overflow);
5252

5353
for (const [cacheKey] of oldestEntries) {
54+
creatorListCache.delete(cacheKey);
5455
logger.debug({
5556
msg: 'Creator list cache eviction',
5657
event: 'creator_list_cache_eviction',
57-
cacheKey,
58-
reason: 'overflow',
59-
cacheSize: creatorListCache.size,
60-
maxSize: MAX_CREATOR_LIST_CACHE_ENTRIES,
58+
cache_key: cacheKey,
59+
reason: 'capacity_overflow',
60+
cache_size_after: creatorListCache.size,
61+
evicted_at: new Date(now).toISOString(),
6162
});
62-
creatorListCache.delete(cacheKey);
6363
}
6464
}
6565

@@ -117,15 +117,15 @@ export function getCachedCreatorList(
117117
}
118118

119119
if (cachedEntry) {
120+
creatorListCache.delete(cacheKey);
120121
logger.debug({
121122
msg: 'Creator list cache eviction',
122123
event: 'creator_list_cache_eviction',
123-
cacheKey,
124-
reason: 'stale',
125-
expiresAt: cachedEntry.expiresAt,
126-
now,
124+
cache_key: cacheKey,
125+
reason: 'ttl_expired',
126+
cache_size_after: creatorListCache.size,
127+
evicted_at: new Date(now).toISOString(),
127128
});
128-
creatorListCache.delete(cacheKey);
129129
}
130130

131131
pruneCreatorListCache(now);

0 commit comments

Comments
 (0)