fix(cdp): guard against malformed invocation globals in cyclotron-hog worker - #73076
Merged
meikelmosby merged 9 commits intoJul 23, 2026
Merged
Conversation
… worker A cyclotron-hog invocation whose state.globals is present but missing project or event caused unguarded dereferences (globals.project.id in addGroupsToGlobals, globals.event.distinct_id in the person-stub branch) to throw an unhandled rejection out of the loadHogFunctions Promise.all, crash-looping the worker on a single poison-pill message and stalling the partition it owned. Guard both read sites: loadHogFunctions now drops a malformed invocation via the existing dequeue path (logged + captured) instead of throwing, and addGroupsToGlobals skips enrichment when project/event is absent. Generated-By: PostHog Code Task-Id: e16d9add-5ec4-4db3-9c24-cb3bdb036b4d
meikelmosby
marked this pull request as ready for review
July 23, 2026 07:23
|
😎 Merged manually by @meikelmosby - details. |
Contributor
|
Hey @meikelmosby! 👋 It looks like your git author email on this PR isn't your
You can fix it for this repo with: git config user.email "you@posthog.com"Or set it globally with |
Contributor
|
Reviews (1): Last reviewed commit: "fix(cdp): guard against malformed invoca..." | Re-trigger Greptile |
The rerun paginator re-enqueues invocations by casting a persisted (and
intentionally minimized) ClickHouse globals snapshot with `as`, trusting it
without checking shape. A snapshot written by an older serializer — or
otherwise drifted — can be missing project/event, which the cyclotron-hog
worker then dereferences unguarded: the source of the malformed poison-pill
state the worker guards hardened against.
Add a zod schema validating the invariants the worker relies on
(project.{id,url}, event.{distinct_id,properties}) while staying permissive
(passthrough) on non-critical fields, and use safeParse at the rehydration
boundary — a row that fails validation is logged and skipped rather than
re-enqueued as a poison pill.
Generated-By: PostHog Code
Task-Id: e16d9add-5ec4-4db3-9c24-cb3bdb036b4d
dmarchuk
approved these changes
Jul 23, 2026
Generated-By: PostHog Code Task-Id: e16d9add-5ec4-4db3-9c24-cb3bdb036b4d
dmarchuk
enabled auto-merge (squash)
July 23, 2026 07:52
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
auto-merge was automatically disabled
July 23, 2026 08:08
Pull Request is not mergeable
meikelmosby
enabled auto-merge (squash)
July 23, 2026 08:20
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
auto-merge was automatically disabled
July 23, 2026 08:46
Pull Request is not mergeable
meikelmosby
enabled auto-merge (squash)
July 23, 2026 09:42
The rehydration schema validation added in this PR rejects the minimal globals the routing tests construct, so the rerunnable-type cases would get null back before their assertion. Give the fixture the project/event shape a real invocation always carries so those cases pass validation and the tests exercise the type gate as intended. Generated-By: PostHog Code Task-Id: e16d9add-5ec4-4db3-9c24-cb3bdb036b4d
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A single malformed
cdp_cyclotron_hoginvocation — one whosestate.globalsis present but missing bothprojectandevent— crash-looped the cyclotron-hog worker and stalled the partition it owned. InloadHogFunctions, two enrichment calls run in onePromise.all:addGroupsToGlobalsdereferencesglobals.project.id, and the person-stub branch dereferencesglobals.event.distinct_id. With those fields absent, both throwTypeError: Cannot read properties of undefined, which surfaces as an unhandled rejection → the worker exits → it restarts, re-reads the same offset, and crashes again. One poison-pill message takes down the consumer and stops the partition from draining.The dereferences themselves have been unchanged for months — this is a latent bug triggered by data, not a code rollout.
Root cause context: every invocation deserialize path (Kafka/WarpStream, cyclotron postgres v1/v2, and the rerun rehydration path) casts the parsed JSON to its type with
asand performs no shape validation. The live producer always setsproject/event, but a message that arrives without them — a legacy/differently-shaped queued message, or a rerun rehydrated from an intentionally-minimized ClickHouseinvocation_globalssnapshot via a blind cast — flows straight into the unguarded derefs.Changes
Two layers: harden the read sites so a bad message can't crash-loop the worker, and validate the shape at the most plausible source instead of blind-casting it.
cdp-cyclotron-worker.consumer.ts:loadHogFunctionsvalidatesglobals.project/globals.eventup front. A malformed invocation is logged,captureException'd, and routed to the existing dequeue path so it's dropped and the partition drains — instead of throwing out of thePromise.all. The guard sits before both crash sites.groups-manager.service.ts:addGroupsToGlobalsskips enrichment (setsgroups = {}) whenproject/eventis absent, as defense-in-depth (it's also called from the hogflow path).schema/cyclotron.ts+rerun-paginator.service.ts: replace the blindparsedGlobals as HogFunctionInvocationGlobalsWithInputscast in the rerun rehydration path with a zodsafeParse. The newHogFunctionInvocationGlobalsSchemavalidates the invariants the worker relies on (project.{id,url},event.{distinct_id,properties}) and stays permissive (passthrough) on everything else so drift on non-critical fields never rejects a good message. A row that fails validation is logged and skipped (counted asrehydrate_failed) rather than re-enqueued as a poison pill.Still open (flagged, not in scope here): the Kafka/postgres deserialize paths remain unvalidated casts, and the equivalent
loadHogFlowspath in the hogflow worker lacks the same guard.How did you test this code?
Test-first for the worker guard: added failing tests, confirmed they reproduced the exact
TypeErrors, then applied the fix and confirmed green.groups-manager.service.test.ts— two parameterized cases (missingproject, missingevent) assertingaddGroupsToGlobalsresolves with empty groups. Before the fix they failed withCannot read properties of undefined (reading 'id')and(reading 'properties'); after, the suite passes. Regression caught: a partialglobalsreaching enrichment throws — no existing test exercised it, because the fixture always backfillsproject/event.schema/cyclotron.test.ts— 8 cases: valid globals accepted; missingproject/eventand wrong-typed critical fields rejected; non-critical fields passed through. Regression caught: loosening the schema (or switching to strict) so a poison-pill shape validates, or so reruns silently dropinputs/request.cdp-cyclotron-worker.test.ts— two cases asserting a malformed invocation is dequeued (DLQ'd), not executed. Regression caught: the worker crash-looping on a poison pill instead of dropping it. This test is DB-backed (resetTestDatabase/createHub) and could not run in the sandbox (no Postgres) — it mirrors the existingdequeue if the hog function cannot be foundpattern and will run in CI.Typecheck clean on all changed files; eslint clean. Runnable suites (
groups-manager,schema/cyclotron) pass locally.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Investigated from a production incident thread: a cyclotron-hog worker stuck Progressing, one partition backed up. Confirmed the thesis before fixing — wrote the failing unit test that reproduces the crash, watched it fail with the reported stack traces, then implemented the guard and watched it pass. Traced the produce→serialize→enqueue→dequeue→deserialize path to establish that no deserialize step validates shape; on a follow-up pass, replaced the rerun path's blind
ascast — the most plausible producer of malformed re-enqueued state — with schema validation. Chose to keep the schema permissive (validate only the dereferenced invariants) so it can't reject valid messages on incidental drift.Skills invoked:
/writing-tests.Created with PostHog from a Slack thread