Skip to content

[OPIK-6370] [BE] fix: tolerate plain-string UUIDs in Redis stream deserialization#6610

Open
thiagohora wants to merge 3 commits intomainfrom
thiagohora/NA-fix-redis-stream-uuid-deserializer-tolerant
Open

[OPIK-6370] [BE] fix: tolerate plain-string UUIDs in Redis stream deserialization#6610
thiagohora wants to merge 3 commits intomainfrom
thiagohora/NA-fix-redis-stream-uuid-deserializer-tolerant

Conversation

@thiagohora
Copy link
Copy Markdown
Contributor

@thiagohora thiagohora commented May 5, 2026

Details

The Redis stream codec used by experiment_denormalization_stream (and any other Java-codec stream) wraps a Jackson ObjectMapper with default typing enabled inside JsonJacksonCodec. Older opik-backend versions ran on a different default-typing configuration and left messages with UUID fields serialized as plain strings in the consumer-group's pending entry list (PEL); the current version's deserializer expects Jackson's As.WRAPPER_ARRAY shape (["java.util.UUID","<uuid>"]).

XAUTOCLAIM repeatedly tries to reclaim those stuck messages on every tick (interval = pending-message-duration, default 10 minutes), the codec fails decoding with MismatchedInputException, and the subscriber emits an ERROR-level log every 10 minutes — generating alert noise indefinitely. The bad message is never acked or removed.

This PR adds a LenientUUIDDeserializer that accepts both shapes:

  • Plain string: "6caf374f-6568-4c6f-aad0-257e0c7296a4"
  • As.WRAPPER_ARRAY: ["java.util.UUID","6caf374f-6568-4c6f-aad0-257e0c7296a4"]

It's registered on a copy of JsonUtils.getMapper() (buildStreamMapper) which is then handed to JsonJacksonCodec inside RedisStreamCodec. Scope is limited to the Redis stream codec — the global JsonUtils.MAPPER is untouched, so HTTP API JSON behavior is unchanged.

After this fix, stuck messages flow through the regular processMessage path. If they're still otherwise invalid (e.g., the message references a deleted experiment), the existing retry-and-NACK logic in BaseRedisSubscriber will eventually ACK them after maxRetries attempts.

Change checklist

  • User facing
  • Documentation update

Issues

  • OPIK-6370

AI-WATERMARK

AI-WATERMARK: yes

  • Tools: Claude Code
  • Model(s): Claude Opus 4.7 (1M context)
  • Scope: assisted (deserializer design, codec wiring, test scaffolding)
  • Human verification: code review + targeted test runs (existing RedisStreamCodecTest + new LenientUUIDDeserializerTest)

Testing

mvn test -Dtest='LenientUUIDDeserializerTest'              # 3/3 pass
mvn test -Dtest='RedisStreamCodecTest'                     # 5/5 pass (no regression)

mvn -DskipTests compile and mvn spotless:check both pass.

The new unit test covers:

  • Plain-string UUID deserialization
  • As.WRAPPER_ARRAY UUID deserialization
  • Round-trip (write plain, read back) — verifies the happy path isn't broken

Not run: full backend test suite (no other code paths touched), end-to-end against a real Redis stream with stale PEL entries (would require synthetically populating the stream — out of scope; the unit test pins the deserialization invariant).

Documentation

N/A — internal infrastructure change, no API or schema impact.

…ation

The Redis stream codec used by experiment_denormalization_stream and other
Java-codec streams wraps a Jackson ObjectMapper with default typing enabled.
Older opik-backend versions ran on a different default-typing configuration
and left messages with UUID fields serialized as plain strings in PEL; the
current version's deserializer expects Jackson's As.WRAPPER_ARRAY shape
(["java.util.UUID","<uuid>"]). XAUTOCLAIM repeatedly tries to reclaim those
stuck messages, fails with MismatchedInputException, and emits an ERROR log
every pending-message-duration window.

Add a LenientUUIDDeserializer that accepts both shapes and register it on
the mapper handed to JsonJacksonCodec inside RedisStreamCodec. Scoped to
the Redis stream codec only — the global JsonUtils.MAPPER is untouched, so
HTTP API JSON behavior is unchanged.

Includes a unit test covering plain-string, As.WRAPPER_ARRAY, and a
round-trip case to confirm we don't break the happy path.
@github-actions github-actions Bot added java Pull requests that update Java code Backend tests Including test files, or tests related like configuration. labels May 5, 2026
@thiagohora thiagohora marked this pull request as ready for review May 5, 2026 14:48
@thiagohora thiagohora requested a review from a team as a code owner May 5, 2026 14:48
@thiagohora thiagohora closed this May 5, 2026
…ializer fires under WRAPPER_ARRAY default typing

The original commit registered LenientUUIDDeserializer via addDeserializer
on the codec mapper, but that only wires it as the value-level deserializer
called AFTER the TypeDeserializer strips the wrapper. With Redisson's
JsonJacksonCodec enabling default typing, every UUID property gets an
AsArrayTypeDeserializer in front of it: it expects START_ARRAY for the
type id and explicitly throws MismatchedInputException on a bare string
before delegating to the value deserializer. So plain-string UUIDs in PEL
were never reaching our lenient logic.

Fix by overriding deserializeWithType: it runs BEFORE the type deserializer
takes over. We peek at the current token and short-circuit when it's a
VALUE_STRING (legacy bare-string format), letting the standard UUIDDeserializer
parse it directly. For START_ARRAY we delegate to the type deserializer's
normal path, so current WRAPPER_ARRAY-format messages still decode correctly.

Also adds a production-shape test that wires LenientUUIDDeserializer onto a
real JsonJacksonCodec (default typing on, snake_case naming, etc.) and
verifies all three round-trips: encoded shape contains the wrapper as
expected, plain-string payloads decode, WRAPPER_ARRAY payloads decode.
This is the test that caught the gap left by the original commit.
@thiagohora thiagohora reopened this May 5, 2026
…t malformed payloads

The wrapper-array branch advanced past the type-id and value tokens but
didn't assert the next token was END_ARRAY, so a payload like
["...","<uuid>","extra"] would silently parse the UUID and leave the
parser positioned mid-array — corrupting the next field. Throw a
MismatchedInputException when the closing token isn't END_ARRAY.

Adds a regression test that builds a 3-element wrapper-array payload and
asserts the deserializer rejects it instead of silently accepting it.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Backend Tests - Integration Group 9

316 tests   316 ✅  4m 32s ⏱️
 24 suites    0 💤
 24 files      0 ❌

Results for commit 2b276da.

♻️ This comment has been updated with latest results.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Backend Tests - Integration Group 12

240 tests   239 ✅  5m 17s ⏱️
 39 suites    1 💤
 39 files      0 ❌

Results for commit 2b276da.

♻️ This comment has been updated with latest results.

@andrescrz andrescrz changed the title [NA] [BE] fix: tolerate plain-string UUIDs in Redis stream deserialization [OPIK-6370] [BE] fix: tolerate plain-string UUIDs in Redis stream deserialization May 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Backend java Pull requests that update Java code tests Including test files, or tests related like configuration.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant