Skip to content

Commit 636e991

Browse files
committed
feat(actuator): prevent duplicate registration and stabilize CI
Changes: - Add a registration status check to prevent duplicate initialization during CI test runs. - Throw TronError on actuator instantiation failures to improve error visibility and debugging. - Narrow package scanning from "org.tron" to "org.tron.core.actuator" to reduce reflection overhead and speed up registration. - Remove JVM args for CI test to avoid JDK8 G1 GC bugs and Evacuation Pause failures. - Optimize console output to prevent test OOM for CI - Adjust memory and parallelism settings - Exclude dnsjava InetAddressResolverProvider
1 parent 3aa591f commit 636e991

5 files changed

Lines changed: 233 additions & 51 deletions

File tree

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,58 @@
11
package org.tron.core.utils;
22

33
import java.util.Set;
4+
import java.util.concurrent.atomic.AtomicBoolean;
45
import lombok.extern.slf4j.Slf4j;
56
import org.reflections.Reflections;
67
import org.tron.core.actuator.AbstractActuator;
8+
import org.tron.core.exception.TronError;
79

810
@Slf4j(topic = "TransactionRegister")
911
public class TransactionRegister {
1012

13+
private static final AtomicBoolean REGISTERED = new AtomicBoolean(false);
14+
private static final String PACKAGE_NAME = "org.tron.core.actuator";
15+
1116
public static void registerActuator() {
12-
Reflections reflections = new Reflections("org.tron");
13-
Set<Class<? extends AbstractActuator>> subTypes = reflections
14-
.getSubTypesOf(AbstractActuator.class);
15-
for (Class _class : subTypes) {
16-
try {
17-
_class.newInstance();
18-
} catch (Exception e) {
19-
logger.error("{} contract actuator register fail!", _class, e);
17+
if (REGISTERED.get()) {
18+
logger.debug("Actuator already registered.");
19+
return;
20+
}
21+
22+
synchronized (TransactionRegister.class) {
23+
if (REGISTERED.get()) {
24+
logger.debug("Actuator already registered.");
25+
return;
26+
}
27+
logger.debug("Register actuator start.");
28+
Reflections reflections = new Reflections(PACKAGE_NAME);
29+
Set<Class<? extends AbstractActuator>> subTypes = reflections
30+
.getSubTypesOf(AbstractActuator.class);
31+
32+
for (Class<? extends AbstractActuator> clazz : subTypes) {
33+
try {
34+
logger.debug("Registering actuator: {} start", clazz.getName());
35+
clazz.getDeclaredConstructor().newInstance();
36+
logger.debug("Registering actuator: {} done", clazz.getName());
37+
} catch (Exception e) {
38+
Throwable cause = e.getCause() != null ? e.getCause() : e;
39+
String detail = cause.getMessage() != null ? cause.getMessage() : cause.toString();
40+
throw new TronError(clazz.getName() + ": " + detail,
41+
e, TronError.ErrCode.ACTUATOR_REGISTER);
42+
}
2043
}
44+
45+
REGISTERED.set(true);
46+
logger.debug("Register actuator done, total {}.", subTypes.size());
2147
}
2248
}
2349

50+
static boolean isRegistered() {
51+
return REGISTERED.get();
52+
}
53+
54+
// For testing only — resets registration state between tests.
55+
static void resetForTesting() {
56+
REGISTERED.set(false);
57+
}
2458
}

common/src/main/java/org/tron/core/exception/TronError.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ public enum ErrCode {
4949
RATE_LIMITER_INIT(1),
5050
SOLID_NODE_INIT(0),
5151
PARAMETER_INIT(1),
52+
ACTUATOR_REGISTER(1),
5253
JDK_VERSION(1);
5354

5455
private final int code;

framework/build.gradle

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,11 +136,11 @@ test {
136136
exclude 'org/tron/core/ShieldedTRC20BuilderTest.class'
137137
exclude 'org/tron/common/runtime/vm/WithdrawRewardTest.class'
138138
}
139-
maxHeapSize = "1024m"
139+
maxHeapSize = "512m"
140+
maxParallelForks = Math.max(1, Math.min(2, Runtime.runtime.availableProcessors()))
140141
doFirst {
141142
// Restart the JVM after every 100 tests to avoid memory leaks and ensure test isolation
142143
forkEvery = 100
143-
jvmArgs "-XX:MetaspaceSize=128m","-XX:MaxMetaspaceSize=256m", "-XX:+UseG1GC"
144144
}
145145
}
146146

@@ -175,7 +175,9 @@ def binaryRelease(taskName, jarName, mainClass) {
175175
exclude "META-INF/*.SF"
176176
exclude "META-INF/*.DSA"
177177
exclude "META-INF/*.RSA"
178-
178+
// for service SPI loader for dnsjava
179+
// see https://issues.apache.org/jira/browse/HADOOP-19288
180+
exclude "META-INF/services/java.net.spi.InetAddressResolverProvider"
179181
manifest {
180182
attributes "Main-Class": "${mainClass}"
181183
}
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
package org.tron.core.utils;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertFalse;
5+
import static org.junit.Assert.assertThrows;
6+
import static org.junit.Assert.assertTrue;
7+
import static org.mockito.Mockito.mockConstruction;
8+
import static org.mockito.Mockito.when;
9+
10+
import java.util.Collections;
11+
import java.util.HashSet;
12+
import java.util.concurrent.atomic.AtomicBoolean;
13+
import java.util.concurrent.atomic.AtomicInteger;
14+
import org.junit.After;
15+
import org.junit.Before;
16+
import org.junit.Test;
17+
import org.junit.runner.RunWith;
18+
import org.mockito.MockedConstruction;
19+
import org.mockito.junit.MockitoJUnitRunner;
20+
import org.reflections.Reflections;
21+
import org.tron.core.actuator.AbstractActuator;
22+
import org.tron.core.actuator.TransferActuator;
23+
import org.tron.core.config.args.Args;
24+
import org.tron.core.exception.TronError;
25+
26+
@RunWith(MockitoJUnitRunner.class)
27+
public class TransactionRegisterTest {
28+
29+
@Before
30+
public void init() {
31+
Args.getInstance().setActuatorSet(new HashSet<>());
32+
TransactionRegister.resetForTesting();
33+
}
34+
35+
@After
36+
public void destroy() {
37+
Args.clearParam();
38+
}
39+
40+
@Test
41+
public void testAlreadyRegisteredSkipRegistration() {
42+
TransactionRegister.registerActuator();
43+
assertTrue("First registration should be completed", TransactionRegister.isRegistered());
44+
45+
TransactionRegister.registerActuator();
46+
assertTrue("Registration should still be true", TransactionRegister.isRegistered());
47+
}
48+
49+
@Test
50+
public void testConcurrentAccessThreadSafe() throws InterruptedException {
51+
final int threadCount = 5;
52+
Thread[] threads = new Thread[threadCount];
53+
final AtomicBoolean testPassed = new AtomicBoolean(true);
54+
55+
for (int i = 0; i < threadCount; i++) {
56+
threads[i] = new Thread(() -> {
57+
try {
58+
TransactionRegister.registerActuator();
59+
} catch (Exception e) {
60+
testPassed.set(false);
61+
}
62+
});
63+
}
64+
65+
for (Thread thread : threads) {
66+
thread.start();
67+
}
68+
69+
for (Thread thread : threads) {
70+
thread.join();
71+
}
72+
73+
assertTrue("All threads should complete without exceptions", testPassed.get());
74+
assertTrue("Registration should be completed", TransactionRegister.isRegistered());
75+
}
76+
77+
@Test
78+
public void testDoubleCheckLockingAtomicBoolean() {
79+
assertFalse("Initial registration state should be false", TransactionRegister.isRegistered());
80+
81+
TransactionRegister.registerActuator();
82+
assertTrue("After first call, should be registered", TransactionRegister.isRegistered());
83+
84+
TransactionRegister.registerActuator();
85+
assertTrue("After second call, should still be registered", TransactionRegister.isRegistered());
86+
}
87+
88+
@Test
89+
public void testRegistrationRunsExactlyOnce() throws InterruptedException {
90+
final int threadCount = 5;
91+
final AtomicInteger constructorCallCount = new AtomicInteger(0);
92+
93+
try (MockedConstruction<Reflections> ignored = mockConstruction(Reflections.class,
94+
(mock, context) -> {
95+
constructorCallCount.incrementAndGet();
96+
when(mock.getSubTypesOf(AbstractActuator.class)).thenReturn(Collections.emptySet());
97+
})) {
98+
99+
Thread[] threads = new Thread[threadCount];
100+
for (int i = 0; i < threadCount; i++) {
101+
threads[i] = new Thread(TransactionRegister::registerActuator);
102+
}
103+
for (Thread t : threads) {
104+
t.start();
105+
}
106+
for (Thread t : threads) {
107+
t.join();
108+
}
109+
110+
assertEquals("Reflections should be constructed exactly once across all threads",
111+
1, constructorCallCount.get());
112+
assertTrue(TransactionRegister.isRegistered());
113+
}
114+
}
115+
116+
@Test
117+
public void testMultipleCallsConsistency() {
118+
assertFalse("Should start unregistered", TransactionRegister.isRegistered());
119+
120+
TransactionRegister.registerActuator();
121+
assertTrue("Should be registered after first call", TransactionRegister.isRegistered());
122+
123+
for (int i = 0; i < 5; i++) {
124+
TransactionRegister.registerActuator();
125+
assertTrue("Should remain registered after call " + (i + 2), TransactionRegister.isRegistered());
126+
}
127+
}
128+
129+
@Test
130+
public void testThrowsTronError() {
131+
try (MockedConstruction<Reflections> ignored = mockConstruction(Reflections.class,
132+
(mock, context) -> when(mock.getSubTypesOf(AbstractActuator.class))
133+
.thenReturn(Collections.singleton(TransferActuator.class)));
134+
MockedConstruction<TransferActuator> ignored1 = mockConstruction(TransferActuator.class,
135+
(mock, context) -> {
136+
throw new RuntimeException("boom");
137+
})) {
138+
TronError error = assertThrows(TronError.class, TransactionRegister::registerActuator);
139+
assertEquals(TronError.ErrCode.ACTUATOR_REGISTER, error.getErrCode());
140+
assertTrue(error.getMessage().contains("TransferActuator"));
141+
}
142+
}
143+
}
Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,42 @@
1-
<configuration>
2-
3-
<!-- FILE appender is disabled -->
4-
5-
<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
6-
<encoder>
7-
<pattern>%d{HH:mm:ss.SSS} %p [%c{1}] %m%n</pattern>
8-
</encoder>
9-
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
10-
<level>INFO</level>
11-
</filter>
12-
</appender>
13-
14-
<appender class="ch.qos.logback.core.rolling.RollingFileAppender"
15-
name="FILE">
16-
<file>./logs/tron-test.log</file>
17-
<rollingPolicy
18-
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
19-
<!-- rollover daily -->
20-
<fileNamePattern>./logs/tron-test-%d{yyyy-MM-dd}.%i.log.zip
21-
</fileNamePattern>
22-
<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
23-
<maxFileSize>100MB</maxFileSize>
24-
<maxHistory>60</maxHistory>
25-
<totalSizeCap>20GB</totalSizeCap>
26-
</rollingPolicy>
27-
<encoder>
28-
<pattern>%d{HH:mm:ss.SSS} %p [%c{1}] %m%n</pattern>
29-
</encoder>
30-
</appender>
31-
32-
<root level="DEBUG">
33-
<appender-ref ref="STDOUT"/>
34-
<appender-ref ref="FILE"/>
35-
</root>
36-
37-
<logger level="DEBUG" name="Test"/>
38-
<logger level="DEBUG" name="Manager"/>
39-
40-
</configuration>
1+
<configuration>
2+
3+
<!-- FILE appender is disabled -->
4+
5+
<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
6+
<encoder>
7+
<pattern>%d{HH:mm:ss.SSS} %p [%c{1}] %m%n</pattern>
8+
</encoder>
9+
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
10+
<level>${console.log.level:-ERROR}</level>
11+
</filter>
12+
</appender>
13+
14+
<appender class="ch.qos.logback.core.rolling.RollingFileAppender"
15+
name="FILE">
16+
<file>./logs/tron-test.log</file>
17+
<bufferSize>8192</bufferSize>
18+
<immediateFlush>true</immediateFlush>
19+
<rollingPolicy
20+
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
21+
<!-- rollover daily -->
22+
<fileNamePattern>./logs/tron-test-%d{yyyy-MM-dd}.%i.log.zip
23+
</fileNamePattern>
24+
<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
25+
<maxFileSize>100MB</maxFileSize>
26+
<maxHistory>60</maxHistory>
27+
<totalSizeCap>20GB</totalSizeCap>
28+
</rollingPolicy>
29+
<encoder>
30+
<pattern>%d{HH:mm:ss.SSS} %p [%c{1}] %m%n</pattern>
31+
</encoder>
32+
</appender>
33+
34+
<root level="DEBUG">
35+
<appender-ref ref="STDOUT"/>
36+
<appender-ref ref="FILE"/>
37+
</root>
38+
39+
<logger level="DEBUG" name="Test"/>
40+
<logger level="DEBUG" name="Manager"/>
41+
42+
</configuration>

0 commit comments

Comments
 (0)