diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/ClientFeatureFlags.java b/db-client-java/src/main/java/com/eventstore/dbclient/ClientFeatureFlags.java new file mode 100644 index 00000000..41925b41 --- /dev/null +++ b/db-client-java/src/main/java/com/eventstore/dbclient/ClientFeatureFlags.java @@ -0,0 +1,10 @@ +package com.eventstore.dbclient; + +public final class ClientFeatureFlags { + /** + * Enables direct DNS name resolution, retrieving all IP addresses associated with a given hostname. This + * functionality was initially implemented to support the now-deprecated TCP API. It is particularly useful in + * scenarios involving clusters, where node discovery is enabled. + */ + public static final String DnsLookup = "dns-lookup"; +} diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/ClusterDiscovery.java b/db-client-java/src/main/java/com/eventstore/dbclient/ClusterDiscovery.java index 9abd66e8..76899d7e 100644 --- a/db-client-java/src/main/java/com/eventstore/dbclient/ClusterDiscovery.java +++ b/db-client-java/src/main/java/com/eventstore/dbclient/ClusterDiscovery.java @@ -1,8 +1,13 @@ package com.eventstore.dbclient; +import com.eventstore.dbclient.resolution.DeferredNodeResolution; +import com.eventstore.dbclient.resolution.DeprecatedNodeResolution; +import com.eventstore.dbclient.resolution.FixedSeedsNodeResolution; +import com.eventstore.dbclient.resolution.NodeResolution; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.*; import java.util.concurrent.CompletableFuture; @@ -13,15 +18,18 @@ class ClusterDiscovery implements Discovery { private static final Logger logger = LoggerFactory.getLogger(ClusterDiscovery.class); private final NodeSelector nodeSelector; - private final List seeds; + private final NodeResolution resolution; ClusterDiscovery(EventStoreDBClientSettings settings) { this.nodeSelector = new NodeSelector(settings.getNodePreference()); if (settings.isDnsDiscover()) { - this.seeds = Collections.singletonList(settings.getHosts()[0]); + if (settings.getFeatures().contains(ClientFeatureFlags.DnsLookup)) + this.resolution = new DeprecatedNodeResolution(settings.getHosts()[0]); + else + this.resolution = new DeferredNodeResolution(settings.getHosts()[0]); } else { - this.seeds = Arrays.asList(settings.getHosts()); + this.resolution = new FixedSeedsNodeResolution(settings.getHosts()); } } @@ -43,7 +51,7 @@ public CompletableFuture run(ConnectionState state) { } void discover(ConnectionState state) { - List candidates = new ArrayList<>(this.seeds); + List candidates = resolution.resolve(); if (candidates.size() > 1) { Collections.shuffle(candidates); diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionSettingsBuilder.java b/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionSettingsBuilder.java index 49571de8..8ea7c00c 100644 --- a/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionSettingsBuilder.java +++ b/db-client-java/src/main/java/com/eventstore/dbclient/ConnectionSettingsBuilder.java @@ -31,6 +31,7 @@ public class ConnectionSettingsBuilder { private Long _defaultDeadline = null; private List _interceptors = new ArrayList<>(); private String _tlsCaFile = null; + private Set _features = new HashSet<>(); ConnectionSettingsBuilder() {} @@ -54,7 +55,8 @@ public EventStoreDBClientSettings buildConnectionSettings() { _keepAliveInterval, _defaultDeadline, _interceptors, - _tlsCaFile); + _tlsCaFile, + _features); } /** @@ -219,6 +221,22 @@ public ConnectionSettingsBuilder tlsCaFile(String filepath) { return this; } + /** + * Add feature flags. + */ + public ConnectionSettingsBuilder features(String... features) { + this._features.addAll(Arrays.asList(features)); + return this; + } + + /** + * Add feature flag. + */ + public ConnectionSettingsBuilder feature(String feature) { + this._features.add(feature); + return this; + } + void parseGossipSeed(String host) { String[] hostParts = host.split(":"); @@ -436,6 +454,10 @@ static EventStoreDBClientSettings parseFromUrl(ConnectionSettingsBuilder builder userKeyFile = entry[1]; break; + case "feature": + builder._features.add(value); + break; + default: logger.warn(String.format("Unknown setting '%s' is ignored", entry[0])); break; diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientSettings.java b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientSettings.java index 4e198b25..8d044e61 100644 --- a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientSettings.java +++ b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClientSettings.java @@ -4,6 +4,7 @@ import java.net.InetSocketAddress; import java.util.List; +import java.util.Set; /** * Gathers all the settings related to a gRPC client with an EventStoreDB database. @@ -39,6 +40,7 @@ public class EventStoreDBClientSettings { private final Long defaultDeadline; private final List interceptors; private final String tlsCaFile; + private final Set features; /** * If the dns discovery is enabled. @@ -160,6 +162,11 @@ public String getTlsCaFile() { return tlsCaFile; } + /** + * Feature flags + */ + public Set getFeatures() { return features; } + EventStoreDBClientSettings( boolean dnsDiscover, int maxDiscoverAttempts, @@ -175,7 +182,8 @@ public String getTlsCaFile() { long keepAliveInterval, Long defaultDeadline, List interceptors, - String tlsCaFile + String tlsCaFile, + Set features ) { this.dnsDiscover = dnsDiscover; this.maxDiscoverAttempts = maxDiscoverAttempts; @@ -192,6 +200,7 @@ public String getTlsCaFile() { this.defaultDeadline = defaultDeadline; this.interceptors = interceptors; this.tlsCaFile = tlsCaFile; + this.features = features; } /** diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/resolution/DeferredNodeResolution.java b/db-client-java/src/main/java/com/eventstore/dbclient/resolution/DeferredNodeResolution.java new file mode 100644 index 00000000..56cb39f9 --- /dev/null +++ b/db-client-java/src/main/java/com/eventstore/dbclient/resolution/DeferredNodeResolution.java @@ -0,0 +1,18 @@ +package com.eventstore.dbclient.resolution; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; + +public class DeferredNodeResolution implements NodeResolution { + private final InetSocketAddress address; + + public DeferredNodeResolution(InetSocketAddress address) { + this.address = address; + } + + @Override + public List resolve() { + return Collections.singletonList(address); + } +} diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/resolution/DeprecatedNodeResolution.java b/db-client-java/src/main/java/com/eventstore/dbclient/resolution/DeprecatedNodeResolution.java new file mode 100644 index 00000000..17b861b0 --- /dev/null +++ b/db-client-java/src/main/java/com/eventstore/dbclient/resolution/DeprecatedNodeResolution.java @@ -0,0 +1,27 @@ +package com.eventstore.dbclient.resolution; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +public class DeprecatedNodeResolution implements NodeResolution { + private final InetSocketAddress address; + + public DeprecatedNodeResolution(InetSocketAddress address) { + this.address = address; + } + + @Override + public List resolve() { + try { + return Arrays.stream(InetAddress.getAllByName(address.getHostName())) + .map(addr -> new InetSocketAddress(addr, address.getPort())) + .collect(Collectors.toList()); + } catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } +} diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/resolution/FixedSeedsNodeResolution.java b/db-client-java/src/main/java/com/eventstore/dbclient/resolution/FixedSeedsNodeResolution.java new file mode 100644 index 00000000..858d316f --- /dev/null +++ b/db-client-java/src/main/java/com/eventstore/dbclient/resolution/FixedSeedsNodeResolution.java @@ -0,0 +1,18 @@ +package com.eventstore.dbclient.resolution; + +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.List; + +public class FixedSeedsNodeResolution implements NodeResolution { + private final InetSocketAddress[] seeds; + + public FixedSeedsNodeResolution(InetSocketAddress[] seeds) { + this.seeds = seeds; + } + + @Override + public List resolve() { + return Arrays.asList(seeds); + } +} diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/resolution/NodeResolution.java b/db-client-java/src/main/java/com/eventstore/dbclient/resolution/NodeResolution.java new file mode 100644 index 00000000..6cd87420 --- /dev/null +++ b/db-client-java/src/main/java/com/eventstore/dbclient/resolution/NodeResolution.java @@ -0,0 +1,8 @@ +package com.eventstore.dbclient.resolution; + +import java.net.InetSocketAddress; +import java.util.List; + +public interface NodeResolution { + List resolve(); +} diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java index 1adffb56..9a5ed76e 100644 --- a/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java +++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/ParseValidConnectionStringTests.java @@ -110,6 +110,14 @@ public static Stream validConnectionStrings() { Arguments.of( "esdb://127.0.0.1:21573?userCertFile=/path/to/cert&userKeyFile=/path/to/key", "{\"dnsDiscover\":false,\"maxDiscoverAttempts\":3,\"discoveryInterval\":500,\"gossipTimeout\":3000,\"nodePreference\":\"leader\",\"tls\":true,\"tlsVerifyCert\":true,\"throwOnAppendFailure\":true,\"hosts\":[{\"address\":\"127.0.0.1\",\"port\":21573}], \"defaultClientCertificate\": {\"clientCertFile\": \"/path/to/cert\", \"clientKeyFile\": \"/path/to/key\"}}" + ), + Arguments.of( + "esdb://localhost?feature=foobar", + "{\"dnsDiscover\":false,\"maxDiscoverAttempts\":3,\"discoveryInterval\":500,\"gossipTimeout\":3000,\"nodePreference\":\"leader\",\"tls\":true,\"tlsVerifyCert\":true,\"throwOnAppendFailure\":true,\"hosts\":[{\"address\":\"localhost\",\"port\":2113}], \"features\": \"foobar\"}" + ), + Arguments.of( + "esdb://localhost?feature=foobar&feature=baz", + "{\"dnsDiscover\":false,\"maxDiscoverAttempts\":3,\"discoveryInterval\":500,\"gossipTimeout\":3000,\"nodePreference\":\"leader\",\"tls\":true,\"tlsVerifyCert\":true,\"throwOnAppendFailure\":true,\"hosts\":[{\"address\":\"localhost\",\"port\":2113}], \"features\": [\"foobar\", \"baz\"]}" ) ); } @@ -215,6 +223,15 @@ private EventStoreDBClientSettings parseJson(String input) throws JsonProcessing builder.addHost(new InetSocketAddress(host.get("address").asText(), host.get("port").asInt())); }); + if (tree.get("features") != null) { + JsonNode features = tree.get("features"); + + if (features.isArray()) + features.elements().forEachRemaining(feature -> builder.feature(feature.asText())); + else + builder.feature(features.asText()); + } + return builder.buildConnectionSettings(); } }