Skip to content

Commit 99c8a33

Browse files
committed
LLClient: Support host selection
Allows users of the Low Level REST client to specify which hosts a request should be run on. They implement the `NodeSelector` interface or reuse a built in selector like `NOT_MASTER_ONLY` to chose which nodes are valid. Using it looks like: ``` Request request = new Request("POST", "/foo/_search"); request.setNodeSelector(NodeSelector.NOT_MASTER_ONLY); ... ``` This introduces a new `Node` object which contains a `HttpHost` and the metadata about the host. At this point that metadata is just `version` and `roles` but I plan to add node attributes in a followup. The canonical way to **get** this metadata is to use the `Sniffer` to pull the information from the Elasticsearch cluster. I've marked this as "breaking-java" because it breaks custom implementations of `HostsSniffer` by renaming the interface to `NodesSniffer` and by changing it from returning a `List<HttpHost>` to a `List<Node>`. It *shouldn't* break anyone else though. Because we expect to find it useful, this also implements `host_selector` support to `do` statements in the yaml tests. Using it looks a little like: ``` --- "example test": - skip: features: host_selector - do: host_selector: version: " - 7.0.0" # same syntax as skip apiname: something: true ``` The `do` section parses the `version` string into a host selector that uses the same version comparison logic as the `skip` section. When the `do` section is executed it passed the off to the `RestClient`, using the `ElasticsearchHostsSniffer` to sniff the required metadata. The idea is to use this in mixed version tests to target a specific version of Elasticsearch so we can be sure about the deprecation logging though we don't currently have any examples that need it. We do, however, have at least one open pull request that requires something like this to properly test it. Closes elastic#21888 (kind of, it isn't in the high level client, but we'll do that in a followup)
1 parent 51fa873 commit 99c8a33

File tree

53 files changed

+2399
-429
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2399
-429
lines changed

client/rest/src/main/java/org/elasticsearch/client/DeadHostState.java

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,47 +29,35 @@
2929
final class DeadHostState implements Comparable<DeadHostState> {
3030

3131
private static final long MIN_CONNECTION_TIMEOUT_NANOS = TimeUnit.MINUTES.toNanos(1);
32-
private static final long MAX_CONNECTION_TIMEOUT_NANOS = TimeUnit.MINUTES.toNanos(30);
32+
static final long MAX_CONNECTION_TIMEOUT_NANOS = TimeUnit.MINUTES.toNanos(30);
3333

3434
private final int failedAttempts;
3535
private final long deadUntilNanos;
36-
private final TimeSupplier timeSupplier;
3736

3837
/**
3938
* Build the initial dead state of a host. Useful when a working host stops functioning
4039
* and needs to be marked dead after its first failure. In such case the host will be retried after a minute or so.
4140
*
42-
* @param timeSupplier a way to supply the current time and allow for unit testing
41+
* @param now the current time in nanoseconds. Prefer a source designed to measure elapsed time like {@link System#nanoTime()}.
4342
*/
44-
DeadHostState(TimeSupplier timeSupplier) {
43+
DeadHostState(long now) {
4544
this.failedAttempts = 1;
46-
this.deadUntilNanos = timeSupplier.nanoTime() + MIN_CONNECTION_TIMEOUT_NANOS;
47-
this.timeSupplier = timeSupplier;
45+
this.deadUntilNanos = now + MIN_CONNECTION_TIMEOUT_NANOS;
4846
}
4947

5048
/**
5149
* Build the dead state of a host given its previous dead state. Useful when a host has been failing before, hence
5250
* it already failed for one or more consecutive times. The more failed attempts we register the longer we wait
5351
* to retry that same host again. Minimum is 1 minute (for a node the only failed once created
54-
* through {@link #DeadHostState(TimeSupplier)}), maximum is 30 minutes (for a node that failed more than 10 consecutive times)
52+
* through {@link #DeadHostState(long)}), maximum is 30 minutes (for a node that failed more than 10 consecutive times)
5553
*
56-
* @param previousDeadHostState the previous state of the host which allows us to increase the wait till the next retry attempt
54+
* @param now the current time in nanoseconds. Prefer a source designed to measure elapsed time like {@link System#nanoTime()}.
5755
*/
58-
DeadHostState(DeadHostState previousDeadHostState, TimeSupplier timeSupplier) {
56+
DeadHostState(DeadHostState previousDeadHostState, long now) {
5957
long timeoutNanos = (long)Math.min(MIN_CONNECTION_TIMEOUT_NANOS * 2 * Math.pow(2, previousDeadHostState.failedAttempts * 0.5 - 1),
6058
MAX_CONNECTION_TIMEOUT_NANOS);
61-
this.deadUntilNanos = timeSupplier.nanoTime() + timeoutNanos;
59+
this.deadUntilNanos = now + timeoutNanos;
6260
this.failedAttempts = previousDeadHostState.failedAttempts + 1;
63-
this.timeSupplier = timeSupplier;
64-
}
65-
66-
/**
67-
* Indicates whether it's time to retry to failed host or not.
68-
*
69-
* @return true if the host should be retried, false otherwise
70-
*/
71-
boolean shallBeRetried() {
72-
return timeSupplier.nanoTime() - deadUntilNanos > 0;
7361
}
7462

7563
/**
@@ -96,19 +84,4 @@ public String toString() {
9684
", deadUntilNanos=" + deadUntilNanos +
9785
'}';
9886
}
99-
100-
/**
101-
* Time supplier that makes timing aspects pluggable to ease testing
102-
*/
103-
interface TimeSupplier {
104-
105-
TimeSupplier DEFAULT = new TimeSupplier() {
106-
@Override
107-
public long nanoTime() {
108-
return System.nanoTime();
109-
}
110-
};
111-
112-
long nanoTime();
113-
}
11487
}
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client;
21+
22+
import static java.util.Collections.unmodifiableSet;
23+
24+
import java.util.HashSet;
25+
import java.util.Objects;
26+
import java.util.Set;
27+
28+
import org.apache.http.HttpHost;
29+
30+
/**
31+
* Metadata about an {@link HttpHost} running Elasticsearch.
32+
*/
33+
public class Node {
34+
/**
35+
* Address that this host claims is its primary contact point.
36+
*/
37+
private final HttpHost host;
38+
/**
39+
* Addresses on which the host is listening. These are useful to have
40+
* around because they allow you to find a host based on any address it
41+
* is listening on.
42+
*/
43+
private final Set<HttpHost> boundHosts;
44+
/**
45+
* Name of the node as configured by the {@code node.name} attribute.
46+
*/
47+
private final String name;
48+
/**
49+
* Version of Elasticsearch that the node is running or {@code null}
50+
* if we don't know the version.
51+
*/
52+
private final String version;
53+
/**
54+
* Roles that the Elasticsearch process on the host has or {@code null}
55+
* if we don't know what roles the node has.
56+
*/
57+
private final Roles roles;
58+
59+
/**
60+
* Create a {@linkplain Node} with metadata. All parameters except
61+
* {@code host} are nullable and implementations of {@link NodeSelector}
62+
* need to decide what to do in their absence.
63+
*/
64+
public Node(HttpHost host, Set<HttpHost> boundHosts, String name, String version, Roles roles) {
65+
if (host == null) {
66+
throw new IllegalArgumentException("host cannot be null");
67+
}
68+
this.host = host;
69+
this.boundHosts = boundHosts;
70+
this.name = name;
71+
this.version = version;
72+
this.roles = roles;
73+
}
74+
75+
/**
76+
* Create a {@linkplain Node} without any metadata.
77+
*/
78+
public Node(HttpHost host) {
79+
this(host, null, null, null, null);
80+
}
81+
82+
/**
83+
* Make a copy of this {@link Node} but replacing its
84+
* {@link #getHost() host}. Use this when the sniffing implementation
85+
* returns a {@link #getHost() host} that is not useful to the client.
86+
*/
87+
public Node withHost(HttpHost host) {
88+
/*
89+
* If the new host isn't in the bound hosts list we add it so the
90+
* result looks sane.
91+
*/
92+
Set<HttpHost> boundHosts = this.boundHosts;
93+
if (false == boundHosts.contains(host)) {
94+
boundHosts = new HashSet<>(boundHosts);
95+
boundHosts.add(host);
96+
boundHosts = unmodifiableSet(boundHosts);
97+
}
98+
return new Node(host, boundHosts, name, version, roles);
99+
}
100+
101+
/**
102+
* Contact information for the host.
103+
*/
104+
public HttpHost getHost() {
105+
return host;
106+
}
107+
108+
/**
109+
* Addresses on which the host is listening. These are useful to have
110+
* around because they allow you to find a host based on any address it
111+
* is listening on.
112+
*/
113+
public Set<HttpHost> getBoundHosts() {
114+
return boundHosts;
115+
}
116+
117+
/**
118+
* @return the name
119+
*/
120+
public String getName() {
121+
return name;
122+
}
123+
124+
/**
125+
* Version of Elasticsearch that the node is running or {@code null}
126+
* if we don't know the version.
127+
*/
128+
public String getVersion() {
129+
return version;
130+
}
131+
132+
/**
133+
* Roles that the Elasticsearch process on the host has or {@code null}
134+
* if we don't know what roles the node has.
135+
*/
136+
public Roles getRoles() {
137+
return roles;
138+
}
139+
140+
@Override
141+
public String toString() {
142+
StringBuilder b = new StringBuilder();
143+
b.append("[host=").append(host);
144+
if (boundHosts != null) {
145+
b.append(", bound=").append(boundHosts);
146+
}
147+
if (name != null) {
148+
b.append(", name=").append(name);
149+
}
150+
if (version != null) {
151+
b.append(", version=").append(version);
152+
}
153+
if (roles != null) {
154+
b.append(", roles=").append(roles);
155+
}
156+
return b.append(']').toString();
157+
}
158+
159+
@Override
160+
public boolean equals(Object obj) {
161+
if (obj == null || obj.getClass() != getClass()) {
162+
return false;
163+
}
164+
Node other = (Node) obj;
165+
return host.equals(other.host)
166+
&& Objects.equals(boundHosts, other.boundHosts)
167+
&& Objects.equals(version, other.version)
168+
&& Objects.equals(name, other.name)
169+
&& Objects.equals(roles, other.roles);
170+
}
171+
172+
@Override
173+
public int hashCode() {
174+
return Objects.hash(host, boundHosts, name, version, roles);
175+
}
176+
177+
/**
178+
* Role information about an Elasticsearch process.
179+
*/
180+
public static final class Roles {
181+
private final boolean masterEligible;
182+
private final boolean data;
183+
private final boolean ingest;
184+
185+
public Roles(boolean masterEligible, boolean data, boolean ingest) {
186+
this.masterEligible = masterEligible;
187+
this.data = data;
188+
this.ingest = ingest;
189+
}
190+
191+
/**
192+
* The node <strong>could</strong> be elected master.
193+
*/
194+
public boolean isMasterEligible() {
195+
return masterEligible;
196+
}
197+
/**
198+
* The node stores data.
199+
*/
200+
public boolean isData() {
201+
return data;
202+
}
203+
/**
204+
* The node runs ingest pipelines.
205+
*/
206+
public boolean isIngest() {
207+
return ingest;
208+
}
209+
210+
@Override
211+
public String toString() {
212+
StringBuilder result = new StringBuilder(3);
213+
if (masterEligible) result.append('m');
214+
if (data) result.append('d');
215+
if (ingest) result.append('i');
216+
return result.toString();
217+
}
218+
219+
@Override
220+
public boolean equals(Object obj) {
221+
if (obj == null || obj.getClass() != getClass()) {
222+
return false;
223+
}
224+
Roles other = (Roles) obj;
225+
return masterEligible == other.masterEligible
226+
&& data == other.data
227+
&& ingest == other.ingest;
228+
}
229+
230+
@Override
231+
public int hashCode() {
232+
return Objects.hash(masterEligible, data, ingest);
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)