Skip to content

fix(sentry): make transport push non-blocking and resilient to consumer failure - #1088

Open
huangdijia wants to merge 1 commit into
mainfrom
sentry-pr/r1
Open

fix(sentry): make transport push non-blocking and resilient to consumer failure#1088
huangdijia wants to merge 1 commit into
mainfrom
sentry-pr/r1

Conversation

@huangdijia

Copy link
Copy Markdown
Contributor

问题

CoHttpTransport 通过有限容量 Channel(发布配置默认 512)缓冲事件,但存在两类永久阻塞风险,会导致请求协程无法结束、协程数量无界堆积 → OOM:

  1. push 超时语义错误:send()$chan?->push($event, $this->timeout) 在通道满时,若 transport_timeout <= 0,内部超时保持 -1(永久阻塞)。请求协程在协程结束时 flush 会卡死,协程数无界增长。
  2. 消费协程异常死亡无兜底:loop()makeHttpTransport() 在 try 之外,一旦抛异常消费协程直接死亡,channel 永远无人消费,所有后续 send() 永久阻塞。
  3. close() 为空实现,不会等待通道排空。

修改

  • push 超时改为安全默认:构造函数解析 sentry.transport_timeout,<= 0 时内部超时为 0(非阻塞,通道满立即跳过事件),> 0 时最多等待 N 秒。超时计算提取为 protected 方法 resolvePushTimeout()
    • 注:Swoole 6.x 中 Channel::push($data, 0) 实际会被当作"无限等待"(源码 wait_push 仅在 timeout > 0 时注册定时器),因此 pushEvent() 在非阻塞模式下先用 isFull() 短路跳过,并以极小正超时(0.001s)兜底竞态,保证调用方协程绝不挂起。
  • 消费协程异常兜底:loop() 消费协程主体(makeHttpTransport、pop、派发)包进 try/catch(Throwable),catch 中通过 $this->clientBuilder?->getLogger() 记录日志,finallycloseChannel();closeChannel() 幂等,通道关闭后 send() 自愈(loop() 检测 chan 为 null 时重建)。
  • close() 支持排空:等待通道排空(最多 $timeout 秒,默认 1 秒,while (! $chan->isEmpty()) msleep(...) + 超时判断,与 workerWatcher 风格一致),然后 closeChannel() 返回 success;通道不存在时直接 success。
  • send() 失败路径告警:chan 为 null 或 push 失败时打 warning 日志(优先 clientBuilder 的 logger,否则通过容器解析 StdoutLoggerInterface,容器解析失败则忽略)。
  • 发布配置:publish/sentry.phptransport_timeout 默认值改为 1.0,并注释说明 <= 0 表示非阻塞、> 0 表示最多等待 N 秒;channel_size/concurrent_limit 及其余配置不变。

测试

新增 tests/Sentry/CoHttpTransportTest.php(Pest,Swoole\Coroutine\run 包裹,通过子类覆盖 loop() 避免真实协程与 HTTP 发送),覆盖:

  • resolvePushTimeout 决策:<= 0 → 0> 0 → 原值;
  • 构造器把 transport_timeout <= 0 映射为非阻塞超时;
  • 通道满时 send() 不阻塞并返回 skipped;
  • 通道不存在时 send() 返回 skipped;
  • 通道有空位时 send() 返回 success;
  • close() 在通道有积压时等待排空后关闭,不阻塞超时;
  • close() 在通道不存在时直接 success。

结果:

vendor/bin/pest tests/Sentry   # 50 passed (88 assertions),连续多次运行一致

说明:worktree 的 vendor 是指向主 checkout 的符号链接,其 composer autoloader 会把 FriendsOfHyperf\Sentry\* 解析到主 checkout 源码,且全量加载时存在预置的 TestEnum 重复声明问题(与本次改动无关),故以 tests/Sentry 目录方式运行(与 --group=sentry 覆盖相同的 43 个既有用例 + 7 个新增)。测试文件顶部显式 require_once worktree 的 CoHttpTransport.php 以测试本地改动。

验证

  • vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff(仅本次 3 个文件):0 处待修复。
  • git diff --check:无错误。
  • composer analyse src/sentry(PHPStan):按要求未运行。

@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@huangdijia, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 52 minutes

Limit details: You’ve used all 2 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: daab74e7-537c-42c1-9935-ea97e38f8337

📥 Commits

Reviewing files that changed from the base of the PR and between c814785 and eb174e7.

📒 Files selected for processing (3)
  • src/sentry/publish/sentry.php
  • src/sentry/src/Transport/CoHttpTransport.php
  • tests/Sentry/CoHttpTransportTest.php

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.

@huangdijia
huangdijia force-pushed the sentry-pr/r1 branch 2 times, most recently from 3150e50 to eb174e7 Compare August 20, 2026 15:30

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 040e5752b1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +206 to +207
} finally {
$this->closeChannel();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Close only the channel owned by the consumer

When the client is flushed and then reused—as happens in EventHandleListener.php:158 and RuntimeContextManager.php:210close() closes and nulls channel A, allowing the next send() to install channel B while A's consumer coroutine unwinds. This finally then calls closeChannel(), which reads the current $this->chan rather than the channel owned by that consumer, so the old consumer can close and null B and cause subsequent events to be skipped; capture each consumer's channel and only clear it when it is still current.

Useful? React with 👍 / 👎.

Comment on lines +187 to +190
// The max seconds to wait when pushing an event into the transport channel
// `<= 0` means non-blocking (skip the event immediately when the channel is full),
// `> 0` means wait at most N seconds for the channel to have capacity.
'transport_timeout' => (float) env('SENTRY_TRANSPORT_TIMEOUT', 1.0),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Document the new transport timeout in every Sentry guide

This introduces SENTRY_TRANSPORT_TIMEOUT, changes its default to one second, and gives non-positive values important non-blocking semantics, but the Transport sections inspected in both component READMEs and all four locale pages still list only channel size, concurrency, and HTTP timeout. Users following those guides therefore cannot discover how to select the new behavior; update all six Sentry documents together.

AGENTS.md reference: AGENTS.md:L129-L130

Useful? React with 👍 / 👎.

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