diff --git a/java-client/src/main/java/org/opensearch/client/transport/sniffer/Node.java b/java-client/src/main/java/org/opensearch/client/transport/sniffer/Node.java new file mode 100644 index 000000000..f9d4f2e89 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/sniffer/Node.java @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport.sniffer; + +import org.apache.hc.core5.http.HttpHost; +import java.util.Objects; + +/** + * Represents a node in the OpenSearch cluster discovered by sniffing. + */ +public class Node { + + private final HttpHost host; + private final String roles; + private final String version; + private final String name; + + public Node(HttpHost host) { + this(host, null, null, null); + } + + public Node(HttpHost host, String roles, String version, String name) { + this.host = Objects.requireNonNull(host, "host cannot be null"); + this.roles = roles; + this.version = version; + this.name = name; + } + + /** + * Returns the host information for this node. + */ + public HttpHost getHost() { + return host; + } + + /** + * Returns the roles of this node (e.g., "master", "data", "ingest"). + */ + public String getRoles() { + return roles; + } + + /** + * Returns the OpenSearch version of this node. + */ + public String getVersion() { + return version; + } + + /** + * Returns the name of this node. + */ + public String getName() { + return name; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + Node node = (Node) obj; + return Objects.equals(host, node.host); + } + + @Override + public int hashCode() { + return Objects.hash(host); + } + + @Override + public String toString() { + return "Node{" + + "host=" + host + + ", roles='" + roles + '\'' + + ", version='" + version + '\'' + + ", name='" + name + '\'' + + '}'; + } +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/sniffer/NodeListCallback.java b/java-client/src/main/java/org/opensearch/client/transport/sniffer/NodeListCallback.java new file mode 100644 index 000000000..4105bc2dd --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/sniffer/NodeListCallback.java @@ -0,0 +1,25 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport.sniffer; + +import java.util.List; + +/** + * Callback interface used to notify when the list of nodes is updated after sniffing. + */ +@FunctionalInterface +public interface NodeListCallback { + + /** + * Called when the node list is updated after sniffing. + * + * @param nodes the updated list of nodes + */ + void onNodeListUpdate(List nodes); +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/sniffer/NodesSniffer.java b/java-client/src/main/java/org/opensearch/client/transport/sniffer/NodesSniffer.java new file mode 100644 index 000000000..84c87fc2f --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/sniffer/NodesSniffer.java @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport.sniffer; + +import java.io.IOException; +import java.util.List; + +/** + * Interface for sniffing OpenSearch cluster nodes. + * Implementations are responsible for discovering available nodes in the cluster. + */ +public interface NodesSniffer { + + /** + * Sniffs the cluster nodes and returns the list of discovered nodes. + * + * @return list of discovered nodes + * @throws IOException if sniffing fails + */ + List sniff() throws IOException; +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/sniffer/OpenSearchNodesSniffer.java b/java-client/src/main/java/org/opensearch/client/transport/sniffer/OpenSearchNodesSniffer.java new file mode 100644 index 000000000..4da510744 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/sniffer/OpenSearchNodesSniffer.java @@ -0,0 +1,171 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport.sniffer; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.EntityUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +/** + * Implementation of {@link NodesSniffer} that discovers nodes by calling the OpenSearch cluster's nodes API. + */ +public class OpenSearchNodesSniffer implements NodesSniffer { + + public static final String NODES_INFO_PATH = "_nodes/http"; + public static final String DEFAULT_SCHEME = "http"; + + private final CloseableHttpClient httpClient; + private final List hosts; + private final String scheme; + private final ObjectMapper objectMapper; + + public OpenSearchNodesSniffer(CloseableHttpClient httpClient, List hosts, String scheme) { + this.httpClient = httpClient; + this.hosts = hosts; + this.scheme = scheme != null ? scheme : DEFAULT_SCHEME; + this.objectMapper = new ObjectMapper(); + } + + @Override + public List sniff() throws IOException { + List sniffedNodes = new ArrayList<>(); + + for (HttpHost host : hosts) { + try { + List nodesFromHost = sniffFromHost(host); + if (!nodesFromHost.isEmpty()) { + return nodesFromHost; // Return nodes from first successful host + } + } catch (IOException e) { + // Try next host if current one fails + continue; + } + } + + throw new IOException("Unable to sniff nodes from any of the provided hosts"); + } + + private List sniffFromHost(HttpHost host) throws IOException { + String nodesInfoUrl = host.toURI() + "/" + NODES_INFO_PATH; + HttpGet request = new HttpGet(nodesInfoUrl); + + try (CloseableHttpResponse response = httpClient.execute(request)) { + if (response.getCode() != 200) { + throw new IOException("Nodes info request failed with status: " + response.getCode()); + } + + try { + String responseBody = EntityUtils.toString(response.getEntity()); + return parseNodesResponse(responseBody); + } catch (Exception e) { + throw new IOException("Failed to parse response body", e); + } + } + } + + private List parseNodesResponse(String responseBody) throws IOException { + List nodes = new ArrayList<>(); + JsonNode root = objectMapper.readTree(responseBody); + JsonNode nodesNode = root.get("nodes"); + + if (nodesNode != null && nodesNode.isObject()) { + Iterator> nodeIterator = nodesNode.fields(); + + while (nodeIterator.hasNext()) { + Map.Entry nodeEntry = nodeIterator.next(); + JsonNode nodeInfo = nodeEntry.getValue(); + + Node node = parseNodeInfo(nodeInfo); + if (node != null) { + nodes.add(node); + } + } + } + + return nodes; + } + + private Node parseNodeInfo(JsonNode nodeInfo) { + try { + JsonNode httpNode = nodeInfo.get("http"); + if (httpNode == null) { + return null; + } + + JsonNode publishAddressNode = httpNode.get("publish_address"); + if (publishAddressNode == null) { + return null; + } + + String publishAddress = publishAddressNode.asText(); + HttpHost host = parseHttpHost(publishAddress); + + String roles = extractRoles(nodeInfo); + String version = extractVersion(nodeInfo); + String name = extractName(nodeInfo); + + return new Node(host, roles, version, name); + } catch (Exception e) { + // Skip malformed node info + return null; + } + } + + private HttpHost parseHttpHost(String publishAddress) { + // Parse format like "127.0.0.1:9200" or "[::1]:9200" + String host; + int port; + + if (publishAddress.startsWith("[")) { + // IPv6 format + int closeBracket = publishAddress.indexOf(']'); + host = publishAddress.substring(1, closeBracket); + port = Integer.parseInt(publishAddress.substring(closeBracket + 2)); + } else { + // IPv4 format + String[] parts = publishAddress.split(":"); + host = parts[0]; + port = Integer.parseInt(parts[1]); + } + + return new HttpHost(scheme, host, port); + } + + private String extractRoles(JsonNode nodeInfo) { + JsonNode rolesNode = nodeInfo.get("roles"); + if (rolesNode != null && rolesNode.isArray()) { + List roles = new ArrayList<>(); + for (JsonNode role : rolesNode) { + roles.add(role.asText()); + } + return String.join(",", roles); + } + return null; + } + + private String extractVersion(JsonNode nodeInfo) { + JsonNode versionNode = nodeInfo.get("version"); + return versionNode != null ? versionNode.asText() : null; + } + + private String extractName(JsonNode nodeInfo) { + JsonNode nameNode = nodeInfo.get("name"); + return nameNode != null ? nameNode.asText() : null; + } +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/sniffer/Sniffer.java b/java-client/src/main/java/org/opensearch/client/transport/sniffer/Sniffer.java new file mode 100644 index 000000000..1d0c29719 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/sniffer/Sniffer.java @@ -0,0 +1,91 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport.sniffer; + +import java.io.Closeable; +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Class responsible for sniffing nodes from an OpenSearch cluster and notifying a {@link NodeListCallback} + * when the sniffed nodes set is updated. + */ +public class Sniffer implements Closeable { + + private final NodesSniffer nodesSniffer; + private final NodeListCallback nodeListCallback; + private final long sniffIntervalMillis; + private final long sniffAfterFailureDelayMillis; + private final ScheduledExecutorService scheduledExecutorService; + private final AtomicBoolean running = new AtomicBoolean(false); + + private volatile ScheduledFuture scheduledSniff; + + Sniffer(NodesSniffer nodesSniffer, NodeListCallback nodeListCallback, + long sniffIntervalMillis, long sniffAfterFailureDelayMillis, + ScheduledExecutorService scheduledExecutorService) { + this.nodesSniffer = nodesSniffer; + this.nodeListCallback = nodeListCallback; + this.sniffIntervalMillis = sniffIntervalMillis; + this.sniffAfterFailureDelayMillis = sniffAfterFailureDelayMillis; + this.scheduledExecutorService = scheduledExecutorService; + } + + /** + * Start the sniffer. The sniffer will run according to the configured interval. + */ + public void start() { + if (running.compareAndSet(false, true)) { + scheduleNextSniff(0); + } + } + + /** + * Triggers sniffing of cluster nodes. Can be called manually to force node discovery. + */ + public void sniffOnFailure() { + if (running.get()) { + if (scheduledSniff != null) { + scheduledSniff.cancel(false); + } + scheduleNextSniff(sniffAfterFailureDelayMillis); + } + } + + private void scheduleNextSniff(long delayMillis) { + if (running.get()) { + scheduledSniff = scheduledExecutorService.schedule(this::performSniffing, delayMillis, TimeUnit.MILLISECONDS); + } + } + + private void performSniffing() { + try { + List sniffedNodes = nodesSniffer.sniff(); + nodeListCallback.onNodeListUpdate(sniffedNodes); + scheduleNextSniff(sniffIntervalMillis); + } catch (Exception e) { + // Log the exception and schedule retry + scheduleNextSniff(sniffAfterFailureDelayMillis); + } + } + + @Override + public void close() throws IOException { + if (running.compareAndSet(true, false)) { + if (scheduledSniff != null) { + scheduledSniff.cancel(false); + } + scheduledExecutorService.shutdown(); + } + } +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferAwareTransport.java b/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferAwareTransport.java new file mode 100644 index 000000000..7fd897f10 --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferAwareTransport.java @@ -0,0 +1,108 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport.sniffer; + +import org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport; +import org.opensearch.client.transport.httpclient5.internal.Node; +import org.apache.hc.core5.http.HttpHost; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; + +/** + * Example of how a sniffer-aware transport could work. + * Note: This is a conceptual implementation showing how the sniffer could integrate + * with the transport layer. A full implementation would require modifications to + * the ApacheHttpClient5Transport class to support dynamic node updates. + */ +public class SnifferAwareTransport implements NodeListCallback { + + private final ApacheHttpClient5Transport transport; + private final CopyOnWriteArrayList nodes; + private final Sniffer sniffer; + + /** + * Creates a sniffer-aware transport wrapper. + * + * @param transport the underlying transport implementation + * @param sniffer the sniffer instance for node discovery + */ + public SnifferAwareTransport(ApacheHttpClient5Transport transport, Sniffer sniffer) { + this.transport = transport; + this.nodes = new CopyOnWriteArrayList<>(); + this.sniffer = sniffer; + } + + @Override + public void onNodeListUpdate(List sniffedNodes) { + // Convert sniffer nodes to transport nodes + List transportNodes = sniffedNodes.stream() + .map(this::convertToTransportNode) + .collect(Collectors.toList()); + + // Update the node list atomically + synchronized (this) { + nodes.clear(); + nodes.addAll(transportNodes); + } + + System.out.println("Updated transport with " + transportNodes.size() + " nodes"); + + // Note: In a full implementation, you would need to: + // 1. Extend ApacheHttpClient5Transport to support dynamic node updates + // 2. Implement a method to update the transport's internal node list + // 3. Handle connection pool updates for the new nodes + } + + private Node convertToTransportNode(org.opensearch.client.transport.sniffer.Node snifferNode) { + HttpHost host = snifferNode.getHost(); + return new Node(host); + } + + /** + * Start the sniffer to begin automatic node discovery. + */ + public void startSniffer() { + sniffer.start(); + } + + /** + * Manually trigger node sniffing on failure. + */ + public void sniffOnFailure() { + sniffer.sniffOnFailure(); + } + + /** + * Get the underlying transport instance. + */ + public ApacheHttpClient5Transport getTransport() { + return transport; + } + + /** + * Get the current list of discovered nodes. + */ + public List getCurrentNodes() { + return new CopyOnWriteArrayList<>(nodes); + } + + /** + * Close both the sniffer and the underlying transport. + */ + public void close() throws IOException { + try { + sniffer.close(); + } finally { + transport.close(); + } + } +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferBuilder.java b/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferBuilder.java new file mode 100644 index 000000000..d2c967adf --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferBuilder.java @@ -0,0 +1,147 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.transport.sniffer; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpHost; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Builder for {@link Sniffer} instances. + */ +public class SnifferBuilder { + + public static final long DEFAULT_SNIFF_INTERVAL = TimeUnit.MINUTES.toMillis(5); + public static final long DEFAULT_SNIFF_AFTER_FAILURE_DELAY = TimeUnit.MINUTES.toMillis(1); + + private final CloseableHttpClient httpClient; + private final NodeListCallback nodeListCallback; + private List hosts; + private String scheme = "http"; + private long sniffIntervalMillis = DEFAULT_SNIFF_INTERVAL; + private long sniffAfterFailureDelayMillis = DEFAULT_SNIFF_AFTER_FAILURE_DELAY; + private ScheduledExecutorService scheduledExecutorService; + private NodesSniffer nodesSniffer; + + /** + * Creates a new SnifferBuilder. + * + * @param httpClient the HTTP client to use for sniffing + * @param nodeListCallback callback to be notified when nodes are updated + */ + public SnifferBuilder(CloseableHttpClient httpClient, NodeListCallback nodeListCallback) { + this.httpClient = Objects.requireNonNull(httpClient, "httpClient cannot be null"); + this.nodeListCallback = Objects.requireNonNull(nodeListCallback, "nodeListCallback cannot be null"); + } + + /** + * Sets the hosts that will be used for sniffing. + * + * @param hosts the list of hosts + * @return this builder + */ + public SnifferBuilder setHosts(List hosts) { + this.hosts = Objects.requireNonNull(hosts, "hosts cannot be null"); + return this; + } + + /** + * Sets the scheme to use when connecting to discovered nodes. + * + * @param scheme the scheme (http or https) + * @return this builder + */ + public SnifferBuilder setScheme(String scheme) { + this.scheme = Objects.requireNonNull(scheme, "scheme cannot be null"); + return this; + } + + /** + * Sets the interval between sniff executions. + * + * @param sniffIntervalMillis interval in milliseconds + * @return this builder + */ + public SnifferBuilder setSniffIntervalMillis(long sniffIntervalMillis) { + if (sniffIntervalMillis <= 0) { + throw new IllegalArgumentException("sniffIntervalMillis must be greater than 0"); + } + this.sniffIntervalMillis = sniffIntervalMillis; + return this; + } + + /** + * Sets the delay between a sniff execution and the next one when the previous one failed. + * + * @param sniffAfterFailureDelayMillis delay in milliseconds + * @return this builder + */ + public SnifferBuilder setSniffAfterFailureDelayMillis(long sniffAfterFailureDelayMillis) { + if (sniffAfterFailureDelayMillis <= 0) { + throw new IllegalArgumentException("sniffAfterFailureDelayMillis must be greater than 0"); + } + this.sniffAfterFailureDelayMillis = sniffAfterFailureDelayMillis; + return this; + } + + /** + * Sets a custom ScheduledExecutorService to be used for scheduling sniff tasks. + * + * @param scheduledExecutorService the executor service + * @return this builder + */ + public SnifferBuilder setScheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { + this.scheduledExecutorService = Objects.requireNonNull(scheduledExecutorService, "scheduledExecutorService cannot be null"); + return this; + } + + /** + * Sets a custom NodesSniffer implementation. + * + * @param nodesSniffer the nodes sniffer + * @return this builder + */ + public SnifferBuilder setNodesSniffer(NodesSniffer nodesSniffer) { + this.nodesSniffer = Objects.requireNonNull(nodesSniffer, "nodesSniffer cannot be null"); + return this; + } + + /** + * Builds the Sniffer instance. + * + * @return a new Sniffer instance + */ + public Sniffer build() { + if (hosts == null || hosts.isEmpty()) { + throw new IllegalStateException("hosts must be set and non-empty"); + } + + NodesSniffer sniffer = this.nodesSniffer; + if (sniffer == null) { + sniffer = new OpenSearchNodesSniffer(httpClient, hosts, scheme); + } + + ScheduledExecutorService executor = this.scheduledExecutorService; + if (executor == null) { + executor = Executors.newScheduledThreadPool(1, r -> { + Thread thread = new Thread(r, "opensearch-sniffer"); + thread.setDaemon(true); + return thread; + }); + } + + return new Sniffer(sniffer, nodeListCallback, sniffIntervalMillis, + sniffAfterFailureDelayMillis, executor); + } +} diff --git a/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferExample.java b/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferExample.java new file mode 100644 index 000000000..a1593c7fb --- /dev/null +++ b/java-client/src/main/java/org/opensearch/client/transport/sniffer/SnifferExample.java @@ -0,0 +1,147 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.client.transport.sniffer; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.core5.http.HttpHost; +import org.opensearch.client.opensearch.OpenSearchClient; +import org.opensearch.client.transport.httpclient5.ApacheHttpClient5Transport; +import org.opensearch.client.transport.httpclient5.ApacheHttpClient5TransportBuilder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Example demonstrating how to use the OpenSearch sniffer implementation. + */ +public class SnifferExample { + + public static void main(String[] args) throws IOException { + // Example 1: Basic sniffer setup + basicSnifferExample(); + + // Example 2: Custom configuration + customConfigurationExample(); + + // Example 3: Integration with OpenSearch client + clientIntegrationExample(); + } + + /** + * Basic example showing minimal sniffer setup. + */ + private static void basicSnifferExample() throws IOException { + // Create HTTP client + CloseableHttpClient httpClient = HttpClients.createDefault(); + + // Define initial hosts + List hosts = Arrays.asList( + new HttpHost("localhost", 9200), + new HttpHost("localhost", 9201) + ); + + // Create sniffer with callback + Sniffer sniffer = new SnifferBuilder(httpClient, nodes -> { + System.out.println("Discovered " + nodes.size() + " nodes:"); + for (Node node : nodes) { + System.out.println(" " + node); + } + }) + .setHosts(hosts) + .build(); + + // Start sniffing + sniffer.start(); + + // Let it run for a while + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + + // Clean up + sniffer.close(); + httpClient.close(); + } + + /** + * Example with custom configuration options. + */ + private static void customConfigurationExample() throws IOException { + CloseableHttpClient httpClient = HttpClients.createDefault(); + + List hosts = Arrays.asList( + new HttpHost("https", "localhost", 9200), + new HttpHost("https", "localhost", 9201) + ); + + Sniffer sniffer = new SnifferBuilder(httpClient, nodes -> { + System.out.println("Updated node list with " + nodes.size() + " nodes"); + }) + .setHosts(hosts) + .setScheme("https") + .setSniffIntervalMillis(TimeUnit.MINUTES.toMillis(2)) // Sniff every 2 minutes + .setSniffAfterFailureDelayMillis(TimeUnit.SECONDS.toMillis(30)) // Retry after 30 seconds on failure + .build(); + + sniffer.start(); + + // Simulate a failure and trigger immediate sniffing + sniffer.sniffOnFailure(); + + sniffer.close(); + httpClient.close(); + } + + /** + * Example showing integration with OpenSearch client. + * Note: This is a simplified example - real integration would require + * extending the transport to support dynamic node updates. + */ + private static void clientIntegrationExample() throws IOException { + // Create the transport using the static builder factory method + ApacheHttpClient5TransportBuilder transportBuilder = ApacheHttpClient5TransportBuilder.builder( + new HttpHost("localhost", 9200), + new HttpHost("localhost", 9201) + ); + + ApacheHttpClient5Transport transport = transportBuilder.build(); + + // Create sniffer + CloseableHttpClient httpClient = HttpClients.createDefault(); + Sniffer sniffer = new SnifferBuilder(httpClient, nodes -> { + System.out.println("Cluster topology changed - " + nodes.size() + " nodes available"); + // Here you could update the transport's node list + // This would require extending the transport to support dynamic node updates + }) + .setHosts(Arrays.asList(new HttpHost("localhost", 9200))) + .build(); + + // Create OpenSearch client + OpenSearchClient client = new OpenSearchClient(transport); + + // Start node discovery + sniffer.start(); + + try { + // Use the client for operations + // Note: Using explicit type instead of 'var' for Java 8 compatibility + org.opensearch.client.opensearch.core.InfoResponse info = client.info(); + System.out.println("Connected to: " + info.version().number()); + + // The sniffer will automatically discover new nodes in the background + + } finally { + // Clean up + sniffer.close(); + client._transport().close(); + httpClient.close(); + } + } +}