Skip to content

fix(pipeline): notify without write lock + real success counts [修复组①] - #20

Merged
2233admin merged 1 commit into
mainfrom
fix/collection-notify-lock-false-success
Jul 18, 2026
Merged

fix(pipeline): notify without write lock + real success counts [修复组①]#20
2233admin merged 1 commit into
mainfrom
fix/collection-notify-lock-false-success

Conversation

@2233admin

Copy link
Copy Markdown
Owner

修复组① — 锁与假信号 (账本 C1/C3/C12/C18/C19/C23)

采集链路专项审计后的第一组修复。Sonnet 实施, Fable 审计通过 (三段式结构 + 作用域/签名疑点逐一排除)。

  • C1+C23 notifier_dispatch 三段式重构: Phase A 短会话建 pending 日志行并 commit; Phase B 零 session 顺序发送 (每 rule 复用一个 notifier 实例, 单条失败不炸批); Phase C 新短会话批量回写结果单次 commit。SQLite 写锁不再横跨网络 I/O
  • C12 dispatch_notifications 返回 {sent, failed} 聚合; step5 报真实数字, 全败升 warning 级; PipelineResult.notifications_sent 从死字段变真值
  • C3 process_with_ai 返回真实富化条数; 未注册 processor_type 落 warning 日志; step4 由真实计数驱动, 0 条时事件改报 "AI 处理失败/跳过" (warning) 而非全绿 "完成"
  • C18 未知 notifier_type 的规则 skip 时落 warning (rule id + type)
  • C19 终态失败 error_message 带 taxonomy 前缀 [{error_type}] {message} (复用 effective_error_type, 零 schema 改动)

Test

  • tests/unit/pipeline + chat 集成: 171 passed 干净
  • 全量 unit+skills: 1362 passed, 2 fail = 账本 P3-6 已知存量 flake (clean main 复核字节一致)
  • 全量 integration: 380 passed, 与 clean main 结果一致, 零回归
  • 新增覆盖: commit-先于-send 结构性断言 / 部分与全部失败聚合 / 未知 processor 计 0 + warning

边界说明

Phase C 崩溃窗口: 发送已发生但结果行留在 pending — 可观测性仍优于旧行为 (旧: 整批日志行直接丢失)。

…,C12,C18,C19,C23]

Restructure notifier_dispatch.dispatch_notifications into three phases so
notification sends never hold the SQLite write lock, and callers get real
outcome counts instead of unconditional success:

- C1/C23: dispatch_notifications now runs Phase A (caller session: query
  rules, create pending NotificationLog rows, flush+commit), Phase B (no
  session open: sequential sends, one resolved notifier instance per rule),
  Phase C (fresh short session: bulk-persist outcomes, single commit).
  Returns {"sent": n, "failed": m}.
- C12: pipeline.py step 5 consumes that aggregate, assigns
  PipelineResult.notifications_sent (previously dead), and logs/emits real
  sent/failed numbers - warning level when every attempted send failed.
- C3: ai_processor.process_with_ai now returns the number of records
  actually enriched (0 on empty input or unregistered processor_type, with
  a warning log) instead of None. pipeline.py step 4 uses that real count
  for ai_processed and only emits "AI 处理完成" when something was actually
  enriched; a config error now emits a warning-level "失败/跳过" event.
- C18: unknown notifier_type on a rule now logs a warning with the rule id
  and notifier_type instead of skipping silently.
- C19: runner.py's terminal-failure handler now prefixes the persisted
  error_message with the same retry-classification pipeline.py already
  computes (effective_error_type), so ops can distinguish retryable from
  permanent failures from the stored message alone.

Tests extended in tests/unit/pipeline/{test_notifier_dispatch,
test_notifier_dispatch_errors,test_ai_processor,test_pipeline}.py and
tests/unit/test_runner.py: phase-commit-before-send ordering, partial/total
send failure aggregates, unknown notifier_type/processor_type warnings, and
the exact error_message prefix format.
@repowise-bot

repowise-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

✅ Health: 7.1

📋 At a glance
4 hotspots touched · 13 new findings introduced.

Files & modules (2)
  • backend (2 files)
    • backend/pipeline/pipeline.py
    • backend/pipeline/runner.py
  • tests (2 files)
    • tests/unit/test_runner.py
    • .../pipeline/test_pipeline.py

🚨 Change risk: 9.3/10 (high)
This change's risk is driven by:

  • more lines added than baseline
  • more scattered than baseline
🔎 More signals (1)

🔥 Hotspots touched (4)

  • backend/pipeline/pipeline.py — 10 commits/90d, 6 dependents · primary owner: xujinghua (100%)
  • backend/pipeline/runner.py — 6 commits/90d, 7 dependents · primary owner: xujinghua (100%)
  • tests/unit/test_runner.py — 1 commits/90d, 0 dependents · primary owner: xujinghua (100%)
1 more
  • .../pipeline/test_pipeline.py — 2 commits/90d, 1 dependents · primary owner: xujinghua (100%)

👀 Suggested reviewers @xujinghua


📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-18 13:35 UTC
Silence on a single PR with [skip repowise] in the title · Per-repo toggle on repowise.dev/settings?tab=bot

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Improvements
    • AI processing now reports the number of records enriched.
    • Notification delivery now reports sent and failed totals, including partial failures.
    • Pipeline activity events provide clearer enrichment and notification outcomes.
    • Processing errors now include categorized error types for easier diagnosis.
    • Notification records are saved before sending, reducing database lock contention.

Walkthrough

AI processing and notification dispatch now return structured counts that the pipeline records in events and results. Notification persistence is separated from network sends, and runner failures now persist taxonomy-prefixed error messages.

Changes

Pipeline outcome reporting

Layer / File(s) Summary
AI enrichment counts and audit events
backend/pipeline/ai_processor.py, backend/pipeline/pipeline.py, tests/unit/pipeline/test_ai_processor.py, tests/unit/pipeline/test_pipeline.py
AI processing returns the number of enriched records, while pipeline events distinguish successful enrichment from zero-result processing.
Phased notification dispatch
backend/pipeline/notifier_dispatch.py, tests/unit/pipeline/test_notifier_dispatch.py, tests/unit/pipeline/test_notifier_dispatch_errors.py
Notification logs are committed before sending, send outcomes are normalized and persisted afterward, and aggregate sent/failed counts are returned.
Notification result propagation
backend/pipeline/pipeline.py, tests/unit/pipeline/test_pipeline.py
Notification counts are included in pipeline results and events, with warnings for all-failed attempts.
Classified pipeline failures
backend/pipeline/runner.py, tests/unit/test_runner.py
Persisted task and run errors now include the effective exception classification. as a prefix.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Pipeline
  participant AIProcessor
  participant NotifierDispatch
  participant NotificationLog
  Pipeline->>AIProcessor: process records
  AIProcessor-->>Pipeline: enriched count
  Pipeline->>NotifierDispatch: dispatch notifications
  NotifierDispatch->>NotificationLog: commit pending rows
  NotifierDispatch->>NotifierDispatch: send and aggregate outcomes
  NotifierDispatch->>NotificationLog: persist final statuses
  NotifierDispatch-->>Pipeline: sent and failed counts
Loading

Possibly related PRs

  • 2233admin/opencli-admin#10: Both changes modify notification dispatch result normalization and acknowledgement-related log fields.

Poem

I’m a rabbit counting bright AI leaves,
And tallying messages sent through the eaves.
Pending logs hop safely before sends fly,
Failed ones get marked, while successes multiply.
Clear warnings bloom when counts fall low—
Taxonomy tags tell the errors where to go.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.63% 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 The title clearly highlights the main pipeline changes: lock-free notification dispatch and real success counts.
Description check ✅ Passed The description is detailed and directly matches the pipeline, logging, and test changes in the pull request.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request restructures the notification dispatching pipeline into three distinct phases (Phase A: queue pending logs, Phase B: execute network sends without an open session, Phase C: persist outcomes in a short-lived session) to prevent slow network requests from holding the SQLite write lock. It also updates the AI processor to return the count of enriched records and logs warnings for misconfigured processors. The pipeline runner is enhanced to include taxonomy classifications in persisted error messages. Review feedback suggests handling potential null values in rule.notifier_config to avoid AttributeError and batching database queries in Phase C of the notification dispatch to resolve an N+1 query issue.

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 +127 to +140
ack_required = bool(_ack_secret(rule.notifier_config))
for record in records:
log = NotificationLog(
log_id = str(uuid.uuid4())
session.add(NotificationLog(
id=log_id,
rule_id=rule.id,
record_id=record.id,
status="pending",
ack_status="not_required",
)
session.add(log)
await session.flush()
))
send_plan.append(_PendingSend(
log_id=log_id,
notifier=notifier,
notifier_config=rule.notifier_config,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

如果 rule.notifier_config 在数据库中为 None(例如未配置的 JSON 列),直接调用 _ack_secret(rule.notifier_config) 会导致 AttributeError: 'NoneType' object has no attribute 'get' 异常。建议在循环前对其进行空值处理,使用 rule.notifier_config or {},以提高代码的健壮性。

        notifier_config = rule.notifier_config or {}
        ack_required = bool(_ack_secret(notifier_config))
        for record in records:
            log_id = str(uuid.uuid4())
            session.add(NotificationLog(
                id=log_id,
                rule_id=rule.id,
                record_id=record.id,
                status="pending",
                ack_status="not_required",
            ))
            send_plan.append(_PendingSend(
                log_id=log_id,
                notifier=notifier,
                notifier_config=notifier_config,

Comment on lines +168 to +175
for outcome in outcomes:
log = await write_session.get(NotificationLog, outcome.log_id)
if log is None:
continue
log.status = outcome.status
log.response_data = outcome.response_data
log.error_message = outcome.error_message
log.ack_status = outcome.ack_status

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

在 Phase C 中,在循环内对每个 outcome 依次调用 write_session.get(NotificationLog, outcome.log_id) 会导致 $O(N)$ 次数据库查询(N+1 问题)。建议使用 IN 子句进行批量查询,将数据库交互次数减少到 $O(1)$,从而显著提升性能。

Suggested change
for outcome in outcomes:
log = await write_session.get(NotificationLog, outcome.log_id)
if log is None:
continue
log.status = outcome.status
log.response_data = outcome.response_data
log.error_message = outcome.error_message
log.ack_status = outcome.ack_status
log_ids = [outcome.log_id for outcome in outcomes]
result = await write_session.execute(
select(NotificationLog).where(NotificationLog.id.in_(log_ids))
)
logs = {log.id: log for log in result.scalars().all()}
for outcome in outcomes:
log = logs.get(outcome.log_id)
if log is None:
continue
log.status = outcome.status
log.response_data = outcome.response_data
log.error_message = outcome.error_message
log.ack_status = outcome.ack_status

@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: 3

🤖 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 `@backend/pipeline/ai_processor.py`:
- Line 142: Update the zip call in the record/enrichment loop to pass
strict=False explicitly, preserving the current truncating behavior while
satisfying Ruff B905.

In `@backend/pipeline/notifier_dispatch.py`:
- Around line 164-178: Update the Phase C persistence flow in run_pipeline so a
commit failure does not discard the accumulated sent and failed delivery counts.
Catch the persistence error, retain and return those counts alongside an
explicit persistence error/status, and ensure callers do not interpret the
result as a generic zero-send notification failure or safe retry.
- Around line 152-159: Close the phase-A database session scope before running
notification I/O: restructure the flow around session flush/commit so the
session context exits before the _send_one loop executes. Update
tests/unit/pipeline/test_notifier_dispatch_errors.py lines 157-217 to assert the
session __aexit__ event occurs before notifier.send(), replacing the
commit-order-only assertion.
🪄 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: 3ee723d5-6fcd-401c-a977-2938b87a8150

📥 Commits

Reviewing files that changed from the base of the PR and between f159cf4 and 553a294.

📒 Files selected for processing (9)
  • backend/pipeline/ai_processor.py
  • backend/pipeline/notifier_dispatch.py
  • backend/pipeline/pipeline.py
  • backend/pipeline/runner.py
  • tests/unit/pipeline/test_ai_processor.py
  • tests/unit/pipeline/test_notifier_dispatch.py
  • tests/unit/pipeline/test_notifier_dispatch_errors.py
  • tests/unit/pipeline/test_pipeline.py
  • tests/unit/test_runner.py

)

enriched = 0
for record, enrichment in zip(records, result.enrichments):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file around the flagged lines.
sed -n '1,240p' backend/pipeline/ai_processor.py

printf '\n---\n'

# Find related uses of records/result.enrichments to infer shape/length assumptions.
rg -n "result\.enrichments|zip\(records" backend/pipeline -S

printf '\n---\n'

# Look for tests or docs that mention enrichment batching or truncation behavior.
rg -n "enrichment|enrichments|truncat|zip\\(" tests backend -S

Repository: 2233admin/opencli-admin

Length of output: 23346


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- backend/processors/base.py ---\n'
sed -n '1,120p' backend/processors/base.py

printf '\n--- tests/unit/pipeline/test_ai_processor.py ---\n'
sed -n '1,220p' tests/unit/pipeline/test_ai_processor.py

printf '\n--- related processor length handling ---\n'
rg -n "len\\(records\\)|len\\(.*enrichments\\)|zip\\(records, result\\.enrichments|strict=False" backend/processors tests/unit/pipeline -S

Repository: 2233admin/opencli-admin

Length of output: 252


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- backend/processors/base.py ---'
sed -n '1,120p' backend/processors/base.py

echo
echo '--- tests/unit/pipeline/test_ai_processor.py ---'
sed -n '1,260p' tests/unit/pipeline/test_ai_processor.py

echo
echo '--- related processor length handling ---'
rg -n "len\\(records\\)|len\\(.*enrichments\\)|zip\\(records, result\\.enrichments|strict=False" backend/processors tests/unit/pipeline -S

Repository: 2233admin/opencli-admin

Length of output: 11622


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- backend/processors/claude_processor.py ---'
sed -n '1,180p' backend/processors/claude_processor.py

echo
echo '--- backend/processors/openai_processor.py ---'
sed -n '1,180p' backend/processors/openai_processor.py

echo
echo '--- backend/processors/local_processor.py ---'
sed -n '1,180p' backend/processors/local_processor.py

echo
echo '--- backend/processors/external_http_processor.py ---'
sed -n '1,220p' backend/processors/external_http_processor.py

Repository: 2233admin/opencli-admin

Length of output: 17871


Make the zip truncation explicit. zip() still silently drops extra items here; add strict=False so the current 1:1 pairing contract is explicit and Ruff B905 is satisfied.

🧰 Tools
🪛 Ruff (0.15.21)

[warning] 142-142: zip() without an explicit strict= parameter

Add explicit value for parameter strict=

(B905)

🤖 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 `@backend/pipeline/ai_processor.py` at line 142, Update the zip call in the
record/enrichment loop to pass strict=False explicitly, preserving the current
truncating behavior while satisfying Ruff B905.

Source: Linters/SAST tools

Comment on lines +152 to +159
if not send_plan:
return {"sent": 0, "failed": 0}

await session.flush()
await session.commit()

# ── Phase B: sends, sequential, no session open ──────────────────────────
outcomes = [await _send_one(task) for task in send_plan]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Ensure the database session is closed before notification I/O.

Committing releases the transaction, but the caller’s session context remains entered during every send.

  • backend/pipeline/notifier_dispatch.py#L152-L159: exit the phase-A session scope before executing _send_one.
  • tests/unit/pipeline/test_notifier_dispatch_errors.py#L157-L217: assert session __aexit__ occurs before notifier.send(), rather than only checking commit order.
📍 Affects 2 files
  • backend/pipeline/notifier_dispatch.py#L152-L159 (this comment)
  • tests/unit/pipeline/test_notifier_dispatch_errors.py#L157-L217
🤖 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 `@backend/pipeline/notifier_dispatch.py` around lines 152 - 159, Close the
phase-A database session scope before running notification I/O: restructure the
flow around session flush/commit so the session context exits before the
_send_one loop executes. Update
tests/unit/pipeline/test_notifier_dispatch_errors.py lines 157-217 to assert the
session __aexit__ event occurs before notifier.send(), replacing the
commit-order-only assertion.

Comment on lines +164 to +178
# ── Phase C: persist outcomes in a fresh short-lived session ─────────────
from backend.database import AsyncSessionLocal

async with AsyncSessionLocal() as write_session:
for outcome in outcomes:
log = await write_session.get(NotificationLog, outcome.log_id)
if log is None:
continue
log.status = outcome.status
log.response_data = outcome.response_data
log.error_message = outcome.error_message
log.ack_status = outcome.ack_status
await write_session.commit()

return {"sent": sent, "failed": failed}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve delivery counts when outcome persistence fails.

If the phase-C commit raises after successful external sends, the aggregate is discarded and run_pipeline reports a generic notification failure with notifications_sent == 0. Return the delivery counts alongside a persistence error/status so already-completed sends are reported accurately and are not mistaken for safe retries.

🤖 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 `@backend/pipeline/notifier_dispatch.py` around lines 164 - 178, Update the
Phase C persistence flow in run_pipeline so a commit failure does not discard
the accumulated sent and failed delivery counts. Catch the persistence error,
retain and return those counts alongside an explicit persistence error/status,
and ensure callers do not interpret the result as a generic zero-send
notification failure or safe retry.

@2233admin
2233admin merged commit 5875f0f into main Jul 18, 2026
5 checks passed
@2233admin
2233admin deleted the fix/collection-notify-lock-false-success branch July 18, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant