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
Original file line number Diff line number Diff line change
@@ -1,24 +1,58 @@
package org.tron.core.utils;

import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import lombok.extern.slf4j.Slf4j;
import org.reflections.Reflections;
import org.tron.core.actuator.AbstractActuator;
import org.tron.core.exception.TronError;

@Slf4j(topic = "TransactionRegister")
public class TransactionRegister {

private static final AtomicBoolean REGISTERED = new AtomicBoolean(false);
private static final String PACKAGE_NAME = "org.tron.core.actuator";

public static void registerActuator() {
Reflections reflections = new Reflections("org.tron");
Set<Class<? extends AbstractActuator>> subTypes = reflections
.getSubTypesOf(AbstractActuator.class);
for (Class _class : subTypes) {
try {
_class.newInstance();
} catch (Exception e) {
logger.error("{} contract actuator register fail!", _class, e);
if (REGISTERED.get()) {
logger.debug("Actuator already registered.");
return;
}

synchronized (TransactionRegister.class) {
if (REGISTERED.get()) {
logger.debug("Actuator already registered.");
return;
}
logger.debug("Register actuator start.");
Reflections reflections = new Reflections(PACKAGE_NAME);
Set<Class<? extends AbstractActuator>> subTypes = reflections
.getSubTypesOf(AbstractActuator.class);

for (Class<? extends AbstractActuator> clazz : subTypes) {
try {
logger.debug("Registering actuator: {} start", clazz.getName());
clazz.getDeclaredConstructor().newInstance();
logger.debug("Registering actuator: {} done", clazz.getName());
} catch (Exception e) {
Throwable cause = e.getCause() != null ? e.getCause() : e;
String detail = cause.getMessage() != null ? cause.getMessage() : cause.toString();
throw new TronError(clazz.getName() + ": " + detail,
e, TronError.ErrCode.ACTUATOR_REGISTER);
}
}

REGISTERED.set(true);
logger.debug("Register actuator done, total {}.", subTypes.size());
}
}

static boolean isRegistered() {
return REGISTERED.get();
}

// For testing only — resets registration state between tests.
static void resetForTesting() {
REGISTERED.set(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public enum ErrCode {
RATE_LIMITER_INIT(1),
SOLID_NODE_INIT(0),
PARAMETER_INIT(1),
ACTUATOR_REGISTER(1),
JDK_VERSION(1);

private final int code;
Expand Down
8 changes: 5 additions & 3 deletions framework/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,11 @@ test {
exclude 'org/tron/core/ShieldedTRC20BuilderTest.class'
exclude 'org/tron/common/runtime/vm/WithdrawRewardTest.class'
}
maxHeapSize = "1024m"
maxHeapSize = "512m"
maxParallelForks = Math.max(1, Math.min(4, Runtime.runtime.availableProcessors()))
doFirst {
// Restart the JVM after every 100 tests to avoid memory leaks and ensure test isolation
forkEvery = 100
jvmArgs "-XX:MetaspaceSize=128m","-XX:MaxMetaspaceSize=256m", "-XX:+UseG1GC"
}
}

Expand Down Expand Up @@ -175,7 +175,9 @@ def binaryRelease(taskName, jarName, mainClass) {
exclude "META-INF/*.SF"
exclude "META-INF/*.DSA"
exclude "META-INF/*.RSA"

// for service SPI loader for dnsjava
// see https://issues.apache.org/jira/browse/HADOOP-19288
exclude "META-INF/services/java.net.spi.InetAddressResolverProvider"
manifest {
attributes "Main-Class": "${mainClass}"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package org.tron.core.utils;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mockConstruction;
import static org.mockito.Mockito.when;

import java.util.Collections;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.MockedConstruction;
import org.mockito.junit.MockitoJUnitRunner;
import org.reflections.Reflections;
import org.tron.core.actuator.AbstractActuator;
import org.tron.core.actuator.TransferActuator;
import org.tron.core.config.args.Args;
import org.tron.core.exception.TronError;

@RunWith(MockitoJUnitRunner.class)
public class TransactionRegisterTest {

@Before
public void init() {
Args.getInstance().setActuatorSet(new HashSet<>());
TransactionRegister.resetForTesting();
}

@After
public void destroy() {
Args.clearParam();
}

@Test
public void testAlreadyRegisteredSkipRegistration() {
TransactionRegister.registerActuator();
assertTrue("First registration should be completed", TransactionRegister.isRegistered());

TransactionRegister.registerActuator();
assertTrue("Registration should still be true", TransactionRegister.isRegistered());
}

@Test
public void testConcurrentAccessThreadSafe() throws InterruptedException {
final int threadCount = 5;
Thread[] threads = new Thread[threadCount];
final AtomicBoolean testPassed = new AtomicBoolean(true);

for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
try {
TransactionRegister.registerActuator();
} catch (Exception e) {
testPassed.set(false);
}
});
}

for (Thread thread : threads) {
thread.start();
}

for (Thread thread : threads) {
thread.join();
}

assertTrue("All threads should complete without exceptions", testPassed.get());
assertTrue("Registration should be completed", TransactionRegister.isRegistered());
}

@Test
public void testDoubleCheckLockingAtomicBoolean() {
assertFalse("Initial registration state should be false", TransactionRegister.isRegistered());

TransactionRegister.registerActuator();
assertTrue("After first call, should be registered", TransactionRegister.isRegistered());

TransactionRegister.registerActuator();
assertTrue("After second call, should still be registered", TransactionRegister.isRegistered());
}

@Test
public void testRegistrationRunsExactlyOnce() {
final AtomicInteger constructorCallCount = new AtomicInteger(0);

try (MockedConstruction<Reflections> ignored = mockConstruction(Reflections.class,
(mock, context) -> {
constructorCallCount.incrementAndGet();
when(mock.getSubTypesOf(AbstractActuator.class)).thenReturn(Collections.emptySet());
})) {

// Call multiple times; Reflections should only be constructed once
for (int i = 0; i < 5; i++) {
TransactionRegister.registerActuator();
}

assertEquals("Reflections should be constructed exactly once regardless of call count",
1, constructorCallCount.get());
assertTrue(TransactionRegister.isRegistered());
}
}

@Test
public void testMultipleCallsConsistency() {
assertFalse("Should start unregistered", TransactionRegister.isRegistered());

TransactionRegister.registerActuator();
assertTrue("Should be registered after first call", TransactionRegister.isRegistered());

for (int i = 0; i < 5; i++) {
TransactionRegister.registerActuator();
assertTrue("Should remain registered after call " + (i + 2),
TransactionRegister.isRegistered());
}
}

@Test
public void testThrowsTronError() {
try (MockedConstruction<Reflections> ignored = mockConstruction(Reflections.class,
(mock, context) -> when(mock.getSubTypesOf(AbstractActuator.class))
.thenReturn(Collections.singleton(TransferActuator.class)));
MockedConstruction<TransferActuator> ignored1 = mockConstruction(TransferActuator.class,
(mock, context) -> {
throw new RuntimeException("boom");
})) {
TronError error = assertThrows(TronError.class, TransactionRegister::registerActuator);
assertEquals(TronError.ErrCode.ACTUATOR_REGISTER, error.getErrCode());
assertTrue(error.getMessage().contains("TransferActuator"));
}
}
}
82 changes: 42 additions & 40 deletions framework/src/test/resources/logback-test.xml
Original file line number Diff line number Diff line change
@@ -1,40 +1,42 @@
<configuration>

<!-- FILE appender is disabled -->

<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %p [%c{1}] %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
</appender>

<appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="FILE">
<file>./logs/tron-test.log</file>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- rollover daily -->
<fileNamePattern>./logs/tron-test-%d{yyyy-MM-dd}.%i.log.zip
</fileNamePattern>
<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %p [%c{1}] %m%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>

<logger level="DEBUG" name="Test"/>
<logger level="DEBUG" name="Manager"/>

</configuration>
<configuration>

<!-- FILE appender is disabled -->

<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %p [%c{1}] %m%n</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>${console.log.level:-ERROR}</level>
</filter>
</appender>

<appender class="ch.qos.logback.core.rolling.RollingFileAppender"
name="FILE">
<file>./logs/tron-test.log</file>
<bufferSize>8192</bufferSize>
<immediateFlush>true</immediateFlush>
<rollingPolicy
class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- rollover daily -->
<fileNamePattern>./logs/tron-test-%d{yyyy-MM-dd}.%i.log.zip
</fileNamePattern>
<!-- each file should be at most 100MB, keep 60 days worth of history, but at most 20GB -->
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
<totalSizeCap>20GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} %p [%c{1}] %m%n</pattern>
</encoder>
</appender>

<root level="DEBUG">
<appender-ref ref="STDOUT"/>
<appender-ref ref="FILE"/>
</root>

<logger level="DEBUG" name="Test"/>
<logger level="DEBUG" name="Manager"/>

</configuration>
Loading