Skip to content

perf(pipeline): LLM timeout+concurrency, bulk AI persist, off-loop parse, batched events [修复组⑤] - #28

Merged
2233admin merged 1 commit into
mainfrom
fix/pipeline-throughput
Jul 18, 2026
Merged

perf(pipeline): LLM timeout+concurrency, bulk AI persist, off-loop parse, batched events [修复组⑤]#28
2233admin merged 1 commit into
mainfrom
fix/pipeline-throughput

Conversation

@2233admin

Copy link
Copy Markdown
Owner

修复组⑤ — 吞吐 (账本 C8/C21/C22/C24/C25)

Sonnet 实施, Fable 审计通过 (diff 结构 + 主 repo 真跑测试)。纯性能, 行为不变。

  • C8+C25 三个 processor (openai/claude/local): 每条 LLM 调用加显式 timeout; per-record 循环从 await-in-for 改 asyncio.gather + Semaphore 限流。gather 保序, per-task try/except 失败隔离 (单条炸不断批), process_with_ai 富化计数契约 (C3) 不变
  • C21 AI 富化落库: per-record session.get → 一次 bulk SELECT ... WHERE id IN(...) + 内存 id→row map, 消 N+1
  • C22 feedparser.parse / BeautifulSoupasyncio.to_thread, 移出事件循环
  • C24emit_many(run_id, events) 单 session + add_all + 单 commit; skill step-loop 切过去; 单 emit() 调用点不动
  • config 新增: llm_request_timeout_seconds=120 (local 原硬编码默认, 零行为变化; openai/claude 之前无超时=真修复), llm_max_concurrency=4

Test

  • 主 repo 真跑 (worktree .venv 撞 lxml cp314 无 wheel, 已在主 repo cp313 env 验证): pytest tests/unit1275 passed, 2 failed, 1 skipped。2 failed = 账本 P3-6 已知存量 flake (nodes_install netbird/ssh GBK), 与本改动无关
  • 新增测试: 三 processor 的 timeout 默认/override/env + semaphore 限流 + 保序 + 失败隔离; emit_many 单 commit/空列表 no-op; AI 落库 bulk 非 N+1 (query count spy)

合并顺序

④⑤⑥ 共同热点 pipeline.py。本 PR 改 C21 区 (AI 落库 ~404-426)。先后合任意, 后者 rebase。

…ed parse, batched events [C8,C21,C22,C24,C25]

- C8/C25: openai/claude/local processors get an explicit per-request LLM
  timeout (new Settings.llm_request_timeout_seconds, default 120s,
  config["timeout"]-overridable) and bound per-record concurrency via
  asyncio.gather + Semaphore (new Settings.llm_max_concurrency, default 4)
  instead of a sequential await-in-a-for-loop. Order is preserved (gather),
  and each record's LLM call keeps its own try/except so one failure can't
  abort the batch.
- C21: pipeline.py's AI-enrichment persist step replaces one session.get()
  per record with a single bulk SELECT ... WHERE id IN (...) + in-memory
  id->row map before one commit. Same field writes (ai_enrichment,
  status="ai_processed").
- C22: rss_channel's feedparser.parse() (collect() and fetch()) and
  web_scraper_channel's BeautifulSoup parse now run via asyncio.to_thread
  instead of blocking the event loop inline.
- C24: new events.emit_many(run_id, events) writes a whole step trace in one
  session + bulk insert + one commit; skill_channel's per-step loop (the
  only tight-loop emit() caller) now uses it. All other emit() call sites
  are untouched.

Tests: LLM timeout/concurrency/order/failure-isolation per processor,
AI-persist query-count spy, feedparser/BeautifulSoup off-thread checks,
emit_many one-commit behavior. Fakes/monkeypatch only, no real network/DB.
@repowise-bot

repowise-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

✅ Health: 7.8

📋 At a glance
5 hotspots touched · 13 new findings introduced · 7 dead-code findings.

Files & modules (2)
  • backend (6 files)
    • backend/pipeline/pipeline.py
    • backend/config.py
    • backend/processors/openai_processor.py
    • backend/processors/claude_processor.py
    • backend/processors/local_processor.py
    • backend/pipeline/events.py
  • tests (2 files)
    • .../channels/test_web_scraper_channel.py
    • .../pipeline/test_pipeline.py

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

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

🔥 Hotspots touched (5)

  • backend/pipeline/pipeline.py — 10 commits/90d, 6 dependents · primary owner: xujinghua (100%)
  • backend/config.py — 14 commits/90d, 15 dependents · primary owner: xujinghua (100%)
  • .../channels/test_web_scraper_channel.py — 2 commits/90d, 0 dependents · primary owner: xujinghua (100%)
2 more
  • .../pipeline/test_pipeline.py — 2 commits/90d, 1 dependents · primary owner: xujinghua (100%)
  • backend/processors/openai_processor.py — 3 commits/90d, 3 dependents · primary owner: xujinghua (100%)

💀 Dead code (7 findings)

  • 💀 backend/processors/claude_processor.py (file-level) (confidence 0.40)
  • 💀 backend/processors/claude_processor.py ClaudeProcessor (confidence 0.70)
  • 💀 backend/processors/openai_processor.py OpenAIProcessor (confidence 1.00)
4 more
  • 💀 backend/processors/local_processor.py (file-level) (confidence 0.40)
  • 💀 backend/processors/local_processor.py LocalProcessor (confidence 0.70)
  • 💀 backend/pipeline/events.py (file-level) (confidence 0.40)
  • 💀 backend/pipeline/events.py emit (confidence 0.70)

👀 Suggested reviewers @xujinghua


📊 Full report · ⭐ Star Repowise · 📥 Install bot · Last updated 2026-07-18 18:14 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

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a569f840-ae86-4831-9578-5979f04ccebe

📥 Commits

Reviewing files that changed from the base of the PR and between cb6c1ab and 4d69050.

📒 Files selected for processing (18)
  • backend/channels/rss_channel.py
  • backend/channels/skill_channel.py
  • backend/channels/web_scraper_channel.py
  • backend/config.py
  • backend/pipeline/events.py
  • backend/pipeline/pipeline.py
  • backend/processors/claude_processor.py
  • backend/processors/local_processor.py
  • backend/processors/openai_processor.py
  • tests/unit/channels/test_rss_channel.py
  • tests/unit/channels/test_rss_fetch.py
  • tests/unit/channels/test_skill_channel_emit_batching.py
  • tests/unit/channels/test_web_scraper_channel.py
  • tests/unit/pipeline/test_events.py
  • tests/unit/pipeline/test_pipeline.py
  • tests/unit/test_claude_processor.py
  • tests/unit/test_local_processor.py
  • tests/unit/test_openai_processor.py

📝 Walkthrough

Summary by CodeRabbit

  • Performance

    • Improved responsiveness when processing RSS feeds and web pages.
    • Accelerated AI enrichment by processing records concurrently with configurable limits.
    • Reduced database work when saving enrichment results.
  • Reliability

    • Added configurable AI request timeouts.
    • Individual enrichment failures no longer stop the rest of a batch.
    • Improved efficiency and reliability of task event updates.
  • Tests

    • Expanded coverage for concurrency, timeouts, background parsing, event batching, and failure handling.

Walkthrough

The PR moves blocking channel parsing to worker threads, batches skill event writes, adds shared LLM timeout and concurrency settings, parallelizes enrichment requests with failure isolation, and replaces per-record enrichment persistence with bulk selection and updates.

Changes

Async processing and persistence

Layer / File(s) Summary
Off-event-loop channel parsing
backend/channels/rss_channel.py, backend/channels/web_scraper_channel.py, tests/unit/channels/*
RSS and HTML parsing run through worker threads, with tests verifying execution outside the event-loop thread.
Batched skill event emission
backend/pipeline/events.py, backend/channels/skill_channel.py, tests/unit/channels/test_skill_channel_emit_batching.py, tests/unit/pipeline/test_events.py
Skill events are built as payload dictionaries and persisted through one-session bulk emission with preserved step ordering and levels.
Bounded LLM enrichment
backend/config.py, backend/processors/*, tests/unit/test_*processor.py
Claude, local, and OpenAI processors use configurable timeouts, semaphore-bounded concurrency, ordered results, and per-record error isolation.
Bulk enrichment persistence
backend/pipeline/pipeline.py, tests/unit/pipeline/test_pipeline.py
AI-enriched records are selected in one query, updated in memory, and committed together.
Estimated code review effort: 4 (Complex) ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Channel
  participant WorkerThread
  participant SkillChannel
  participant EventStore
  Channel->>WorkerThread: parse RSS or HTML content
  WorkerThread-->>Channel: return parsed content
  SkillChannel->>EventStore: emit_many(run_id, event payloads)
  EventStore-->>SkillChannel: commit batched task events
Loading

Possibly related PRs

Poem

A rabbit watched the workers run,
While feeds parsed softly, one by one.
LLM calls hopped in a bounded line,
Events batched neatly, rows aligned.
“Less blocking!” cried the hare—
Then nibbled tests with care.

✨ 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 introduces several performance and concurrency optimizations across the backend. Key changes include offloading synchronous parsing tasks (BeautifulSoup and feedparser) to thread pools using 'asyncio.to_thread', batching event emissions via a new 'emit_many' database helper, optimizing database updates to avoid N+1 queries, and implementing bounded concurrency and configurable timeouts for LLM processors. Feedback on these changes suggests chunking the bulk query in 'pipeline.py' to prevent hitting SQLite's 999 parameter limit, and using '.get()' with defaults in 'emit_many' to avoid 'KeyError' exceptions that could fail an entire batch of events.

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 +411 to +417
if enriched_ids:
async with AsyncSessionLocal() as session:
db_recs = (
await session.execute(
select(CollectedRecord).where(CollectedRecord.id.in_(enriched_ids))
)
).scalars().all()

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

在 SQLite 中,单个 SQL 查询中的参数(变量)数量默认限制为 999 个。如果 enriched_ids 的数量超过 999,执行 CollectedRecord.id.in_(enriched_ids) 查询时会抛出 OperationalError: too many SQL variables 异常。

为了提高代码的健壮性,建议对 enriched_ids 进行分批(Chunking)查询,以确保在处理大批量数据时不会触发 SQLite 的参数限制。

Suggested change
if enriched_ids:
async with AsyncSessionLocal() as session:
db_recs = (
await session.execute(
select(CollectedRecord).where(CollectedRecord.id.in_(enriched_ids))
)
).scalars().all()
if enriched_ids:
async with AsyncSessionLocal() as session:
db_recs = []
for i in range(0, len(enriched_ids), 999):
chunk = enriched_ids[i : i + 999]
res = await session.execute(
select(CollectedRecord).where(CollectedRecord.id.in_(chunk))
)
db_recs.extend(res.scalars().all())

Comment on lines +53 to +63
session.add_all([
TaskRunEvent(
run_id=run_id,
level=event.get("level", "info"),
step=event["step"],
message=event["message"],
detail=event.get("detail"),
elapsed_ms=event.get("elapsed_ms"),
)
for event in events
])

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

emit_many 批量写入事件时,直接使用 event["step"]event["message"] 进行字典取值。如果传入的事件列表中有任何一个事件字典缺失了 "step""message" 键,将会抛出 KeyError 异常。

虽然整个操作被包裹在 try...except 块中,但一个事件的格式错误会导致整批事件全部写入失败。建议使用 .get() 方法并提供合理的默认值,以提高公共工具函数的容错性和健壮性。

Suggested change
session.add_all([
TaskRunEvent(
run_id=run_id,
level=event.get("level", "info"),
step=event["step"],
message=event["message"],
detail=event.get("detail"),
elapsed_ms=event.get("elapsed_ms"),
)
for event in events
])
session.add_all([
TaskRunEvent(
run_id=run_id,
level=event.get("level", "info"),
step=event.get("step", "unknown"),
message=event.get("message", ""),
detail=event.get("detail"),
elapsed_ms=event.get("elapsed_ms"),
)
for event in events
])

@2233admin
2233admin merged commit 9cf38ac into main Jul 18, 2026
4 of 5 checks passed
@2233admin
2233admin deleted the fix/pipeline-throughput branch July 18, 2026 18:20
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