Skip to content

fix: build telemetry providers lazily so comfy install can upgrade pydantic_core (BE-3289)#535

Open
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-3289-lazy-telemetry-import
Open

fix: build telemetry providers lazily so comfy install can upgrade pydantic_core (BE-3289)#535
mattmillerai wants to merge 2 commits into
mainfrom
matt/be-3289-lazy-telemetry-import

Conversation

@mattmillerai

Copy link
Copy Markdown
Collaborator

ELI-5

comfy said hello to its analytics library on startup. That library quietly
loaded a compiled Windows DLL and never let go of it. Later in the same run,
comfy install asked uv to upgrade that exact DLL — and Windows refuses to
overwrite a file that a running program has open. uv gave up halfway, leaving
the package in pieces, and every comfy command after that crashed on import.

Now we don't say hello to analytics until we actually have something to send. If
a run never sends telemetry, the DLL is never loaded, and uv can replace it
freely.

The failure

Every open PR has been failing the test / "Windows Specific Commands" check,
unrelated to its own diff:

ImportError: cannot import name '__version__' from 'pydantic_core' (unknown location)

Root cause

The ticket hypothesized a half-installed dependency tree from the preceding
non-fatal uv pip install exit-2. That's the symptom. The line the log
truncation was hiding is the actual cause — from
the failing job:

Prepared 59 packages in 1m 26s
error: failed to remove file `D:\a\comfy-cli\comfy-cli\venv\Lib\site-packages\pydantic_core/_pydantic_core.cp312-win_amd64.pyd`: Access is denied. (os error 5)

The chain:

  1. mixpanel 5.2.0 (unpinned; CI installs via pip install -e ., which does not
    read uv.lock, so it resolves fresh every run) added a hard pydantic
    dependency for its feature-flags module. CI log: Collecting pydantic<2.12,>=2.0.0 (from mixpanel->comfy-cli==0.0.0).
  2. comfy_cli/tracking.py imported mixpanel at module scope, so every
    comfy process loaded _pydantic_core.cp312-win_amd64.pyd and held it open.
  3. comfy install shells out to uv pip install --requirement requirements.compiled
    against its own interpreter's environment. ComfyUI's pins need a different
    pydantic-core, so uv must replace that .pyd.
  4. Windows will not unlink a DLL mapped into a running process → Access is denied (os error 5) → uv exits 2, having already unlinked the distribution metadata.
  5. pydantic_core is now half-removed ("unknown location"), so the next command in
    the step (comfy --here launch) dies at import.

This is why it started repo-wide with no code change, and why the ordinary
multi-platform Windows test job still passes — that job never runs comfy install,
so nothing ever tries to replace the loaded DLL.

Fix

Defer the mixpanel / posthog imports and provider construction to the first
send (_get_providers(), called from _dispatch). All send paths are already
gated on consent, so CI (--skip-prompt, no consent) never constructs a provider
and never loads the extension — leaving uv free to replace it.

This is the ticket's step 4, promoted from "resilience win" to the actual fix. It
also drops an SDK import from CLI startup.

Deliberately not pinning mixpanel (the ticket's step 3): the pin would treat
the symptom, freeze us off analytics updates, and still leave the bug armed for any
future dependency that ships a compiled extension overlapping ComfyUI's pins. It
would also not help a consented user (see Known limitation).

Evidence

Empirical, on import comfy_cli.cmdline (the exact chain in the traceback):

pydantic_core loaded into the process?
origin/main yes_pydantic_core.cpython-314-darwin.so
this branch no — not in sys.modules

The new tests/comfy_cli/test_tracking_lazy_import.py asserts this in fresh
subprocesses (the pytest process has already imported too much to assert on
in-process). All 5 fail on origin/main's tracking.py and pass here — the
mechanism, not just my premise.

Known limitation (judgment call, flagged deliberately)

This fully fixes CI and any run without telemetry consent. A user with
telemetry enabled on Windows can still trip the same DLL lock if an event fires
before comfy install reaches its uv subprocess. Fixing that properly means not
installing into the interpreter's own environment (or sending telemetry
out-of-process) — a much larger change, well outside this ticket. Called out rather
than silently left: happy to file a follow-up.

Verification

  • ruff check . — pass
  • ruff format --check . — pass
  • pytest — 2569 passed, 37 skipped. 9 pre-existing failures in
    registry/test_config_parser.py + nodes/test_node_init.py reproduce
    identically on a clean origin/main tree (verified by stashing this diff);
    they are unrelated to telemetry and not touched here.
  • The real acceptance test is this PR's own test-windows.yml run — it triggers on
    comfy_cli/**, which this diff touches.

Note for review

tests/e2e/verify_tracking_live.py iterated tracking.PROVIDERS directly, which is
now None until the first send. Switched to _get_providers(). E2E is opt-in
(TEST_E2E=true), so no suite run would have caught it.

_get_providers() is double-checked-locked: module-level construction was
single-shot via the import lock, and a lazy global would otherwise let a racing
build strand a PostHog client — and its unflushed queue — in the list that lost.
Nothing tracks off-thread today (the run_cli pools spawn subprocesses); this
preserves a property the old code got for free rather than relying on that.

…dantic_core (BE-3289)

`mixpanel` 5.2.0 added a hard `pydantic` dependency for its feature-flags
module, so `from mixpanel import Mixpanel` at tracking.py import time pulled the
compiled `pydantic_core` extension into every `comfy` process and held it open
for the process lifetime.

`comfy install` shells out to `uv pip install` against its own interpreter's
environment. On Windows an open DLL cannot be replaced, so uv could not upgrade
pydantic_core:

    error: failed to remove file `...\pydantic_core\_pydantic_core.cp312-win_amd64.pyd`:
    Access is denied. (os error 5)

uv exited 2 having already unlinked the distribution metadata, leaving the
package half-removed, so the next `comfy` invocation in the same job died with
`ImportError: cannot import name '__version__' from 'pydantic_core' (unknown
location)`. mixpanel is unpinned and CI installs via `pip install -e .`, which is
why this began failing on every PR with no code change.

Defer the mixpanel/posthog imports and provider construction to the first send.
A run that never sends telemetry — no consent, or DO_NOT_TRACK — never loads the
extension at all, so uv is free to replace it. Behavior is unchanged: providers
are still built on first dispatch, just not at import.
@mattmillerai mattmillerai added the agent-coded PR authored by the agent-work loop label Jul 17, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review July 17, 2026 07:00
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 16 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 23d07698-6318-4b0b-97a9-55065747c1dc

📥 Commits

Reviewing files that changed from the base of the PR and between a732f5c and 93f2d8d.

📒 Files selected for processing (3)
  • comfy_cli/tracking.py
  • tests/comfy_cli/test_tracking_lazy_import.py
  • tests/e2e/verify_tracking_live.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch matt/be-3289-lazy-telemetry-import
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch matt/be-3289-lazy-telemetry-import

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

@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. bug Something isn't working labels Jul 17, 2026
@mattmillerai mattmillerai added the cursor-review Request Cursor bot review label Jul 17, 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 2 finding(s).

Severity Count
🟠 High 1
🟢 Low 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
…BE-3289)

Deferring the mixpanel/posthog imports moved them off startup and onto the
user's command path, so the failure they used to hit at import can now surface
mid-run. _get_providers() is called in _dispatch's loop header, outside the
per-provider try/except that only wraps .track() — so an ImportError from a
half-removed pydantic_core (exactly the state this PR exists to prevent, but
still reachable from an earlier bad install) propagated through track_event and
took the user's command down.

Build each provider independently and cache the result, including []: one
unusable SDK no longer silences the other, telemetry goes quiet instead of
crashing, and a doomed import isn't retried on every later event.

Also take _PROVIDERS_LOCK before reading PROVIDERS in the atexit flush, so an
in-flight build resolves first rather than the hook seeing None mid-construction
and exiting without draining a racing thread's PostHog queue. The lock is
released before flushing so a slow network drain can't block a concurrent send.

Both regression tests fail on the pre-fix tracking.py and pass here.

Raised by cursor-review (5 of 8 reviewers).

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

Copy link
Copy Markdown
Collaborator Author

Pushed 93f2d8d addressing both cursor-review findings (details on each thread).

The failing build check is pre-existing and not from this PR — deferring to a follow-up

Run pytest / build fails with 9 tests unrelated to this diff:

FAILED tests/comfy_cli/registry/test_config_parser.py::... - ValueError: Comment cannot contain line breaks   (x8)
FAILED tests/comfy_cli/command/nodes/test_node_init.py::test_node_init_strips_credentials - assert 1 == 0

I chased the root cause rather than re-asserting the "pre-existing" claim in the PR body. comfy_cli/registry/config_parser.py:75 passes a multi-line string to tomlkit's Item.comment(), which newer tomlkit rejects:

if "\n" in comment or "\r" in comment:
    raise ValueError("Comment cannot contain line breaks")

tomlkit is unpinned (pyproject.toml:49). uv.lock pins 0.13.3, but the build job installs via pip install -e ., which does not read uv.lock and now resolves tomlkit 0.15.1. Same class of bug as BE-3289 — an unpinned dep drifting under CI — but a different dep in a different file.

Evidence it is not this PR: on a Python 3.10 venv matching CI (3.10.20) with a fresh resolve, the same 8 failures reproduce on a clean origin/main worktree. With tomlkit 0.13.3, test_config_parser.py is 81/81 green. test_node_init_strips_credentials is a downstream casualty of the same raise.

It blocks every PR in the repo, so it wants its own PR rather than riding along in an unrelated one — filing a follow-up.

Verification of this push

  • Full suite on Python 3.10: 2571 passed, 18 skipped; the only 9 failures are the pre-existing tomlkit set above, reproduced identically on clean main.
  • ruff check / ruff format --check clean on both touched files. (A local ruff 0.12.7 flags 14 UP038 in untouched files; CI pins 0.15.15 where that rule is gone, and ruff_check passes.)
  • Both new regression tests fail on the pre-fix tracking.py and pass here — testing the mechanism, not just the premise.

@mattmillerai

Copy link
Copy Markdown
Collaborator Author

Correction to my comment above: no follow-up needed — the tomlkit fix is already in flight. My root-cause diagnosis landed independently on the same line, which is a good cross-check:

Both target config_parser.py's multi-line .comment() call. Those two look like duplicates of each other and probably want de-duping, but either one turns the build check green repo-wide.

So the build failure here needs nothing from this PR — it clears once one of those merges. Not filing a duplicate ticket. This PR's own diff is unaffected: mergeStateStatus is UNSTABLE (not BLOCKED), and every other check passes.

@mattmillerai

Copy link
Copy Markdown
Collaborator Author

Resolver pass: both review threads (the unguarded lazy construction and the _flush_all_providers lock) were addressed in 93f2d8d and are resolved. No merge conflict, no code changes needed here. Two notes for whoever reviews this:

The acceptance test passes. test / "Windows Specific Commands" — the exact job this PR exists to fix, and the one that has been red repo-wide — is green on this branch: https://github.com/Comfy-Org/comfy-cli/actions/runs/29569410962/job/87849415605. That is the mechanism confirmed end-to-end, not just the unit tests.

The remaining red build job is pre-existing and unrelated to this diff. It fails 9 tests in registry/test_config_parser.py + nodes/test_node_init.py with ValueError: Comment cannot contain line breaks. This PR touches only comfy_cli/tracking.py and its tests — config_parser.py is not in the diff.

I verified the cause rather than just asserting it is pre-existing. tomlkit is unpinned (pyproject.toml:49) and CI installs via pip install -e ., which does not read uv.lock, so it resolves tomlkit==0.15.1; uv.lock pins 0.13.3. tomlkit 0.15.1 added validation that rejects the multi-line strings config_parser.py passes to .comment() (lines 75 and 250). Empirically, in a clean worktree: full suite green on 0.13.3 (2478 passed), and installing only tomlkit==0.15.1 and changing nothing else reproduces exactly the CI failure — same 9 tests.

Amusingly this is the same class of bug this PR fixes (an unpinned dependency resolving fresh in CI), just via a different package. It blocks every PR in the repo, so it wants its own PR rather than being folded in here — filed as a follow-up.

@mattmillerai

Copy link
Copy Markdown
Collaborator Author

Correction to my note above: the tomlkit break is already being fixed — no new follow-up needed, and I did not file one. Existing coverage:

So nothing further is needed on #535 itself: its threads are resolved, its target job is green, and its only red is owned by the PRs above. Merging #537 (or either of #533/#536) should turn build green here with no change to this branch.

mattmillerai added a commit that referenced this pull request Jul 17, 2026
f91cab6 accidentally carried a 287-line uv.lock regeneration that has
nothing to do with the HTTP oversize fix. The drift is real but
pre-existing: #490 added the `bench` optional-dependency extra
(anthropic>=0.40) to pyproject.toml without relocking, so main's lock has
been stale since it merged — `uv lock --check` fails on main today. Any
bare `uv run` in the repo silently re-locks and re-dirties the file,
which is how it got picked up here.

Re-locking is not this PR's job, and carrying it here invites conflicts
with the dependency PRs already in flight (#533/#535/#536). No CI job
consumes uv.lock, so reverting to main's copy is behavior-neutral.
Filed as a follow-up instead.

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

Copy link
Copy Markdown
Collaborator Author

CI status note (from a review sweep): the only failing check — `build` (pytest.yml, the full-suite job that installs deps unpinned) — is not caused by this PR. It is the repo-wide tomlkit 0.15.1 break (released 2026-07-17), which tightened .comment() to reject multi-line strings and fails 9 tests/comfy_cli/registry/test_config_parser.py cases. Reproduced locally: those tests pass on the lockfile's tomlkit 0.13.3 and fail only under 0.15.1; this PR does not touch config_parser. That break is already tracked/fixed in #533 / #536 (single-line comment emission) and #537 (install from uv.lock). Once one of those lands on main and this branch is rebased, build goes green.

This PR itself is clean: no merge conflict, both Cursor review threads resolved in 93f2d8d, and its own tests (tests/comfy_cli/test_tracking_lazy_import.py, 7 cases) pass locally with ruff check/format clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded PR authored by the agent-work loop bug Something isn't working cursor-review Request Cursor bot review size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant