Skip to content

Commit eae0e47

Browse files
committed
fix: refresh background
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent 1ee0ece commit eae0e47

2 files changed

Lines changed: 116 additions & 34 deletions

File tree

services/libs/data-access-layer/src/members/base.ts

Lines changed: 86 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ export async function queryMembersAdvanced(
225225
const cachedCount = countOnly ? await cache.getCount(countCacheKey) : null
226226

227227
if (cachedResult) {
228+
log.info(
229+
{ cacheKey, segmentId, search: normalizedSearch, limit, offset, orderBy },
230+
'Members advanced query cache hit — returning cached result, scheduling background refresh',
231+
)
228232
refreshCacheInBackground(bgQx, redis, cacheKey, {
229233
filter,
230234
search: normalizedSearch,
@@ -238,12 +242,14 @@ export async function queryMembersAdvanced(
238242
includeAllAttributes,
239243
attributeSettings,
240244
})
241-
242-
log.info(`Members advanced query cache hit: ${cacheKey}`)
243245
return cachedResult
244246
}
245247

246248
if (countOnly && cachedCount !== null) {
249+
log.info(
250+
{ countCacheKey, segmentId, search: normalizedSearch },
251+
'Members advanced count cache hit — returning cached count, scheduling background refresh',
252+
)
247253
refreshCountCacheInBackground(bgQx, redis, countCacheKey, {
248254
filter,
249255
search: normalizedSearch,
@@ -252,8 +258,6 @@ export async function queryMembersAdvanced(
252258
includeAllAttributes,
253259
attributeSettings,
254260
})
255-
256-
log.debug(`Members advanced count query cache hit: ${countCacheKey}`)
257261
return {
258262
rows: [],
259263
count: cachedCount,
@@ -262,19 +266,65 @@ export async function queryMembersAdvanced(
262266
}
263267
}
264268

265-
return await executeQuery(qx, redis, cacheKey, {
266-
filter,
267-
search: normalizedSearch,
268-
limit,
269-
offset,
270-
orderBy,
271-
segmentId,
272-
countOnly,
273-
fields,
274-
include,
275-
includeAllAttributes,
276-
attributeSettings,
277-
})
269+
log.info(
270+
{
271+
cacheKey,
272+
countCacheKey,
273+
segmentId,
274+
search: normalizedSearch,
275+
limit,
276+
offset,
277+
orderBy,
278+
countOnly,
279+
},
280+
'Members advanced query cache miss — executing query synchronously',
281+
)
282+
283+
try {
284+
return await executeQuery(qx, redis, cacheKey, {
285+
filter,
286+
search: normalizedSearch,
287+
limit,
288+
offset,
289+
orderBy,
290+
segmentId,
291+
countOnly,
292+
fields,
293+
include,
294+
includeAllAttributes,
295+
attributeSettings,
296+
})
297+
} catch (error) {
298+
log.warn(
299+
{ cacheKey, countCacheKey, segmentId, search: normalizedSearch, countOnly, err: error },
300+
'Members advanced query failed on cache miss — scheduling background refresh for next retry',
301+
)
302+
if (countOnly) {
303+
refreshCountCacheInBackground(bgQx, redis, countCacheKey, {
304+
filter,
305+
search: normalizedSearch,
306+
segmentId,
307+
include,
308+
includeAllAttributes,
309+
attributeSettings,
310+
})
311+
} else {
312+
refreshCacheInBackground(bgQx, redis, cacheKey, {
313+
filter,
314+
search: normalizedSearch,
315+
limit,
316+
offset,
317+
orderBy,
318+
segmentId,
319+
countOnly: false,
320+
fields,
321+
include,
322+
includeAllAttributes,
323+
attributeSettings,
324+
})
325+
}
326+
throw error
327+
}
278328
}
279329

280330
export async function executeQuery(
@@ -567,29 +617,36 @@ async function refreshCacheInBackground(
567617
redis: RedisClient,
568618
cacheKey: string,
569619
params: IQueryMembersAdvancedParams,
620+
countOnly = false,
570621
): Promise<void> {
622+
const label = countOnly ? 'count cache' : 'query cache'
623+
const cache = new MemberQueryCache(redis)
624+
const acquired = await cache.tryAcquireRefreshLock(cacheKey)
625+
if (!acquired) {
626+
log.debug(
627+
{ cacheKey },
628+
`Members advanced ${label} refresh already in progress — skipping duplicate`,
629+
)
630+
return
631+
}
571632
try {
572-
log.info(`Refreshing members advanced query cache in background: ${cacheKey}`)
573-
await executeQuery(qx, redis, cacheKey, params)
574-
log.info(`Members advanced query cache refreshed in background: ${cacheKey}`)
633+
log.info({ cacheKey }, `Members advanced ${label} background refresh started`)
634+
await executeQuery(qx, redis, cacheKey, countOnly ? { ...params, countOnly: true } : params)
635+
log.info({ cacheKey }, `Members advanced ${label} background refresh completed`)
575636
} catch (error) {
576-
log.warn('Background cache refresh failed:', error)
637+
log.warn({ cacheKey, err: error }, `Members advanced ${label} background refresh failed`)
638+
} finally {
639+
await cache.releaseRefreshLock(cacheKey)
577640
}
578641
}
579642

580-
async function refreshCountCacheInBackground(
643+
function refreshCountCacheInBackground(
581644
qx: QueryExecutor,
582645
redis: RedisClient,
583646
cacheKey: string,
584647
params: IQueryMembersAdvancedParams,
585648
): Promise<void> {
586-
try {
587-
log.info(`Refreshing members advanced count cache in background: ${cacheKey}`)
588-
await executeQuery(qx, redis, cacheKey, { ...params, countOnly: true })
589-
log.info(`Members advanced count cache refreshed in background: ${cacheKey}`)
590-
} catch (error) {
591-
log.warn('Background count cache refresh failed:', error)
592-
}
649+
return refreshCacheInBackground(qx, redis, cacheKey, params, true)
593650
}
594651

595652
export async function queryMembers<T extends MemberField>(

services/libs/data-access-layer/src/members/queryCache.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { createHash } from 'crypto'
1+
import { createHash, randomBytes } from 'crypto'
22

33
import { getServiceLogger } from '@crowd/logging'
44
import { RedisCache, RedisClient } from '@crowd/redis'
@@ -19,10 +19,33 @@ const log = getServiceLogger()
1919
export class MemberQueryCache {
2020
private cache: RedisCache
2121
private countCache: RedisCache
22+
private lockCache: RedisCache
2223

2324
constructor(redis: RedisClient) {
2425
this.cache = new RedisCache('members-advanced', redis, log)
2526
this.countCache = new RedisCache('members-count', redis, log)
27+
this.lockCache = new RedisCache('members-refresh-lock', redis, log)
28+
}
29+
30+
// Returns true if lock was acquired (no other refresh in progress for this key).
31+
// Uses a cryptographically random token to distinguish "we set it" from "already existed".
32+
// TTL ensures the lock auto-expires if the refresh crashes without releasing it.
33+
async tryAcquireRefreshLock(cacheKey: string, ttlSeconds = 90): Promise<boolean> {
34+
try {
35+
const token = randomBytes(16).toString('hex')
36+
const stored = await this.lockCache.setIfNotExistsOrGet(cacheKey, token, ttlSeconds)
37+
return stored === token
38+
} catch {
39+
return true // fail open: if Redis is down, let the refresh proceed
40+
}
41+
}
42+
43+
async releaseRefreshLock(cacheKey: string): Promise<void> {
44+
try {
45+
await this.lockCache.delete(cacheKey)
46+
} catch {
47+
// best effort
48+
}
2649
}
2750

2851
buildCacheKey(params: {
@@ -116,12 +139,13 @@ export class MemberQueryCache {
116139

117140
async invalidateAll(): Promise<void> {
118141
try {
119-
const [resultsDeleted, countsDeleted] = await Promise.all([
142+
const [resultsDeleted, countsDeleted, locksDeleted] = await Promise.all([
120143
this.cache.deleteAll(),
121144
this.countCache.deleteAll(),
145+
this.lockCache.deleteAll(),
122146
])
123147
log.info(
124-
`Invalidated member query cache: ${resultsDeleted} result entries, ${countsDeleted} count entries`,
148+
`Invalidated member query cache: ${resultsDeleted} result entries, ${countsDeleted} count entries, ${locksDeleted} locks`,
125149
)
126150
} catch (error) {
127151
log.warn('Error invalidating member query cache', { error })
@@ -130,12 +154,13 @@ export class MemberQueryCache {
130154

131155
async invalidateByPattern(pattern: string): Promise<void> {
132156
try {
133-
const [resultsDeleted, countsDeleted] = await Promise.all([
157+
const [resultsDeleted, countsDeleted, locksDeleted] = await Promise.all([
134158
this.cache.deleteByKeyPattern(pattern),
135159
this.countCache.deleteByKeyPattern(pattern),
160+
this.lockCache.deleteByKeyPattern(pattern),
136161
])
137162
log.info(
138-
`Invalidated member query cache by pattern: ${resultsDeleted} result entries, ${countsDeleted} count entries deleted for pattern ${pattern}`,
163+
`Invalidated member query cache by pattern: ${resultsDeleted} result entries, ${countsDeleted} count entries, ${locksDeleted} locks deleted for pattern ${pattern}`,
139164
)
140165
} catch (error) {
141166
log.warn('Error invalidating member query cache by pattern', { error, pattern })

0 commit comments

Comments
 (0)