Skip to content

Commit e44f7b6

Browse files
committed
fix: cache regression
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent da04ed8 commit e44f7b6

5 files changed

Lines changed: 64 additions & 22 deletions

File tree

backend/src/database/repositories/memberRepository.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ import {
5050
includeMemberToSegments,
5151
} from '@crowd/data-access-layer/src/members/segments'
5252
import { IDbMemberData } from '@crowd/data-access-layer/src/members/types'
53-
import { optionsQx } from '@crowd/data-access-layer/src/queryExecutor'
53+
import { optionsBgQx, optionsQx } from '@crowd/data-access-layer/src/queryExecutor'
5454
import {
5555
fetchManySegments,
5656
getSegmentMergeSuggestionCounts,
@@ -1259,7 +1259,7 @@ class MemberRepository {
12591259
let memberResponse = null
12601260

12611261
const qx = optionsQx(options)
1262-
const bgQx = optionsQx({ ...options, transaction: null })
1262+
const bgQx = optionsBgQx(options)
12631263

12641264
memberResponse = await queryMembersAdvanced(qx, bgQx, options.redis, {
12651265
filter: { id: { eq: id } },

backend/src/services/memberService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import {
2121
insertMemberSegmentAggregates,
2222
queryMembersAdvanced,
2323
} from '@crowd/data-access-layer/src/members'
24-
import { QueryExecutor, optionsQx } from '@crowd/data-access-layer/src/queryExecutor'
24+
import { QueryExecutor, optionsBgQx, optionsQx } from '@crowd/data-access-layer/src/queryExecutor'
2525
import {
2626
decrementMemberMergeSuggestionCounts,
2727
fetchManySegments,
@@ -944,7 +944,7 @@ export default class MemberService extends LoggerBase {
944944

945945
async findAllAutocomplete(data) {
946946
const qx = optionsQx(this.options)
947-
const bgQx = optionsQx({ ...this.options, transaction: null })
947+
const bgQx = optionsBgQx(this.options)
948948

949949
return queryMembersAdvanced(qx, bgQx, this.options.redis, {
950950
filter: data.filter,
@@ -970,7 +970,7 @@ export default class MemberService extends LoggerBase {
970970
}
971971

972972
const qx = optionsQx(this.options)
973-
const bgQx = optionsQx({ ...this.options, transaction: null })
973+
const bgQx = optionsBgQx(this.options)
974974

975975
return queryMembersAdvanced(qx, bgQx, this.options.redis, {
976976
...data,

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,11 @@ export async function queryMembersAdvanced(
195195
// Initialize cache
196196
const cache = new MemberQueryCache(redis)
197197

198-
// Build cache key — countOnly is excluded so count and full-result caches share the same key.
199-
// Normalize empty search to null so "" and null produce the same key (buildSearchCTE treats them identically).
198+
// Normalize search once: trim whitespace, lowercase (buildSearchCTE lowercases anyway),
199+
// convert empty string to null so "" and null hash identically.
200+
const normalizedSearch = search?.trim().toLowerCase() || null
201+
202+
// Full result key — includes pagination and projection so page 1 and page 2 are separate entries.
200203
const cacheKey = cache.buildCacheKey({
201204
fields,
202205
filter,
@@ -205,18 +208,26 @@ export async function queryMembersAdvanced(
205208
limit,
206209
offset,
207210
orderBy,
208-
search: search?.trim() || null,
211+
search: normalizedSearch,
212+
segmentId,
213+
})
214+
215+
// Count key — excludes pagination/projection and include flags since the count query
216+
// only depends on filter, search, and segmentId (buildCountQuery hardcodes includeMemberOrgs=false).
217+
const countCacheKey = cache.buildCountCacheKey({
218+
filter,
219+
search: normalizedSearch,
209220
segmentId,
210221
})
211222

212223
// Try to get from cache first
213224
const cachedResult = countOnly ? null : await cache.get(cacheKey)
214-
const cachedCount = countOnly ? await cache.getCount(cacheKey) : null
225+
const cachedCount = countOnly ? await cache.getCount(countCacheKey) : null
215226

216227
if (cachedResult) {
217228
refreshCacheInBackground(bgQx, redis, cacheKey, {
218229
filter,
219-
search,
230+
search: normalizedSearch,
220231
limit,
221232
offset,
222233
orderBy,
@@ -233,16 +244,16 @@ export async function queryMembersAdvanced(
233244
}
234245

235246
if (countOnly && cachedCount !== null) {
236-
refreshCountCacheInBackground(bgQx, redis, cacheKey, {
247+
refreshCountCacheInBackground(bgQx, redis, countCacheKey, {
237248
filter,
238-
search,
249+
search: normalizedSearch,
239250
segmentId,
240251
include,
241252
includeAllAttributes,
242253
attributeSettings,
243254
})
244255

245-
log.debug(`Members advanced count query cache hit: ${cacheKey}`)
256+
log.debug(`Members advanced count query cache hit: ${countCacheKey}`)
246257
return {
247258
rows: [],
248259
count: cachedCount,
@@ -253,7 +264,7 @@ export async function queryMembersAdvanced(
253264

254265
return await executeQuery(qx, redis, cacheKey, {
255266
filter,
256-
search,
267+
search: normalizedSearch,
257268
limit,
258269
offset,
259270
orderBy,
@@ -299,6 +310,11 @@ export async function executeQuery(
299310
}: IQueryMembersAdvancedParams,
300311
): Promise<PageData<IDbMemberData>> {
301312
const cache = new MemberQueryCache(redis)
313+
const countCacheKey = cache.buildCountCacheKey({
314+
filter,
315+
search: search ?? null,
316+
segmentId,
317+
})
302318
const withAggregates = !!segmentId
303319
const searchConfig = buildSearchCTE(search)
304320

@@ -352,8 +368,7 @@ export async function executeQuery(
352368
const result = await qx.selectOne(countQuery, params)
353369
const count = parseInt(result.count, 10)
354370

355-
// Cache the count
356-
await cache.setCount(cacheKey, count, 21600) // 6 hours TTL
371+
await cache.setCount(countCacheKey, count, 21600)
357372

358373
return {
359374
rows: [],
@@ -540,8 +555,8 @@ export async function executeQuery(
540555
const result = { rows, count, limit, offset }
541556

542557
await Promise.all([
543-
cache.set(cacheKey, result, 21600), // 6 hours TTL
544-
cache.setCount(cacheKey, count, 21600), // also warm count cache so countOnly=true hits next time
558+
cache.set(cacheKey, result, 21600),
559+
cache.setCount(countCacheKey, count, 21600),
545560
])
546561

547562
return result

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

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export class MemberQueryCache {
3030
filter?: Record<string, unknown>
3131
include?: IncludeOptions
3232
includeAllAttributes?: boolean
33-
limit: number
34-
offset: number
33+
limit?: number
34+
offset?: number
3535
orderBy?: string
3636
search?: string
3737
segmentId?: string
@@ -61,6 +61,18 @@ export class MemberQueryCache {
6161
return `members_advanced:${hash}`
6262
}
6363

64+
buildCountCacheKey(params: {
65+
filter?: Record<string, unknown>
66+
search?: string
67+
segmentId?: string
68+
}): string {
69+
return this.buildCacheKey({
70+
filter: params.filter,
71+
search: params.search,
72+
segmentId: params.segmentId,
73+
})
74+
}
75+
6476
async get(cacheKey: string): Promise<PageData<IDbMemberData> | null> {
6577
try {
6678
const cachedResult = await this.cache.get(cacheKey)

services/libs/data-access-layer/src/queryExecutor.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@ export function formatQuery(query: string, params?: object): string {
2020
}
2121

2222
export class SequelizeQueryExecutor implements QueryExecutor {
23-
constructor(private readonly sequelize: Sequelize) {}
23+
constructor(
24+
private readonly sequelize: Sequelize,
25+
private readonly noTransaction = false,
26+
) {}
2427

2528
protected prepareOptions(options: any): any {
26-
return options
29+
// When noTransaction=true, explicitly opt out of any CLS or implicit
30+
// transaction binding — used for background/fire-and-forget work that
31+
// must not inherit a parent request's transaction.
32+
return this.noTransaction ? { ...options, transaction: null } : options
2733
}
2834

2935
select(query: string, params?: object): Promise<any> {
@@ -166,3 +172,12 @@ export function optionsQx(options: any): QueryExecutor {
166172

167173
return new SequelizeQueryExecutor(seq)
168174
}
175+
176+
/**
177+
* Creates a QueryExecutor for fire-and-forget background work.
178+
* Always runs outside any transaction — safe to use after the caller's
179+
* request transaction has been committed.
180+
*/
181+
export function optionsBgQx(options: any): QueryExecutor {
182+
return new SequelizeQueryExecutor(options.database.sequelize, true)
183+
}

0 commit comments

Comments
 (0)