Skip to content

Fix DSM pathway timestamp corruption: ZigZag-encode varints to match every other Datadog tracer - #6170

Open
olivia-gusto wants to merge 1 commit into
DataDog:masterfrom
olivia-gusto:olivia-gusto/fix-dsm-pathway-varint-zigzag
Open

Fix DSM pathway timestamp corruption: ZigZag-encode varints to match every other Datadog tracer#6170
olivia-gusto wants to merge 1 commit into
DataDog:masterfrom
olivia-gusto:olivia-gusto/fix-dsm-pathway-varint-zigzag

Conversation

@olivia-gusto

Copy link
Copy Markdown

What does this PR do?

Fixes Datadog::DataStreams::PathwayContext to ZigZag-encode/decode the two pathway timestamps (pathway_start, edge_start), matching every other Datadog tracer's wire format for DSM.

Motivation

internal/datastreams.Encode/Decode in dd-trace-go (via sketches-go's EncodeVarint64/DecodeVarint64) ZigZag-map these two fields before applying plain unsigned LEB128. This gem previously skipped the ZigZag step and used plain unsigned LEB128 directly. The bytes still parse successfully on the Go side — DecodeVarint64 never errors — but the un-ZigZag step silently produces a garbage timestamp offset by years to decades from the true value, depending on the parity of the raw millisecond value.

This isn't a "which side is canonical" ambiguity — checking every other Datadog tracer that implements DSM shows Ruby is the sole outlier:

SDK Scheme
Go (dd-trace-go) ZigZag (sketches-go's EncodeVarint64/DecodeVarint64)
Python (dd-trace-py) ZigZag (ddtrace/internal/datastreams/encoding.py: v >> 63 ^ (v << 1))
JavaScript (dd-trace-js) ZigZag (packages/dd-trace/src/datastreams/encoding.js, explicitly documented as such)
Java (dd-trace-java) ZigZag (VarEncodingHelper.encodeSignedVarLong/decodeSignedVarLong via sketches-java)
.NET (dd-trace-dotnet) ZigZag (VarEncodingHelper.WriteVarLongZigZag/ReadVarLongZigZag)
Ruby (dd-trace-rb) Plain unsigned LEB128 (this file, pre-fix)
C++ (dd-trace-cpp) N/A -- DSM not implemented

Net effect (pre-fix): any pathway where a Ruby producer using this gem feeds a Python, JavaScript, Java, .NET, or Go DSM consumer (or vice versa) shows wildly incorrect latency in Data Streams Monitoring -- no exception on either side, so it typically only surfaces as anomalous dashboard data. Ruby-to-Ruby pathways are unaffected since both ends share the same (previously missing) ZigZag step consistently.

Originally investigated and filed as DataDog/dd-trace-go#5163 against the Go side; re-filed here once the cross-SDK survey above made clear Ruby is the one that needs to change, not Go.

Verification

Tested end-to-end against the real, unmodified source of every other Datadog tracer that implements DSM (not reimplementations) -- Go, Python, Java (via sketches-java, the library dd-trace-java calls into), and JavaScript:

  1. Cross-SDK encode parity: encoding the same input (hash=424242, pathwayStart=1786000000123ms, edgeStart=1786000005456ms) with each SDK's own real, unmodified code produces byte-identical output on every one of them: MnkGAAAAAAD2kare+meg5are+mc= -- Go, Python, Java, JavaScript, and this fix all agree.
  2. Real production path, real Kafka, real cross-language decode: hot-patched this fix into the actual Datadog::DataStreams::Processor/WaterDrop::Producer code, produced one real message to a real local Kafka broker, consumed the raw header back, then decoded that exact real header with Go's, Python's, and Java's own real Decode/decode_var_int_64/decodeSignedVarLong implementations. All three independently decoded it to the same correct hash and timestamp, matching real time (not decades-off garbage).
  3. Regression check: reverted the fix locally and re-decoded the same real Go-encoded payload -- it came back as 2083-03-11 instead of the correct 2026 date (roughly double the true epoch-ms value, which is exactly what un-ZigZagging a plain-unsigned value produces for an even input). This matches the multi-decade-offset symptom reported in production DSM dashboards.

(.NET's dd-trace-dotnet was confirmed via source reading only -- VarEncodingHelper.WriteVarLongZigZag/ReadVarLongZigZag naming is unambiguous -- no .NET runtime was available to execute it directly, but happy to if useful.)

Added a regression test (spec/datadog/data_streams/pathway_context_spec.rb) with a real dd-trace-go-produced base64 fixture, asserting both correct decode and byte-identical encode -- this locks in cross-SDK wire compatibility going forward rather than only testing this gem's internal round-trip (which the existing tests already covered and would not have caught this bug, since they only ever encode and decode with the same, symmetrically-broken implementation).

All existing tests in spec/datadog/data_streams/pathway_context_spec.rb continue to pass unmodified (8 examples), plus the 2 new regression tests (10/10 total).

How to test the change?

bundle exec rspec spec/datadog/data_streams/pathway_context_spec.rb

…every other Datadog tracer

Datadog::DataStreams::PathwayContext encoded pathway_start/edge_start as plain
unsigned LEB128, but every other Datadog tracer that implements DSM (dd-trace-go,
dd-trace-py, dd-trace-js, dd-trace-java via sketches-java, dd-trace-dotnet)
ZigZag-maps these same fields before applying LEB128. Ruby was the sole outlier.

Any non-Ruby DSM consumer decoding a pathway produced by this gem silently
un-ZigZags a value that was never ZigZag-mapped, producing a garbage timestamp
years to decades off from the true value -- no exception raised on either side.

Verified end-to-end against the real, unmodified source of dd-trace-go, dd-trace-py,
and sketches-java (the library dd-trace-java calls into) -- not reimplementations:
all three produce byte-identical wire output to this fix for the same input, and
all three independently decode a real Kafka message (produced through a real
application's production Karafka/WaterDrop code path with this fix applied) to
the correct timestamp. Also confirmed dd-trace-js's real encoding.js matches.
Reverting the fix and re-decoding the same real Go-encoded payload reproduces the
exact failure mode (a ~57-year-off date), matching the production symptom this
was filed for.

Added a regression test locking in wire compatibility against a real
dd-trace-go-produced fixture.

@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: a83ce7e3a5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


# If high bit is clear, we're done
return value unless (byte & 0x80).nonzero?
return (value >> 1) ^ -(value & 1) unless (byte & 0x80).nonzero?

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 Preserve legacy pathway decoding during upgrades

During a rolling upgrade, or when consuming queued messages whose dd-pathway-ctx-base64 header was produced by a previous dd-trace-rb release, this unconditional ZigZag decode misreads the old unsigned millisecond varints as signed ZigZag values; for example the legacy bytes for 1786000000123 ms decode to a 1941 timestamp, causing set_checkpoint to record decades of pathway/edge latency instead of preserving DSM continuity. Please keep a legacy decode path, version marker, or sanity fallback for inbound contexts before switching emitters to the new wire format.

AGENTS.md reference: AGENTS.md:L115-L120

Useful? React with 👍 / 👎.

@vpellan vpellan added the community Was opened by a community member label Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Was opened by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants