[ 
https://issues.apache.org/jira/browse/IGNITE-28935?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Anton Vinogradov updated IGNITE-28935:
--------------------------------------
    Description: 
h3. Goal

Make it clear which marshaller is used for each message field. Today this is 
set in 60 places. It should be set in none.

h3. Why

An object field of a message becomes bytes either by {{JdkMarshaller}} or by 
{{BinaryMarshaller}}. The marshaller is chosen per message class: the 
{{@UseBinaryMarshaller}} annotation plus the call in {{CoreMessagesProvider}}.

Numbers from master, counted in the generated companions:
* core registers 297 messages: 58 binary, 239 jdk;
* 195 {{*Marshaller}} companions are generated, but only 30 keep a 
{{Marshaller}};
* the choice changes the result for 24 classes only;
* there are 60 annotations. Only 13 of them reach a companion that takes a 
{{Marshaller}} at all, and of those
only 11 change any byte. The other 47 are noise.

How the numbers are counted. The annotation decides one thing only: which 
{{Marshaller}} instance is passed to the
constructor of the generated companion. The generator emits a constructor with 
a {{Marshaller}} only when the class
has something to marshal ({{marshallable || hasMarshalled}}); for everything 
else the companion has a no-arg
constructor and {{register(...)}} drops the marshaller it was given
({{ctor.getParameterCount() == 0 ? newInstance() : newInstance(marsh)}}). So:
{code}
git grep -l "@UseBinaryMarshaller" master -- '*.java' | wc -l          # 60 (58 
core, 2 calcite)
grep -rl "private final Marshaller marshaller" \
    modules/core/target/generated-sources/codegen | wc -l              # 30 of 
195 companions
{code}
The intersection of the two sets is 13. Within those 13:
* generated code calls {{U.marshal(marshaller, ...)}} - 8: 
{{BinaryMetadataVersionInfo}}, {{GridCacheQueryRequest}},
{{GridCacheQueryResponse}}, {{GridEventStorageMessage}}, 
{{GridJobSiblingsResponse}},
{{GridNearAtomicSingleUpdateInvokeRequest}}, {{GridTaskResultResponse}}, 
{{IgniteTxEntry}};
* only the {{msg.marshal(marshaller)}} hook, and the hand-written body does use 
the marshaller - 3:
{{GridDhtAtomicUpdateRequest}} (dead code, removed by IGNITE-28936), 
{{GridNearAtomicFullUpdateRequest}}, {{GridTopicMessage}};
* only the hook, and the marshaller is not used at all - 2: 
{{GridCacheEntryInfo}} (TTL recalculation),
{{CacheContinuousQueryEntry}} (data reset).

The annotation is meant to say "this message carries user classes", but that is 
not what it does. Here is the
complete list of fields where user data travels with jdk today:

|| class || fields ||
| {{TcpDiscoveryNode}} | {{consistentId}}, {{attrs}} |
| {{TcpDiscoveryNodeAddFinishedMessage}} | {{clientNodeAttrs}} |
| {{StoredCacheData}} | {{ccfg}}, {{qryEntities}} |
| {{DynamicCacheChangeRequest}} | {{startCfg}}, {{nearCacheCfg}}, {{schema}} |
| {{ChangeGlobalStateMessage}} | {{baselineTopology}} |
| {{QueryEntityMessage}}, {{QueryEntityExMessage}} | {{dfltFieldValues}} |
| {{LazyServiceConfigurationMessage}} | {{affKey}} |
| {{PluginsDataBagItem}} | {{data}} |
| {{SerializableDataBagItemWrapper}} | {{data}} |
| {{PartitionHashRecord}} | {{consistentId}}, {{updateCntr}} |
| {{TransactionsHashRecord}} | {{locConsistentId}}, {{rmtConsistentId}} |
| {{ErrorMessage}} | {{err}} - a {{Throwable}}, user classes included |

The same jdk marshaller also carries user classes to disk, outside messages: 
{{cache_data.dat}}, the metastore and
snapshots keep a {{CacheConfiguration}} with its {{NodeFilter}}, 
{{ExpiryPolicyFactory}}, {{CacheStoreFactory}},
{{Interceptor}} and {{AffinityFunction}} inside.

The rule that actually holds is about *class registration*, not about user 
data. Before binary can write a type it must map class name to type id for the 
whole cluster: {{MarshallerContextImpl#registerClassName}} -> 
{{proposeMapping}} -> {{fut.get()}}, which waits for discovery. Ignite classes 
avoid that call because 2276 names are pre-accepted from 
{{META-INF/classnames.properties}} - but that list is not closed under nesting. 
Marshalling a plain {{CacheConfiguration}} needs 9 types, and two of them are 
missing from the list:
* {{javax.cache.configuration.FactoryBuilder$SingletonFactory}}
* {{javax.cache.expiry.EternalExpiryPolicy}}

They come from the default expiry policy, with no user class involved. So a 
discovery message that carries a cache configuration would trigger a 
cluster-wide registration from inside the discovery thread. 
{{failIfUnregistered = true}} does not help: it turns the deadlock into an 
{{UnregisteredClassException}}, and it fires for internal types as well.

So "discovery uses jdk" is not a property of the binary format. It is a 
consequence of where the marshalling happens and of what is pre-registered. The 
call site knows both; the message class knows neither.

Binding the marshaller to the class also produces three defects:

# The annotation is not {{@Inherited}}, but the generator builds a companion 
from all fields, including inherited ones. Nine parent/child pairs are 
registered in different groups. Checked with an experiment: add a blob field to 
{{GridDistributedTxPrepareRequest}} (binary), and the generated 
{{GridNearTxPrepareRequestMarshaller}} (jdk) marshals the same field with jdk. 
No such field exists today, so this is a trap, not a live bug.
# A nested message overrides the outer one, because the marshaller is taken by 
{{directType}}. {{ErrorMessage}} (jdk) travels inside binary messages, and 
{{BinaryMetadataVersionInfo}} (binary) travels inside the jdk discovery data 
bag. The same {{BinaryMetadata}} in {{MetadataUpdateProposedMessage}} uses jdk.
# {{MarshallableMessage}} means two things at once: "call me before send" and 
"I need a marshaller". 8 of its 21 implementations use neither the marshaller 
nor the class loader.

h3. Measurements

Round trip marshal+unmarshal, 5000 iterations after 2000 warm-up ones, JDK 17, 
local marshaller context.
Size in bytes / time in microseconds.

|| object || jdk || optimized || binary, type registered || binary, class name 
on the wire ||
| node attributes, Map<String,Object> x50 | 2725 / 30.0 | 2426 / 11.3 | 2423 / 
5.2 | 2423 / 5.0 |
| CacheConfiguration | 4539 / 39.4 | 1011 / 15.8 | 857 / 24.0 | 1382 / 160.2 |
| QueryEntity | 1144 / 9.9 | 345 / 2.9 | 332 / 2.4 | 454 / 35.7 |
| IgniteCheckedException | 709 / 6.7 | 710 / 6.4 | 715 / 7.4 | 715 / 9.2 |

What this says:
* binary is the fastest and the smallest option *when the types are already 
registered*;
* the exception is the same everywhere, because binary hands every 
{{Throwable}} to {{OptimizedMarshaller}}:
  {{BinaryUtils#isCustomJavaSerialization}} walks the whole class hierarchy, so 
any {{Externalizable}} class or a
  class with {{writeObject}}/{{readObject}} takes that path;
* writing the class name instead of a type id costs both size and a lot of time 
- the descriptor of an
  unregistered type is not cached and is rebuilt by reflection on every call.

h3. Plan

Three subtasks change the bytes on the wire. They should be done *first*, while 
2.19 is not released yet. After the release the same changes would break 
rolling upgrade and would need a feature flag plus support for both formats.

*Do first - these change the wire format:*

# IGNITE-28936 - *remove dead marshalling in {{GridDhtAtomicUpdateRequest}}.* 
The flag is always {{false}}, so this code never runs. Result: 4 fields less on 
the wire.
# IGNITE-28939 - *turn blob fields of internal types into normal message 
fields* ({{BinaryMetadata}}, {{BaselineTopology}}, job siblings, query 
entities). Result: 24 blob fields become about 19, and the {{BinaryMetadata}} 
mismatch is gone.
# IGNITE-28940 - *pass the marshaller from the call site.* This is the main 
change. Communication passes binary, discovery passes jdk. Four classes change 
their format on purpose. Result: no annotations left, and a wrong marshaller 
becomes impossible.

*Can be done any time - no wire format change:*

# IGNITE-28937 - *one registration form in {{CoreMessagesProvider}}.* 
{{withSchema}} and {{withNoSchema}} are the same code now. Result: the 
marshaller is written in one place, generated code does not change.
# IGNITE-28938 - *split {{MarshallableMessage}}.* 8 of 21 classes only need a 
callback. Result: they stop getting a marshaller. Small task: if it is done 
before IGNITE-28940, that one becomes smaller.
# IGNITE-28941 - *add a test for the discovery path.* Result: the rule is 
checked by CI, not kept in mind.
# IGNITE-28942 - *research: can binary replace jdk entirely.* No production 
change expected; see the section below.

h3. Considered and not planned: dropping JdkMarshaller

Binary can be self-contained. When a type is not registered it writes 
{{UNREGISTERED_TYPE_ID}} plus the class name ({{BinaryWriterExImpl#preWrite}}), 
and with a full footer the schema travels inside the object. Checked: an object 
written by one context is read by a completely empty one, cycles included, 84 
bytes against 121 for jdk. With the default compact footer the same object 
fails to read - "Cannot find metadata for object with compact footer" - because 
the field layout still comes from the metadata that discovery distributes.

So the format can replace jdk, but the move is not worth it today:
* {{compactFooter}} is a node-wide setting, a per-call full footer mode would 
have to be added;
* the unregistered path is 6.7x slower until descriptors are cached;
* on-disk formats (cache configurations, metastore, snapshots, dumps) are 
written with jdk and read by offline tools, so this becomes a data migration;
* {{OptimizedMarshaller}} stays anyway - binary delegates every 
{{Externalizable}} type to it.

That is three implementations turning into two, at the price of a new format 
mode and a migration. See IGNITE-28942 for the details and for the open 
questions.

h3. On-disk data

The plan does not touch anything that is already stored. Disk paths do not go 
through {{MessageMarshalling}}:
cache configurations and the metastore are written with jdk, binary metadata 
through its own {{Externalizable}}
methods, and the direct message protocol ({{MessageSerializer}}) is used for 
the network only - never for files.
Wire companions in classes that are both {{Serializable}} and {{Message}} are 
{{transient}}, so the two formats do
not interfere. IGNITE-28939 is the only subtask that touches classes living on 
disk; it carries the rules that keep
the stored format unchanged. No migration is required by any subtask.

h3. Expected result

|| || now || after ||
| places that choose the marshaller | 60 annotations | 0 |
| companions that keep a {{Marshaller}} | 30 | about 12 |
| classes that get a marshaller and never use it | 6 | 0 |
| blob fields of internal types | 24 | about 19 |
| wrong marshaller from inheritance or nesting | possible | not possible |

  was:
h3. Goal

Make it clear which marshaller is used for each message field. Today this is 
set in 60 places. It should be set in none.

h3. Why

An object field of a message becomes bytes either by {{JdkMarshaller}} or by 
{{BinaryMarshaller}}. The marshaller is chosen per message class: the 
{{@UseBinaryMarshaller}} annotation plus the call in {{CoreMessagesProvider}}.

Numbers from master, counted in the generated companions:
* core registers 297 messages: 58 binary, 239 jdk;
* 195 {{*Marshaller}} companions are generated, but only 30 keep a 
{{Marshaller}};
* the choice changes the result for 24 classes only;
* there are 60 annotations, and only 13 of them matter. The other 47 do nothing.

The annotation is meant to say "this message carries user classes", but that is 
not what it does. User data goes with jdk in {{TcpDiscoveryNode#attrs}}, 
{{LazyServiceConfigurationMessage#affKey}}, {{StoredCacheData#ccfg}} and 
{{QueryEntityMessage#dfltFieldValues}}.

The rule that actually holds is about *class registration*, not about user 
data. Before binary can write a type it must map class name to type id for the 
whole cluster: {{MarshallerContextImpl#registerClassName}} -> 
{{proposeMapping}} -> {{fut.get()}}, which waits for discovery. Ignite classes 
avoid that call because 2276 names are pre-accepted from 
{{META-INF/classnames.properties}} - but that list is not closed under nesting. 
Marshalling a plain {{CacheConfiguration}} needs 9 types, and two of them are 
missing from the list:
* {{javax.cache.configuration.FactoryBuilder$SingletonFactory}}
* {{javax.cache.expiry.EternalExpiryPolicy}}

They come from the default expiry policy, with no user class involved. So a 
discovery message that carries a cache configuration would trigger a 
cluster-wide registration from inside the discovery thread. 
{{failIfUnregistered = true}} does not help: it turns the deadlock into an 
{{UnregisteredClassException}}, and it fires for internal types as well.

So "discovery uses jdk" is not a property of the binary format. It is a 
consequence of where the marshalling happens and of what is pre-registered. The 
call site knows both; the message class knows neither.

Binding the marshaller to the class also produces three defects:

# The annotation is not {{@Inherited}}, but the generator builds a companion 
from all fields, including inherited ones. Nine parent/child pairs are 
registered in different groups. Checked with an experiment: add a blob field to 
{{GridDistributedTxPrepareRequest}} (binary), and the generated 
{{GridNearTxPrepareRequestMarshaller}} (jdk) marshals the same field with jdk. 
No such field exists today, so this is a trap, not a live bug.
# A nested message overrides the outer one, because the marshaller is taken by 
{{directType}}. {{ErrorMessage}} (jdk) travels inside binary messages, and 
{{BinaryMetadataVersionInfo}} (binary) travels inside the jdk discovery data 
bag. The same {{BinaryMetadata}} in {{MetadataUpdateProposedMessage}} uses jdk.
# {{MarshallableMessage}} means two things at once: "call me before send" and 
"I need a marshaller". 8 of its 21 implementations use neither the marshaller 
nor the class loader.

h3. Measurements

Round trip marshal+unmarshal, 5000 iterations after 2000 warm-up ones, JDK 17, 
local marshaller context.
Size in bytes / time in microseconds.

|| object || jdk || optimized || binary, type registered || binary, class name 
on the wire ||
| node attributes, Map<String,Object> x50 | 2725 / 30.0 | 2426 / 11.3 | 2423 / 
5.2 | 2423 / 5.0 |
| CacheConfiguration | 4539 / 39.4 | 1011 / 15.8 | 857 / 24.0 | 1382 / 160.2 |
| QueryEntity | 1144 / 9.9 | 345 / 2.9 | 332 / 2.4 | 454 / 35.7 |
| IgniteCheckedException | 709 / 6.7 | 710 / 6.4 | 715 / 7.4 | 715 / 9.2 |

What this says:
* binary is the fastest and the smallest option *when the types are already 
registered*;
* the exception is the same everywhere, because binary hands every 
{{Throwable}} to {{OptimizedMarshaller}}:
  {{BinaryUtils#isCustomJavaSerialization}} walks the whole class hierarchy, so 
any {{Externalizable}} class or a
  class with {{writeObject}}/{{readObject}} takes that path;
* writing the class name instead of a type id costs both size and a lot of time 
- the descriptor of an
  unregistered type is not cached and is rebuilt by reflection on every call.

h3. Plan

Three subtasks change the bytes on the wire. They should be done *first*, while 
2.19 is not released yet. After the release the same changes would break 
rolling upgrade and would need a feature flag plus support for both formats.

*Do first - these change the wire format:*

# IGNITE-28936 - *remove dead marshalling in {{GridDhtAtomicUpdateRequest}}.* 
The flag is always {{false}}, so this code never runs. Result: 4 fields less on 
the wire.
# IGNITE-28939 - *turn blob fields of internal types into normal message 
fields* ({{BinaryMetadata}}, {{BaselineTopology}}, job siblings, query 
entities). Result: 24 blob fields become about 19, and the {{BinaryMetadata}} 
mismatch is gone.
# IGNITE-28940 - *pass the marshaller from the call site.* This is the main 
change. Communication passes binary, discovery passes jdk. Four classes change 
their format on purpose. Result: no annotations left, and a wrong marshaller 
becomes impossible.

*Can be done any time - no wire format change:*

# IGNITE-28937 - *one registration form in {{CoreMessagesProvider}}.* 
{{withSchema}} and {{withNoSchema}} are the same code now. Result: the 
marshaller is written in one place, generated code does not change.
# IGNITE-28938 - *split {{MarshallableMessage}}.* 8 of 21 classes only need a 
callback. Result: they stop getting a marshaller. Small task: if it is done 
before IGNITE-28940, that one becomes smaller.
# IGNITE-28941 - *add a test for the discovery path.* Result: the rule is 
checked by CI, not kept in mind.
# IGNITE-28942 - *research: can binary replace jdk entirely.* No production 
change expected; see the section below.

h3. Considered and not planned: dropping JdkMarshaller

Binary can be self-contained. When a type is not registered it writes 
{{UNREGISTERED_TYPE_ID}} plus the class name ({{BinaryWriterExImpl#preWrite}}), 
and with a full footer the schema travels inside the object. Checked: an object 
written by one context is read by a completely empty one, cycles included, 84 
bytes against 121 for jdk. With the default compact footer the same object 
fails to read - "Cannot find metadata for object with compact footer" - because 
the field layout still comes from the metadata that discovery distributes.

So the format can replace jdk, but the move is not worth it today:
* {{compactFooter}} is a node-wide setting, a per-call full footer mode would 
have to be added;
* the unregistered path is 6.7x slower until descriptors are cached;
* on-disk formats (cache configurations, metastore, snapshots, dumps) are 
written with jdk and read by offline tools, so this becomes a data migration;
* {{OptimizedMarshaller}} stays anyway - binary delegates every 
{{Externalizable}} type to it.

That is three implementations turning into two, at the price of a new format 
mode and a migration. See IGNITE-28942 for the details and for the open 
questions.

h3. On-disk data

The plan does not touch anything that is already stored. Disk paths do not go 
through {{MessageMarshalling}}:
cache configurations and the metastore are written with jdk, binary metadata 
through its own {{Externalizable}}
methods, and the direct message protocol ({{MessageSerializer}}) is used for 
the network only - never for files.
Wire companions in classes that are both {{Serializable}} and {{Message}} are 
{{transient}}, so the two formats do
not interfere. IGNITE-28939 is the only subtask that touches classes living on 
disk; it carries the rules that keep
the stored format unchanged. No migration is required by any subtask.

h3. Expected result

|| || now || after ||
| places that choose the marshaller | 60 annotations | 0 |
| companions that keep a {{Marshaller}} | 30 | about 12 |
| classes that get a marshaller and never use it | 6 | 0 |
| blob fields of internal types | 24 | about 19 |
| wrong marshaller from inheritance or nesting | possible | not possible |


> Simplify how the marshaller is chosen for message fields
> --------------------------------------------------------
>
>                 Key: IGNITE-28935
>                 URL: https://issues.apache.org/jira/browse/IGNITE-28935
>             Project: Ignite
>          Issue Type: Task
>          Components: messaging
>            Reporter: Anton Vinogradov
>            Assignee: Anton Vinogradov
>            Priority: Major
>             Fix For: 2.19
>
>
> h3. Goal
> Make it clear which marshaller is used for each message field. Today this is 
> set in 60 places. It should be set in none.
> h3. Why
> An object field of a message becomes bytes either by {{JdkMarshaller}} or by 
> {{BinaryMarshaller}}. The marshaller is chosen per message class: the 
> {{@UseBinaryMarshaller}} annotation plus the call in {{CoreMessagesProvider}}.
> Numbers from master, counted in the generated companions:
> * core registers 297 messages: 58 binary, 239 jdk;
> * 195 {{*Marshaller}} companions are generated, but only 30 keep a 
> {{Marshaller}};
> * the choice changes the result for 24 classes only;
> * there are 60 annotations. Only 13 of them reach a companion that takes a 
> {{Marshaller}} at all, and of those
> only 11 change any byte. The other 47 are noise.
> How the numbers are counted. The annotation decides one thing only: which 
> {{Marshaller}} instance is passed to the
> constructor of the generated companion. The generator emits a constructor 
> with a {{Marshaller}} only when the class
> has something to marshal ({{marshallable || hasMarshalled}}); for everything 
> else the companion has a no-arg
> constructor and {{register(...)}} drops the marshaller it was given
> ({{ctor.getParameterCount() == 0 ? newInstance() : newInstance(marsh)}}). So:
> {code}
> git grep -l "@UseBinaryMarshaller" master -- '*.java' | wc -l          # 60 
> (58 core, 2 calcite)
> grep -rl "private final Marshaller marshaller" \
>     modules/core/target/generated-sources/codegen | wc -l              # 30 
> of 195 companions
> {code}
> The intersection of the two sets is 13. Within those 13:
> * generated code calls {{U.marshal(marshaller, ...)}} - 8: 
> {{BinaryMetadataVersionInfo}}, {{GridCacheQueryRequest}},
> {{GridCacheQueryResponse}}, {{GridEventStorageMessage}}, 
> {{GridJobSiblingsResponse}},
> {{GridNearAtomicSingleUpdateInvokeRequest}}, {{GridTaskResultResponse}}, 
> {{IgniteTxEntry}};
> * only the {{msg.marshal(marshaller)}} hook, and the hand-written body does 
> use the marshaller - 3:
> {{GridDhtAtomicUpdateRequest}} (dead code, removed by IGNITE-28936), 
> {{GridNearAtomicFullUpdateRequest}}, {{GridTopicMessage}};
> * only the hook, and the marshaller is not used at all - 2: 
> {{GridCacheEntryInfo}} (TTL recalculation),
> {{CacheContinuousQueryEntry}} (data reset).
> The annotation is meant to say "this message carries user classes", but that 
> is not what it does. Here is the
> complete list of fields where user data travels with jdk today:
> || class || fields ||
> | {{TcpDiscoveryNode}} | {{consistentId}}, {{attrs}} |
> | {{TcpDiscoveryNodeAddFinishedMessage}} | {{clientNodeAttrs}} |
> | {{StoredCacheData}} | {{ccfg}}, {{qryEntities}} |
> | {{DynamicCacheChangeRequest}} | {{startCfg}}, {{nearCacheCfg}}, {{schema}} |
> | {{ChangeGlobalStateMessage}} | {{baselineTopology}} |
> | {{QueryEntityMessage}}, {{QueryEntityExMessage}} | {{dfltFieldValues}} |
> | {{LazyServiceConfigurationMessage}} | {{affKey}} |
> | {{PluginsDataBagItem}} | {{data}} |
> | {{SerializableDataBagItemWrapper}} | {{data}} |
> | {{PartitionHashRecord}} | {{consistentId}}, {{updateCntr}} |
> | {{TransactionsHashRecord}} | {{locConsistentId}}, {{rmtConsistentId}} |
> | {{ErrorMessage}} | {{err}} - a {{Throwable}}, user classes included |
> The same jdk marshaller also carries user classes to disk, outside messages: 
> {{cache_data.dat}}, the metastore and
> snapshots keep a {{CacheConfiguration}} with its {{NodeFilter}}, 
> {{ExpiryPolicyFactory}}, {{CacheStoreFactory}},
> {{Interceptor}} and {{AffinityFunction}} inside.
> The rule that actually holds is about *class registration*, not about user 
> data. Before binary can write a type it must map class name to type id for 
> the whole cluster: {{MarshallerContextImpl#registerClassName}} -> 
> {{proposeMapping}} -> {{fut.get()}}, which waits for discovery. Ignite 
> classes avoid that call because 2276 names are pre-accepted from 
> {{META-INF/classnames.properties}} - but that list is not closed under 
> nesting. Marshalling a plain {{CacheConfiguration}} needs 9 types, and two of 
> them are missing from the list:
> * {{javax.cache.configuration.FactoryBuilder$SingletonFactory}}
> * {{javax.cache.expiry.EternalExpiryPolicy}}
> They come from the default expiry policy, with no user class involved. So a 
> discovery message that carries a cache configuration would trigger a 
> cluster-wide registration from inside the discovery thread. 
> {{failIfUnregistered = true}} does not help: it turns the deadlock into an 
> {{UnregisteredClassException}}, and it fires for internal types as well.
> So "discovery uses jdk" is not a property of the binary format. It is a 
> consequence of where the marshalling happens and of what is pre-registered. 
> The call site knows both; the message class knows neither.
> Binding the marshaller to the class also produces three defects:
> # The annotation is not {{@Inherited}}, but the generator builds a companion 
> from all fields, including inherited ones. Nine parent/child pairs are 
> registered in different groups. Checked with an experiment: add a blob field 
> to {{GridDistributedTxPrepareRequest}} (binary), and the generated 
> {{GridNearTxPrepareRequestMarshaller}} (jdk) marshals the same field with 
> jdk. No such field exists today, so this is a trap, not a live bug.
> # A nested message overrides the outer one, because the marshaller is taken 
> by {{directType}}. {{ErrorMessage}} (jdk) travels inside binary messages, and 
> {{BinaryMetadataVersionInfo}} (binary) travels inside the jdk discovery data 
> bag. The same {{BinaryMetadata}} in {{MetadataUpdateProposedMessage}} uses 
> jdk.
> # {{MarshallableMessage}} means two things at once: "call me before send" and 
> "I need a marshaller". 8 of its 21 implementations use neither the marshaller 
> nor the class loader.
> h3. Measurements
> Round trip marshal+unmarshal, 5000 iterations after 2000 warm-up ones, JDK 
> 17, local marshaller context.
> Size in bytes / time in microseconds.
> || object || jdk || optimized || binary, type registered || binary, class 
> name on the wire ||
> | node attributes, Map<String,Object> x50 | 2725 / 30.0 | 2426 / 11.3 | 2423 
> / 5.2 | 2423 / 5.0 |
> | CacheConfiguration | 4539 / 39.4 | 1011 / 15.8 | 857 / 24.0 | 1382 / 160.2 |
> | QueryEntity | 1144 / 9.9 | 345 / 2.9 | 332 / 2.4 | 454 / 35.7 |
> | IgniteCheckedException | 709 / 6.7 | 710 / 6.4 | 715 / 7.4 | 715 / 9.2 |
> What this says:
> * binary is the fastest and the smallest option *when the types are already 
> registered*;
> * the exception is the same everywhere, because binary hands every 
> {{Throwable}} to {{OptimizedMarshaller}}:
>   {{BinaryUtils#isCustomJavaSerialization}} walks the whole class hierarchy, 
> so any {{Externalizable}} class or a
>   class with {{writeObject}}/{{readObject}} takes that path;
> * writing the class name instead of a type id costs both size and a lot of 
> time - the descriptor of an
>   unregistered type is not cached and is rebuilt by reflection on every call.
> h3. Plan
> Three subtasks change the bytes on the wire. They should be done *first*, 
> while 2.19 is not released yet. After the release the same changes would 
> break rolling upgrade and would need a feature flag plus support for both 
> formats.
> *Do first - these change the wire format:*
> # IGNITE-28936 - *remove dead marshalling in {{GridDhtAtomicUpdateRequest}}.* 
> The flag is always {{false}}, so this code never runs. Result: 4 fields less 
> on the wire.
> # IGNITE-28939 - *turn blob fields of internal types into normal message 
> fields* ({{BinaryMetadata}}, {{BaselineTopology}}, job siblings, query 
> entities). Result: 24 blob fields become about 19, and the {{BinaryMetadata}} 
> mismatch is gone.
> # IGNITE-28940 - *pass the marshaller from the call site.* This is the main 
> change. Communication passes binary, discovery passes jdk. Four classes 
> change their format on purpose. Result: no annotations left, and a wrong 
> marshaller becomes impossible.
> *Can be done any time - no wire format change:*
> # IGNITE-28937 - *one registration form in {{CoreMessagesProvider}}.* 
> {{withSchema}} and {{withNoSchema}} are the same code now. Result: the 
> marshaller is written in one place, generated code does not change.
> # IGNITE-28938 - *split {{MarshallableMessage}}.* 8 of 21 classes only need a 
> callback. Result: they stop getting a marshaller. Small task: if it is done 
> before IGNITE-28940, that one becomes smaller.
> # IGNITE-28941 - *add a test for the discovery path.* Result: the rule is 
> checked by CI, not kept in mind.
> # IGNITE-28942 - *research: can binary replace jdk entirely.* No production 
> change expected; see the section below.
> h3. Considered and not planned: dropping JdkMarshaller
> Binary can be self-contained. When a type is not registered it writes 
> {{UNREGISTERED_TYPE_ID}} plus the class name 
> ({{BinaryWriterExImpl#preWrite}}), and with a full footer the schema travels 
> inside the object. Checked: an object written by one context is read by a 
> completely empty one, cycles included, 84 bytes against 121 for jdk. With the 
> default compact footer the same object fails to read - "Cannot find metadata 
> for object with compact footer" - because the field layout still comes from 
> the metadata that discovery distributes.
> So the format can replace jdk, but the move is not worth it today:
> * {{compactFooter}} is a node-wide setting, a per-call full footer mode would 
> have to be added;
> * the unregistered path is 6.7x slower until descriptors are cached;
> * on-disk formats (cache configurations, metastore, snapshots, dumps) are 
> written with jdk and read by offline tools, so this becomes a data migration;
> * {{OptimizedMarshaller}} stays anyway - binary delegates every 
> {{Externalizable}} type to it.
> That is three implementations turning into two, at the price of a new format 
> mode and a migration. See IGNITE-28942 for the details and for the open 
> questions.
> h3. On-disk data
> The plan does not touch anything that is already stored. Disk paths do not go 
> through {{MessageMarshalling}}:
> cache configurations and the metastore are written with jdk, binary metadata 
> through its own {{Externalizable}}
> methods, and the direct message protocol ({{MessageSerializer}}) is used for 
> the network only - never for files.
> Wire companions in classes that are both {{Serializable}} and {{Message}} are 
> {{transient}}, so the two formats do
> not interfere. IGNITE-28939 is the only subtask that touches classes living 
> on disk; it carries the rules that keep
> the stored format unchanged. No migration is required by any subtask.
> h3. Expected result
> || || now || after ||
> | places that choose the marshaller | 60 annotations | 0 |
> | companions that keep a {{Marshaller}} | 30 | about 12 |
> | classes that get a marshaller and never use it | 6 | 0 |
> | blob fields of internal types | 24 | about 19 |
> | wrong marshaller from inheritance or nesting | possible | not possible |



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to