Skip to content

Comments

feat: 增加 Redis Pub/Sub 缓存失效模块与单测#494

Closed
ding113 wants to merge 1 commit intodevfrom
fix/issue-492-redis-pubsub-tests
Closed

feat: 增加 Redis Pub/Sub 缓存失效模块与单测#494
ding113 wants to merge 1 commit intodevfrom
fix/issue-492-redis-pubsub-tests

Conversation

@ding113
Copy link
Owner

@ding113 ding113 commented Jan 1, 2026

Summary

Adds comprehensive unit test coverage for the Redis Pub/Sub cache invalidation module and integrates graceful shutdown for subscriber connections.

Problem

Related Issues:

While PR #493 implemented the core Redis Pub/Sub functionality for cross-process cache invalidation, it lacked:

  • Unit test coverage for the new pubsub.ts module
  • Graceful shutdown of subscriber connections on process exit
  • Test coverage for edge cases and error scenarios

Solution

This PR adds:

  1. Comprehensive Unit Tests (src/lib/redis/__tests__/pubsub.test.ts +369 lines):

    • Message publishing and subscription workflows
    • Multi-callback subscription with reference counting
    • Graceful degradation when Redis unavailable
    • Error handling isolation (callbacks, subscribe/unsubscribe failures)
    • Subscriber connection error events
    • State cleanup and memory leak prevention
  2. Graceful Shutdown Integration (src/instrumentation.ts):

    • Added closeSubscriber() call in Next.js shutdown hook
    • Prevents resource leaks during SIGTERM/SIGINT
    • Properly closes subscriber connections before Redis cleanup

Changes

Core Changes

  • src/lib/redis/__tests__/pubsub.test.ts (+369) - Full test suite for Redis Pub/Sub module
  • src/instrumentation.ts (+10) - Subscriber cleanup in shutdown handler

Supporting Changes

  • src/lib/redis/pubsub.ts (+128) - Core Pub/Sub module (included for reference)

Testing

Automated Tests

  • 11 comprehensive test cases covering:
    • publishCacheInvalidation() - Success, Redis unavailable, publish errors
    • subscribeCacheInvalidation() - Multi-callback, cleanup, error isolation
    • closeSubscriber() - Graceful shutdown, quit errors
    • Edge cases: duplicate errors, unsubscribe errors, subscriber error events

Test Coverage Scenarios

 Message publishing to channel
 Graceful handling when Redis not configured
 Error logging when publish fails (no throw)
 Callback registration and message reception
 Subscribe once per channel, unsubscribe on last cleanup
 Callback error isolation (one fails, others continue)
 Subscriber connection error event handling
 Subscribe failure recovery (allows retry)
 Unsubscribe error logging (no throw)
 closeSubscriber() quits connection and clears state
 Quit error graceful handling

Verification Commands

bun run test src/lib/redis/__tests__/pubsub.test.ts  # Run Pub/Sub tests
bun run test                                           # All tests pass
bun run lint                                           # Linting passes
bun run typecheck                                      # Type checks pass
bun run build                                          # Production build succeeds

Technical Details

Test Architecture:

  • Uses Vitest with mocked Redis client (EventEmitter-based)
  • Simulates real Redis behavior: duplicate(), subscribe(), publish(), quit()
  • Tests both happy paths and error scenarios
  • Validates graceful degradation patterns

Shutdown Flow:

SIGTERM/SIGINT received
  ↓
shutdownHandler()
  ↓
stopCacheCleanup() → closeSubscriber() → closeRedis()
  ↓
Prevents resource leaks

Why Graceful Shutdown Matters:

  • Subscriber uses dedicated Redis connection (via duplicate())
  • Without closeSubscriber(), connection remains open during shutdown
  • Can cause warnings or delayed process exit
  • Proper cleanup ensures clean shutdown in container environments

Breaking Changes

None - This is a test coverage enhancement.

Checklist

  • Code follows project conventions
  • Self-review completed
  • Tests pass locally (11/11 tests passing)
  • Documentation updated (test coverage validates API behavior)

变更说明(中文)

  • 新增 Redis Pub/Sub 缓存失效模块的完整单元测试(369 行)
  • 补齐边界场景:Redis 不可用、publish/subscribe/unsubscribe 异常、subscriber error 事件、回调抛错隔离、多回调与清理逻辑
  • 在 Next.js instrumentation 退出钩子中补充关闭订阅连接,避免退出时资源泄漏

验证(中文)

bun run test src/lib/redis/__tests__/pubsub.test.ts
bun run test
bun run lint
bun run typecheck
bun run build

Description enhanced by Claude AI

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @ding113, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

此拉取请求旨在通过引入一个基于 Redis Pub/Sub 的缓存失效通知机制来增强系统的缓存管理能力。它提供了一种高效的方式来通知服务中的其他组件缓存已更新或失效,从而确保数据一致性。同时,为了保证系统的健壮性和可靠性,该模块还包含了对各种异常情况的详细处理和全面的单元测试,并确保在应用关闭时能够优雅地释放资源。

Highlights

  • 新增 Redis Pub/Sub 缓存失效模块: 引入了一个新的 Redis 发布/订阅模块,用于处理缓存失效通知,包括发布消息、订阅频道和优雅关闭订阅连接的功能。
  • 完善的单元测试: 为新的 Pub/Sub 模块添加了全面的单元测试,覆盖了 Redis 不可用、发布/订阅/取消订阅异常、订阅者错误事件、回调抛错隔离、多回调处理以及清理逻辑等多种边界场景。
  • 集成优雅关闭订阅连接: 在 Next.js 的 instrumentation 退出钩子中增加了关闭 Redis Pub/Sub 订阅连接的逻辑,以避免应用程序退出时可能出现的资源泄漏。
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request area:core labels Jan 1, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

本次 PR 引入了 Redis Pub/Sub 模块用于缓存失效通知,并补充了全面的单元测试,这是一个很棒的功能增强。代码结构清晰,对 Redis 不可用、API 异常等边界情况都做了优雅降级处理。

我发现了一个与并发订阅相关的严重竞态条件,可能导致回调丢失,需要立即修复。此外,我还提出了一些关于代码简化和测试健壮性的建议,以提高代码质量和可维护性。

整体而言,这是一次高质量的提交,在修复了指出的关键问题后即可合并。

}

// 订阅成功后才更新本地状态
const callbacks = existing ?? new Set<CacheInvalidationCallback>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

这里存在一个严重的竞态条件,可能导致回调丢失。
当两个 subscribeCacheInvalidation 调用并发地为一个新 channel 执行时:

  1. 两个调用都会发现 existingundefined
  2. 两个调用都会执行 await sub.subscribe(channel)
  3. 第一个完成的调用会使用其作用域内的 existing (此时为 undefined) 创建一个新的 Set,添加它的回调,然后设置到 subscriptions Map 中。
  4. 第二个完成的调用也会使用它自己作用域内的 existing (同样为 undefined) 创建一个全新的 Set,这会覆盖第一个调用设置的 Set,导致第一个回调丢失。

问题在于 existing 变量在 await 之后是过时的。为了修复这个问题,你应该在 await 之后重新从 subscriptions Map 中获取回调集合,而不是使用陈旧的 existing 变量。

这将确保你总是在最新的回调集合上进行操作,从而避免数据丢失。

Suggested change
const callbacks = existing ?? new Set<CacheInvalidationCallback>();
const callbacks = subscriptions.get(channel) ?? new Set<CacheInvalidationCallback>();

Comment on lines +93 to +105
try {
void currentSubscriber.unsubscribe(channel).catch((error) => {
logger.warn("[RedisPubSub] Failed to unsubscribe cache invalidation", {
channel,
error,
});
});
} catch (error) {
logger.warn("[RedisPubSub] Failed to unsubscribe cache invalidation", {
channel,
error,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

unsubscribe 的错误处理逻辑有些重复和复杂。try...catch 块和 .catch() 处理程序都在做同样的事情(记录警告),这使得代码难以阅读和维护。这可以被简化。

你可以使用 Promise.resolve() 来包装 unsubscribe 调用。这会捕获同步抛出的错误并将其转换为一个 rejected promise,然后你就可以用一个 .catch() 来处理所有类型的错误。

          Promise.resolve(currentSubscriber.unsubscribe(channel)).catch((error) => {
            logger.warn("[RedisPubSub] Failed to unsubscribe cache invalidation", {
              channel,
              error,
            });
          });

expect(() => cleanup()).not.toThrow();

// 等待 microtask queue 清空,确保 Promise.resolve().then().catch() 链完成
await new Promise((resolve) => setTimeout(resolve, 10));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在测试中使用 setTimeout 加一个固定的延迟(如 10ms)来等待异步操作完成,这种做法比较脆弱,可能会导致测试不稳定(flaky)。测试的执行时间可能因环境而异。

为了使测试更健壮,建议使用 setImmediate 来等待事件循环的下一个 tick。这可以确保所有已排队的 microtask 都已执行完毕,而无需依赖任意的等待时间。

Suggested change
await new Promise((resolve) => setTimeout(resolve, 10));
await new Promise((resolve) => setImmediate(resolve));

@github-actions
Copy link
Contributor

github-actions bot commented Jan 1, 2026

🧪 测试结果

测试类型 状态
代码质量
单元测试
集成测试
API 测试

总体结果: ✅ 所有测试通过

@github-actions github-actions bot added the size/L Large PR (< 1000 lines) label Jan 1, 2026
try {
const sub = ensureSubscriber(baseClient);

const existing = subscriptions.get(channel);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] [LOGIC-BUG] Concurrent subscriptions can drop callbacks

src/lib/redis/pubsub.ts:70

Why this is a problem: existing is captured before await sub.subscribe(channel). If two subscribeCacheInvalidation() calls for the same channel run concurrently, the later call can overwrite subscriptions with a new Set created from stale existing, dropping previously registered callbacks.

Suggested fix:

const existing = subscriptions.get(channel);
const isFirstSubscriberForChannel = !existing;

if (isFirstSubscriberForChannel) {
  await sub.subscribe(channel);
}

// Re-read after await to avoid clobbering concurrent subscribers
const callbacks = subscriptions.get(channel) ?? new Set<CacheInvalidationCallback>();
callbacks.add(callback);
subscriptions.set(channel, callbacks);


for (const cb of callbacks) {
try {
cb();
Copy link
Contributor

@github-actions github-actions bot Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[High] [ERROR-SILENT] Async callback rejections can become unhandled

src/lib/redis/pubsub.ts:27

Why this is a problem: In the "message" handler, callbacks are invoked via cb() inside try/catch, which only catches synchronous throws. In TypeScript, an async () => {} is assignable to () => void, so a rejecting async callback will surface as an unhandled promise rejection (no catch), which can destabilize the process.

Suggested fix:

type CacheInvalidationCallback = () => void | Promise<void>;

for (const cb of callbacks) {
  try {
    const maybePromise = cb();
    void Promise.resolve(maybePromise).catch((error) => {
      logger.error("[RedisPubSub] Callback error", { channel, error });
    });
  } catch (error) {
    logger.error("[RedisPubSub] Callback error", { channel, error });
  }
}

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

This PR introduces a new Redis Pub/Sub cache invalidation utility plus tests, and wires subscriber shutdown into instrumentation.
Two high-risk issues exist in the new subscribeCacheInvalidation() implementation: a concurrency bug that can drop callbacks, and missing handling for async callback rejections that can surface as unhandled promise rejections.

PR Size: L

  • Lines changed: 507
  • Files changed: 3
  • Split suggestion: (1) src/lib/redis/pubsub.ts + src/lib/redis/__tests__/pubsub.test.ts, (2) src/instrumentation.ts shutdown-hook wiring.

Issues Found

Category Critical High Medium Low
Logic/Bugs 0 1 0 0
Security 0 0 0 0
Error Handling 0 1 0 0
Types 0 0 0 0
Comments/Docs 0 0 0 0
Tests 0 0 0 0
Simplification 0 0 0 0

Critical Issues (Must Fix)

None.

High Priority Issues (Should Fix)

  • src/lib/redis/pubsub.ts:70 [High] [LOGIC-BUG] Concurrent subscribeCacheInvalidation() calls can overwrite subscriptions with stale state and drop callbacks.
  • src/lib/redis/pubsub.ts:27 [High] [ERROR-SILENT] Async callback rejections are not caught and can become unhandled promise rejections.

Review Coverage

  • Logic and correctness
  • Security (OWASP Top 10)
  • Error handling
  • Type safety
  • Documentation accuracy
  • Test coverage
  • Code clarity

Automated review by Codex AI

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Reviewed PR #494 and applied label size/L (507 lines, 3 files).
  • Left 2 inline comments on new code:
    • src/lib/redis/pubsub.ts:70[High] [LOGIC-BUG] concurrent subscribeCacheInvalidation() calls can drop callbacks due to stale existing across await.
    • src/lib/redis/pubsub.ts:27[High] [ERROR-SILENT] async callback rejections aren’t handled and can become unhandled promise rejections.
  • Submitted the required summary review on the PR (includes L-size split suggestion).

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

No significant issues identified in this PR.

PR Size: L

  • Lines changed: 507
  • Files changed: 3

This is a large PR focused on adding comprehensive test coverage. The implementation demonstrates strong engineering practices with proper error handling, graceful degradation, and thorough test coverage.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean
  • Type safety - Clean
  • Documentation accuracy - Clean
  • Test coverage - Comprehensive (11 test cases covering all scenarios)
  • Code clarity - Good

Key Strengths

  • Comprehensive test coverage: 11 test cases covering happy paths, error scenarios, edge cases, and graceful degradation
  • Proper error isolation: All error scenarios log warnings and continue execution (fail-open pattern)
  • Resource management: Graceful shutdown integration prevents connection leaks
  • Reference counting: Multi-callback subscription with proper cleanup
  • Mock architecture: Well-designed EventEmitter-based mocks simulate real Redis behavior

Automated review by Claude AI

@ding113 ding113 closed this Jan 1, 2026
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Jan 1, 2026
@ding113 ding113 deleted the fix/issue-492-redis-pubsub-tests branch January 27, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:core enhancement New feature or request size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant