Skip to content

fix(spark): serialize connector records under Kryo without a registrator - #25

Merged
ChristopherWirt merged 1 commit into
developfrom
fix-kyro
Oct 29, 2026
Merged

ChristopherWirt merged 1 commit into
developfrom
fix-kyro

Conversation

@a392n328

@a392n328 a392n328 commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Jobs that run with spark.serializer=KryoSerializer, which Uniffle remote shuffle requires, cannot send the connector's records. Kryo's default FieldSerializer fails to build a serializer for VortexWriterCommitMessage, VortexFilePartition and ResolvedTransform ("can't get field offset on a record class" with spark.kryo.unsafe=true). It also corrupts PartitionedWriterCommitMessage, because it rebuilds the immutable List.of() it holds by calling add().

Until now this was worked around with an out-of-tree KryoRegistrator jar that registered the records with JavaSerializer. Registering classes gives them numeric Kryo IDs. Uniffle creates the driver's Kryo instance early, along a different path from the executors, so the IDs could differ between the two sides. That would corrupt unrelated objects sent over the wire, and is the likely cause of the HighlyCompressedMapStatus deserialization failures ("Index 48 out of bounds for length 3").

Annotate the four types with @DefaultSerializer(JavaSerializer.class). Kryo now picks Java serialization for them without any registration, so they take no ID slot. Users no longer need spark.kryo.registrator or a driver extraClassPath entry. This relies on spark.kryo.registrationRequired being false, which is Spark's default.

KryoSerializationTest round-trips each type through Spark's KryoSerializer with spark.kryo.unsafe both off and on; all cases fail without the annotations. The test JVM opens java.lang.invoke and java.util, which Spark's KryoSerializer needs on Java 17 and which Spark's launcher normally adds.

Jobs that run with spark.serializer=KryoSerializer, which Uniffle remote
shuffle requires, cannot send the connector's records. Kryo's default
FieldSerializer fails to build a serializer for VortexWriterCommitMessage,
VortexFilePartition and ResolvedTransform ("can't get field offset on a
record class"), so a write job aborts when it returns its commit
messages to the driver. Kryo also corrupts PartitionedWriterCommitMessage,
because it rebuilds the immutable List.of() it holds by calling add().

Until now this was worked around with an out-of-tree KryoRegistrator jar
that registered the records with JavaSerializer. Registering classes
gives them numeric Kryo IDs. Uniffle creates the driver's Kryo instance
early, along a different path from the executors, so the IDs could
differ between the two sides. That would corrupt unrelated objects sent
over the wire, and is the likely cause of the HighlyCompressedMapStatus
deserialization failures ("Index 48 out of bounds for length 3").

Annotate the four types with @DefaultSerializer(JavaSerializer.class).
Kryo now picks Java serialization for them without any registration,
so they take no ID slot. Users no longer need spark.kryo.registrator or
a driver extraClassPath entry. Jobs with
spark.kryo.registrationRequired=true can list the four classes in
spark.kryo.classesToRegister, and Kryo still uses the annotation.

KryoSerializationTest round-trips each type through Spark's
KryoSerializer with spark.kryo.unsafe both off and on, and with the
types registered by name under registrationRequired. VortexKryoWriteTest
runs partitioned and unpartitioned writes and reads in a Kryo session.
Without the annotations every case fails, and the write jobs abort with
the "can't get field offset on a record class" error. The test JVM opens
java.lang.invoke and java.util, which Spark's KryoSerializer needs on
Java 17 and which Spark's launcher normally adds.

Signed-off-by: Anirudh Narasimman <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* @param bucketCount -1 if not a bucket transform
*/
// Kryo's FieldSerializer cannot rebuild records, so under KryoSerializer this uses Java serialization.
@DefaultSerializer(JavaSerializer.class)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Always alarm bells whenever I see JavaSerializer - but probably fine.

@ChristopherWirt

Copy link
Copy Markdown
Collaborator

This is really a Spark ↔ Vortex bug rather than a Uniffle one: upstream develop has the same four types with no Kryo handling, and any job using spark.serializer=KryoSerializer will hit it. Worth sending upstream. Before we do:

  1. Reframe the description around Kryo. Lead with "writes fail under KryoSerializer". Cut the Uniffle registration-ID theory to one line at most: it's unproven and not upstream's concern.
  2. Check it fails with spark.kryo.unsafe=false (the default). VortexKryoWriteTest only sets unsafe=true. If writes also abort with default settings, it's a bug for everyone and much easier to land.
  3. Drop the registrator jar / extraClassPath line. That's our setup, not upstream's.

Also, the PR body says it relies on registrationRequired=false, but the commit message and tests show classesToRegister works too. Worth aligning.

@ChristopherWirt
ChristopherWirt merged commit dc7618c into develop Oct 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants