Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use address book to determine node id #10453

Merged
merged 11 commits into from
Dec 18, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,36 @@

package com.hedera.node.app;

import static com.swirlds.logging.legacy.LogMarker.EXCEPTION;
import static com.swirlds.platform.system.SystemExitCode.CONFIGURATION_ERROR;
import static com.swirlds.platform.system.SystemExitCode.NODE_ADDRESS_MISMATCH;
import static com.swirlds.platform.system.SystemExitUtils.exitSystem;
import static com.swirlds.platform.util.BootstrapUtils.checkNodesToRun;
import static com.swirlds.platform.util.BootstrapUtils.getNodesToRun;
import static java.util.Objects.requireNonNull;

import com.hedera.node.app.config.ConfigProviderImpl;
import com.hedera.node.config.data.HederaConfig;
import com.swirlds.common.constructable.ConstructableRegistry;
import com.swirlds.common.io.utility.FileUtils;
import com.swirlds.common.platform.NodeId;
import com.swirlds.config.api.ConfigurationBuilder;
import com.swirlds.config.extensions.sources.SystemEnvironmentConfigSource;
import com.swirlds.config.extensions.sources.SystemPropertiesConfigSource;
import com.swirlds.platform.CommandLineArgs;
import com.swirlds.platform.PlatformBuilder;
import com.swirlds.platform.config.legacy.ConfigurationException;
import com.swirlds.platform.config.legacy.LegacyConfigProperties;
import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader;
import com.swirlds.platform.system.Platform;
import com.swirlds.platform.system.SoftwareVersion;
import com.swirlds.platform.system.SwirldMain;
import com.swirlds.platform.system.SwirldState;
import com.swirlds.platform.system.address.AddressBook;
import com.swirlds.platform.util.BootstrapUtils;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.List;
import java.util.Set;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

Expand All @@ -48,7 +64,9 @@ public class ServicesMain implements SwirldMain {
*/
private final SwirldMain delegate;

/** Create a new instance */
/**
* Create a new instance
*/
public ServicesMain() {
final var configProvider = new ConfigProviderImpl(false);
final var hederaConfig = configProvider.getConfiguration().getConfigData(HederaConfig.class);
Expand All @@ -61,25 +79,33 @@ public ServicesMain() {
}
}

/** {@inheritDoc} */
/**
* {@inheritDoc}
*/
@Override
public SoftwareVersion getSoftwareVersion() {
return delegate.getSoftwareVersion();
}

/** {@inheritDoc} */
/**
* {@inheritDoc}
*/
@Override
public void init(@NonNull final Platform ignored, @NonNull final NodeId nodeId) {
delegate.init(ignored, nodeId);
}

/** {@inheritDoc} */
/**
* {@inheritDoc}
*/
@Override
public SwirldState newState() {
return delegate.newState();
}

/** {@inheritDoc} */
/**
* {@inheritDoc}
*/
@Override
public void run() {
delegate.run();
Expand All @@ -95,7 +121,29 @@ public static void main(final String... args) throws Exception {
final var registry = ConstructableRegistry.getInstance();

final Hedera hedera = new Hedera(registry);
final NodeId selfId = args != null && args.length > 0 ? new NodeId(Integer.parseInt(args[0])) : new NodeId(0);

// Determine which node to run locally
// Load config.txt address book file and parse address book
final AddressBook addressBook = loadAddressBook(PlatformBuilder.DEFAULT_CONFIG_FILE_NAME);
// parse command line arguments
final CommandLineArgs commandLineArgs = CommandLineArgs.parse(args);

// Only allow 1 node to be specified by the command line arguments.
if (commandLineArgs.localNodesToStart().size() > 1) {
logger.error(
EXCEPTION.getMarker(),
"Multiple nodes were supplied via the command line. Only one node can be started per java process.");
exitSystem(NODE_ADDRESS_MISMATCH);
}

// get the list of configured nodes from the address book
// for each node in the address book, check if it has a local IP (local to this computer)
// additionally if a command line arg is supplied then limit matching nodes to that node id
final List<NodeId> nodesToRun = getNodesToRun(addressBook, commandLineArgs.localNodesToStart());
// hard exit if no nodes are configured to run
checkNodesToRun(nodesToRun);

final NodeId selfId = ensureSingleNode(nodesToRun, commandLineArgs.localNodesToStart());

final var config = ConfigurationBuilder.create()
.withSource(SystemEnvironmentConfigSource.getInstance())
Expand All @@ -110,4 +158,64 @@ public static void main(final String... args) throws Exception {
platform.start();
hedera.run();
}

/**
* Selects the node to run locally from either the command line arguments or the address book.
*
* @param nodesToRun the list of nodes configured to run based on the address book.
* @param localNodesToStart the node ids specified on the command line.
* @return the node which should be run locally.
* @throws ConfigurationException if more than one node would be started or the requested node is not configured.
*/
private static NodeId ensureSingleNode(
@NonNull final List<NodeId> nodesToRun, @NonNull final Set<NodeId> localNodesToStart) {
requireNonNull(nodesToRun);
requireNonNull(localNodesToStart);
// If no node is specified on the command line and detection by AB IP address is ambiguous, exit.
if (nodesToRun.size() > 1 && localNodesToStart.isEmpty()) {
logger.error(
EXCEPTION.getMarker(),
"Multiple nodes are configured to run. Only one node can be started per java process.");
exitSystem(NODE_ADDRESS_MISMATCH);
throw new ConfigurationException(
"Multiple nodes are configured to run. Only one node can be started per java process.");
}

// If a node is specified on the command line, use that node.
final NodeId requestedNodeId = localNodesToStart.stream().findFirst().orElse(null);

// If a node is specified on the command line but does not have a matching local IP address in the AB, exit.
if (nodesToRun.size() > 1 && !nodesToRun.contains(requestedNodeId)) {
logger.error(
EXCEPTION.getMarker(),
"The requested node id {} is not configured to run. Please check the address book.",
requestedNodeId);
exitSystem(NODE_ADDRESS_MISMATCH);
throw new ConfigurationException(String.format(
"The requested node id %s is not configured to run. Please check the address book.",
requestedNodeId));
}

// Return either the node requested via the command line or the only matching node from the AB.
return requestedNodeId != null ? requestedNodeId : nodesToRun.get(0);
}

/**
* Loads the address book from the specified path.
*
* @param addressBookPath the relative path and file name of the address book.
* @return the address book.
*/
private static AddressBook loadAddressBook(@NonNull final String addressBookPath) {
requireNonNull(addressBookPath);
try {
final LegacyConfigProperties props =
LegacyConfigPropertiesLoader.loadConfigFile(FileUtils.getAbsolutePath(addressBookPath));
return props.getAddressBook();
} catch (final Exception e) {
logger.error(EXCEPTION.getMarker(), "Error loading address book", e);
exitSystem(CONFIGURATION_ERROR);
throw e;
}
}
}
1 change: 1 addition & 0 deletions hedera-node/hedera-app/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
requires com.swirlds.base;
requires com.swirlds.config.extensions;
requires com.swirlds.fcqueue;
requires com.swirlds.logging;
requires grpc.netty;
requires io.grpc;
requires io.netty.handler;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@
package com.hedera.node.app;

import static com.hedera.node.app.service.mono.context.AppsManager.APPS;
import static com.swirlds.platform.system.SystemExitCode.NODE_ADDRESS_MISMATCH;
import static com.swirlds.platform.system.status.PlatformStatus.*;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.verify;

import com.hedera.node.app.service.mono.ServicesApp;
Expand All @@ -43,27 +46,39 @@
import com.hedera.node.app.service.mono.utils.SystemExits;
import com.swirlds.common.notification.NotificationEngine;
import com.swirlds.common.platform.NodeId;
import com.swirlds.platform.config.legacy.ConfigurationException;
import com.swirlds.platform.config.legacy.LegacyConfigProperties;
import com.swirlds.platform.config.legacy.LegacyConfigPropertiesLoader;
import com.swirlds.platform.listeners.PlatformStatusChangeListener;
import com.swirlds.platform.listeners.PlatformStatusChangeNotification;
import com.swirlds.platform.listeners.ReconnectCompleteListener;
import com.swirlds.platform.listeners.StateWriteToDiskCompleteListener;
import com.swirlds.platform.system.Platform;
import com.swirlds.platform.system.SystemExitUtils;
import com.swirlds.platform.system.state.notifications.IssListener;
import com.swirlds.platform.system.state.notifications.NewSignedStateListener;
import com.swirlds.platform.util.BootstrapUtils;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;

@ExtendWith(MockitoExtension.class)
final class ServicesMainTest {
private static final MockedStatic<LegacyConfigPropertiesLoader> legacyConfigPropertiesLoaderMockedStatic =
mockStatic(LegacyConfigPropertiesLoader.class);
private static final MockedStatic<BootstrapUtils> bootstrapUtilsMockedStatic = mockStatic(BootstrapUtils.class);

private final NodeId selfId = new NodeId(123L);
private final NodeId unselfId = new NodeId(666L);

Expand Down Expand Up @@ -130,6 +145,9 @@ final class ServicesMainTest {
@Mock
private RecordStreamManager recordStreamManager;

@Mock
private LegacyConfigProperties legacyConfigProperties;

private final ServicesMain subject = new ServicesMain();

@Test
Expand All @@ -138,6 +156,48 @@ void throwsErrorOnMissingApp() {
Assertions.assertThrows(AssertionError.class, () -> subject.init(platform, unselfId));
}

// no local nodes specified but more than one match in address book
@Test
void hardExitOnTooManyLocalNodes() {
withBadCommandLineArgs();
String[] args = {};

try (MockedStatic<SystemExitUtils> systemExitUtilsMockedStatic = mockStatic(SystemExitUtils.class)) {
assertThatThrownBy(() -> ServicesMain.main(args)).isInstanceOf(ConfigurationException.class);

systemExitUtilsMockedStatic.verify(() -> SystemExitUtils.exitSystem(NODE_ADDRESS_MISMATCH));
}
}

// local node specified which does not match the address book
@Test
void hardExitOnNonMatchingNodeId() {
withBadCommandLineArgs();
String[] args = {"-local", "1234"}; // 1234 does not match anything in address book

try (MockedStatic<SystemExitUtils> systemExitUtilsMockedStatic = mockStatic(SystemExitUtils.class)) {
assertThatThrownBy(() -> ServicesMain.main(args)).isInstanceOf(ConfigurationException.class);

systemExitUtilsMockedStatic.verify(() -> SystemExitUtils.exitSystem(NODE_ADDRESS_MISMATCH));
}
}

// more than one local node specified which matches the address book
@Test
void hardExitOnTooManyMatchingNodes() {
withBadCommandLineArgs();
String[] args = {"-local", "1", "2"}; // both "1" and "2" match entries in address book

try (MockedStatic<SystemExitUtils> systemExitUtilsMockedStatic = mockStatic(SystemExitUtils.class)) {
systemExitUtilsMockedStatic
.when(() -> SystemExitUtils.exitSystem(any()))
.thenThrow(new UnsupportedOperationException());
assertThatThrownBy(() -> ServicesMain.main(args)).isInstanceOf(UnsupportedOperationException.class);

systemExitUtilsMockedStatic.verify(() -> SystemExitUtils.exitSystem(NODE_ADDRESS_MISMATCH));
}
}

@Test
void returnsSerializableVersion() {
assertInstanceOf(SerializableSemVers.class, subject.getSoftwareVersion());
Expand Down Expand Up @@ -257,6 +317,20 @@ void failsHardIfCannotInit() throws NoSuchAlgorithmException {
verify(systemExits).fail(1);
}

private void withBadCommandLineArgs() {
legacyConfigPropertiesLoaderMockedStatic
.when(() -> LegacyConfigPropertiesLoader.loadConfigFile(any()))
.thenReturn(legacyConfigProperties);

List<NodeId> nodeIds = new ArrayList<>();
nodeIds.add(new NodeId(1));
nodeIds.add(new NodeId(2));

bootstrapUtilsMockedStatic
.when(() -> BootstrapUtils.getNodesToRun(any(), any()))
.thenReturn(nodeIds);
}

private void withDoomedApp() {
APPS.save(selfId, app);
given(app.nativeCharset()).willReturn(nativeCharset);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,8 @@ public void start() {
"-Dhedera.workflows.enabled=true",
"-Dprometheus.endpointPortNumber=" + (10000 + nodeId),
"com.hedera.node.app.ServicesMain",
"" + nodeId)
"-local",
Long.toString(nodeId))
.directory(workingDir.toFile())
.redirectOutput(stdout.toFile())
.redirectError(stderr.toFile());
Expand Down
Loading