Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
138 changes: 124 additions & 14 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ jobs:
restore-keys: macos26-${{ matrix.arch }}-gradle-

- name: Build
run: ./gradlew clean build --no-daemon
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew clean build --no-daemon --console=plain 2>&1 | tee ci-logs/build.log

- name: Toolkit jar smoke test
run: |
Expand All @@ -63,6 +67,17 @@ jobs:
java -jar "$JAR" db archive -h
java -jar "$JAR" keystore --help

- name: Upload test diagnostics
if: failure()
uses: actions/upload-artifact@v6
with:
name: tron-test-logs-${{ github.job }}-jdk${{ matrix.java }}-${{ matrix.arch }}
path: |
**/logs/tron-test.log
ci-logs/*.log
if-no-files-found: warn
retention-days: 7

build-ubuntu:
name: Build ubuntu24 (JDK 17 / aarch64)
if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'ubuntu' }}
Expand Down Expand Up @@ -91,7 +106,11 @@ jobs:
restore-keys: ubuntu24-aarch64-gradle-

- name: Build
run: ./gradlew clean build --no-daemon
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew clean build --no-daemon --console=plain 2>&1 | tee ci-logs/build.log

- name: Toolkit jar smoke test
run: |
Expand All @@ -102,6 +121,17 @@ jobs:
java -jar "$JAR" db archive -h
java -jar "$JAR" keystore --help

- name: Upload test diagnostics
if: failure()
uses: actions/upload-artifact@v6
with:
name: tron-test-logs-${{ github.job }}
path: |
**/logs/tron-test.log
ci-logs/*.log
if-no-files-found: warn
retention-days: 7

docker-build-rockylinux:
name: Build rockylinux (JDK 8 / x86_64)
if: ${{ github.event_name == 'pull_request' || inputs.job == 'all' || inputs.job == 'rockylinux' }}
Expand All @@ -113,15 +143,27 @@ jobs:

env:
GRADLE_USER_HOME: /github/home/.gradle
LANG: en_US.UTF-8
LC_ALL: en_US.UTF-8
LANG: C.utf8
LC_ALL: C.utf8

steps:
- name: Install dependencies (Rocky 8 + JDK8)
run: |
set -euxo pipefail
dnf -y install java-1.8.0-openjdk-devel git wget unzip which jq bc curl glibc-langpack-en
dnf -y groupinstall "Development Tools"
# Rocky 8 already provides CA certificates, JNI runtime libraries, tar and gzip.
# Its built-in C.utf8 locale provides UTF-8 without an extra language pack.
# git-core provides checkout commands; zstd supports actions/cache compression.
# Download more packages concurrently when mirror requests are slow.
dnf -y --setopt=install_weak_deps=False --setopt=max_parallel_downloads=10 install \
java-1.8.0-openjdk-devel git-core zstd
# Set JAVA_HOME so the Gradle wrapper does not need which.
javac_path=$(command -v javac)
javac_real=$(readlink -f "$javac_path")
jdk_bin=$(dirname "$javac_real")
jdk_home=$(dirname "$jdk_bin")
test -x "$jdk_home/bin/java"
test -x "$jdk_home/bin/javac"
printf 'JAVA_HOME=%s\n' "$jdk_home" >> "$GITHUB_ENV"

- name: Checkout code
uses: actions/checkout@v5
Expand All @@ -143,7 +185,11 @@ jobs:
run: ./gradlew --stop || true

- name: Build
run: ./gradlew clean build --no-daemon
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew clean build --no-daemon --console=plain 2>&1 | tee ci-logs/build.log

- name: Toolkit jar smoke test
run: |
Expand All @@ -155,7 +201,22 @@ jobs:
java -jar "$JAR" keystore --help

- name: Test with RocksDB engine
run: ./gradlew :framework:testWithRocksDb --no-daemon
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew :framework:testWithRocksDb --no-daemon --console=plain 2>&1 | tee ci-logs/rocksdb-test.log

- name: Upload test diagnostics
if: failure()
uses: actions/upload-artifact@v6
with:
name: tron-test-logs-${{ github.job }}
path: |
**/logs/tron-test.log
ci-logs/*.log
if-no-files-found: warn
retention-days: 7

docker-build-debian11:
name: Build debian11 (JDK 8 / x86_64)
Expand Down Expand Up @@ -197,7 +258,11 @@ jobs:
debian11-x86_64-gradle-

- name: Build
run: ./gradlew clean build --no-daemon --no-build-cache
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew clean build --no-daemon --no-build-cache --console=plain 2>&1 | tee ci-logs/build.log

- name: Toolkit jar smoke test
run: |
Expand All @@ -209,10 +274,18 @@ jobs:
java -jar "$JAR" keystore --help

- name: Test with RocksDB engine
run: ./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache --console=plain 2>&1 | tee ci-logs/rocksdb-test.log

- name: Generate module coverage reports
run: ./gradlew jacocoTestReport --no-daemon
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew jacocoTestReport --no-daemon --console=plain 2>&1 | tee ci-logs/coverage.log

- name: Upload PR coverage reports
uses: actions/upload-artifact@v6
Expand All @@ -222,6 +295,17 @@ jobs:
**/build/reports/jacoco/test/jacocoTestReport.xml
if-no-files-found: error

- name: Upload test diagnostics
if: failure()
uses: actions/upload-artifact@v6
with:
name: tron-test-logs-${{ github.job }}
path: |
**/logs/tron-test.log
ci-logs/*.log
if-no-files-found: warn
retention-days: 7

coverage-base:
name: Coverage Base (JDK 8 / x86_64)
if: ${{ github.event_name == 'pull_request' }}
Expand Down Expand Up @@ -260,19 +344,33 @@ jobs:
coverage-base-x86_64-gradle-

- name: Build (base)
id: base_build
# Test failures on the base branch are tolerated: merge-order races can
# leave the base with a pre-existing failing test that is unrelated to
# this PR. The only output we need from this job is the jacoco XML for
# coverage diffing, so we must not let a stale test failure block it.
continue-on-error: true
run: ./gradlew clean build --no-daemon --no-build-cache
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew clean build --no-daemon --no-build-cache --console=plain 2>&1 | tee ci-logs/build.log

- name: Test with RocksDB engine (base)
id: base_rocksdb_test
continue-on-error: true
run: ./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew :framework:testWithRocksDb --no-daemon --no-build-cache --console=plain 2>&1 | tee ci-logs/rocksdb-test.log

- name: Generate module coverage reports (base)
run: ./gradlew jacocoTestReport --no-daemon
shell: bash
run: |
set -euo pipefail
mkdir -p ci-logs
./gradlew jacocoTestReport --no-daemon --console=plain 2>&1 | tee ci-logs/coverage.log

- name: Upload base coverage reports
uses: actions/upload-artifact@v6
Expand All @@ -282,6 +380,18 @@ jobs:
**/build/reports/jacoco/test/jacocoTestReport.xml
if-no-files-found: warn

- name: Upload test diagnostics
# Preserve logs for test failures tolerated by continue-on-error above.
if: ${{ failure() || steps.base_build.outcome == 'failure' || steps.base_rocksdb_test.outcome == 'failure' }}
uses: actions/upload-artifact@v6
with:
name: tron-test-logs-${{ github.job }}
path: |
**/logs/tron-test.log
ci-logs/*.log
if-no-files-found: warn
retention-days: 7

coverage-gate:
name: Coverage Gate
needs: [docker-build-debian11, coverage-base]
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/pr-cancel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ jobs:
'pr-build.yml',
'codeql.yml',
'integration-test-single-node.yml',
'integration-test-multinode.yml',
];
const headSha = context.payload.pull_request.head.sha;
const prNumber = context.payload.pull_request.number;
Expand Down
10 changes: 5 additions & 5 deletions framework/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
id "org.gradle.test-retry" version "1.5.9"
// id "org.gradle.test-retry" version "1.5.9"
id "org.sonarqube" version "2.6"
id "com.gorylenko.gradle-git-properties" version "2.4.1"
}
Expand Down Expand Up @@ -110,10 +110,10 @@ run {
}

def configureTestTask = { Task t ->
t.retry {
maxRetries = 5
maxFailures = 20
}
// t.retry {
// maxRetries = 5
// maxFailures = 20
// }
t.testLogging {
exceptionFormat = 'full'
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public class BackupServer implements AutoCloseable {

private BackupManager backupManager;

private Channel channel;
private volatile Channel channel;

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.

[Question] What's the motivation for making channel volatile here? Reading the surrounding code, this appears to fix a close-vs-bind race: close() can set shutdown while bind() is still in flight and observe a null channel, and the post-bind guard at L79–82 then closes the late-bound channel. Two questions:

  1. Is this race reachable in production, or only in tests? The new BackupServerLifecycleTest#testCloseDuringBind suggests the failing scenario came from the test side. If this production change is primarily to make unit tests pass, is that the right trade-off — or would fixing the test lifecycle be cleaner, or should this be split into a separate fix: PR with its own justification? It also isn't mentioned in the PR title/body, which currently only describes CI changes.

  2. If the cross-thread visibility fix is warranted, should executor (~L35) be volatile as well? It follows the same pattern: written by initServer(), read unsynchronized by close(). A cross-thread close() can still observe a stale-null executor and skip shutdownAndAwaitTermination, leaking the non-daemon worker thread even though the channel is now closed.


private volatile boolean shutdown = false;

Expand Down Expand Up @@ -75,6 +75,12 @@ public void initChannel(NioDatagramChannel ch)

channel = b.bind(port).sync().channel();

// close() may have run while bind was still in progress.
if (shutdown) {
channel.close().sync();
break;
}

logger.info("Backup server started, bind port {}", port);

channel.closeFuture().sync();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.google.common.util.concurrent;

import org.tron.common.math.StrictMathWrapper;

/** Test-only clock for exercising real Guava permit accounting without wall-clock sleeps. */
public final class FakeTimeRateLimiter {

private FakeTimeRateLimiter() {
}

public static RateLimiter create(double permitsPerSecond) {
return RateLimiter.create(permitsPerSecond, new Stopwatch());
}

public static RateLimiter createWithStoredPermit(double permitsPerSecond) {
Stopwatch clock = new Stopwatch();
RateLimiter limiter = RateLimiter.create(permitsPerSecond, clock);
clock.sleepMicrosUninterruptibly((long) StrictMathWrapper.ceil(1_000_000 / permitsPerSecond));
return limiter;
}

private static final class Stopwatch extends RateLimiter.SleepingStopwatch {
private long micros;

@Override
protected long readMicros() {
return micros;
}

@Override
protected void sleepMicrosUninterruptibly(long sleepMicros) {
micros += sleepMicros;
}
}
}
17 changes: 13 additions & 4 deletions framework/src/test/java/org/tron/common/BaseMethodTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
@Slf4j
public abstract class BaseMethodTest {

@Rule
public final VMConfigRule vmConfigRule = new VMConfigRule();

@Rule
public final TemporaryFolder temporaryFolder = new TemporaryFolder();

Expand Down Expand Up @@ -77,11 +80,17 @@ protected void afterInit() {

@After
public final void destroyContext() {
beforeDestroy();
if (context != null) {
context.close(); // triggers appT.shutdown() via TronApplicationContext
try {
beforeDestroy();
} finally {
try {
if (context != null) {
context.close(); // triggers appT.shutdown() via TronApplicationContext
}
} finally {
Args.clearParam();
}
}
Args.clearParam();
}

protected void beforeDestroy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.tron.common;

import org.junit.Assert;
import org.junit.Test;
import org.mockito.Mockito;
import org.tron.common.application.TronApplicationContext;
import org.tron.core.config.args.Args;

public class BaseMethodTestLifecycleTest {

@Test
public void closesContextWhenSubclassCleanupFails() {
BaseMethodTest fixture = new BaseMethodTest() {
@Override
protected void beforeDestroy() {
throw new IllegalStateException("intentional cleanup failure");
}
};
fixture.context = Mockito.mock(TronApplicationContext.class);
Args.setParam(new String[0], TestConstants.TEST_CONF);
try {
IllegalStateException failure = Assert.assertThrows(IllegalStateException.class,
fixture::destroyContext);
Assert.assertEquals("intentional cleanup failure", failure.getMessage());
Mockito.verify(fixture.context).close();
Assert.assertEquals(0, Args.getInstance().getHttpMaxMessageSize());
} finally {
Args.clearParam();
}
}
}
Loading
Loading