diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClient.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClient.java index fe781a16..0c1ae0af 100644 --- a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClient.java +++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClient.java @@ -22,13 +22,22 @@ public abstract class NodeClient { private Agent[] agents; private EventLoopGroup workerGroup; private Session session; + protected NodeClientConfig config; public NodeClient() { } - public NodeClient(HandshakeAgent handshakeAgent, Agent... agents) { + /** + * Constructor with NodeClientConfig for configurable connection behavior. + * + * @param config the connection configuration + * @param handshakeAgent the handshake agent + * @param agents the protocol agents + */ + public NodeClient(NodeClientConfig config, HandshakeAgent handshakeAgent, Agent... agents) { this.sessionListener = new SessionListenerAdapter(); + this.config = config != null ? config : NodeClientConfig.defaultConfig(); this.handshakeAgent = handshakeAgent; this.agents = agents; @@ -37,6 +46,16 @@ public NodeClient(HandshakeAgent handshakeAgent, Agent... agents) { attachHandshakeListener(); } + /** + * Constructor with default configuration (for backward compatibility). + * + * @param handshakeAgent the handshake agent + * @param agents the protocol agents + */ + public NodeClient(HandshakeAgent handshakeAgent, Agent... agents) { + this(NodeClientConfig.defaultConfig(), handshakeAgent, agents); + } + private void attachHandshakeListener() { handshakeAgent.addListener(new HandshakeAgentListener() { @Override @@ -80,7 +99,7 @@ public void initChannel(Channel ch) SocketAddress socketAddress = createSocketAddress(); - session = new Session(socketAddress, b, handshakeAgent, agents); + session = new Session(socketAddress, b, config, handshakeAgent, agents); session.setSessionListener(sessionListener); session.start(); } catch (Exception e) { @@ -92,6 +111,16 @@ public boolean isRunning() { return session != null; } + /** + * Get the current NodeClientConfig. + * This is primarily for use by subclasses to access configuration options. + * + * @return the current configuration + */ + protected NodeClientConfig getConfig() { + return config; + } + public void shutdown() { if (showConnectionLog()) log.info("Shutdown connection !!!"); @@ -190,6 +219,7 @@ public void connected() { } private boolean showConnectionLog() { - return log.isDebugEnabled() || (handshakeAgent != null && !handshakeAgent.isSuppressConnectionInfoLog()); + return (config != null && config.isEnableConnectionLogging()) && + (log.isDebugEnabled() || (handshakeAgent != null && !handshakeAgent.isSuppressConnectionInfoLog())); } } diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfig.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfig.java new file mode 100644 index 00000000..f9c2c8d1 --- /dev/null +++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfig.java @@ -0,0 +1,74 @@ +package com.bloxbean.cardano.yaci.core.network; + +import lombok.*; + +/** + * Configuration for NodeClient connection behavior. + * This class controls reconnection policies, retry strategies, and logging preferences. + * + *

Use the builder pattern to create instances:

+ *
{@code
+ * NodeClientConfig config = NodeClientConfig.builder()
+ *     .autoReconnect(false)
+ *     .maxRetryAttempts(3)
+ *     .build();
+ * }
+ * + */ +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@EqualsAndHashCode +@ToString +@Builder(toBuilder = true) +public class NodeClientConfig { + + /** + * Whether to automatically reconnect when connection is lost. + * Default: true (maintains backward compatibility for long-running processes like indexers) + * Set to false for short-lived connections (e.g., peer discovery) + */ + @Builder.Default + private final boolean autoReconnect = true; + + /** + * Initial delay in milliseconds between retry attempts during connection establishment. + * Default: 8000ms (8 seconds) + */ + @Builder.Default + private final int initialRetryDelayMs = 8000; + + /** + * Maximum number of retry attempts before giving up. + * Default: Integer.MAX_VALUE (unlimited retries for backward compatibility) + */ + @Builder.Default + private final int maxRetryAttempts = Integer.MAX_VALUE; + + /** + * Whether to enable connection-related logging (connect, disconnect, reconnect messages). + * Default: true + */ + @Builder.Default + private final boolean enableConnectionLogging = true; + + /** + * Connection timeout in milliseconds for establishing TCP connections. + * This configures Netty's CONNECT_TIMEOUT_MILLIS option. + * Default: 30000ms (30 seconds) + * + * For short-lived connections like peer discovery, consider using a lower value (e.g., 10000ms). + * For long-running connections, the default is appropriate. + */ + @Builder.Default + private final int connectionTimeoutMs = 30000; + + /** + * Creates a default configuration with backward-compatible settings. + * This is equivalent to calling {@code NodeClientConfig.builder().build()} + * + * @return a new NodeClientConfig with default values + */ + public static NodeClientConfig defaultConfig() { + return NodeClientConfig.builder().build(); + } +} diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/Session.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/Session.java index 1d49b99b..03b0b1af 100644 --- a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/Session.java +++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/Session.java @@ -18,21 +18,47 @@ class Session implements Disposable { private final SocketAddress socketAddress; private final Bootstrap clientBootstrap; private Channel activeChannel; - private final AtomicBoolean shouldReconnect; //Not used currently + private final AtomicBoolean shouldReconnect; private final HandshakeAgent handshakeAgent; private final Agent[] agents; + private final NodeClientConfig config; private SessionListener sessionListener; - public Session(SocketAddress socketAddress, Bootstrap clientBootstrap, HandshakeAgent handshakeAgent, Agent[] agents) { + /** + * Constructor with NodeClientConfig for configurable connection behavior. + * + * @param socketAddress the socket address to connect to + * @param clientBootstrap the Netty bootstrap + * @param config the connection configuration + * @param handshakeAgent the handshake agent + * @param agents the protocol agents + */ + public Session(SocketAddress socketAddress, Bootstrap clientBootstrap, NodeClientConfig config, + HandshakeAgent handshakeAgent, Agent[] agents) { this.socketAddress = socketAddress; this.clientBootstrap = clientBootstrap; - this.shouldReconnect = new AtomicBoolean(true); + this.config = config != null ? config : NodeClientConfig.defaultConfig(); + this.shouldReconnect = new AtomicBoolean(this.config.isAutoReconnect()); this.handshakeAgent = handshakeAgent; this.agents = agents; } + /** + * Constructor with default configuration (for backward compatibility). + * + * @param socketAddress the socket address to connect to + * @param clientBootstrap the Netty bootstrap + * @param handshakeAgent the handshake agent + * @param agents the protocol agents + * @deprecated Use {@link #Session(SocketAddress, Bootstrap, NodeClientConfig, HandshakeAgent, Agent[])} instead + */ + @Deprecated + public Session(SocketAddress socketAddress, Bootstrap clientBootstrap, HandshakeAgent handshakeAgent, Agent[] agents) { + this(socketAddress, clientBootstrap, NodeClientConfig.defaultConfig(), handshakeAgent, agents); + } + public void setSessionListener(SessionListener sessionListener) { if (this.sessionListener != null) throw new RuntimeException("SessionListener is already there. Can't set another SessionListener"); @@ -43,15 +69,21 @@ public void setSessionListener(SessionListener sessionListener) { public Disposable start() throws InterruptedException { //Create a new connectFuture ChannelFuture connectFuture = null; - while (connectFuture == null && shouldReconnect.get()) { + // Always try to connect at least once, then retry only if shouldReconnect is true + do { try { connectFuture = clientBootstrap.connect(socketAddress).sync(); } catch (Exception e) { log.error("Connection failed", e); - Thread.sleep(8000); - log.debug("Trying to reconnect !!!"); + if (shouldReconnect.get()) { + Thread.sleep(config.getInitialRetryDelayMs()); + log.debug("Trying to reconnect !!!"); + } else { + // If auto-reconnect is disabled, fail fast + throw e; + } } - } + } while (connectFuture == null && shouldReconnect.get()); handshakeAgent.reset(); for (Agent agent: agents) { @@ -134,6 +166,7 @@ public void handshake() { } private boolean showConnectionLog() { - return log.isDebugEnabled() || (handshakeAgent != null && !handshakeAgent.isSuppressConnectionInfoLog()); + return config.isEnableConnectionLogging() && + (log.isDebugEnabled() || (handshakeAgent != null && !handshakeAgent.isSuppressConnectionInfoLog())); } } diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/TCPNodeClient.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/TCPNodeClient.java index 1e25053f..b62a6f7c 100644 --- a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/TCPNodeClient.java +++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/TCPNodeClient.java @@ -21,12 +21,33 @@ public class TCPNodeClient extends NodeClient { private String host; private int port; - public TCPNodeClient(String host, int port, HandshakeAgent handshakeAgent, Agent... agents) { - super(handshakeAgent, agents); + /** + * Constructor with NodeClientConfig for configurable connection behavior. + * + * @param host the host to connect to + * @param port the port to connect to + * @param config the connection configuration + * @param handshakeAgent the handshake agent + * @param agents the protocol agents + */ + public TCPNodeClient(String host, int port, NodeClientConfig config, HandshakeAgent handshakeAgent, Agent... agents) { + super(config, handshakeAgent, agents); this.host = host; this.port = port; } + /** + * Constructor with default configuration (for backward compatibility). + * + * @param host the host to connect to + * @param port the port to connect to + * @param handshakeAgent the handshake agent + * @param agents the protocol agents + */ + public TCPNodeClient(String host, int port, HandshakeAgent handshakeAgent, Agent... agents) { + this(host, port, NodeClientConfig.defaultConfig(), handshakeAgent, agents); + } + @Override protected SocketAddress createSocketAddress() { return new InetSocketAddress(host, port); @@ -46,5 +67,6 @@ protected Class getChannelClass() { protected void configureChannel(Bootstrap bootstrap) { bootstrap.option(ChannelOption.SO_KEEPALIVE, true); bootstrap.option(ChannelOption.TCP_NODELAY, true); + bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConfig().getConnectionTimeoutMs()); } } diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/UnixSocketNodeClient.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/UnixSocketNodeClient.java index 65d4dbce..b7e31bbf 100644 --- a/core/src/main/java/com/bloxbean/cardano/yaci/core/network/UnixSocketNodeClient.java +++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/network/UnixSocketNodeClient.java @@ -4,6 +4,7 @@ import com.bloxbean.cardano.yaci.core.protocol.handshake.HandshakeAgent; import com.bloxbean.cardano.yaci.core.util.OSUtil; import io.netty.bootstrap.Bootstrap; +import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.epoll.EpollDomainSocketChannel; import io.netty.channel.epoll.EpollEventLoopGroup; @@ -21,11 +22,30 @@ public class UnixSocketNodeClient extends NodeClient { private String nodeSocketFile; - public UnixSocketNodeClient(String nodeSocketFile, HandshakeAgent handshakeAgent, Agent... agents) { - super(handshakeAgent, agents); + /** + * Constructor with NodeClientConfig for configurable connection behavior. + * + * @param nodeSocketFile the path to the Unix domain socket + * @param config the connection configuration + * @param handshakeAgent the handshake agent + * @param agents the protocol agents + */ + public UnixSocketNodeClient(String nodeSocketFile, NodeClientConfig config, HandshakeAgent handshakeAgent, Agent... agents) { + super(config, handshakeAgent, agents); this.nodeSocketFile = nodeSocketFile; } + /** + * Constructor with default configuration (for backward compatibility). + * + * @param nodeSocketFile the path to the Unix domain socket + * @param handshakeAgent the handshake agent + * @param agents the protocol agents + */ + public UnixSocketNodeClient(String nodeSocketFile, HandshakeAgent handshakeAgent, Agent... agents) { + this(nodeSocketFile, NodeClientConfig.defaultConfig(), handshakeAgent, agents); + } + @Override protected SocketAddress createSocketAddress() { return new DomainSocketAddress(new File(nodeSocketFile)); @@ -49,6 +69,6 @@ protected Class getChannelClass() { @Override protected void configureChannel(Bootstrap bootstrap) { - + bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, getConfig().getConnectionTimeoutMs()); } } diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/PeerSharingAgent.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/PeerSharingAgent.java index ac1a6628..847d1302 100644 --- a/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/PeerSharingAgent.java +++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/PeerSharingAgent.java @@ -14,7 +14,7 @@ public class PeerSharingAgent extends Agent { public static final int DEFAULT_REQUEST_AMOUNT = 10; public static final int MAX_REQUEST_AMOUNT = 100; - public static final long RESPONSE_TIMEOUT_MS = 30000; // 30 seconds + public static final long RESPONSE_TIMEOUT_MS = 5000; private boolean shutDown; private Queue requestQueue; diff --git a/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/serializers/PeerSharingSerializers.java b/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/serializers/PeerSharingSerializers.java index 983d32f4..953a7ab7 100644 --- a/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/serializers/PeerSharingSerializers.java +++ b/core/src/main/java/com/bloxbean/cardano/yaci/core/protocol/peersharing/serializers/PeerSharingSerializers.java @@ -10,6 +10,7 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; @@ -100,7 +101,7 @@ private Array serializePeerAddress(PeerAddress peerAddress) { array.add(new UnsignedInteger(0)); // Convert 4 bytes to word32 - ByteBuffer buffer = ByteBuffer.wrap(addressBytes); + ByteBuffer buffer = ByteBuffer.wrap(addressBytes).order(ByteOrder.LITTLE_ENDIAN); long ipv4AsLong = buffer.getInt() & 0xFFFFFFFFL; // Convert to unsigned array.add(new UnsignedInteger(ipv4AsLong)); @@ -111,7 +112,7 @@ private Array serializePeerAddress(PeerAddress peerAddress) { array.add(new UnsignedInteger(1)); // Convert 16 bytes to 4 word32s - ByteBuffer buffer = ByteBuffer.wrap(addressBytes); + ByteBuffer buffer = ByteBuffer.wrap(addressBytes).order(ByteOrder.LITTLE_ENDIAN); for (int i = 0; i < 4; i++) { long word32 = buffer.getInt() & 0xFFFFFFFFL; // Convert to unsigned array.add(new UnsignedInteger(word32)); @@ -136,7 +137,7 @@ private PeerAddress deserializePeerAddress(DataItem di) { int port = ((UnsignedInteger) dataItemList.get(2)).getValue().intValue(); // Convert word32 back to IPv4 address - byte[] addressBytes = ByteBuffer.allocate(4).putInt((int) ipv4Long).array(); + byte[] addressBytes = ByteBuffer.allocate(4).order(ByteOrder.LITTLE_ENDIAN).putInt((int) ipv4Long).array(); try { InetAddress inetAddress = InetAddress.getByAddress(addressBytes); return PeerAddress.ipv4(inetAddress.getHostAddress(), port); @@ -146,7 +147,7 @@ private PeerAddress deserializePeerAddress(DataItem di) { } else if (type == 1) { // IPv6 byte[] addressBytes = new byte[16]; - ByteBuffer buffer = ByteBuffer.wrap(addressBytes); + ByteBuffer buffer = ByteBuffer.wrap(addressBytes).order(ByteOrder.LITTLE_ENDIAN); // Convert 4 word32s back to 16 bytes for (int i = 1; i <= 4; i++) { @@ -154,7 +155,11 @@ private PeerAddress deserializePeerAddress(DataItem di) { buffer.putInt((int) word32); } - int port = ((UnsignedInteger) dataItemList.get(5)).getValue().intValue(); + // Handle both V11-12 (8 elements) and V13+ (6 elements) formats + // V11-12: [type, addr1-4, flowInfo, scopeId, port] - port at index 7 + // V13+: [type, addr1-4, port] - port at index 5 + int portIndex = dataItemList.size() == 8 ? 7 : 5; + int port = ((UnsignedInteger) dataItemList.get(portIndex)).getValue().intValue(); try { InetAddress inetAddress = InetAddress.getByAddress(addressBytes); diff --git a/core/src/test/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfigTest.java b/core/src/test/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfigTest.java new file mode 100644 index 00000000..ad5d1ddd --- /dev/null +++ b/core/src/test/java/com/bloxbean/cardano/yaci/core/network/NodeClientConfigTest.java @@ -0,0 +1,241 @@ +package com.bloxbean.cardano.yaci.core.network; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class NodeClientConfigTest { + + @Test + void testDefaultConfig() { + NodeClientConfig config = NodeClientConfig.defaultConfig(); + + assertNotNull(config); + assertTrue(config.isAutoReconnect(), "Auto-reconnect should be enabled by default"); + assertEquals(8000, config.getInitialRetryDelayMs(), "Default retry delay should be 8000ms"); + assertEquals(Integer.MAX_VALUE, config.getMaxRetryAttempts(), "Default max retry attempts should be unlimited"); + assertTrue(config.isEnableConnectionLogging(), "Connection logging should be enabled by default"); + assertEquals(30000, config.getConnectionTimeoutMs(), "Default connection timeout should be 30000ms"); + } + + @Test + void testBuilderWithDefaults() { + NodeClientConfig config = NodeClientConfig.builder().build(); + + assertNotNull(config); + assertTrue(config.isAutoReconnect()); + assertEquals(8000, config.getInitialRetryDelayMs()); + assertEquals(Integer.MAX_VALUE, config.getMaxRetryAttempts()); + assertTrue(config.isEnableConnectionLogging()); + } + + @Test + void testBuilderWithCustomAutoReconnect() { + NodeClientConfig config = NodeClientConfig.builder() + .autoReconnect(false) + .build(); + + assertFalse(config.isAutoReconnect(), "Auto-reconnect should be disabled"); + // Other fields should still have default values + assertEquals(8000, config.getInitialRetryDelayMs()); + assertEquals(Integer.MAX_VALUE, config.getMaxRetryAttempts()); + assertTrue(config.isEnableConnectionLogging()); + } + + @Test + void testBuilderWithAllCustomValues() { + NodeClientConfig config = NodeClientConfig.builder() + .autoReconnect(false) + .initialRetryDelayMs(5000) + .maxRetryAttempts(3) + .enableConnectionLogging(false) + .build(); + + assertFalse(config.isAutoReconnect()); + assertEquals(5000, config.getInitialRetryDelayMs()); + assertEquals(3, config.getMaxRetryAttempts()); + assertFalse(config.isEnableConnectionLogging()); + } + + @Test + void testToBuilder() { + NodeClientConfig original = NodeClientConfig.builder() + .autoReconnect(true) + .initialRetryDelayMs(10000) + .maxRetryAttempts(5) + .enableConnectionLogging(true) + .build(); + + // Modify one field using toBuilder + NodeClientConfig modified = original.toBuilder() + .autoReconnect(false) + .build(); + + // Original should be unchanged + assertTrue(original.isAutoReconnect()); + assertEquals(10000, original.getInitialRetryDelayMs()); + assertEquals(5, original.getMaxRetryAttempts()); + assertTrue(original.isEnableConnectionLogging()); + + // Modified should have the change + assertFalse(modified.isAutoReconnect()); + // Other fields should be copied from original + assertEquals(10000, modified.getInitialRetryDelayMs()); + assertEquals(5, modified.getMaxRetryAttempts()); + assertTrue(modified.isEnableConnectionLogging()); + } + + @Test + void testImmutability() { + NodeClientConfig config = NodeClientConfig.builder() + .autoReconnect(true) + .build(); + + // Verify all fields are final (by attempting to use reflection - should fail or show final) + assertNotNull(config); + assertTrue(config.isAutoReconnect()); + + // Create new config with modified values + NodeClientConfig newConfig = config.toBuilder() + .autoReconnect(false) + .build(); + + // Original should still be true + assertTrue(config.isAutoReconnect()); + // New should be false + assertFalse(newConfig.isAutoReconnect()); + } + + @Test + void testEqualsAndHashCode() { + NodeClientConfig config1 = NodeClientConfig.builder() + .autoReconnect(false) + .initialRetryDelayMs(5000) + .maxRetryAttempts(3) + .enableConnectionLogging(false) + .build(); + + NodeClientConfig config2 = NodeClientConfig.builder() + .autoReconnect(false) + .initialRetryDelayMs(5000) + .maxRetryAttempts(3) + .enableConnectionLogging(false) + .build(); + + NodeClientConfig config3 = NodeClientConfig.builder() + .autoReconnect(true) // Different value + .initialRetryDelayMs(5000) + .maxRetryAttempts(3) + .enableConnectionLogging(false) + .build(); + + // Same values should be equal + assertEquals(config1, config2); + assertEquals(config1.hashCode(), config2.hashCode()); + + // Different values should not be equal + assertNotEquals(config1, config3); + } + + @Test + void testToString() { + NodeClientConfig config = NodeClientConfig.builder() + .autoReconnect(false) + .initialRetryDelayMs(5000) + .maxRetryAttempts(3) + .enableConnectionLogging(false) + .build(); + + String toString = config.toString(); + + assertNotNull(toString); + assertTrue(toString.contains("autoReconnect")); + assertTrue(toString.contains("false")); + assertTrue(toString.contains("5000")); + assertTrue(toString.contains("3")); + } + + @Test + void testPeerDiscoveryUseCase() { + // Test configuration for peer discovery (short-lived connections) + NodeClientConfig config = NodeClientConfig.builder() + .autoReconnect(false) + .maxRetryAttempts(3) + .build(); + + assertFalse(config.isAutoReconnect(), "Peer discovery should not auto-reconnect"); + assertEquals(3, config.getMaxRetryAttempts(), "Should limit retry attempts"); + } + + @Test + void testIndexerUseCase() { + // Test configuration for long-running indexer (should use defaults) + NodeClientConfig config = NodeClientConfig.defaultConfig(); + + assertTrue(config.isAutoReconnect(), "Indexer should auto-reconnect"); + assertEquals(Integer.MAX_VALUE, config.getMaxRetryAttempts(), "Should retry indefinitely"); + } + + @Test + void testCustomRetryStrategy() { + NodeClientConfig config = NodeClientConfig.builder() + .autoReconnect(true) + .initialRetryDelayMs(1000) // 1 second + .maxRetryAttempts(10) + .build(); + + assertTrue(config.isAutoReconnect()); + assertEquals(1000, config.getInitialRetryDelayMs()); + assertEquals(10, config.getMaxRetryAttempts()); + } + + @Test + void testDisableLogging() { + NodeClientConfig config = NodeClientConfig.builder() + .enableConnectionLogging(false) + .build(); + + assertFalse(config.isEnableConnectionLogging(), "Connection logging should be disabled"); + // Other fields should have defaults + assertTrue(config.isAutoReconnect()); + } + + @Test + void testCustomConnectionTimeout() { + NodeClientConfig config = NodeClientConfig.builder() + .connectionTimeoutMs(10000) + .build(); + + assertEquals(10000, config.getConnectionTimeoutMs(), "Connection timeout should be 10000ms"); + // Other fields should have defaults + assertTrue(config.isAutoReconnect()); + assertEquals(8000, config.getInitialRetryDelayMs()); + } + + @Test + void testPeerDiscoveryOptimizedConfig() { + // Test configuration optimized for peer discovery + NodeClientConfig config = NodeClientConfig.builder() + .autoReconnect(false) + .connectionTimeoutMs(10000) // Faster timeout + .build(); + + assertFalse(config.isAutoReconnect(), "Auto-reconnect should be disabled for peer discovery"); + assertEquals(10000, config.getConnectionTimeoutMs(), "Should use faster 10-second timeout"); + } + + @Test + void testConnectionTimeoutInToBuilder() { + NodeClientConfig original = NodeClientConfig.builder() + .connectionTimeoutMs(5000) + .build(); + + NodeClientConfig modified = original.toBuilder() + .connectionTimeoutMs(15000) + .build(); + + assertEquals(5000, original.getConnectionTimeoutMs(), "Original should be unchanged"); + assertEquals(15000, modified.getConnectionTimeoutMs(), "Modified should have new timeout"); + } +} diff --git a/helper/src/integrationTest/java/com/bloxbean/cardano/yaci/helper/PeerDiscoveryIT.java b/helper/src/integrationTest/java/com/bloxbean/cardano/yaci/helper/PeerDiscoveryIT.java index f8eba137..39285873 100644 --- a/helper/src/integrationTest/java/com/bloxbean/cardano/yaci/helper/PeerDiscoveryIT.java +++ b/helper/src/integrationTest/java/com/bloxbean/cardano/yaci/helper/PeerDiscoveryIT.java @@ -66,7 +66,6 @@ public void testPeerDiscoveryPreview() { assertNotNull(peer.getAddress()); assertTrue(peer.getPort() > 0 && peer.getPort() <= 65535); }); - } finally { peerDiscovery.shutdown(); } @@ -218,4 +217,49 @@ public void testPeerAddressValidation() { peerDiscovery.shutdown(); } } + + @Test + public void testGetAcceptVersionInfo() { + PeerDiscovery peerDiscovery = new PeerDiscovery( + Constants.PREPROD_PUBLIC_RELAY_ADDR, + Constants.PREPROD_PUBLIC_RELAY_PORT, + Constants.PREPROD_PROTOCOL_MAGIC, 5); + + try { + // Discover peers to trigger handshake + Mono> peersMono = peerDiscovery.discover(); + peersMono.block(Duration.ofSeconds(30)); + + // Verify we got version information after successful handshake + assertTrue(peerDiscovery.getAcceptVersion().isPresent(), + "Should have version info after successful handshake"); + + var acceptVersion = peerDiscovery.getAcceptVersion().get(); + assertNotNull(acceptVersion.getVersionNumber(), + "Version number should not be null"); + assertNotNull(acceptVersion.getVersionData(), + "Version data should not be null"); + + System.out.println("\n=== Protocol Version Information ==="); + System.out.println("Protocol Version Number: " + acceptVersion.getVersionNumber()); + + // If it's N2N version data, extract additional details + if (acceptVersion.getVersionData() instanceof com.bloxbean.cardano.yaci.core.protocol.handshake.messages.N2NVersionData) { + var versionData = (com.bloxbean.cardano.yaci.core.protocol.handshake.messages.N2NVersionData) acceptVersion.getVersionData(); + + System.out.println("Network Magic: " + versionData.getNetworkMagic()); + System.out.println("Initiator Only Mode: " + versionData.getInitiatorOnlyDiffusionMode()); + System.out.println("Peer Sharing Enabled: " + (versionData.getPeerSharing() != 0)); + System.out.println("Query Support: " + versionData.getQuery()); + System.out.println("====================================\n"); + + // Validate the network magic matches what we expect + assertEquals(Constants.PREPROD_PROTOCOL_MAGIC, versionData.getNetworkMagic(), + "Network magic should match preprod"); + } + + } finally { + peerDiscovery.shutdown(); + } + } } diff --git a/helper/src/main/java/com/bloxbean/cardano/yaci/helper/PeerDiscovery.java b/helper/src/main/java/com/bloxbean/cardano/yaci/helper/PeerDiscovery.java index 865de9e9..21ab8e32 100644 --- a/helper/src/main/java/com/bloxbean/cardano/yaci/helper/PeerDiscovery.java +++ b/helper/src/main/java/com/bloxbean/cardano/yaci/helper/PeerDiscovery.java @@ -1,9 +1,11 @@ package com.bloxbean.cardano.yaci.helper; import com.bloxbean.cardano.yaci.core.network.NodeClient; +import com.bloxbean.cardano.yaci.core.network.NodeClientConfig; import com.bloxbean.cardano.yaci.core.network.TCPNodeClient; import com.bloxbean.cardano.yaci.core.protocol.handshake.HandshakeAgent; import com.bloxbean.cardano.yaci.core.protocol.handshake.HandshakeAgentListener; +import com.bloxbean.cardano.yaci.core.protocol.handshake.messages.AcceptVersion; import com.bloxbean.cardano.yaci.core.protocol.handshake.messages.N2NVersionData; import com.bloxbean.cardano.yaci.core.protocol.handshake.messages.Reason; import com.bloxbean.cardano.yaci.core.protocol.handshake.messages.VersionTable; @@ -16,6 +18,7 @@ import reactor.core.publisher.Mono; import java.util.List; +import java.util.Optional; import java.util.function.Consumer; @Slf4j @@ -29,20 +32,59 @@ public class PeerDiscovery extends ReactiveFetcher> { private VersionTable versionTable; private final String peerRequestKey = "PEER_REQUEST"; private int requestAmount = PeerSharingAgent.DEFAULT_REQUEST_AMOUNT; + private NodeClientConfig nodeClientConfig; + private AcceptVersion acceptVersion; + + /** + * Constructor with default settings optimized for peer discovery. + * Uses 10-second connection timeout and disables auto-reconnect for fast failure. + */ public PeerDiscovery(String host, int port, long protocolMagic) { this(host, port, protocolMagic, PeerSharingAgent.DEFAULT_REQUEST_AMOUNT); } + /** + * Constructor with custom request amount and default connection settings. + * Uses 10-second connection timeout and disables auto-reconnect for fast failure. + */ public PeerDiscovery(String host, int port, long protocolMagic, int requestAmount) { + this(host, port, protocolMagic, requestAmount, createDefaultPeerDiscoveryConfig()); + } + + /** + * Constructor with full control over connection behavior via NodeClientConfig. + * This allows customization of connection timeout, auto-reconnect, retry attempts, etc. + * + * @param host the host to connect to + * @param port the port to connect to + * @param protocolMagic the protocol magic (network identifier) + * @param requestAmount the number of peers to request + * @param nodeClientConfig the connection configuration + */ + public PeerDiscovery(String host, int port, long protocolMagic, int requestAmount, NodeClientConfig nodeClientConfig) { this.host = host; this.port = port; this.protocolMagic = protocolMagic; this.requestAmount = Math.min(Math.max(requestAmount, 1), PeerSharingAgent.MAX_REQUEST_AMOUNT); + this.nodeClientConfig = nodeClientConfig; this.versionTable = N2NVersionTableConstant.v11AndAbove(protocolMagic, false, 1, false); init(); } + /** + * Creates default NodeClientConfig optimized for peer discovery: + * - Auto-reconnect disabled (fail fast) + * - 10-second connection timeout (faster than default 30s) + * - Connection logging enabled + */ + private static NodeClientConfig createDefaultPeerDiscoveryConfig() { + return NodeClientConfig.builder() + .autoReconnect(false) + .connectionTimeoutMs(10000) // 10 seconds for peer discovery + .build(); + } + private void init() { handshakeAgent = new HandshakeAgent(versionTable); peerSharingAgent = new PeerSharingAgent(); @@ -74,8 +116,11 @@ public void handshakeOk() { if (versionData.getPeerSharing() == 0) { log.warn("Peer sharing is disabled on remote node {}:{}", host, port); } + + acceptVersion = handshakeAgent.getProtocolVersion(); } else { log.warn("Could not determine peer sharing support for {}:{}", host, port); + acceptVersion = null; } peerSharingAgent.sendNextMessage(); @@ -87,7 +132,8 @@ public void handshakeError(Reason reason) { } }); - nodeClient = new TCPNodeClient(host, port, handshakeAgent, peerSharingAgent); + // Use the configured NodeClientConfig (optimized for peer discovery by default) + nodeClient = new TCPNodeClient(host, port, nodeClientConfig, handshakeAgent, peerSharingAgent); } @Override @@ -180,4 +226,8 @@ public String getHost() { public int getPort() { return port; } + + public Optional getAcceptVersion() { + return Optional.ofNullable(acceptVersion); + } }