Skip to content

fix(telemetry): bound network I/O so telemetry can't outlive the run envelope (BE-3403)#564

Merged
mattmillerai merged 4 commits into
mainfrom
matt/be-3403-bound-telemetry-io
Jul 22, 2026
Merged

fix(telemetry): bound network I/O so telemetry can't outlive the run envelope (BE-3403)#564
mattmillerai merged 4 commits into
mainfrom
matt/be-3403-bound-telemetry-io

Conversation

@mattmillerai

Copy link
Copy Markdown
Collaborator

ELI-5

When you run comfy run, the CLI quietly phones home telemetry. If the telemetry server accepts the connection but then goes silent (a firewall/proxy "blackhole"), the old code waited forever for a reply — so comfy run --wait could hang with zero output for minutes, and even after the real result was printed the process kept lingering. This PR puts a stopwatch on every telemetry network call so the CLI can never wait more than a few seconds on a dead endpoint.

What & why

BE-3354 reproduced (live, macOS) an indefinite hang: with telemetry consent ON and a telemetry endpoint that accepts TCP but never responds, comfy --json-stream run --wait blocked with 0 bytes out for 103s, stuck in requests.post(..., timeout=None). Two root causes, both in comfy_cli/tracking.py:

  1. MixpanelProvider.__init__ built Mixpanel(token), whose default Consumer uses request_timeout=None → an unbounded, synchronous requests.post on the main thread. track_event("execution_start") fires this before the run emits anything, and track_event("execution_success") fires it after the terminal envelope is already on stdout (which comfy-local-mcp, reading stdout to EOF, experiences as a full tool timeout — BE-3329).
  2. _flush_all_providers (atexit) had no deadline; PostHog's flush()queue.join() with the default max_retries=3 × timeout=15 could drain for ~50s, post-envelope.

Changes (all in comfy_cli/tracking.py)

  • Mixpanel: Mixpanel(token, consumer=MixpanelConsumer(request_timeout=10)) — every send bounded to 10s.
  • PostHog: Posthog(..., max_retries=1, timeout=10) (was defaults 3/15) — worst-case consumer drain ~50s → ~21s.
  • atexit flush deadline: _flush_all_providers now runs each provider's flush() in a daemon thread and join(timeout=5.0); a still-alive thread past the deadline logs a warning and is abandoned (in-flight events dropped). Telemetry is best-effort by contract — dropping events beats wedging every consumer of the CLI's stdout. The per-provider try/except is preserved in _flush_one.

Tests

  • test_flush_returns_before_deadline_when_a_provider_hangs: a stub provider whose flush() blocks 60s is registered in PROVIDERS; _flush_all_providers() returns in well under the hang (assert < 8s), and a healthy provider after it is still drained.
  • test_mixpanel_client_has_bounded_request_timeout: asserts MixpanelProvider().client._consumer._request_timeout == 10 so the default-None regression can't silently return.
  • Existing tracking tests pass unchanged: pytest tests/ -k track125 passed. ruff check + ruff format clean.

Empirical verification (real blackhole socket, not just unit tests)

Measured against a local socket that accepts TCP and never replies:

  • Mixpanel track() on the blackhole: 10.0s then raises (the Consumer's urllib3 retry adapter does not re-retry the read timeout — one attempt, bounded exactly at request_timeout).
  • PostHog flush() drain with max_retries=1, timeout=10: ~21.7s — but our daemon-thread join(timeout=5.0) abandons it at 5s, so _flush_all_providers returns at ~5s.

Before the fix these were unbounded (∞). The two documented hang points are now bounded.

Judgment calls / honest note on the envelope→exit gap

Per the ticket's non-goal, PostHog's own internal atexit (Client.join) is left in place (with max_retries=1, timeout=10 it is self-bounded). Capping it further is not trivially achievable without removing it, which the ticket forbids — so I left it. Consequently the measured worst-case envelope→exit gap in the blackhole case is ~21s, dominated by (a) the specified 10s Mixpanel execution_success send and (b) PostHog's retained internal atexit drain. This is within the ticket's hard < ~30s total-penalty bound and is a night-and-day improvement over the pre-fix unbounded hang (and comfortably under comfy-local-mcp's 120s timeout), but it is slightly above the ticket's ~16s envelope→exit estimate — flagging that transparently rather than contorting the code (or removing PostHog's atexit) to hit the estimate. Typical (reachable-endpoint) penalty stays < 2s.

Negative-claim falsification

Not applicable: this change denies no capability. It bounds best-effort I/O and logs a warning only on a blackhole; telemetry still sends normally when the network is healthy. No "not supported"/dead-end/deny path is added.

BE-3403

…envelope (BE-3403)

Mixpanel's default Consumer used request_timeout=None (unbounded, synchronous
requests.post on the main thread) and _flush_all_providers had no deadline, so
with consent on and a blackholed telemetry endpoint (accepts TCP, never
responds) 'comfy run --wait' blocked indefinitely: execution_start hung before
any output, and execution_success + the atexit flush lingered after the terminal
envelope was already on stdout (BE-3354/BE-3329).

- MixpanelProvider builds Mixpanel(token, consumer=Consumer(request_timeout=10)).
- PostHogProvider passes max_retries=1, timeout=10 (was 3/15), dropping the
  worst-case consumer drain from ~50s to ~21s.
- _flush_all_providers runs each provider's flush in a daemon thread with a
  5s join deadline; a hung provider is abandoned (in-flight events dropped)
  rather than wedging every consumer of the CLI's stdout.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 845b20ad-255e-4c97-a289-2291f61168f4

📥 Commits

Reviewing files that changed from the base of the PR and between 29dd108 and 740d734.

📒 Files selected for processing (2)
  • comfy_cli/tracking.py
  • tests/comfy_cli/test_tracking_providers.py

📝 Walkthrough

Walkthrough

Telemetry provider construction now uses bounded network settings, and shutdown flushing runs providers concurrently with a shared deadline. Regression tests cover provider configuration, PostHog handler removal, and completion while another provider remains blocked.

Changes

Telemetry shutdown controls

Layer / File(s) Summary
Provider transport and shutdown configuration
comfy_cli/tracking.py, tests/comfy_cli/test_tracking_providers.py
Mixpanel uses a 10-second request timeout and limited retries; PostHog uses bounded settings and unregisters its default atexit join handler.
Concurrent deadline-bounded flushing
comfy_cli/tracking.py, tests/comfy_cli/test_tracking_providers.py
Provider flushes run in daemon threads and are joined against one shared deadline, with regression coverage for blocked and healthy providers.

Sequence Diagram(s)

sequenceDiagram
  participant Atexit
  participant FlushAllProviders
  participant MixpanelProvider
  participant PostHogProvider

  Atexit->>FlushAllProviders: invoke shutdown flush
  FlushAllProviders->>MixpanelProvider: start daemon flush
  FlushAllProviders->>PostHogProvider: start daemon flush
  MixpanelProvider-->>FlushAllProviders: complete or remain blocked
  PostHogProvider-->>FlushAllProviders: complete flush
  FlushAllProviders->>FlushAllProviders: join until shared deadline
Loading

Suggested reviewers: bigcat88, robinjhuang

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-3403-bound-telemetry-io
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-3403-bound-telemetry-io

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

@mattmillerai mattmillerai added the agent-coded PR authored by the agent-work loop label Jul 18, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review July 18, 2026 00:58
@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 18, 2026
@mattmillerai mattmillerai added the cursor-review Request Cursor bot review label Jul 18, 2026

@github-actions github-actions 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.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 5 finding(s).

Severity Count
🟠 High 3
🟢 Low 1
⚪ Nit 1

Panel: 6/8 reviewers contributed findings.

Reviewers that did not contribute: kimi-k2.5:adversarial (empty), kimi-k2.5:edge-case (empty)

Comment thread comfy_cli/tracking.py Outdated
Comment thread comfy_cli/tracking.py Outdated
Comment thread comfy_cli/tracking.py
Comment thread comfy_cli/tracking.py Outdated
Comment thread comfy_cli/tracking.py
…shared flush deadline (BE-3403)

Address cursor-review panel findings that partially defeated this PR's goal
of keeping telemetry I/O from outliving the run envelope:

- MixpanelConsumer defaults to retry_limit=4 with backoff, so request_timeout=10
  bounded only a single attempt (~40s+ worst case). track() sends inline on the
  main thread and flush() is a no-op, so this send is never covered by the atexit
  daemon deadline. Pass retry_limit=1 so a blackholed send is a single ~10s try.
- Posthog's constructor registers its own atexit.register(self.join), which
  flushes synchronously on the main thread at shutdown, unbounded by
  _flush_all_providers' daemon deadline (~21s against a blackholed endpoint).
  Unregister it so our bounded flush is the only shutdown drain path.
- _flush_all_providers now starts all provider threads, then joins them against a
  single shared 5s deadline (was 5s per provider = 5s x N), and wraps
  start()/join() defensively so the atexit hook can't raise past the envelope.

Tests: assert Mixpanel retry total == 1, and that PostHog unregisters its join.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mattmillerai

Copy link
Copy Markdown
Collaborator Author

CI note: the two red checks — build (9 ValueError: Comment cannot contain line breaks failures in registry/config_parser + node_init tests) and test / Windows Specific Commands (ImportError: cannot import name '__version__' from 'pydantic_core') — are the two known repo-wide infra breaks currently failing on every open PR regardless of content (tomlkit multi-line comment rejection → BE-3290; Windows pydantic_core .pyd overwrite flake). This PR touches only comfy_cli/tracking.py and its test, neither of which relates to those jobs. All telemetry-specific checks (ruff, the test job at 54s, GPU/macos/ubuntu platform runs, CodeQL) are green, and all 5 Cursor review findings are resolved. Ready to merge once the shared infra checks recover.

@mattmillerai

Copy link
Copy Markdown
Collaborator Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Jul 22, 2026

@bigcat88 bigcat88 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.

Re-verified locally with main merged in: tracking suites (126) and full unit suite (2491) green. The bounded Mixpanel consumer (10s, retry_limit=1), tightened PostHog budget, shared 5s daemon-thread flush deadline, and the atexit.unregister of PostHog's own join are all correct — the unregister especially, since LIFO atexit ordering would otherwise run PostHog's unbounded join before the bounded flush. One nit before merging: the PR body's 'Judgment calls' section still says PostHog's internal atexit was left in place — the final code removes it, so please refresh the description to match.

@coderabbitai
coderabbitai Bot requested review from bigcat88 and robinjhuang July 22, 2026 18:08
@mattmillerai
mattmillerai merged commit aeb9350 into main Jul 22, 2026
15 checks passed
@mattmillerai
mattmillerai deleted the matt/be-3403-bound-telemetry-io branch July 22, 2026 18:55
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

agent-coded PR authored by the agent-work loop cursor-review Request Cursor bot review lgtm This PR has been approved by a maintainer size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants