Skip to content

IGNITE-28940 Choose the marshaller by transport, not by message class - #13462

Merged
anton-vinogradov merged 29 commits into
apache:masterfrom
anton-vinogradov:ignite-28940
Aug 14, 2026
Merged

IGNITE-28940 Choose the marshaller by transport, not by message class#13462
anton-vinogradov merged 29 commits into
apache:masterfrom
anton-vinogradov:ignite-28940

Conversation

@anton-vinogradov

Copy link
Copy Markdown
Contributor

The marshaller used for a message's @Marshalled fields was a property of the
message class: @UseBinaryMarshaller decided it, and the factory bound the
chosen marshaller into the generated companion at registration time.

Why that is the wrong place

Binary cannot be used where marshalling cannot afford a cluster-wide class
registration, and that is a property of the call site. The registration is
MarshallerContextImpl#registerClassName -> proposeMapping -> fut.get(),
which waits for discovery, so it must never happen on a discovery thread. Ignite
classes usually skip it thanks to META-INF/classnames.properties, but that list
is not closed under nesting - a plain CacheConfiguration needs types that are
not on it.

The call sites are already separated by transport, and the hand-written half of
the API works this way: MarshallableMessage#marshal(Marshaller) takes the
marshaller as a parameter. Only generated code kept it in a field.

Change

  • Marshaller is now a parameter of MessageMarshaller and MessageMarshalling;
    the generator stops storing it and passes it down to nested messages.
  • Communication call sites pass ctx.marshaller(), discovery call sites pass
    marshallerContext().jdkMarshaller().
  • @UseBinaryMarshaller (60 classes), AbstractMessageFactoryProvider#init and
    both marshaller fields are gone, as is initProvider in IgniteKernal.

Wire format

The format is now a function of the transport, not of the class, so a class that
travels both transports is marshalled differently on each. Deliberate changes:

class change
ErrorMessage, PartitionHashRecord, TransactionsHashRecord jdk -> binary, on the communication leg
BinaryMetadataVersionInfo binary -> jdk, on the discovery leg
StoredCacheData jdk -> binary, on the communication leg (snapshot restore)

StoredCacheData is not in the ticket's list: it was found while reviewing this
change. It carries CacheConfiguration and QueryEntity blobs and reaches
communication through SnapshotRestoreOperationResponse -> SingleNodeMessage,
so the transport rule moves it to binary. Registration is allowed there, and
binary is the smaller of the two for a CacheConfiguration.

BinaryMetadataVersionInfo needed one more fix: the same long-lived instance
from the local metadata cache travels discovery in the data bag and communication
in MetadataResponseMessage, and a marshalled instance keeps its serialized
form. MetadataResponseMessage#metadataVersionInfo now stores a copy, so bytes
of one transport cannot leak into the other.

Verified

MessageProcessorTest (codegen goldens regenerated), IgniteCoreMessagesSerializationTest,
DirectMarshallingMessagesTest, MessageMarshalOnceTest, CompressedMessageTest,
GridIoManagerOrderedUnmarshalFailureTest, DiscoveryUnmarshalVulnerabilityTest,
QueryEntityMessageSerializationTest, LazyServiceConfigurationMessageSerializationTest,
SecurityBasicPermissionSetSerializationTest, TxDeadlockDetectionMessageMarshallingTest,
GridCacheQueryResponseUnmarshalTest, BinaryMetadataRegistrationInsideEntryProcessorTest,
GridCacheAtomicFullApiSelfTest, the continuous-query and p2p suites, and the
calcite PlanExecutionTest / ContinuousExecutionTest /
CalciteCommunicationMessageSerializationTest.

Full build of all modules and the strict checkstyle profile are clean.

🤖 Generated with Claude Code

anton-vinogradov and others added 2 commits August 11, 2026 01:09
The marshaller was picked from the message: @UseBinaryMarshaller on the
class decided it, and the factory bound the chosen one into the generated
companion at registration. But binary cannot be used where marshalling
cannot afford a cluster-wide class registration, and that is a property
of the call site, not of the message: registering waits for discovery, so
a discovery thread must never do it.

The marshaller is now a parameter of MessageMarshaller and
MessageMarshalling, passed down to nested messages by the generated code.
Communication call sites pass ctx.marshaller(), discovery call sites pass
the jdk one. The annotation, the two marshaller fields of the factory
provider and its init() are gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A message instance that travels both transports must not carry bytes of
one into the other: BinaryMetadataVersionInfo lives in the local metadata
cache, goes to discovery in the data bag and to communication in
MetadataResponseMessage, and a marshalled instance keeps its serialized
form. The response now stores a copy.

GridTestUtils.loadMarshaller still asked the generated companion for a
constructor taking a Marshaller, which the generator no longer writes,
and IncrementalSnapshotTest injected an ErrorMessage marshalled with jdk
into a communication message the receiver now reads with binary.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
anton-vinogradov and others added 8 commits August 11, 2026 18:14
…t on a discovery thread

Marshalling is done by the sending thread, and a schema-aware marshaller registers unknown class names in the
cluster, which takes a discovery round. On a discovery thread that round never completes - this very thread is
the one to deliver the answer.

ErrorMessage carries an arbitrary user class and travels both transports, so it always uses the JDK marshaller
of the local node. DistributedProcess sends the result of a process from a discovery thread whenever the
process finishes synchronously, so it now hands the send over to the system pool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… send over to the pool

A coordinator change between the handover and the actual send made the deferred task and the resend of the node
left listener target the same new coordinator, so the result was sent twice. Resolving the coordinator upfront
keeps the former addressing: the deferred send goes to the failed coordinator and is dropped, and the resend
stays the only live one.

The local coordinator branch marshals nothing, so it no longer goes through the pool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ts marshaller

Every caller had to know which marshaller its transport speaks and pass it next to the kernal context the
marshaller came from. Two entry points now name the transport itself: CommunicationMarshalling for the
schema-aware one and DiscoveryMarshalling for the JDK one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oints

The classes are final and hold static methods only, so nothing can instantiate them anyway.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nsports

The wire form of a @Marshalled field is cached in its companion field, so an instance marshalled by one transport
hands the other transport bytes of a format it does not read. A shared instance crosses transports in
DistributedProcess, which sends the result of a process by communication and then by discovery, and in the binary
metadata cache, whose entries answer requests and travel the data bag at once.

The new @JdkMarshalled tells the generated marshaller to use the JDK marshaller of the local node whatever the
transport speaks, so the cached bytes stay readable by both. It replaces what these classes did by hand: the
private helper of ErrorMessage and the defensive copy of MetadataResponseMessage, both dropped.

PartitionHashRecord kept the cached bytes across java serialization, since its byte fields were not transient
unlike the ones of its neighbours. DistributedProcessMarshalThreadTest covers the guard that keeps a result off
the discovery thread.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ng it off the discovery thread

The result travels both transports, so the marshaller has to be the same on both legs and it has to be the one
that needs no class name registration. @JdkMarshalled on SingleNodeMessage states exactly that, and it covers
every payload of every process, including the ones not written yet.

That makes the discovery thread guard of sendSingleMessage pointless: marshalling the result no longer waits for
a discovery round, so it is safe where master has always done it. The method is back to its former shape, and
with it goes the handover to the pool that reordered the send against the node left resend.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

/runall

@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

/top

@anton-vinogradov

anton-vinogradov commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

/runall


🚀 RunAll queuedbuild 9275680 · live progress & verdict: Ignite PR Checker. The verdict lands here when the run finishes.
🛑 Superseded by a newer /run-all.

@anton-vinogradov

anton-vinogradov commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@anton-vinogradov 🚀 RunAll queuedbuild 9275976 · live progress & verdict: Ignite PR Checker. Your previous run was cancelled — this one supersedes it. The verdict lands here when the run finishes.
🏁 Run finished. ♻️ Auto re-run #3 — 1 suite that failed mid-run, ≈ settled by 05:57 MSK — details in the verdict comment.

Ignite asserts that every test class belongs to a suite, and the check runs before the test chain, so the whole
RunAll stopped at it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

/run-all

@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

@anton-vinogradov that looks like an Ignite PR Checker command — but the checker doesn't know your accounts yet, so nothing was triggered. Everything it does runs under your own accounts (there is no bot); setting that up takes about two minutes:

  1. Log in at https://ignite-pr-checker.is-a.dev with a TeamCity (ci2) access token — create one at ci2 → Profile → Access Tokens.
  2. In settings (⚙) switch on at least one option — Auto re-run blocker suites needs nothing extra — and save your GitHub login in the PR-commands field. That's enough: commands work, the checker acks and narrates from its own account.
  3. The full experience — switch on Comment my runs' verdicts on the GitHub PR with a GitHub personal access token (create one here, classic, public_repo scope): acks and the live run status then come from your own account, plus checkstyle autofix becomes available. Auto-visa all my runs posts the verdict to the IGNITE ticket (needs a JIRA PAT).

Then comment here:

  • /run-all — queue the whole RunAll chain under your TeamCity account (/run-all top — at the top of the build queue);
  • /top — move the run your command started to the top of the queue while it still waits.

Your command comment gets a 🚀 and narrates the run — live ETA, finish, auto re-run waves — and the verdict lands as one comment that updates in place until everything settles. Tokens are stored encrypted, and only while the options are on.

@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

/top

1 similar comment
@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

/top

@anton-vinogradov

anton-vinogradov commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

/runall


🚀 RunAll queuedbuild 9278217 · live progress & verdict: Ignite PR Checker. Your previous run was cancelled — this one supersedes it. The verdict lands here when the run finishes.
🏁 Run finished — analysing; the verdict comment follows.

anton-vinogradov and others added 5 commits August 12, 2026 18:41
Extracting the send was only needed to hand it to the pool from the discovery thread guard, which is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rshaller in unmarshalNio

The pin belongs to the message, so a subclass has to marshal the inherited fields the same way its parent does.
It did not: CalciteErrorMessage took the marshaller of the transport while ErrorMessage took the JDK one, which
sent an exception through binary on communication where master sent it through jdk.

The nio method emitted the name of the pinned marshaller without declaring it, so a message that combined
@JdkMarshalled with @NioField did not compile at all.

The codegen test now covers both: the fixture carries a @NioField, and a subclass of it checks that the pin
reaches the generated companion of a child.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Documented
@Target(value = TYPE)
@Retention(RUNTIME)
public @interface UseBinaryMarshaller {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why binary changed on opposite JDK?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Now Discovery uses jdk, Communication - binary, with some exceptions.

* Marshalling of the discovery transport, which speaks the JDK marshaller: a discovery message is marshalled on a
* discovery thread, where waiting for a cluster-wide type registration would never finish.
*/
public final class DiscoveryMarshalling {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why it is in communication package? Also package managers might be revised

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agree, the package does not fit them. Moving these two means moving MessageMarshalling as well, since they sit next to it and it comes from master, so I would rather do all three in a separate issue than widen this one.

import org.jetbrains.annotations.Nullable;

/** Marshalling of the communication transport, which speaks the schema-aware marshaller. */
public final class CommunicationMarshalling {

@Vladsz83 Vladsz83 Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only minority of the messages requires BinaryMarshaller. Why we use it for all of them? It is able to marshal non-serializable and won't fail where it should. Is it faster than JDKMarshaller? Any proofs? It processes schema. Won't we get any perf. drop on marshalling with it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Measured it. Binary is faster on every payload a message field actually carries, except exceptions, where the two are equal.

Round trip (marshal + unmarshal), corretto-17, 2000 warmup + 20000 iterations, three runs, numbers from the last one:

payload jdk, us/op binary, us/op speedup jdk, bytes binary, bytes
small POJO 6.21 3.43 1.8x 121 40
list of 100 POJOs 57.72 22.24 2.6x 2048 4196
map of 50 String to UUID 14.95 5.38 2.8x 1626 1346
exception with a cause 20.59 24.79 0.83x 2138 2144
CacheConfiguration 27.60 7.60 3.6x 3432 636
QueryEntity 7.74 1.48 5.2x 783 161

Two results are worth reading carefully.

Exceptions are a tie, and that is expected. Throwable declares writeObject, so BinaryUtils.isCustomJavaSerialization sends it to OptimizedMarshaller - the binary format never runs. That is also why ErrorMessage pins jdk in this PR: binary buys nothing there and would only register the class name of every new exception type cluster-wide.

Binary is bigger on a list of equal objects. It writes the schema per object, while jdk writes the class descriptor once and back-references it. On everything else binary is smaller, up to 5x on CacheConfiguration.

The schema processing you asked about is what pays for itself here: it is written per object, but reading does not walk the whole graph.

The benchmark is a throwaway test class, not part of the PR. It builds both marshallers the way tests do - Marshallers.jdk() and createStandaloneBinaryMarshaller() - and times U.unmarshal(marsh, U.marshal(marsh, obj), loader) in a loop. I can attach it if you want to re-run it.

}

MessageMarshalling.unmarshal(e, ctx, cctx.cacheObjectContext(), ldr);
CommunicationMarshalling.unmarshal(e, ctx, cctx.cacheObjectContext(), ldr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The same. Why Communication marshaling? How do I know it? To me, MessageMarshalling look much better. It will decide what and how to do. Now I need to decide. Let's reconsider.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Let's refactor naming, linking to the thread in a separate issue.
This one has another goal.


try {
MessageMarshalling.unmarshal(cacheMsg, cctx.kernalContext(), null, cctx.deploy().globalLoader());
CommunicationMarshalling.unmarshal(cacheMsg, cctx.kernalContext(), null, cctx.deploy().globalLoader());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same. Why CommunicationMarshalling ? Why if someone puts DiscoveryMarshalling? Is it deprecated, protected?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Tests will fail :)

* All changes must be made with the respect of RU rules.
*/
// Travels both transports: by discovery when a cache starts, by communication when a snapshot is restored.
@JdkMarshalled

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Even so, why not marshall with the marshaller choosen? Why discovery ort Communication should change marshaller?

@anton-vinogradov anton-vinogradov Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

They are not changing marshallers, they keep it as is with minor fixes, which was luckily passed before.


try {
MessageMarshalling.unmarshal(req, ctx, null, U.resolveClassLoader(clsLdr, ctx.config()));
CommunicationMarshalling.unmarshal(req, ctx, null, U.resolveClassLoader(clsLdr, ctx.config()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Same. MessageMarshalling looks better. Noone cares which marshalling to call. Just pick up MessageMarshalling. Now we need to know.

boolean loc = ctx.localNodeId().equals(res.nodeId()) && !ctx.config().isMarshalLocalJobs();

if (!loc)
MessageMarshalling.unmarshal(res, ctx, null, U.resolveClassLoader(dep.classLoader(), ctx.config()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

All the others places too. MessageMarshalling looks better. Gets the work of marsahaling type choise incapsulated. Not, exposed to a developer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@anton-vinogradov

anton-vinogradov commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9278815 · 147 suites ran, 0 reused

⚠️ This run doesn't cover the PR fully:

  • 1 commit(s) pushed since this run — it tested older code

Everything below is what it did manage to say.

🔎 No blockers found — but the run above can't prove the PR is clean. 22 pre-existing/flaky tests filtered out. Re-run once the above is sorted out.

♻️ Settled after 2 auto re-run wave(s): #1 — early: Control Utility 1; #2 — 1 broken suite(s).

// Marshal eagerly: the heavy partition-map copy lands in the "Full message preparing" stage, and the
// message cached in FinishState is sent to late joiners as is (the send-path marshal-once turns no-op).
MessageMarshalling.marshal(msg, cctx.kernalContext(), null);
CommunicationMarshalling.marshal(msg, cctx.kernalContext(), null);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Refactoring this issue might set us to revert or rewrite this ticket again I'm afraid. We split marshalling type. This is the nase of this ticket

* goes to the coordinator by communication and comes back in the {@link FullMessage} by discovery, while a marshalled
* field caches its wire form for the second leg.
*/
public class DistributedProcessResultMarshallingTest extends GridCommonAbstractTest {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One Test for one @Test? Can we put it in some existing Test?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Let's keep it easy and separate.

@shishkovilja shishkovilja left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Compilation currently fails:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.15.0:compile (default-compile) on project ignite-core: Compilation failure: Compilation failure: 
[ERROR] /home/shish/IdeaProjects/ignite/modules/core/src/main/java/org/apache/ignite/internal/processors/continuous/ContinousRoutineLocalInfo.java:[24,34] cannot find symbol
[ERROR]   symbol:   class UseBinaryMarshaller
[ERROR]   location: package org.apache.ignite.internal
[ERROR] /home/shish/IdeaProjects/ignite/modules/core/src/main/java/org/apache/ignite/internal/processors/continuous/ContinousRoutineLocalInfo.java:[31,2] cannot find symbol
[ERROR]   symbol: class UseBinaryMarshaller

*
* @return the companion, or {@code null} when it is not generated and {@code required} is {@code false}.
*/
@SuppressWarnings("unchecked")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
@SuppressWarnings("unchecked")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

AbstractMessageFactoryProvider.java:94: warning: [unchecked] unchecked cast

Comment thread modules/core/src/main/java/org/apache/ignite/internal/util/ErrorMessage.java Outdated
… notes into javadoc

The test now captures the marshaller handed to the payload and asserts its type, instead of reading the first
bytes of the wire form. The note next to every pinned message moved into the class javadoc, and the marshaller
lookup of the test plugin provider says why a missing companion is normal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
anton-vinogradov and others added 6 commits August 13, 2026 19:58
The result goes to the coordinator by communication and comes back in the FullMessage by discovery, so the test
collects the marshaller of every leg and asserts that each one is the JDK marshaller.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Only two tests used it, and the cache-free overload serves them just as well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y path

The note claimed a discovery message is marshalled on a discovery thread and that a registration never finishes.
Both are true in the common case and imprecise in general: a joining node marshals on its start thread, the
cluster-wide round is either a class name mapping or a metadata version, and a type the cluster has already
accepted marshals without any round at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… pinned

The parent hands the marshaller of its transport to the nested message, and a pinned one has to ignore it. That is
how ErrorMessage travels inside cache responses today, and codegen cannot show it: the parent only passes the
marshaller on, while the companion of the nested message is resolved at run time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comparing the first bytes of a stream says little at the place it is read. Unmarshalling the wire form with the
JDK marshaller says the same thing directly: only that marshaller reads what it wrote.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…essage back

Counting marshal calls did not prove what the test claimed: discovery alone produces several of them, so the
check passed even if the communication leg disappeared. The test now records what the payload got before the
single node message went out, and requires both that leg and a later one.

The hand-written marshaller of the test message also lacked the half that the generator always emits: it
marshalled the nested message but never unmarshalled it back, so the nested error travelled as bytes and never
arrived as an object.

The captured marshallers are cleared before a test rather than after, so a failure in stopAllGrids cannot leak
them into the next one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@anton-vinogradov

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9281035 · 147 suites ran, 0 reused

No blockers — nothing in this run looks caused by this PR. 22 pre-existing/flaky tests filtered out.

Comment thread modules/core/src/main/java/org/apache/ignite/internal/util/ErrorMessage.java Outdated

@shishkovilja shishkovilja left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review: Message Marshalling Refactoring

Summary

A marshalling refactoring: the Marshaller is now threaded as an explicit parameter through MessageMarshaller.marshal/unmarshal and the message-marshalling helpers, instead of being stored/initialized inside message factories. The MessageMarshalling helper was split into CommunicationMarshalling (uses binary/schema-aware marshaller) and DiscoveryMarshalling (uses the JDK marshaller), and the @UseBinaryMarshaller annotation was renamed to @JdkMarshalled and moved from core to codegen.

Overall this is a well-reasoned, internally consistent refactor with correct transport routing and no critical defects. The separation of communication vs. discovery marshalling is a genuine improvement: it removes the risk of a discovery-ring thread deadlocking on a binary-marshaller class-name round.

Scope: 134 files, +967 / −611 lines (staged).

Findings

Suggestion

  1. No automated guard for the cross-transport invariant

    • Files: JdkMarshalled.java, CommunicationMarshalling.java:31, DiscoveryMarshalling.java:38-44
    • What's wrong: After this change, a non-@JdkMarshalled message with a marshaller-dependent blob gets binary on the communication leg and JDK on the discovery leg. Any both-transport message that's marshaller-dependent but not annotated @JdkMarshalled would break under cross-transport deserialization.
    • Why it matters: The six confirmed cases (SingleNodeMessage, ErrorMessage, StoredCacheData, BinaryMetadataVersionInfo, PartitionHashRecord, TransactionsHashRecord) are correctly pinned, but this invariant is only enforced by manual analysis.
    • Suggested fix: Add a test that enumerates registered messages reachable from both the discovery and communication legs and asserts any @Marshalled/MarshallableMessage member shared by both is @JdkMarshalled (or make it a codegen error).
  2. Static utility classes lost their private constructors

    • Files: MessageMarshalling.java:41, CommunicationMarshalling.java, DiscoveryMarshalling.java
    • What's wrong: The diff removed the existing private MessageMarshalling() {} and the new classes never added one, leaving implicit public default constructors.
    • Why it matters: This violates Ignite's convention for static-utility holders.
    • Suggested fix: Restore a /** */ private <Class>() { } in all three.

Nice to have

  1. Codegen prependPinnedMarshaller uses a substring heuristic

    • File: MessageMarshallerGenerator.java:1013-1022
    • What's wrong: The jdkMarsh declaration is emitted when no generated line contains "jdkMarsh". A field whose accessor happened to contain that literal could falsely suppress/trigger the declaration.
    • Why it matters: Fragile; today works for all generated companions but is not robust to future field names.
    • Suggested fix: Track a boolean "marsh var referenced" during codegen instead of re-scanning lines.
  2. Generated reference files are out of sync with the generator

    • Files: TestJdkMarshalledMessageMarshaller.java, TestJdkMarshalledChildMessageMarshaller.java
    • What's wrong: These end with a trailing \n, but MessageMarshallerGenerator does not emit one. The other updated resources in this diff had theirs stripped to match.
    • Why it matters: Compile-testing normalizes whitespace so tests pass, but the files are not byte-identical to generator output.
    • Suggested fix: Regenerate these via the actual MessageProcessor.
  3. MessagesPluginProvider.marshaller(...) swallows all failures

    • File: modules/core/src/test/.../spi/MessagesPluginProvider.java:62
    • What's wrong: The catch-all for loadMarshaller's wrapped RuntimeException returns null, hiding real generation bugs.
    • Why it matters: A genuine defect in marshaller generation would be silently hidden.
    • Suggested fix: Restrict swallowing to the "companion absent" case (e.g., inspect cause for ClassNotFoundException).
  4. JdkMarshalled Javadoc implies runtime behavior

    • File: JdkMarshalled.java:~26-30
    • What's wrong: With @Retention(CLASS) it's a compile-time hint consumed only by the generator, but the Javadoc reads like it does runtime work.
    • Suggested fix: Word it as a compile-time hint, e.g., "instructs the generated marshaller to pin the JDK marshaller".
  5. Unused marshaller resolved for @JdkMarshalled messages on the comm leg

    • File: CommunicationMarshalling.java
    • What's wrong: kctx.marshaller() (binary) is computed and passed in, but the generated @JdkMarshalled body ignores it and re-resolves jdkMarshaller().
    • Why it matters: One trivial final-field getter per message; cleanup only.
  6. ServiceSingleNodeDeploymentResult keeps a now-redundant Serializable marker

    • File: ServiceSingleNodeDeploymentResult.java
    • What's wrong: Implements Serializable only for legacy reasons while being a Message; harmless now that the annotation is gone.
    • Suggested fix: Optionally revisit.

Verdict

Approve — no critical issues; the refactor is correct and well-structured.

Most valuable follow-up: finding #1 (automating the cross-transport @JdkMarshalled invariant), which is currently only checked manually.


Review date: 2026-08-14

anton-vinogradov and others added 4 commits August 14, 2026 17:55
… drop a dead field

The notes now name Communication and Discovery the way the rest of the code does, and the one on
SingleNodeMessage no longer claims the FullMessage carries single node messages - it carries the results
collected from them. The test says what the process actually does, and CommunicationMarshalling says what a
schema-aware marshaller is.

hasMarshalled stopped being read when the generated companion lost its marshaller constructor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Possible compatibility issues. Please, check rolling upgrade cases

This PR modifies protected classes (with Order annotation).
Changes to these classes can break rolling upgrade compatibility.

Affected files:

  • modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/message/GenericValueMessage.java
  • modules/calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/message/QueryStartRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/managers/communication/GridIoUserMessage.java
  • modules/core/src/main/java/org/apache/ignite/internal/managers/eventstorage/EventsDataBagItem.java
  • modules/core/src/main/java/org/apache/ignite/internal/managers/eventstorage/GridEventStorageRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/managers/eventstorage/GridEventStorageResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheEntryPredicateAdapter.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheEvictionEntry.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/CacheInvokeDirectResult.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheReturn.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/StoredCacheData.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/binary/BinaryMetadataVersionInfo.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/GridCacheTtlUpdateRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/GridDistributedLockRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/GridDistributedLockResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/GridDistributedTxPrepareRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/GridNearUnlockRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtLockRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtLockResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtTxFinishResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtTxPrepareRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtTxPrepareResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/GridDhtUnlockRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/TransactionAttributesAwareRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/AtomicApplicationAttributesAwareRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicSingleUpdateRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridDhtAtomicUpdateResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridNearAtomicFullUpdateRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridNearAtomicSingleUpdateFilterRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridNearAtomicSingleUpdateInvokeRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridNearAtomicSingleUpdateRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/GridNearAtomicUpdateResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/NearCacheUpdates.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/atomic/UpdateErrors.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/preloader/GridDhtForceKeysRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/preloader/GridDhtForceKeysResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/dht/preloader/GridDhtPartitionSupplyMessage.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearGetRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearGetResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearSingleGetRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearSingleGetResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/distributed/near/GridNearTxPrepareResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/GridCacheQueryResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/continuous/CacheContinuousQueryEntry.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/query/continuous/CacheContinuousQueryHandler.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/transactions/IgniteTxEntry.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/transactions/IgniteTxKey.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/transactions/TxEntryValueHolder.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/transactions/TxLocksRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/transactions/TxLocksResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/verify/PartitionHashRecord.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/verify/TransactionsHashRecord.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/datastreamer/DataStreamerEntry.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/datastreamer/DataStreamerReceiverMessage.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/rest/handlers/task/GridTaskResultResponse.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/service/ServiceSingleNodeDeploymentResult.java
  • modules/core/src/main/java/org/apache/ignite/internal/util/ErrorMessage.java
  • modules/core/src/main/java/org/apache/ignite/internal/util/distributed/SingleNodeMessage.java
  • modules/core/src/test/java/org/apache/ignite/internal/codegen/MessageProcessorTest.java

@anton-vinogradov

anton-vinogradov commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

/runall


🚀 RunAll queuedbuild 9283013 · live progress & verdict: Ignite PR Checker. The verdict lands here when the run finishes.
🏁 Run finished — the verdict comment has the full story.

@anton-vinogradov

anton-vinogradov commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9283013 · 147 suites ran, 0 reused

No blockers — nothing in this run looks caused by this PR. 26 pre-existing/flaky tests filtered out.

♻️ Settled after 1 auto re-run wave(s): #1 — 2 watch + 2 broken suite(s).

@anton-vinogradov
anton-vinogradov merged commit 3c6463f into apache:master Aug 14, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants