/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.test.transport;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.ClusterModule;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.CheckedBiConsumer;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.component.Lifecycle;
import org.elasticsearch.common.component.LifecycleListener;
import org.elasticsearch.common.io.stream.BytesStreamOutput;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.network.NetworkService;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.BoundTransportAddress;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.BigArrays;
import org.elasticsearch.common.util.concurrent.AbstractRunnable;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.indices.breaker.NoneCircuitBreakerService;
import org.elasticsearch.node.Node;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.tasks.TaskManager;
import org.elasticsearch.test.tasks.MockTaskManager;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.ConnectTransportException;
import org.elasticsearch.transport.ConnectionProfile;
import org.elasticsearch.transport.MockTcpTransport;
import org.elasticsearch.transport.RequestHandlerRegistry;
import org.elasticsearch.transport.Transport;
import org.elasticsearch.transport.TransportException;
import org.elasticsearch.transport.TransportInterceptor;
import org.elasticsearch.transport.TransportRequest;
import org.elasticsearch.transport.TransportRequestOptions;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.transport.TransportServiceAdapter;
import java.io.IOException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
/**
* A mock transport service that allows to simulate different network topology failures.
* Internally it maps TransportAddress objects to rules that inject failures.
* Adding rules for a node is done by adding rules for all bound addresses of a node
* (and the publish address, if different).
* Matching requests to rules is based on the transport address associated with the
* discovery node of the request, namely by DiscoveryNode.getAddress().
* This address is usually the publish address of the node but can also be a different one
* (for example, @see org.elasticsearch.discovery.zen.ping.unicast.UnicastZenPing, which constructs
* fake DiscoveryNode instances where the publish address is one of the bound addresses).
*/
public final class MockTransportService extends TransportService {
private final Map<DiscoveryNode, List<Transport.Connection>> openConnections = new HashMap<>();
public static class TestPlugin extends Plugin {
@Override
public List<Setting<?>> getSettings() {
return Arrays.asList(MockTaskManager.USE_MOCK_TASK_MANAGER_SETTING);
}
}
public static MockTransportService createNewService(Settings settings, Version version, ThreadPool threadPool,
@Nullable ClusterSettings clusterSettings) {
NamedWriteableRegistry namedWriteableRegistry = new NamedWriteableRegistry(ClusterModule.getNamedWriteables());
final Transport transport = new MockTcpTransport(settings, threadPool, BigArrays.NON_RECYCLING_INSTANCE,
new NoneCircuitBreakerService(), namedWriteableRegistry, new NetworkService(settings, Collections.emptyList()), version);
return createNewService(settings, transport, version, threadPool, clusterSettings);
}
public static MockTransportService createNewService(Settings settings, Transport transport, Version version, ThreadPool threadPool,
@Nullable ClusterSettings clusterSettings) {
return new MockTransportService(settings, transport, threadPool, TransportService.NOOP_TRANSPORT_INTERCEPTOR,
boundAddress ->
new DiscoveryNode(Node.NODE_NAME_SETTING.get(settings), UUIDs.randomBase64UUID(), boundAddress.publishAddress(),
Node.NODE_ATTRIBUTES.get(settings).getAsMap(), DiscoveryNode.getRolesFromSettings(settings), version),
clusterSettings);
}
private final Transport original;
/**
* Build the service.
*
* @param clusterSettings if non null the the {@linkplain TransportService} will register with the {@link ClusterSettings} for settings
* updates for {@link #TRACE_LOG_EXCLUDE_SETTING} and {@link #TRACE_LOG_INCLUDE_SETTING}.
*/
public MockTransportService(Settings settings, Transport transport, ThreadPool threadPool, TransportInterceptor interceptor,
@Nullable ClusterSettings clusterSettings) {
this(settings, transport, threadPool, interceptor, (boundAddress) ->
DiscoveryNode.createLocal(settings, boundAddress.publishAddress(), settings.get(Node.NODE_NAME_SETTING.getKey(),
UUIDs.randomBase64UUID())), clusterSettings);
}
/**
* Build the service.
*
* @param clusterSettings if non null the the {@linkplain TransportService} will register with the {@link ClusterSettings} for settings
* updates for {@link #TRACE_LOG_EXCLUDE_SETTING} and {@link #TRACE_LOG_INCLUDE_SETTING}.
*/
public MockTransportService(Settings settings, Transport transport, ThreadPool threadPool, TransportInterceptor interceptor,
Function<BoundTransportAddress, DiscoveryNode> localNodeFactory,
@Nullable ClusterSettings clusterSettings) {
super(settings, new LookupTestTransport(transport), threadPool, interceptor, localNodeFactory, clusterSettings);
this.original = transport;
}
public static TransportAddress[] extractTransportAddresses(TransportService transportService) {
HashSet<TransportAddress> transportAddresses = new HashSet<>();
BoundTransportAddress boundTransportAddress = transportService.boundAddress();
transportAddresses.addAll(Arrays.asList(boundTransportAddress.boundAddresses()));
transportAddresses.add(boundTransportAddress.publishAddress());
return transportAddresses.toArray(new TransportAddress[transportAddresses.size()]);
}
@Override
protected TaskManager createTaskManager() {
if (MockTaskManager.USE_MOCK_TASK_MANAGER_SETTING.get(settings)) {
return new MockTaskManager(settings);
} else {
return super.createTaskManager();
}
}
/**
* Clears all the registered rules.
*/
public void clearAllRules() {
transport().transports.clear();
}
/**
* Clears the rule associated with the provided transport service.
*/
public void clearRule(TransportService transportService) {
for (TransportAddress transportAddress : extractTransportAddresses(transportService)) {
clearRule(transportAddress);
}
}
/**
* Clears the rule associated with the provided transport address.
*/
public void clearRule(TransportAddress transportAddress) {
Transport transport = transport().transports.remove(transportAddress);
if (transport instanceof ClearableTransport) {
((ClearableTransport) transport).clearRule();
}
}
/**
* Returns the original Transport service wrapped by this mock transport service.
*/
public Transport original() {
return original;
}
/**
* Adds a rule that will cause every send request to fail, and each new connect since the rule
* is added to fail as well.
*/
public void addFailToSendNoConnectRule(TransportService transportService) {
for (TransportAddress transportAddress : extractTransportAddresses(transportService)) {
addFailToSendNoConnectRule(transportAddress);
}
}
/**
* Adds a rule that will cause every send request to fail, and each new connect since the rule
* is added to fail as well.
*/
public void addFailToSendNoConnectRule(TransportAddress transportAddress) {
addDelegate(transportAddress, new DelegateTransport(original) {
@Override
public void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfile,
CheckedBiConsumer<Connection, ConnectionProfile, IOException> connectionValidator)
throws ConnectTransportException {
if (original.nodeConnected(node) == false) {
// connecting to an already connected node is a no-op
throw new ConnectTransportException(node, "DISCONNECT: simulated");
}
}
@Override
protected void sendRequest(Connection connection, long requestId, String action, TransportRequest request,
TransportRequestOptions options) throws IOException {
simulateDisconnect(connection, original, "DISCONNECT: simulated");
}
});
}
/**
* Adds a rule that will cause matching operations to throw ConnectTransportExceptions
*/
public void addFailToSendNoConnectRule(TransportService transportService, final String... blockedActions) {
addFailToSendNoConnectRule(transportService, new HashSet<>(Arrays.asList(blockedActions)));
}
/**
* Adds a rule that will cause matching operations to throw ConnectTransportExceptions
*/
public void addFailToSendNoConnectRule(TransportAddress transportAddress, final String... blockedActions) {
addFailToSendNoConnectRule(transportAddress, new HashSet<>(Arrays.asList(blockedActions)));
}
/**
* Adds a rule that will cause matching operations to throw ConnectTransportExceptions
*/
public void addFailToSendNoConnectRule(TransportService transportService, final Set<String> blockedActions) {
for (TransportAddress transportAddress : extractTransportAddresses(transportService)) {
addFailToSendNoConnectRule(transportAddress, blockedActions);
}
}
/**
* Adds a rule that will cause matching operations to throw ConnectTransportExceptions
*/
public void addFailToSendNoConnectRule(TransportAddress transportAddress, final Set<String> blockedActions) {
addDelegate(transportAddress, new DelegateTransport(original) {
@Override
public void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfile,
CheckedBiConsumer<Connection, ConnectionProfile, IOException> connectionValidator)
throws ConnectTransportException {
original.connectToNode(node, connectionProfile, connectionValidator);
}
@Override
protected void sendRequest(Connection connection, long requestId, String action, TransportRequest request,
TransportRequestOptions options) throws IOException {
if (blockedActions.contains(action)) {
logger.info("--> preventing {} request", action);
simulateDisconnect(connection, original, "DISCONNECT: prevented " + action + " request");
}
connection.sendRequest(requestId, action, request, options);
}
});
}
/**
* Adds a rule that will cause ignores each send request, simulating an unresponsive node
* and failing to connect once the rule was added.
*/
public void addUnresponsiveRule(TransportService transportService) {
for (TransportAddress transportAddress : extractTransportAddresses(transportService)) {
addUnresponsiveRule(transportAddress);
}
}
/**
* Adds a rule that will cause ignores each send request, simulating an unresponsive node
* and failing to connect once the rule was added.
*/
public void addUnresponsiveRule(TransportAddress transportAddress) {
addDelegate(transportAddress, new DelegateTransport(original) {
@Override
public void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfile,
CheckedBiConsumer<Connection, ConnectionProfile, IOException> connectionValidator)
throws ConnectTransportException {
if (original.nodeConnected(node) == false) {
// connecting to an already connected node is a no-op
throw new ConnectTransportException(node, "UNRESPONSIVE: simulated");
}
}
@Override
protected void sendRequest(Connection connection, long requestId, String action, TransportRequest request,
TransportRequestOptions options) throws IOException {
// don't send anything, the receiving node is unresponsive
}
});
}
/**
* Adds a rule that will cause ignores each send request, simulating an unresponsive node
* and failing to connect once the rule was added.
*
* @param duration the amount of time to delay sending and connecting.
*/
public void addUnresponsiveRule(TransportService transportService, final TimeValue duration) {
for (TransportAddress transportAddress : extractTransportAddresses(transportService)) {
addUnresponsiveRule(transportAddress, duration);
}
}
/**
* Adds a rule that will cause ignores each send request, simulating an unresponsive node
* and failing to connect once the rule was added.
*
* @param duration the amount of time to delay sending and connecting.
*/
public void addUnresponsiveRule(TransportAddress transportAddress, final TimeValue duration) {
final long startTime = System.currentTimeMillis();
addDelegate(transportAddress, new ClearableTransport(original) {
private final Queue<Runnable> requestsToSendWhenCleared = new LinkedBlockingDeque<Runnable>();
private boolean cleared = false;
TimeValue getDelay() {
return new TimeValue(duration.millis() - (System.currentTimeMillis() - startTime));
}
@Override
public void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfile,
CheckedBiConsumer<Connection, ConnectionProfile, IOException> connectionValidator)
throws ConnectTransportException {
if (original.nodeConnected(node)) {
// connecting to an already connected node is a no-op
return;
}
TimeValue delay = getDelay();
if (delay.millis() <= 0) {
original.connectToNode(node, connectionProfile, connectionValidator);
return;
}
// TODO: Replace with proper setting
TimeValue connectingTimeout = NetworkService.TcpSettings.TCP_CONNECT_TIMEOUT.getDefault(Settings.EMPTY);
try {
if (delay.millis() < connectingTimeout.millis()) {
Thread.sleep(delay.millis());
original.connectToNode(node, connectionProfile, connectionValidator);
} else {
Thread.sleep(connectingTimeout.millis());
throw new ConnectTransportException(node, "UNRESPONSIVE: simulated");
}
} catch (InterruptedException e) {
throw new ConnectTransportException(node, "UNRESPONSIVE: simulated");
}
}
@Override
protected void sendRequest(Connection connection, long requestId, String action, TransportRequest request,
TransportRequestOptions options) throws IOException {
// delayed sending - even if larger then the request timeout to simulated a potential late response from target node
TimeValue delay = getDelay();
if (delay.millis() <= 0) {
connection.sendRequest(requestId, action, request, options);
return;
}
// poor mans request cloning...
RequestHandlerRegistry reg = MockTransportService.this.getRequestHandler(action);
BytesStreamOutput bStream = new BytesStreamOutput();
request.writeTo(bStream);
final TransportRequest clonedRequest = reg.newRequest();
clonedRequest.readFrom(bStream.bytes().streamInput());
Runnable runnable = new AbstractRunnable() {
AtomicBoolean requestSent = new AtomicBoolean();
@Override
public void onFailure(Exception e) {
logger.debug("failed to send delayed request", e);
}
@Override
protected void doRun() throws IOException {
if (requestSent.compareAndSet(false, true)) {
connection.sendRequest(requestId, action, clonedRequest, options);
}
}
};
// store the request to send it once the rule is cleared.
synchronized (this) {
if (cleared) {
runnable.run();
} else {
requestsToSendWhenCleared.add(runnable);
threadPool.schedule(delay, ThreadPool.Names.GENERIC, runnable);
}
}
}
@Override
public void clearRule() {
synchronized (this) {
assert cleared == false;
cleared = true;
requestsToSendWhenCleared.forEach(Runnable::run);
}
}
});
}
/**
* Adds a new delegate transport that is used for communication with the given transport service.
*
* @return <tt>true</tt> iff no other delegate was registered for any of the addresses bound by transport service.
*/
public boolean addDelegate(TransportService transportService, DelegateTransport transport) {
boolean noRegistered = true;
for (TransportAddress transportAddress : extractTransportAddresses(transportService)) {
noRegistered &= addDelegate(transportAddress, transport);
}
return noRegistered;
}
/**
* Adds a new delegate transport that is used for communication with the given transport address.
*
* @return <tt>true</tt> iff no other delegate was registered for this address before.
*/
public boolean addDelegate(TransportAddress transportAddress, DelegateTransport transport) {
return transport().transports.put(transportAddress, transport) == null;
}
private LookupTestTransport transport() {
return (LookupTestTransport) transport;
}
/**
* simulates a disconnect by disconnecting from the underlying transport and throwing a
* {@link ConnectTransportException}
*/
private void simulateDisconnect(DiscoveryNode node, Transport transport, String reason) {
simulateDisconnect(node, transport, reason, null);
}
/**
* simulates a disconnect by disconnecting from the underlying transport and throwing a
* {@link ConnectTransportException}, due to a specific cause exception
*/
private void simulateDisconnect(DiscoveryNode node, Transport transport, String reason, @Nullable Throwable e) {
if (transport.nodeConnected(node)) {
// this a connected node, disconnecting from it will be up the exception
transport.disconnectFromNode(node);
} else {
throw new ConnectTransportException(node, reason, e);
}
}
/**
* simulates a disconnect by closing the connection and throwing a
* {@link ConnectTransportException}
*/
private void simulateDisconnect(Transport.Connection connection, Transport transport, String reason) throws IOException {
connection.close();
simulateDisconnect(connection.getNode(), transport, reason);
}
/**
* A lookup transport that has a list of potential Transport implementations to delegate to for node operations,
* if none is registered, then the default one is used.
*/
private static class LookupTestTransport extends DelegateTransport {
final ConcurrentMap<TransportAddress, Transport> transports = ConcurrentCollections.newConcurrentMap();
LookupTestTransport(Transport transport) {
super(transport);
}
private Transport getTransport(DiscoveryNode node) {
Transport transport = transports.get(node.getAddress());
if (transport != null) {
return transport;
}
return this.transport;
}
@Override
public boolean nodeConnected(DiscoveryNode node) {
return getTransport(node).nodeConnected(node);
}
@Override
public void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfile,
CheckedBiConsumer<Connection, ConnectionProfile, IOException> connectionValidator)
throws ConnectTransportException {
getTransport(node).connectToNode(node, connectionProfile, connectionValidator);
}
@Override
public void disconnectFromNode(DiscoveryNode node) {
getTransport(node).disconnectFromNode(node);
}
@Override
public Connection getConnection(DiscoveryNode node) {
return getTransport(node).getConnection(node);
}
@Override
public Connection openConnection(DiscoveryNode node, ConnectionProfile profile) throws IOException {
return getTransport(node).openConnection(node, profile);
}
}
/**
* A pure delegate transport.
* Can be extracted to a common class if needed in other places in the codebase.
*/
public static class DelegateTransport implements Transport {
protected final Transport transport;
public DelegateTransport(Transport transport) {
this.transport = transport;
}
@Override
public void transportServiceAdapter(TransportServiceAdapter service) {
transport.transportServiceAdapter(service);
}
@Override
public BoundTransportAddress boundAddress() {
return transport.boundAddress();
}
@Override
public TransportAddress[] addressesFromString(String address, int perAddressLimit) throws UnknownHostException {
return transport.addressesFromString(address, perAddressLimit);
}
@Override
public boolean nodeConnected(DiscoveryNode node) {
return transport.nodeConnected(node);
}
@Override
public void connectToNode(DiscoveryNode node, ConnectionProfile connectionProfile,
CheckedBiConsumer<Connection, ConnectionProfile, IOException> connectionValidator)
throws ConnectTransportException {
transport.connectToNode(node, connectionProfile, connectionValidator);
}
@Override
public void disconnectFromNode(DiscoveryNode node) {
transport.disconnectFromNode(node);
}
@Override
public long serverOpen() {
return transport.serverOpen();
}
@Override
public List<String> getLocalAddresses() {
return transport.getLocalAddresses();
}
@Override
public long newRequestId() {
return transport.newRequestId();
}
@Override
public Connection getConnection(DiscoveryNode node) {
return new FilteredConnection(transport.getConnection(node)) {
@Override
public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options)
throws IOException, TransportException {
DelegateTransport.this.sendRequest(connection, requestId, action, request, options);
}
};
}
@Override
public Connection openConnection(DiscoveryNode node, ConnectionProfile profile) throws IOException {
return new FilteredConnection(transport.openConnection(node, profile)) {
@Override
public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options)
throws IOException, TransportException {
DelegateTransport.this.sendRequest(connection, requestId, action, request, options);
}
};
}
@Override
public Lifecycle.State lifecycleState() {
return transport.lifecycleState();
}
@Override
public void addLifecycleListener(LifecycleListener listener) {
transport.addLifecycleListener(listener);
}
@Override
public void removeLifecycleListener(LifecycleListener listener) {
transport.removeLifecycleListener(listener);
}
@Override
public void start() {
transport.start();
}
@Override
public void stop() {
transport.stop();
}
@Override
public void close() { transport.close(); }
@Override
public Map<String, BoundTransportAddress> profileBoundAddresses() {
return transport.profileBoundAddresses();
}
protected void sendRequest(Transport.Connection connection, long requestId, String action, TransportRequest request,
TransportRequestOptions options) throws IOException {
connection.sendRequest(requestId, action, request, options);
}
}
/**
* The delegate transport instances defined in this class mock various kinds of disruption types. This subclass adds a method
* {@link #clearRule()} so that when the disruptions are cleared (see {@link #clearRule(TransportService)}) this gives the
* disruption a possibility to run clean-up actions.
*/
public abstract static class ClearableTransport extends DelegateTransport {
public ClearableTransport(Transport transport) {
super(transport);
}
/**
* Called by {@link #clearRule(TransportService)}
*/
public abstract void clearRule();
}
List<Tracer> activeTracers = new CopyOnWriteArrayList<>();
public static class Tracer {
public void receivedRequest(long requestId, String action) {
}
public void responseSent(long requestId, String action) {
}
public void responseSent(long requestId, String action, Throwable t) {
}
public void receivedResponse(long requestId, DiscoveryNode sourceNode, String action) {
}
public void requestSent(DiscoveryNode node, long requestId, String action, TransportRequestOptions options) {
}
}
public void addTracer(Tracer tracer) {
activeTracers.add(tracer);
}
public boolean removeTracer(Tracer tracer) {
return activeTracers.remove(tracer);
}
public void clearTracers() {
activeTracers.clear();
}
@Override
protected Adapter createAdapter() {
return new MockAdapter();
}
class MockAdapter extends Adapter {
@Override
protected boolean traceEnabled() {
return super.traceEnabled() || activeTracers.isEmpty() == false;
}
@Override
protected void traceReceivedRequest(long requestId, String action) {
super.traceReceivedRequest(requestId, action);
for (Tracer tracer : activeTracers) {
tracer.receivedRequest(requestId, action);
}
}
@Override
protected void traceResponseSent(long requestId, String action) {
super.traceResponseSent(requestId, action);
for (Tracer tracer : activeTracers) {
tracer.responseSent(requestId, action);
}
}
@Override
protected void traceResponseSent(long requestId, String action, Exception e) {
super.traceResponseSent(requestId, action, e);
for (Tracer tracer : activeTracers) {
tracer.responseSent(requestId, action, e);
}
}
@Override
protected void traceReceivedResponse(long requestId, DiscoveryNode sourceNode, String action) {
super.traceReceivedResponse(requestId, sourceNode, action);
for (Tracer tracer : activeTracers) {
tracer.receivedResponse(requestId, sourceNode, action);
}
}
@Override
protected void traceRequestSent(DiscoveryNode node, long requestId, String action, TransportRequestOptions options) {
super.traceRequestSent(node, requestId, action, options);
for (Tracer tracer : activeTracers) {
tracer.requestSent(node, requestId, action, options);
}
}
}
private static class FilteredConnection implements Transport.Connection {
protected final Transport.Connection connection;
private FilteredConnection(Transport.Connection connection) {
this.connection = connection;
}
@Override
public DiscoveryNode getNode() {
return connection.getNode();
}
@Override
public Version getVersion() {
return connection.getVersion();
}
@Override
public void sendRequest(long requestId, String action, TransportRequest request, TransportRequestOptions options)
throws IOException, TransportException {
connection.sendRequest(requestId, action, request, options);
}
@Override
public void close() throws IOException {
connection.close();
}
@Override
public Object getCacheKey() {
return connection.getCacheKey();
}
}
public Transport getOriginalTransport() {
Transport transport = transport();
while (transport instanceof DelegateTransport) {
transport = ((DelegateTransport) transport).transport;
}
return transport;
}
@Override
public Transport.Connection openConnection(DiscoveryNode node, ConnectionProfile profile) throws IOException {
FilteredConnection filteredConnection = new FilteredConnection(super.openConnection(node, profile)) {
final AtomicBoolean closed = new AtomicBoolean(false);
@Override
public void close() throws IOException {
try {
super.close();
} finally {
if (closed.compareAndSet(false, true)) {
synchronized (openConnections) {
List<Transport.Connection> connections = openConnections.get(node);
boolean remove = connections.remove(this);
assert remove;
if (connections.isEmpty()) {
openConnections.remove(node);
}
}
}
}
}
};
synchronized (openConnections) {
List<Transport.Connection> connections = openConnections.computeIfAbsent(node,
(n) -> new CopyOnWriteArrayList<>());
connections.add(filteredConnection);
}
return filteredConnection;
}
@Override
protected void doClose() throws IOException {
super.doClose();
synchronized (openConnections) {
assert openConnections.size() == 0 : "still open connections: " + openConnections;
}
}
public DiscoveryNode getLocalDiscoNode() {
return this.getLocalNode();
}
}