Conversation
📝 WalkthroughWalkthroughThe PR refactors memory management and listener cleanup across loader and cache layers. Changes include removing empty refresh tracking entries, simplifying load deletion logic, optimizing cache resolution, and properly removing Redis message listeners on close. New tests verify these cleanup behaviors. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
No actionable comments were generated in the recent review. 🎉 🧹 Recent nitpick comments
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary
Memory leak fixes and performance optimizations across the codebase, identified through static analysis.
Fix 1: Remove event listeners on Redis consumer
close()Both
RedisNotificationConsumerandRedisGroupNotificationConsumerattached an anonymous function toredis.on('message', ...)duringsubscribe(), but never removed it inclose(). Since the handler holds a closure overthis.targetCache(the in-memory cache), the listener prevented garbage collection of the entire cache tree after the consumer was closed.Before:
After:
Fix 2: Clean up empty Sets in
GroupLoader.groupRefreshFlagsWhen a background async refresh completed for a grouped key, the key was deleted from the group's
SetingroupRefreshFlags, but the now-emptySetitself was never removed from theMap. Over time, every group that ever triggered a background refresh would leave behind an emptySetpermanently in memory.Before:
After:
This matches the cleanup pattern already used in
AbstractGroupCache.deleteGroupRunningLoad.Fix 3: Avoid creating empty group load Maps in
getInMemoryOnlygetInMemoryOnly()calledthis.resolveGroupLoads(group)to check whether a key had a running load.resolveGroupLoadscreates and inserts a new emptyMapif none exists for the group — a side effect that's unnecessary when the intent is a read-only check. Every call togetInMemoryOnlyfor a group without active loads would allocate aMapthat persists until the group's loads are cleaned up.Before:
After:
Returns
undefined(falsy) when noMapexists — semantically identical, without the allocation side effect.getAsyncOnly()still callsresolveGroupLoads()when it actually needs to store a load.Fix 4: Optimize
InMemoryGroupCache.getManyFromGroupgetManyFromGroupcalledthis.getFromGroup(keys[i], group)inside a loop, which calledthis.resolveGroup(groupId)on every iteration — repeatedly looking up the same group cache from the LRU.Before:
After:
Reduces N hash lookups to 1.
Fix 5: Optimize
unique()to skip allocation when input has no duplicatesThe original
unique()always created a new array viaArray.from(new Set(arr)), even when the input had no duplicates (the common case ingetManycalls).Before:
After:
Returns the original array reference when all elements are already unique, avoiding an unnecessary array allocation on the hot path.
Fix 6: Remove redundant
has()beforedelete()in LoaderforceSetValueandforceRefreshboth guardedthis.runningLoads.delete(key)withif (this.runningLoads.has(key)).Map.delete()is already a no-op for missing keys, so the guard was redundant. The lines were also marked with/* v8 ignore next -- @preserve */confirming the branch was unreachable in tests.Before:
After:
Test plan
RedisNotificationPublisher.spec.ts— verifieslistenerCount('message')drops to 0 afterclose()RedisGroupNotificationPublisher.spec.ts— same for group consumerGroupLoader-async-refresh.spec.ts— verifiesgroupRefreshFlagsMap has no empty Set after refresh completesGroupLoader-main.spec.ts— verifiesgetInMemoryOnlydoes not create emptyrunningLoadsentryunique.spec.ts— verifies same-reference return when no duplicates, new-array return when duplicates existSummary by CodeRabbit
Bug Fixes
Performance
Tests