Skip to content

IGNITE-28528 Revise marshalling of the node filter in StartRequestData - #13440

Open
anton-vinogradov wants to merge 25 commits into
apache:masterfrom
anton-vinogradov:ignite-deployment-descriptor
Open

IGNITE-28528 Revise marshalling of the node filter in StartRequestData#13440
anton-vinogradov wants to merge 25 commits into
apache:masterfrom
anton-vinogradov:ignite-deployment-descriptor

Conversation

@anton-vinogradov

@anton-vinogradov anton-vinogradov commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

The node filter in StartRequestData was marshalled by hand, and the receiver
had to build the deployment class loader itself before reading the filter.

Why the code looked like that

A message that carries peer-deployed classes said nothing about it. It held the
deployment as four or five flat fields, so every receiver rebuilt the deployment
from those fields and passed the resulting loader into the unmarshal call. Eight
places did exactly that. StartRequestData also repeated a "peer class loading
is off" branch, although GridDeploymentManager already covers that case: with
peer class loading off it looks the deployment up locally.

Change

  • DeploymentAware lets a message state that its classes come with a deployment,
    the same way CacheIdAware states the need for a cache object context.
  • GridDeploymentManager.classLoader(DeploymentAware) is now the single place
    that turns a carried deployment into a class loader.
  • The generated marshaller asks for that loader when the caller passes none, so
    receivers no longer resolve it by hand.
  • Messages carry GridDeploymentInfoBean as a whole instead of the flat fields.
    This changes the wire format.

DeploymentAware is independent of DeferredUnmarshalMessage, which says who
starts the read: a receiver that reports a missing deployment back to the sender
has to start the read itself, inside its own error handling.

Reading a discovery message off the reading thread

The socket-reading threads of both discovery SPIs unmarshalled every message
they read. A continuous-query start request cannot be read there: obtaining the
deployment of its node filter may block on a network request, and a missing user
class has to be tolerated so the acknowledgement is still sent. Both readers now
honour DeferredUnmarshalMessage, as GridIoManager already did, and the
continuous processor reads the request where that work is allowed.

StartRequestData is then left as plain data. Its marshal/unmarshal methods
are gone: the fields are marshalled by the generated marshaller, and preparing
the handler moved to the processor that builds and consumes the message.

Verified

MessageProcessorTest (a new case covers the resolved loader and keepBytes),
GridEventStorageCheckAllEventsSelfTest, GridEventStorageManagerSelfTest,
GridEventConsumeSelfTest, GridMessageListenSelfTest,
GridP2PContinuousDeploymentSelfTest, P2PClassLoadingFailureHandlingTest,
IgniteNoCustomEventsOnNodeStart, DataStreamerImplSelfTest,
GridP2PContinuousDeploymentClientDisconnectTest, and the continuous-query
suites CacheContinuousQueryOperationP2PTest,
CacheContinuousQueryOperationFromCallbackTest,
CacheContinuousQueryFailoverAtomicSelfTest,
GridCacheContinuousQueryNodesFilteringTest,
IgniteCacheContinuousQueryClientTest,
IgniteCacheContinuousQueryReconnectTest, ContinuousQueryBuffersCleanupTest.

🤖 Generated with Claude Code

anton-vinogradov and others added 6 commits August 6, 2026 01:43
The sender of the classes is already inside the descriptor: the class loader id carries
it, and the loader asserts as much. So getGlobalDeployment no longer asks for it, and
the dead nodeFilter parameter is gone with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GridEventStorageRequest kept the deployment as four flat fields, so it had nothing to
pass as a descriptor. It now carries GridDeploymentInfoBean, like the messages that
already do, and resolves through the descriptor.

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>
…lers

A message that carries classes deployed on another node now says so with
DeploymentAware, the same way CacheIdAware states the need for a cache
context. The generated marshaller asks the deployment manager for the
class loader, so a receiver no longer repeats that resolution by hand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
anton-vinogradov and others added 11 commits August 6, 2026 03:48
Keeping the serialized copy is now a property of the field, @Marshalled
keepBytes, instead of a property of the deferred-unmarshal marker: the
marker covers GridCacheMessage, so tying it there dropped the memory
saving for every cache message.

Restores the diagnostics of two error paths, adds a codegen test for the
resolved class loader and for keepBytes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Marking StartRequestData deployment-aware put the deployment resolution
into its generated unmarshal, and that unmarshal runs from the discovery
socket reader, where obtaining a deployment may block on a network
request. The message keeps its deployment as a plain field instead and
asks the deployment manager for the loader where blocking is allowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The socket-reading threads of both discovery SPIs unmarshalled every
message they read. A continuous-query start request cannot be read
there: obtaining the deployment of its node filter may block on a
network request, and a missing user class has to be tolerated so the
acknowledgement is still sent.

Both readers now honour DeferredUnmarshalMessage, as the communication
manager already did, and the continuous processor reads the request
where that work is allowed. StartRequestData is left as plain data: its
marshal/unmarshal methods are gone, the fields are marshalled by the
generated marshaller, and preparing the handler moved to the processor
that builds and consumes the message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The generated marshaller drops the serialized copy of a field once the
object is restored, which a message that travels the discovery ring
cannot afford: every node would pass on a copy of its own making. For
the handler that is not even equivalent - p2pUnmarshal unpacks its user
objects and nothing packs them back, so they would go on the wire
outside peer class loading.

The request stays plain data then: the fields keep their serialized
form, and the continuous processor restores the objects, asking the
deployment manager for the class loader of the node filter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Whether the serialized copy of a field may be dropped once the object is
restored is a property of the message, not of the field: a message read
and then sent on must reach every node in the form it left the sender.
ForwardedMessage states that, so the start request goes back on the
generated marshaller, and its handler keeps the bytes it arrived with -
p2pUnmarshal unpacks the user objects inside it and nothing packs them
back.

Reading it is deferred, as before: both discovery readers honour the
marker, and the continuous processor reads the message where obtaining a
deployment is allowed to block.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The handler survives being read and sent on: its writeExternal writes
the deployable wrapper whenever there is one, so what p2pUnmarshal
restores never reaches the wire. The node filter is simply marshalled
again, as it is on every other discovery message with a marshalled
field. Keeping the serialized copy was guarding against nothing.

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

# Conflicts:
#	modules/core/src/main/java/org/apache/ignite/internal/processors/datastreamer/DataStreamerRequest.java
Fixes the import order the strict checkstyle profile rejects, and drops
what earlier iterations left behind: an unused constant, an unused
overload, and a javadoc line describing a design that is no longer there.

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The internal code uses the Bean suffix nowhere else: what is left is
Spring, public API and JMX. This class is a Message carrying a
deployment descriptor inside other messages, like BinaryMetadataVersionInfo
or NodeEncryptionKeys, and its name now says so. It cannot simply drop
the suffix, since it implements the GridDeploymentInfo interface.

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

The generated resolution ran only when the caller passed no class loader,
but the convenience overloads pass the local one, so a caller using them
would read peer-deployed classes without peer class loading. A message
that carries its own deployment knows the loader better than its caller.

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

anton-vinogradov commented Aug 8, 2026

Copy link
Copy Markdown
Contributor Author

/runall


🎨 Checkstyle autofix: fixed 2 of 15 violation(s) in 2 file(s) — commit 7454525. 13 remain (javadoc/naming/wrapping need a human) — the Checkstyle suite may still fail.
🚀 RunAll queuedbuild 9266790 · live progress & verdict: Ignite PR Checker. The verdict lands here when the run finishes.
🏁 Run finished — analysing; the verdict comment follows.

@anton-vinogradov

anton-vinogradov commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

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

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

  • a newer run is still going — its unfinished suites can still fail

Everything below is what it did manage to say.

👀 6 test(s) started failing on this code — not proven blockers yet: too few runs of this revision to tell a break from a flake, so a re-run of the suite decides it.

  • PDS 10: org.apache.ignite.testsuites.IgnitePdsTestSuite10: org.apache.ignite.internal.processors.cache.distributed.dht.preloader.HistoricalRebalanceCheckpointTest.testDelayedToBackupsRequests1BackupMorePuts
  • Disk Page Compressions 3: org.apache.ignite.testsuites.IgnitePdsCompressionTestSuite3: org.apache.ignite.internal.processors.localtask.DurableBackgroundTasksProcessorSelfTest.testDontDeleteTaskIfItsRestart
  • Cache 19: org.apache.ignite.testsuites.IgniteCacheTestSuite17: org.apache.ignite.internal.processors.cache.transactions.TxPartitionCounterStateOnePrimaryTwoBackupsHistoryRebalanceTest.testPartialCommit_2TX_4
  • Client Nodes: org.apache.ignite.testsuites.IgniteClientReconnectTestSuite: org.apache.ignite.internal.IgniteClientReconnectAtomicsWithLostPartitionsTest.testAtomicSequenceAddAngGet
  • Client Nodes: org.apache.ignite.testsuites.IgniteClientReconnectTestSuite: org.apache.ignite.internal.IgniteClientReconnectAtomicsWithLostPartitionsTest.testAtomicStampedCompareAndSet
  • Client Nodes: org.apache.ignite.testsuites.IgniteClientReconnectTestSuite: org.apache.ignite.internal.IgniteClientReconnectAtomicsWithLostPartitionsTest.testAtomicLongGet

⚠️ No proven blocker yet — this is not an all-clear: see the tests above. 51 pre-existing/flaky filtered out.

Auto re-run #2 in progress — 2 blocker suite(s) re-queued (attempt 2/2), ≈ settled by 09:13 MSK. This comment updates when they settle.
Earlier re-runs: #1 — 3 blocker suite(s).

anton-vinogradov and others added 3 commits August 9, 2026 05:16
A discovery custom message is not carried as bytes: TcpDiscoveryCustomEventMessage
holds it as a nested message field, so the generated marshaller of the envelope
reads the whole tree on the socket-reading thread. There a missing user class is
swallowed with a warning, and the routine start never reports it back to the
node that started it.

The node filter and the handler therefore stay serialized on the message, and
the continuous processor restores them where the failure can be reported. The
deployment resolution is still a single call, which is what the ticket asks for.

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

anton-vinogradov commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Ignite PR Checker verdict · RunAll build 9267428 · 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. 25 pre-existing/flaky tests filtered out. Re-run once the above is sorted out.

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

@anton-vinogradov

anton-vinogradov commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

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

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

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

rsrcName,
clsName,
depInfo.userVersion(),
depInfo.classLoaderId().globalId(),

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.

It's confusing that depInfo.classLoaderId().globalId() is used as sndNodeId. Why is that?

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.

The two are the same node by construction, so this was not a shortcut - but you are right that nothing at the call site said so.

A class loader id carries the id of the node that created it, and GridDeploymentClassLoader asserts it in both places where such a loader is built:

assert nodeId.equals(clsLdrId.globalId());   // :184, constructor
assert nodeId.equals(ldrId.globalId());      // :324, register(nodeId, ldrId), for a participant

So a message describing a deployment cannot name a sender that disagrees with the loader it describes: the old code, which passed the sender separately, blew up on that assert if the two ever diverged.

I have made it readable instead of implied: GridDeploymentInfo#nodeId() now returns it, with the invariant written in its javadoc, and the call site reads depInfo.nodeId().

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.

I was wrong here, and your question is what led me to it - thanks.

The two nodes are not the same in general. GridCacheDeploymentManager states it directly:

assert sndId.equals(ldrId.globalId()) || participants != null;

and GridDeploymentManager#deploy hands out the deployment of the original owner when the class arrived by peer loading - the comment there calls it a nested execution:

// Check for nested execution. In that case, if task
// is available locally by name, then we should ignore class loader ID.
dep = checkDeployment(ldrStore.getDeployment(ldr.classLoaderId()), "perLoader");

So a node that received classes from another one passes them further as a participant of the same deployment, while being the sender itself.

And the receiving side needs the sender, not the owner: GridDeploymentPerVersionStore asks it for the classes, records it with addParticipant, and refuses the deployment when discovery().node(senderNodeId) == null. Deriving the node from the loader id would break exactly the case where the owner has left and the sender still holds the classes.

Fixed: the resolution takes the node as a parameter again. Since generated code has no way to know the sender, the DeploymentAware interface is gone with it, and the callers - which do know the sender - pass it in.


if (dep == null)
throw new IgniteDeploymentCheckedException("Failed to obtain deployment for class: " + clsName);
GridDeployment dep = ctx.deploy().globalDeployment(depInfo, clsName);

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.

Was the nodeId method parameter lost during refactoring, or is it no longer required and should be deleted?

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.

No longer required - and it turned out to be true well past this method.

The deployment is now resolved from the descriptor the message carries, so the node id has nothing to do here. Following it upwards, it had nothing to do anywhere else either: it went through GridContinuousHandler#p2pUnmarshal, its three implementations and the private helper in CacheContinuousQueryHandler, and every one of them only fed it to an assert nodeId != null.

Deleted along the whole chain, so the signature is p2pUnmarshal(GridKernalContext) now.

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.

Correction to my previous reply: the parameter is back.

The deployment resolution needs the sending node after all - it is not derivable from the loader id, see the other thread. So p2pUnmarshal(UUID nodeId, GridKernalContext ctx) keeps its signature, and the node is passed down to the resolution instead of only feeding an assert.

@petrov-mg

Copy link
Copy Markdown
Contributor

Generated code for GridEventStorageRequestMarshaller looks confusing:

     /** */
    @Override public void unmarshal(GridEventStorageRequest msg, GridKernalContext kctx, CacheObjectContext cacheObjCtx, ClassLoader clsLdr) throws IgniteCheckedException {
        CacheObjectContext ctx = cacheObjCtx;

        clsLdr = kctx.deploy().classLoader(msg);

Why is clsLdr overwritten here?

The deployment resolution no longer takes the sender node id, so the
parameter had nothing left to do: it travelled through the continuous
handler interface, three implementations and a private helper, used by
an assert alone.

Where the id is still needed, GridDeploymentInfo#nodeId now says where
it comes from, instead of reaching into the loader id at the call site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
return getGlobalDeployment(depInfo.deployMode(),
rsrcName,
clsName,
depInfo.userVersion(),

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.

Is depInfo guaranteed to be non-null here? In some places it seems that it can be null, for example in org.apache.ignite.internal.managers.communication.GridIoUserMessage#deploymentInfo.

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.

It cannot be null today, but only by an invariant that is nowhere stated - you are right not to trust it.

On the sending side both fields come from the same deployment, and the send fails outright when there is none:

dep = ctx.deploy().deploy(cls0, U.detectClassLoader(cls0));

if (dep == null)
    throw new IgniteDeploymentCheckedException(...);

depClsName = cls0.getName();

So deploymentClassName() != null implies deploymentInfo() != null, and the guard on the receiving side happens to check the first while the code dereferences the second.

Rather than document that, I made it structural: the resolution now accepts a null deployment and returns null for it, so nothing to resolve gives nothing back, and the caller reports it with the message it already has. The throwing overload turns the same case into its usual IgniteDeploymentCheckedException.

anton-vinogradov and others added 3 commits August 10, 2026 01:16
The sender and the owner of a class loader are not the same node in
general: a node that got classes by peer loading passes them on as a
participant, which GridCacheDeploymentManager states outright, and
GridDeploymentManager#deploy hands out the deployment of the original
owner for a nested execution. The receiving side needs the sender, not
the owner: that is who is asked for the classes, who is recorded as a
participant, and whose departure makes the deployment unusable.

Deriving it from the loader id was therefore wrong, and no generated code
can supply it, so DeploymentAware is gone and the resolution takes the
node as a parameter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A message may carry none - GridIoUserMessage does when peer class loading
is off - and the caller then relied on a second field being null in step.
Resolving nothing now yields nothing, so the caller reports it the way it
already does instead of depending on that.

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

github-actions Bot commented Aug 9, 2026

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/core/src/main/java/org/apache/ignite/internal/managers/communication/GridIoUserMessage.java
  • modules/core/src/main/java/org/apache/ignite/internal/managers/deployment/GridDeploymentInfoBean.java
  • modules/core/src/main/java/org/apache/ignite/internal/managers/deployment/GridDeploymentInfoMessage.java
  • modules/core/src/main/java/org/apache/ignite/internal/managers/eventstorage/GridEventStorageRequest.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/cache/GridCacheMessage.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/continuous/StartRequestData.java
  • modules/core/src/main/java/org/apache/ignite/internal/processors/datastreamer/DataStreamerRequest.java

@anton-vinogradov

anton-vinogradov commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Generated code for GridEventStorageRequestMarshaller looks confusing:

@Override public void unmarshal(GridEventStorageRequest msg, GridKernalContext kctx, CacheObjectContext cacheObjCtx, ClassLoader clsLdr) throws IgniteCheckedException {
    CacheObjectContext ctx = cacheObjCtx;

    clsLdr = kctx.deploy().classLoader(msg);

Why is clsLdr overwritten here?

Good catch, and it was a symptom of the same thing your inline question uncovered - that generated code cannot pick the class loader for a message.

Overwriting the parameter was deliberate but wrong. It came from DeploymentAware: a message declared that it carries its own deployment, and the generated marshaller resolved the loader from it. I made the assignment unconditional because the convenience overloads pass a loader rather than null:

default void unmarshal(M msg, GridKernalContext kctx) {
    unmarshal(msg, kctx, null, U.resolveClassLoader(kctx.config()));
}

so a caller using them would silently read peer-deployed classes with the local loader. Ignoring the argument fixed that symptom and hid a worse problem: the resolution needs the sending node, which the generated code has no way to know, and the sender is not always the owner of the class loader.

So the whole thing is gone now. DeploymentAware, the resolution in the generator and its test are removed - modules/codegen is no longer part of this PR at all - and the resolution takes the node as a parameter, passed by the callers that know it.

What is left is what the ticket asked for: the deployment travels as one descriptor instead of four flat fields, and the eight places that used to assemble it by hand now make a single call. The diff went from 35 files to 25, and from +577/-497 to +248/-475.

Since the change is materially different from what the last RunAll saw, a fresh run is needed.

@anton-vinogradov

anton-vinogradov commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

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

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

  • 1 suite(s) ran far fewer tests than the same suites on master

Everything below is what it did manage to say.

🔍 1 suite(s) ran fewer tests than on master (tests that never ran can't fail):

  • Platform C++ CMake (Linux Clang): 728 tests vs 1047 on master (−30%)

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

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

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.

2 participants