Skip to content

fix: 规则保存后即时生效(同步内存缓存 + 刷新列表),无需手动点刷新#1244

Merged
ding113 merged 3 commits into
devfrom
fix/rules-auto-refresh-on-save
Jun 3, 2026
Merged

fix: 规则保存后即时生效(同步内存缓存 + 刷新列表),无需手动点刷新#1244
ding113 merged 3 commits into
devfrom
fix/rules-auto-refresh-on-save

Conversation

@ding113

@ding113 ding113 commented Jun 3, 2026

Copy link
Copy Markdown
Owner

问题

请求过滤器(Request Filters)和错误规则(Error Rules)在新增/编辑/删除/启停后,需要手动点击"刷新缓存"按钮才会真正生效。用户在请求过滤器侧可稳定复现。

复现与根因分析

定位到代理运行时有一层进程内内存缓存RequestFilterEngine / ErrorRuleDetector),代理请求实际命中的是这层缓存,而非数据库。

回答用户的两个问题:

  1. 数据修改后会自动刷新吗?
    • 请求过滤器:UI 列表会刷新(filter-dialog/filter-table 已调用 router.refresh()),但代理内存缓存只靠仓储层 fire-and-forget 的事件触发 void reload(),动作层从不 await
    • 错误规则:UI 列表完全不刷新add/edit/toggle/delete 均未调用 router.refresh(),仅靠 revalidatePath——经 REST 客户端调用时不会触发客户端重渲染),代理内存缓存同样只靠 fire-and-forget 事件。
  2. 是否需要主动点刷新按钮? 是。"刷新缓存"按钮是唯一会 await reload() 同步刷新内存缓存的路径。

此外 RequestFilterEngine.reload() 有一个隐患:if (this.isLoading) return; 会在已有 reload 进行中时静默丢弃并发的 reload(没有补跑队列,不同于已经健壮的 ErrorRuleDetector)。

改动

服务端(让规则对代理即时生效)

  • RequestFilterEngine.reload():移植 ErrorRuleDetectoractiveReloadPromise + reloadRequestedWhileLoading 补跑队列,并发 reload 不再被丢弃;ensureInitialized() 会等待在途 reload 完成,避免读到旧快照。
  • request-filters / error-rules 的 create/update/delete 动作:写库成功后直接 await reload(),确保返回前本进程缓存已是最新。仍保留事件广播(Redis pub/sub)用于多 worker 同步。

前端(让列表即时更新)

  • 错误规则的 add-rule-dialogedit-rule-dialogrule-list-table(启停 + 删除)、refresh-cache-button 全部在成功后调用 router.refresh(),与请求过滤器已有行为对齐。

测试(TDD,先红后绿)

新增 4 个测试文件,均先确认 RED 再实现转 GREEN:

  • request-filter-engine-reload-queue.test.ts:并发/事件触发的 reload 不再被丢弃,await 的 reload 能观察到最新快照。
  • request-filters-cache-reload.test.ts / error-rules-cache-reload.test.ts:create/update/delete 成功后会 await 引擎/检测器的 reload(),失败时不会。
  • error-rules-list-refresh-ui.test.tsx:错误规则启停/删除/编辑保存后会调用 router.refresh()

验证

  • bun run build
  • bun run typecheck
  • bun run lint
  • bun run test ✅ 6262 passed / 13 skipped(696 文件)

Related PRs

🤖 Generated with Claude Code

Greptile Summary

This PR fixes stale in-memory caches in RequestFilterEngine and ErrorRuleDetector by: (1) porting the activeReloadPromise + reloadRequestedWhileLoading do/while queued-reload pattern from ErrorRuleDetector to RequestFilterEngine, and (2) adding explicit await reload() calls in the create/update/delete server actions so the cache is synced before the response is returned, eliminating the "must click Refresh Cache" workaround. UI components for error-rules also receive router.refresh() calls on mutation success to align them with the existing behavior in request-filter components.

  • Engine reload logic (request-filter-engine.ts): replaces the silent-discard if (isLoading) return with a do/while queued-reload loop identical to ErrorRuleDetector's; ensureInitialized() gains an activeReloadPromise fast-path for cold-start deduplication.
  • Action-layer cache sync (request-filters.ts, error-rules.ts): all create/update/delete actions now await reload() after the DB write; ErrorRuleDetector.reload() gains a queueIfRunning option so the refreshCacheAction can force a full re-read while normal action calls reuse any in-flight promise without queuing a redundant second pass.
  • UI list refresh (add-rule-dialog, edit-rule-dialog, rule-list-table, refresh-cache-button): adds router.refresh() on success, mirroring the existing behavior in the request-filter equivalents.

Confidence Score: 5/5

The change is safe to merge. Core reload logic is correct in all single-mutation and concurrent-mutation scenarios; cache integrity is maintained through the do/while queued-reload pattern, and action failures are handled gracefully without flipping mutation results.

The queued do/while pattern correctly handles both the common case (no pre-existing in-flight reload) and the concurrent case (pre-existing reload from a prior mutation) — the event handler's queue=true ensures a follow-up iteration that the awaited promise covers. The only notable gap is a test that uses the wrong calling convention for the action path, but this does not affect production behaviour. All mutation paths are guarded, the best-effort catch blocks prevent reload errors from poisoning successful writes, and the full test suite passes.

tests/unit/lib/request-filter-engine-reload-queue.test.ts — test #3 should model the action's reload(false) convention alongside the event handler's reload() call to accurately represent the concurrent-mutation scenario.

Important Files Changed

Filename Overview
src/lib/request-filter-engine.ts Replaces silent-discard reload guard with queued do/while pattern; adds activeReloadPromise fast-path to ensureInitialized. Core logic is correct, but unconditional return after awaiting activeReloadPromise in ensureInitialized (pre-existing issue) remains; see previous thread.
src/lib/error-rule-detector.ts Adds queueIfRunning option to reload(); event handler retains queueIfRunning:true for correctness; action calls default (false) to avoid redundant DB reads. Change is minimal and correct.
src/actions/error-rules.ts Adds try/catch await reload() after each successful mutation and uses queueIfRunning:true for the manual refresh action. Best-effort cache sync correctly does not flip ok:false on reload failure.
src/actions/request-filters.ts Adds try/catch await reload(false) after each successful create/update/delete; reload placed correctly after null-check guard so it only fires on actual DB success.
tests/unit/lib/request-filter-engine-reload-queue.test.ts Covers queued-reload and event-driven scenarios well, but test #3 uses reload() (queue=true default) for the "action's awaited reload" rather than reload(false) as the action actually calls, making the test comment misleading.
tests/unit/actions/error-rules-cache-reload.test.ts Good coverage: verifies reload is called on success and skipped on failure, and that a reload error doesn't flip the action result.
tests/unit/actions/request-filters-cache-reload.test.ts Good coverage for create/update/delete paths; create test explicitly verifies reload(false) argument. Update and delete tests don't assert the argument but verify call count.
tests/unit/error-rules-list-refresh-ui.test.tsx Comprehensive UI regression tests for all mutation flows (toggle, delete, edit, add, refresh cache) verifying router.refresh() is called.

Sequence Diagram

sequenceDiagram
    participant UI as UI Component
    participant Action as Server Action
    participant Repo as Repository Layer
    participant EE as EventEmitter
    participant Engine as RequestFilterEngine / ErrorRuleDetector
    participant DB as Database

    UI->>Action: createRequestFilterAction(data)
    Action->>Repo: createRequestFilter(data)
    Repo->>DB: INSERT
    DB-->>Repo: created record
    Repo->>EE: emit("requestFiltersUpdated") [sync]
    EE->>Engine: "void reload() [queue=true, fire-and-forget]"
    Note over Engine: activeReloadPromise = new do/while loop
    Repo-->>Action: created record

    Action->>Engine: await reload(false)
    Note over Engine: activeReloadPromise already set →<br/>return existing promise (no second round queued)
    Engine->>DB: SELECT active filters
    DB-->>Engine: [fresh rows including new record]
    Engine-->>Engine: "loadFilters() → isInitialized = true"
    Engine-->>Action: promise resolves (cache is current)

    Action->>UI: "{ ok: true }"
    UI->>UI: router.refresh()

    Note over Engine,DB: Concurrent mutation scenario (pre-existing in-flight reload)
    Engine->>DB: "SELECT (reload #prev, started before write)"
    Action->>Engine: "[event fires] void reload() [queue=true]"
    Note over Engine: reloadRequestedWhileLoading = true
    Action->>Engine: await reload(false) → returns promisePrev
    DB-->>Engine: [stale snapshot]
    Engine->>DB: SELECT again (second do/while iteration)
    DB-->>Engine: [fresh rows]
    Engine-->>Action: promisePrev resolves (both iterations done)
Loading

Comments Outside Diff (1)

  1. src/lib/request-filter-engine.ts, line 470-492 (link)

    P2 Fast-path in applyGlobal / applyForProvider can bypass the new reload-wait

    Both applyGlobal and applyForProvider have an early return if (this.isInitialized && this.<filters>.length === 0) return; that fires before ensureInitialized() is called. When a user adds the very first global guard filter, the engine is initialized with an empty list. A concurrent proxy request entering this fast path will return immediately — even if activeReloadPromise is set for a reload that would load the new filter. The new ensureInitialized() guard added in this PR only helps when there are already filters in the bucket. This is a pre-existing design trade-off, but it limits the correctness guarantee the PR aims to provide for the "add first rule" case.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/request-filter-engine.ts
    Line: 470-492
    
    Comment:
    **Fast-path in `applyGlobal` / `applyForProvider` can bypass the new reload-wait**
    
    Both `applyGlobal` and `applyForProvider` have an early return `if (this.isInitialized && this.<filters>.length === 0) return;` that fires before `ensureInitialized()` is called. When a user adds the very first global guard filter, the engine is initialized with an empty list. A concurrent proxy request entering this fast path will return immediately — even if `activeReloadPromise` is set for a reload that would load the new filter. The new `ensureInitialized()` guard added in this PR only helps when there are already filters in the bucket. This is a pre-existing design trade-off, but it limits the correctness guarantee the PR aims to provide for the "add first rule" case.
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
tests/unit/lib/request-filter-engine-reload-queue.test.ts:1102-1131
**Test models the wrong calling convention for the action path**

This test uses the default `reload()` (queue=true) for "the action's awaited reload", but the action code calls `reload(false)`. If the second call were changed to `reload(false)`, no second DB read would be queued (because only the event handler's `reload(true)` sets `reloadRequestedWhileLoading`), so the test would resolve with 1 filter rather than 5 — contradicting the assertion.

To accurately model the real scenario where an in-flight reload is already running when a mutation lands, the test needs **three** calls: one for the pre-existing reload (started before the write), one simulating the event handler (`void reload()`, queue=true, which sets `reloadRequestedWhileLoading = true`), and one for the action (`reload(false)`, which reuses the promise). The current two-call structure with `reload(true)` as the "action" call inflates the guarantee and leaves the actual `reload(false)` path untested.

Reviews (3): Last reviewed commit: "fix(cache): tolerate reload failures and..." | Re-trigger Greptile

Previously, after creating, editing, or deleting an error rule or
request filter, the in-memory cache was not updated and the UI list
remained stale until the user clicked the refresh cache button.
This meant proxy requests continued to use old rule versions until
a manual refresh.

Now server actions directly trigger engine and detector reloads
(errorRuleDetector.reload() and requestFilterEngine.reload()) and
UI components call router.refresh() to reflect changes immediately.
Additionally, request-filter-engine was strengthened to handle
concurrent reloads: if a reload is in-flight and another is requested,
a second DB read is queued and executed automatically, preventing
dropped updates. The UI for error rules (add, edit, toggle, delete,
refresh button) now refreshes the list via router.refresh after
successful mutations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

在 Actions 的错误规则与请求过滤器 mutation 成功后新增本进程缓存重载调用;多个 error-rules UI 组件在成功后调用 router.refresh() 刷新页面;RequestFilterEngine 将 reload() 改为并发合并 + 补跑队列模型;新增对应单元与回归测试覆盖。

变更

缓存同步与 UI 刷新实现

Layer / File(s) Summary
请求过滤器并发重载队列
src/lib/request-filter-engine.ts
RequestFilterEngine 新增 activeReloadPromisereloadRequestedWhileLoading,重写 reload(queue) 为合并并发并支持补跑(do/while 循环与链式 Promise),并调整 ensureInitialized()
错误规则缓存同步与 UI 刷新
src/actions/error-rules.ts, src/app/[locale]/settings/error-rules/_components/*.tsx
error-rules 的 create/update/delete actions 在成功后调用 errorRuleDetector.reload()(best-effort,失败记录 warn);Add/Edit dialogs、RuleListTable、RefreshCacheButton 在成功后调用 router.refresh()
请求过滤器缓存同步
src/actions/request-filters.ts
request-filters 的 create/update/delete actions 在成功后调用 requestFilterEngine.reload(false) 以立即同步内存缓存(best-effort,失败记录 warn),保留现有页面重验证。
测试与回归覆盖
tests/unit/actions/error-rules-cache-reload.test.ts, tests/unit/actions/request-filters-cache-reload.test.ts, tests/unit/error-rules-list-refresh-ui.test.tsx, tests/unit/lib/request-filter-engine-reload-queue.test.ts
新增与扩展测试覆盖:actions 在成功时触发 detector/engine 的 reload()(包含失败不影响返回的场景);RequestFilterEngine 的并发与排队行为;UI 组件在成功后触发 router.refresh() 的回归用例。

总体概览

该 PR 在 mutation 成功后实现本进程缓存的 best-effort 重载与页面级刷新,并为请求过滤器引擎引入并发复用与排队补跑语义,配套增加单元与回归测试以验证新行为。

代码审查工作量

🎯 4 (Complex) | ⏱️ ~45 分钟

可能相关的 PR

  • ding113/claude-code-hub#1006: 涉及 ErrorRuleDetector.reload() 的并发/队列语义调整,和本 PR 在错误规则缓存同步层面直接相关。
  • ding113/claude-code-hub#1199: 修改 src/actions/request-filters.ts 的变更与本 PR 在 request-filters action 层存在实现交叉。
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed 标题清晰准确地反映了 PR 的主要目标:规则保存后即时生效,无需手动点刷新,涵盖了代码变更的核心问题解决。
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed PR 描述清晰地说明了问题、根因、改动内容和测试验证,与代码变更内容相关。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/rules-auto-refresh-on-save

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added bug Something isn't working area:Error Rule area:UI labels Jun 3, 2026

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces immediate memory cache synchronization and UI refreshing upon creating, updating, or deleting error rules and request filters, along with a queuing mechanism in RequestFilterEngine to handle concurrent reloads. The reviewer provided valuable feedback to optimize performance and availability: first, by checking initialization status before waiting for active reloads in ensureInitialized to avoid blocking proxy requests with database queries; and second, by introducing a queue parameter to bypass redundant, back-to-back reload operations triggered concurrently by event emissions and direct action calls.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines 451 to 457
private async ensureInitialized(): Promise<void> {
// 有在途 reload(含排队补跑)时等待它完成,避免读到补跑前的旧快照。
if (this.activeReloadPromise) {
await this.activeReloadPromise;
return;
}
if (this.isInitialized) return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

在代理请求的路径中,延迟和可用性是至关重要的。目前的 ensureInitialized 实现将 activeReloadPromise 的等待放在了 this.isInitialized 判断之前。这意味着,一旦管理员在后台修改了规则触发了 reload,所有并发的代理请求都将被阻塞,直到数据库查询和缓存重构完成。这不仅会增加代理请求的延迟,还让代理请求的可用性直接强绑定到了数据库的可用性上(如果数据库慢或宕机,代理请求也会挂起或失败)。

在缓存设计中,对于代理服务,最终一致性(在后台加载完成前继续使用旧的内存快照)远比强一致性(阻塞请求等待最新快照)更合适。

建议将 this.isInitialized 的判断移到最前面,只有在缓存完全未初始化(如服务刚启动)时才阻塞等待在途的 reload

Suggested change
private async ensureInitialized(): Promise<void> {
// 有在途 reload(含排队补跑)时等待它完成,避免读到补跑前的旧快照。
if (this.activeReloadPromise) {
await this.activeReloadPromise;
return;
}
if (this.isInitialized) return;
private async ensureInitialized(): Promise<void> {
if (this.isInitialized) return;
// 仅在未初始化且有在途 reload 时等待其完成,避免在已初始化的情况下阻塞代理请求。
if (this.activeReloadPromise) {
await this.activeReloadPromise;
return;
}

Comment thread src/lib/request-filter-engine.ts Outdated
Comment on lines 341 to 347
async reload(): Promise<void> {
if (this.isLoading) return;
this.isLoading = true;

try {
const { getActiveRequestFilters } = await import("@/repository/request-filters");
const filters = await getActiveRequestFilters();
this.loadFilters(filters);
} catch (error) {
logger.error("[RequestFilterEngine] Failed to reload filters", { error });
} finally {
this.isLoading = false;
if (this.activeReloadPromise) {
// 已有 reload 在途时排队补跑一轮:保存规则后的显式 reload 不能被丢弃,
// 否则代理仍会命中旧的内存快照,必须等用户手动点"刷新缓存"才生效。
this.reloadRequestedWhileLoading = true;
return this.activeReloadPromise;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

目前的 reload() 实现中,只要有在途的 reload,就会无条件设置 this.reloadRequestedWhileLoading = true 并排队补跑一轮。

然而,在新增/编辑/删除规则的 Action 中,代码会先调用 emitRequestFiltersUpdated()(这会同步触发本地事件监听器并启动一个 reload),紧接着又调用 await requestFilterEngine.reload()。这会导致 reloadRequestedWhileLoading 总是被设为 true,从而强制执行第二次完全冗余的 reload 循环(包括一次额外的数据库查询)。

这不仅使数据库查询开销翻倍,还导致管理员在 UI 上保存规则时的等待时间翻倍(因为 Action 会等待两个完整的 reload 周期完成)。

建议为 reload() 增加一个可选的 queue 参数(默认为 true),在 Action 中显式传入 false,这样 Action 就可以直接等待已经由事件触发的、包含最新数据的在途 reload,而无需排队补跑第二轮。

  async reload(queue = true): Promise<void> {
    if (this.activeReloadPromise) {
      // 已有 reload 在途时排队补跑一轮:保存规则后的显式 reload 不能被丢弃,
      // 否则代理仍会命中旧的内存快照,必须等用户手动点"刷新缓存"才生效。
      if (queue) {
        this.reloadRequestedWhileLoading = true;
      }
      return this.activeReloadPromise;
    }

Comment thread src/actions/request-filters.ts Outdated
Comment on lines +256 to +257
// 立即同步内存缓存,确保新规则对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

配合 requestFilterEngine.reload(queue) 的优化,这里应该传入 false。因为 createRequestFilter 内部已经调用了 emitRequestFiltersUpdated() 触发了本地事件驱动的 reload,此处我们只需要 await 该在途的 reload 即可,无需排队补跑第二轮,从而避免冗余的数据库查询并缩短响应时间。

Suggested change
// 立即同步内存缓存,确保新规则对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload();
// 立即同步内存缓存,确保新规则对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload(false);

Comment thread src/actions/request-filters.ts Outdated
Comment on lines +383 to +384
// 立即同步内存缓存,确保规则改动对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

配合 requestFilterEngine.reload(queue) 的优化,这里应该传入 false。因为 updateRequestFilter 内部已经调用了 emitRequestFiltersUpdated() 触发了本地事件驱动的 reload,此处我们只需要 await 该在途的 reload 即可,无需排队补跑第二轮,从而避免冗余的数据库查询并缩短响应时间。

Suggested change
// 立即同步内存缓存,确保规则改动对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload();
// 立即同步内存缓存,确保规则改动对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload(false);

Comment thread src/actions/request-filters.ts Outdated
Comment on lines +400 to +401
// 立即同步内存缓存,确保删除对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

配合 requestFilterEngine.reload(queue) 的优化,这里应该传入 false。因为 deleteRequestFilter 内部已经调用了 emitRequestFiltersUpdated() 触发了本地事件驱动的 reload,此处我们只需要 await 该在途的 reload 即可,无需排队补跑第二轮,从而避免冗余的数据库查询并缩短响应时间。

Suggested change
// 立即同步内存缓存,确保删除对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload();
// 立即同步内存缓存,确保删除对代理请求即时生效,无需用户手动点"刷新缓存"。
await requestFilterEngine.reload(false);

Comment thread src/actions/error-rules.ts Outdated
Comment on lines +181 to +182
// 本进程立即同步内存缓存,确保规则改动对代理请求即时生效,无需手动点"刷新缓存"。
await errorRuleDetector.reload();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

这里存在与 request-filters 类似的双重 reload 性能问题。emitErrorRulesUpdated() 会触发本地事件监听器启动一个 reload,随后紧接着调用 await errorRuleDetector.reload() 会导致 reloadRequestedWhileLoading 被设为 true,从而强制排队补跑第二轮。

这导致了冗余的数据库查询,并使管理员保存规则时的响应时间翻倍。建议后续对 ErrorRuleDetector 也进行类似的 reload(queue = false) 优化,并在此处传入 false

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/actions/error-rules-cache-reload.test.ts (1)

68-118: ⚡ Quick win

建议补充 reload() 抛错时的行为覆盖。

当前用例验证了成功路径与“删除目标不存在”路径,但未覆盖 reload() 抛错的场景。结合 src/actions/error-rules.ts 中 reload 失败会被误报为整体失败的问题,建议新增一条断言:当 reloadMock.mockRejectedValueOnce(...) 时,写入已成功的动作仍返回 ok: true,以锁定修复后的语义。

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/actions/error-rules-cache-reload.test.ts` around lines 68 - 118,
Add a test ensuring a failing reload does not flip a successful action to
failed: in the existing suite add a case that mocks
reloadMock.mockRejectedValueOnce(new Error("boom")), calls each action under
test (e.g., createErrorRuleAction({...}), updateErrorRuleAction(1, {...}), or
deleteErrorRuleAction(1) as appropriate), and asserts the action result still
returns ok: true while reloadMock was called (to confirm reload was attempted
but its rejection does not change the action's success semantics).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/actions/error-rules.ts`:
- Around line 179-184: The current flow lets errorRuleDetector.reload() failures
turn a successful repo.createErrorRule into an overall failure; change each
action (createErrorRuleAction, updateErrorRuleAction, deleteErrorRuleAction) so
that the DB operation (repo.createErrorRule / repo.updateErrorRule /
repo.deleteErrorRule) and emitErrorRulesUpdated() determine success, while
errorRuleDetector.reload() and revalidatePath("/settings/error-rules") are
executed in their own try/catch that logs the error (or processLogger.error) but
does not re-throw or flip the action result; in short, wrap calls to
errorRuleDetector.reload() and revalidatePath(...) in a safe catch block and
ensure the action returns { ok: true, ... } whenever the repository operation
succeeded.

---

Nitpick comments:
In `@tests/unit/actions/error-rules-cache-reload.test.ts`:
- Around line 68-118: Add a test ensuring a failing reload does not flip a
successful action to failed: in the existing suite add a case that mocks
reloadMock.mockRejectedValueOnce(new Error("boom")), calls each action under
test (e.g., createErrorRuleAction({...}), updateErrorRuleAction(1, {...}), or
deleteErrorRuleAction(1) as appropriate), and asserts the action result still
returns ok: true while reloadMock was called (to confirm reload was attempted
but its rejection does not change the action's success semantics).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 58a005c2-d4dd-4668-a31e-0b75954c6cab

📥 Commits

Reviewing files that changed from the base of the PR and between 08599ec and c8811c6.

📒 Files selected for processing (11)
  • src/actions/error-rules.ts
  • src/actions/request-filters.ts
  • src/app/[locale]/settings/error-rules/_components/add-rule-dialog.tsx
  • src/app/[locale]/settings/error-rules/_components/edit-rule-dialog.tsx
  • src/app/[locale]/settings/error-rules/_components/refresh-cache-button.tsx
  • src/app/[locale]/settings/error-rules/_components/rule-list-table.tsx
  • src/lib/request-filter-engine.ts
  • tests/unit/actions/error-rules-cache-reload.test.ts
  • tests/unit/actions/request-filters-cache-reload.test.ts
  • tests/unit/error-rules-list-refresh-ui.test.tsx
  • tests/unit/lib/request-filter-engine-reload-queue.test.ts

Comment thread src/actions/error-rules.ts
@github-actions github-actions Bot added the size/L Large PR (< 1000 lines) label Jun 3, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

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 fixes a real user-facing bug where request filters and error rules required a manual "Refresh Cache" click after mutations. The fix is cohesive: server-side actions now await reload() after DB writes to synchronize the in-memory cache, and client-side error-rule components now call router.refresh() to match the request-filters behavior that already existed. The RequestFilterEngine.reload() method is correctly ported from the proven ErrorRuleDetector queueing pattern (activeReloadPromise + reloadRequestedWhileLoading do/while loop with post-settlement safety net). Test coverage is solid with 4 new test files covering reload queueing, cache-refresh-on-mutation, and UI refresh behavior.

PR Size: L

  • Lines changed: 718
  • Files changed: 11

Issues Found

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

No significant issues identified. All changes are in new/modified lines, correctly scoped to the fix, and covered by tests.

Split Suggestion (L PR)

The PR is already cohesive around a single bug fix. If a split were desired, the natural seam would be: (1) server-side reload + engine queueing (core fix) and (2) client-side router.refresh() additions (UX polish) — but both halves are tightly coupled to the same user story, so splitting adds process overhead without real benefit.

Review Coverage

  • Logic and correctness - Clean
  • Security (OWASP Top 10) - Clean
  • Error handling - Clean (all new await reload() calls are inside existing try/catch blocks; engine reload has internal error logging)
  • Type safety - Clean
  • Documentation accuracy - Clean (comments accurately describe behavior)
  • Test coverage - Adequate (4 new test files with 10 test cases covering reload queue, mutation-triggered reload, and UI refresh)
  • Code clarity - Good (reload queueing pattern mirrors proven ErrorRuleDetector implementation)

Automated review by Claude AI

Comment thread src/actions/error-rules.ts Outdated
Comment on lines +179 to +182
// 刷新缓存(事件广播,支持多 worker 同步)
await emitErrorRulesUpdated();
// 本进程立即同步内存缓存,确保规则改动对代理请求即时生效,无需手动点"刷新缓存"。
await errorRuleDetector.reload();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Double DB read per error-rules mutation

emitErrorRulesUpdated() synchronously fires the errorRulesUpdated event, whose local listener immediately calls void this.reload(), starting the first reload and setting activeReloadPromise. The very next line then calls await errorRuleDetector.reload(), which — because activeReloadPromise is already set — sets reloadRequestedWhileLoading = true and piggybacks on the first reload, causing a mandatory second DB read after the first completes.

The result is 2 DB round-trips per create/update/delete, whereas request-filters (which does not call an equivalent emitRequestFiltersUpdated() from the action layer) only does 1. If the intent is to keep the event for multi-worker Redis pub/sub sync and also guarantee the local process is current, consider whether the local EventEmitter listener should be skipped when the action already calls reload() explicitly, or simply remove the local EventEmitter subscription from ErrorRuleDetector and rely solely on the action's direct reload() for local sync and Redis pub/sub for remote workers.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/actions/error-rules.ts
Line: 179-182

Comment:
**Double DB read per error-rules mutation**

`emitErrorRulesUpdated()` synchronously fires the `errorRulesUpdated` event, whose local listener immediately calls `void this.reload()`, starting the first reload and setting `activeReloadPromise`. The very next line then calls `await errorRuleDetector.reload()`, which — because `activeReloadPromise` is already set — sets `reloadRequestedWhileLoading = true` and piggybacks on the first reload, causing a mandatory second DB read after the first completes.

The result is 2 DB round-trips per create/update/delete, whereas `request-filters` (which does not call an equivalent `emitRequestFiltersUpdated()` from the action layer) only does 1. If the intent is to keep the event for multi-worker Redis pub/sub sync and also guarantee the local process is current, consider whether the local EventEmitter listener should be skipped when the action already calls `reload()` explicitly, or simply remove the local EventEmitter subscription from `ErrorRuleDetector` and rely solely on the action's direct `reload()` for local sync and Redis pub/sub for remote workers.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +453 to +456
if (this.activeReloadPromise) {
await this.activeReloadPromise;
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 ensureInitialized() skips re-initialization after a silently-failed reload

If activeReloadPromise is set and the in-flight reload fails internally (exception caught and logged, isInitialized never set to true), this method awaits the promise, then returns unconditionally without falling through to the initializationPromise path. The caller proceeds with an uninitialized engine (empty filter arrays). The next proxy request will trigger a fresh initializationPromise, so the gap is transient — but for that one request, filters may be silently skipped. Adding if (this.isInitialized) return; after the await would close this gap.

Suggested change
if (this.activeReloadPromise) {
await this.activeReloadPromise;
return;
}
if (this.activeReloadPromise) {
await this.activeReloadPromise;
if (this.isInitialized) return;
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/request-filter-engine.ts
Line: 453-456

Comment:
**`ensureInitialized()` skips re-initialization after a silently-failed reload**

If `activeReloadPromise` is set and the in-flight reload fails internally (exception caught and logged, `isInitialized` never set to `true`), this method awaits the promise, then returns unconditionally without falling through to the `initializationPromise` path. The caller proceeds with an uninitialized engine (empty filter arrays). The next proxy request will trigger a fresh `initializationPromise`, so the gap is transient — but for that one request, filters may be silently skipped. Adding `if (this.isInitialized) return;` after the await would close this gap.

```suggestion
    if (this.activeReloadPromise) {
      await this.activeReloadPromise;
      if (this.isInitialized) return;
    }
```

How can I resolve this? If you propose a fix, please make it concise.

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

Previous implementation awaited the active reload promise on every
ensureInitialized call, potentially blocking the proxy hot path
while a reload was in progress. Since loadFilters performs atomic
bucket swaps, the proxy can read a consistent snapshot without
waiting. This change removes the blocking await, relying on the
action-side awaited reload to guarantee immediate effect after
saves.

Add regression coverage for AddRuleDialog and RefreshCacheButton
to verify that router.refresh is called after creating a rule
and refreshing the cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

After mutation, the cache reload was called synchronously and its failure
would cause the action to return an error, potentially leading to
double-create on retry. Now the reload is best-effort, logging a warning
on failure while the successful write is preserved.

Additionally, the reload methods now accept a parameter to control
queueing when a reload is already in progress. Mutation actions pass
false to reuse the emit-triggered reload, avoiding a redundant second DB
read and speeding up the save response. The ensureInitialized method
also reuses an in-flight reload during cold start to prevent duplicate
reads.

Added regression tests for reload-failure tolerance and reload(false)
reuse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ding113

ding113 commented Jun 3, 2026

Copy link
Copy Markdown
Owner Author

已处理 Review 意见(commit 384e9cc6

CodeRabbit(Major)— reload 失败把已成功的写入误报为失败
create/update/delete 三个 action 里的 await detector/engine.reload() 原本在外层 try/catch 内,reload 抛错会让已成功的 DB 写入返回 { ok: false },用户重试可能重复创建。现已把 reload 改为尽力而为:单独 try/catch,失败仅 logger.warn,不影响写入的成功语义(跨 worker 同步仍由 emit*Updated() 事件保证)。request-filters 三处同样处理。新增回归测试:reloadMock.mockRejectedValueOnce(...) 时 action 仍返回 ok: true

Gemini(High)— ensureInitialized 阻塞代理热路径
已将 if (this.isInitialized) return; 前置;仅在「尚未初始化」且有在途 reload 时才复用它,已初始化的代理请求不再因后台 reload 阻塞在 DB 查询上(loadFilters 对各 bucket 同步整体替换,读到的恒为一致快照)。

Gemini(Medium ×5)— 保存时的双重 reload
reload() 增加 queue 参数(RequestFilterEngine.reload(queue=true) / ErrorRuleDetector.reload({queueIfRunning}))。emit 已触发一次携带最新数据的在途 reload,mutation action 改为复用它(request-filters 传 reload(false),error-rules 走默认不补跑),避免对同一次写入做两次冗余 DB 读、并缩短保存响应。手动「刷新缓存」按钮(refreshCacheAction)显式传 queueIfRunning: true,保证读到刚同步的默认规则。新增测试锁定 reload(false) 只读一次。

之前一轮已处理ensureInitialized 阻塞问题的初版修复(commit 3aceb7fe),以及 AddRuleDialog / RefreshCacheButtonrouter.refresh() 回归覆盖。

本地全量校验通过:build ✅ · typecheck ✅ · lint ✅ · test ✅ 6267 passed / 13 skipped。

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/lib/request-filter-engine.ts (1)

341-383: 💤 Low value

reload() 的 finally 递归返回值未被正确传播。

在 Line 378,finally 块中的 return this.reload() 返回值不会被 .finally() 传播给调用方。.finally() 的返回值仅在抛出异常或返回一个 rejected Promise 时才会影响结果链。这意味着当 shouldRestart 为 true 时,调用方拿到的是第一轮 reloadLoop 的 resolved promise,而非补跑那一轮的 promise。

虽然在当前业务场景下(best-effort reload)这不会导致数据不一致,但与 ErrorRuleDetector 中使用 async finally + return this.reload() 的写法存在细微差异,可能导致调用方误以为 await 返回时缓存已是最新。

建议:对齐 ErrorRuleDetector 的写法,使用 async finally
-    this.activeReloadPromise = reloadLoop.finally(() => {
+    this.activeReloadPromise = reloadLoop.finally(async () => {
       // 极窄窗口:do/while 已判定无需继续,但 finally 微任务执行前又来了新请求,
       // 此处再检查一次,避免晚到的补跑被静默吞掉。
       const shouldRestart = this.reloadRequestedWhileLoading;
       this.activeReloadPromise = null;
       if (shouldRestart) {
-        return this.reload();
+        await this.reload();
       }
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/request-filter-engine.ts` around lines 341 - 383, The finally
callback on reloadLoop doesn't propagate the recursive reload promise to
callers; change how this.activeReloadPromise is set so the restart is awaited
and returned: instead of using reloadLoop.finally(...), wrap awaiting reloadLoop
in an async IIFE (or an async wrapper) that awaits reloadLoop, then clears
this.activeReloadPromise, checks this.reloadRequestedWhileLoading and if true
returns await this.reload() so the returned promise reflects the restarted run.
Update references around reload(), reloadLoop and this.activeReloadPromise to
use this async wrapper approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/lib/request-filter-engine.ts`:
- Around line 341-383: The finally callback on reloadLoop doesn't propagate the
recursive reload promise to callers; change how this.activeReloadPromise is set
so the restart is awaited and returned: instead of using
reloadLoop.finally(...), wrap awaiting reloadLoop in an async IIFE (or an async
wrapper) that awaits reloadLoop, then clears this.activeReloadPromise, checks
this.reloadRequestedWhileLoading and if true returns await this.reload() so the
returned promise reflects the restarted run. Update references around reload(),
reloadLoop and this.activeReloadPromise to use this async wrapper approach.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d5914e83-402a-49eb-b3d8-682cffb501dd

📥 Commits

Reviewing files that changed from the base of the PR and between 3aceb7f and 384e9cc.

📒 Files selected for processing (7)
  • src/actions/error-rules.ts
  • src/actions/request-filters.ts
  • src/lib/error-rule-detector.ts
  • src/lib/request-filter-engine.ts
  • tests/unit/actions/error-rules-cache-reload.test.ts
  • tests/unit/actions/request-filters-cache-reload.test.ts
  • tests/unit/lib/request-filter-engine-reload-queue.test.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/actions/request-filters.ts
  • tests/unit/actions/request-filters-cache-reload.test.ts
  • tests/unit/actions/error-rules-cache-reload.test.ts
  • tests/unit/lib/request-filter-engine-reload-queue.test.ts

@github-actions

github-actions Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

🧪 测试结果

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

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

@ding113
ding113 merged commit 09d549c into dev Jun 3, 2026
12 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in Claude Code Hub Roadmap Jun 3, 2026
@ding113
ding113 deleted the fix/rules-auto-refresh-on-save branch June 3, 2026 13:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Error Rule area:UI bug Something isn't working size/L Large PR (< 1000 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant