Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6d5e392
IGNITE-25489 Export registered message table
chesnokoff Sep 9, 2026
685161e
IGNITE-25489 Export message table as XML using JDK APIs
chesnokoff Sep 11, 2026
8a4aec6
IGNITE-25489 Structure XML fields and generate message table during M…
chesnokoff Sep 11, 2026
086bac8
IGNITE-25489 Generate XML with DOM and simplify Maven arguments
chesnokoff Sep 11, 2026
0484389
IGNITE-25489 Restore explicit XML formatting with StAX
chesnokoff Sep 11, 2026
991c050
IGNITE-25489 Simplify schema reading and extract XML writing methods
chesnokoff Sep 11, 2026
34ab7fc
IGNITE-25489 Generate tracked message table with an opt-in Maven profile
chesnokoff Sep 11, 2026
74dd148
IGNITE-25489 Check generated message table freshness in CI
chesnokoff Sep 14, 2026
4f918de
IGNITE-25489 Separate message collection, schema data and XML writer
chesnokoff Sep 14, 2026
5a1e107
IGNITE-25489 Separate collector and DTOs and model serialization anno…
chesnokoff Sep 14, 2026
0160e96
IGNITE-25489 Omit provider names from message table
chesnokoff Sep 14, 2026
d20cf81
IGNITE-25489 Write field annotations before field data
chesnokoff Sep 14, 2026
96f3d9c
IGNITE-25489 Label PRs with message table changes
chesnokoff Sep 14, 2026
d005056
IGNITE-25489 Include field order in message table
chesnokoff Sep 14, 2026
7e1a067
IGNITE-25489 Use global field order in message table
chesnokoff Sep 14, 2026
70efbcc
IGNITE-25489 Calculate field order with explicit offset
chesnokoff Sep 14, 2026
85955a3
IGNITE-25489 Separate ordered and marshalled fields in XML
chesnokoff Sep 14, 2026
2349764
IGNITE-25489 Simplify message table classpath handling
chesnokoff Sep 14, 2026
867de08
IGNITE-25489 Add message table test suite and XML header
chesnokoff Sep 14, 2026
084e4a3
IGNITE-25489 Move message schema test messages to package
chesnokoff Sep 14, 2026
087cd9e
IGNITE-25489 Clarify message table documentation
chesnokoff Sep 14, 2026
02d598b
IGNITE-25489 Refine message table documentation
chesnokoff Sep 14, 2026
6338996
IGNITE-25489 Document message table providers
chesnokoff Sep 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 26 additions & 63 deletions .github/workflows/check-protected-classes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,105 +13,68 @@
# See the License for the specific language governing permissions and
# limitations under the License.

name: Protected Classes
name: Message Table Compatibility

# pull_request_target for the write token needed to comment/label the PR. The fork's changes are
# read through the API as data and never checked out or executed, so no allow-unsafe-pr-checkout and
# no working copy of the PR code on the runner. Do not add a step that builds or runs the PR code.
# checked only through GitHub metadata and never checked out or executed, so no allow-unsafe-pr-checkout
# and no working copy of the PR code on the runner. Do not add a step that builds or runs the PR code.
on: pull_request_target

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
check-protected-classes:
check-message-table:
runs-on: ubuntu-latest
name: Rolling Upgrade check
permissions:
issues: write
pull-requests: write
checks: write
steps:
- name: Detect protected class changes and report
- name: Detect message table changes and report
uses: actions/github-script@v8
with:
script: |
const ORDER_USE = '@Order(';
const ORDER_IMPORT = 'import org.apache.ignite.internal.Order;';
const ORDER_PKG = 'package org.apache.ignite.internal;';
const MESSAGE_TABLE = 'modules/compatibility-check/src/main/resources/messages/table.xml';
const MARKER = '<!-- ignite-rolling-upgrade-check -->';
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
const baseSha = pr.base.sha;
const headSha = pr.head.sha;

// Changed files are read from the API as data; the fork's code is never checked out or executed.
// Changed file names are read from the API as data; the fork's code is never checked out or executed.
const files = await github.paginate(github.rest.pulls.listFiles, {
owner, repo, pull_number: pr.number, per_page: 100,
});

// Reproduce `git diff --no-renames --diff-filter=ADM`: a rename is a delete(old)+add(new) pair.
// A protected class carries the @Order annotation; an added file is defined by the head revision,
// a deleted/modified one by the base.
const revisions = [];
for (const f of files) {
if (f.status === 'added' || f.status === 'copied')
revisions.push([f.filename, headSha]);
else if (f.status === 'removed' || f.status === 'modified' || f.status === 'changed')
revisions.push([f.filename, baseSha]);
else if (f.status === 'renamed') {
revisions.push([f.previous_filename, baseSha]);
revisions.push([f.filename, headSha]);
}
}
const messageTableChanged = files.some(f =>
f.filename === MESSAGE_TABLE || f.previous_filename === MESSAGE_TABLE);

const isProtected = async (path, ref) => {
let meta;
try {
({ data: meta } = await github.rest.repos.getContent({ owner, repo, path, ref }));
} catch (e) {
if (e.status === 404) return false;
throw e;
}
if (Array.isArray(meta) || meta.type !== 'file') return false;
let content = meta.content ? Buffer.from(meta.content, 'base64').toString('utf8') : '';
if (!content && meta.sha) {
const { data: blob } = await github.rest.git.getBlob({ owner, repo, file_sha: meta.sha });
content = Buffer.from(blob.content, blob.encoding).toString('utf8');
}
const lines = content.split(/\r?\n/);
const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number,
});
const existing = comments.find(c => c.body && c.body.includes(MARKER));

return content.includes(ORDER_USE)
&& (lines.includes(ORDER_IMPORT) || lines.includes(ORDER_PKG));
};
if (!messageTableChanged) {
if (existing)
await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id });

const hits = [];
for (const [path, ref] of revisions) {
if (path.endsWith('.java')
&& !path.startsWith('modules/core/src/test/resources/codegen/')
&& await isProtected(path, ref)) hits.push(path);
return;
}

if (hits.length === 0) return;

// File names come from the fork; render them as inert inline code so they cannot inject
// markdown (backticks, @mentions, links) into content posted under the bot's write token.
const safe = f => '`' + String(f).replace(/[`\r\n]/g, '') + '`';
const list = hits.map(f => '- ' + safe(f)).join('\n');
const safePath = '`' + MESSAGE_TABLE + '`';
const summary = [
'This PR modifies protected classes (with **Order** annotation).',
'Changes to these classes can break rolling upgrade compatibility.',
'This PR changes the generated communication message table.',
'This does not mean that rolling upgrade compatibility is broken, but the diff needs a review.',
'',
'**Affected files:**',
list,
'**Review target:**',
'- ' + safePath,
'',
'Inspect the XML diff for changed message ids, classes, fields, types or serialization annotations.',
].join('\n');
const body = MARKER + '\n## Possible compatibility issues. Please, check rolling upgrade cases\n\n' + summary + '\n';
const body = MARKER + '\n## Message table changed. Please review rolling upgrade compatibility\n\n' + summary + '\n';

const comments = await github.paginate(github.rest.issues.listComments, {
owner, repo, issue_number: pr.number,
});
const existing = comments.find(c => c.body.includes(MARKER));
if (existing) {
await github.rest.issues.deleteComment({ owner, repo, comment_id: existing.id });
}
Expand All @@ -127,5 +90,5 @@ jobs:
head_sha: headSha,
status: 'completed',
conclusion: 'neutral',
output: { title: 'Possible compatibility issues', summary },
output: { title: 'Message table changed', summary },
});
9 changes: 8 additions & 1 deletion .github/workflows/commit-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
run: |
set -o pipefail
rc=0
./mvnw test-compile -Pall-java,licenses,lgpl,checkstyle,examples,check-licenses -B -V -T 1C 2>&1 | tee mvn-codestyle.log || rc=$?
./mvnw test-compile -Pall-java,licenses,lgpl,checkstyle,examples,check-licenses,message-table -B -V -T 1C 2>&1 | tee mvn-codestyle.log || rc=$?
if [ "$rc" -ne 0 ] && grep -q "COMPILATION ERROR" mvn-codestyle.log; then
echo "::error title=Compilation failed::Java compilation failed - this is a compile error, not a checkstyle violation. The flood of 'cannot find symbol' for generated *Walker/*Serializer/*Factory classes is a cascade: javac drops annotation-processor output when compilation fails. Fix the real error(s) listed in the build log group below first."
echo "::group::Likely root-cause compile errors (generated-class cascade filtered out)"
Expand All @@ -119,6 +119,13 @@ jobs:
fi
exit "$rc"

- name: Check message table is up to date
run: |
if ! git diff --exit-code -- modules/compatibility-check/src/main/resources/messages/table.xml; then
echo "::error title=Message table is outdated::Run the build with -Pmessage-table and commit the updated modules/compatibility-check/src/main/resources/messages/table.xml."
exit 1
fi

- name: Run abandoned tests checks.
# Reuse classes from the previous step; the differing profiles otherwise trigger a full reactor recompile.
run : |
Expand Down
56 changes: 56 additions & 0 deletions modules/compatibility-check/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Message table

Exports registered Ignite messages to
[`src/main/resources/messages/table.xml`](src/main/resources/messages/table.xml) to control compatibility changes.
The table contains message IDs, classes, ordered wire fields and serialization annotations.
Logical `@Marshalled` fields are written separately from ordered wire fields.

The table covers messages registered by
[`CoreMessagesProvider`](../core/src/main/java/org/apache/ignite/internal/CoreMessagesProvider.java),
[`GridH2ValueMessageFactory`](../indexing/src/main/java/org/apache/ignite/internal/processors/query/h2/twostep/msg/GridH2ValueMessageFactory.java),
[`CalciteMessageFactory`](../calcite/src/main/java/org/apache/ignite/internal/processors/query/calcite/message/CalciteMessageFactory.java) and
[`ZkMessageFactory`](../zookeeper/src/main/java/org/apache/ignite/spi/discovery/zk/internal/ZkMessageFactory.java).
Unregistered messages and third-party providers are out of scope. `CompressedMessage` is included
with an empty schema because it has a hand-written serializer.

This module only generates the table. It does not compare revisions or decide whether a
change is compatible.

Example:

```xml
<message id="13013" class="org.apache.ignite.internal.processors.plugin.PluginsDataBagItem">
<orderedFields>
<field order="0">
<type>byte[]</type>
<name>dataBytes</name>
</field>
</orderedFields>
<marshalledFields>
<field>
<annotations>
<org.apache.ignite.internal.Marshalled>value=dataBytes</org.apache.ignite.internal.Marshalled>
</annotations>
<type>java.util.Map&lt;java.lang.String,java.io.Serializable&gt;</type>
<name>data</name>
</field>
</marshalledFields>
</message>
```

## Generate

Use JDK 17. Enable the `message-table` profile to update the table directly:

```sh
./mvnw -pl modules/compatibility-check -Pmessage-table process-classes
```

Without the profile, the generation step does not run. Review the XML diff and
include it in the commit when message definitions change.

After generation, CI can check that the committed table is up to date with:

```sh
git diff --exit-code -- modules/compatibility-check/src/main/resources/messages/table.xml
```
107 changes: 107 additions & 0 deletions modules/compatibility-check/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-parent-internal</artifactId>
<version>${revision}</version>
<relativePath>../../parent-internal/pom.xml</relativePath>
</parent>
<artifactId>ignite-compatibility-check</artifactId>
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-codegen</artifactId>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-tools</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-indexing</artifactId>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-calcite</artifactId>
</dependency>
<dependency>
<groupId>org.apache.ignite</groupId>
<artifactId>ignite-zookeeper</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
</configuration>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<proc>none</proc>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>message-table</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>generate-message-table</id>
<phase>process-classes</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>${java.home}/bin/java</executable>
<arguments>
<argument>-cp</argument>
<classpath/>
<argument>org.apache.ignite.tools.compatibility.messages.MessageTable</argument>
<argument>${project.basedir}/src/main/resources/messages/table.xml</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Loading