/**
* Copyright 2016 LinkedIn Corp. All rights reserved.
*
* Licensed 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.
*/
package com.github.ambry.network;
import com.github.ambry.config.NetworkConfig;
import com.github.ambry.utils.Time;
import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The NetworkClient provides a method for sending a list of requests in the form of {@link Send} to a host:port,
* and receive responses for sent requests. Requests that come in via {@link #sendAndPoll(List, int)} call,
* that could not be immediately sent is queued and an attempt will be made in subsequent invocations of the call (or
* until they time out).
* (Note: We will empirically determine whether, rather than queueing a request,
* a request should be failed if connections could not be checked out if pool limit for its hostPort has been reached
* and all connections to the hostPort are unavailable).
*
* This class is not thread safe.
*/
public class NetworkClient implements Closeable {
private final Selector selector;
private final ConnectionTracker connectionTracker;
private final NetworkConfig networkConfig;
private final NetworkMetrics networkMetrics;
private final Time time;
private final LinkedList<RequestMetadata> pendingRequests;
private final HashMap<String, RequestMetadata> connectionIdToRequestInFlight;
private final HashMap<String, RequestMetadata> pendingConnectionsToAssociatedRequests;
private final AtomicLong numPendingRequests;
private final int checkoutTimeoutMs;
private boolean closed = false;
private static final Logger logger = LoggerFactory.getLogger(NetworkClient.class);
/**
* Instantiates a NetworkClient.
* @param selector the {@link Selector} for this NetworkClient
* @param maxConnectionsPerPortPlainText the maximum number of connections per node per plain text port
* @param maxConnectionsPerPortSsl the maximum number of connections per node per ssl port
* @param networkConfig the {@link NetworkConfig} for this NetworkClient
* @param networkMetrics the metrics to track the network related metrics
* @param checkoutTimeoutMs the maximum time a request should remain in this NetworkClient's pending queue waiting
* for an available connection to its destination.
* @param time The Time instance to use.
*/
public NetworkClient(Selector selector, NetworkConfig networkConfig, NetworkMetrics networkMetrics,
int maxConnectionsPerPortPlainText, int maxConnectionsPerPortSsl, int checkoutTimeoutMs, Time time) {
this.selector = selector;
this.connectionTracker = new ConnectionTracker(maxConnectionsPerPortPlainText, maxConnectionsPerPortSsl);
this.networkConfig = networkConfig;
this.networkMetrics = networkMetrics;
this.checkoutTimeoutMs = checkoutTimeoutMs;
this.time = time;
pendingRequests = new LinkedList<>();
numPendingRequests = new AtomicLong(0);
connectionIdToRequestInFlight = new HashMap<>();
pendingConnectionsToAssociatedRequests = new HashMap<>();
networkMetrics.registerNetworkClientPendingConnections(numPendingRequests);
}
/**
* Attempt to send the given requests and poll for responses from the network via the associated selector. Any
* requests that could not be sent out will be added to a queue. Every time this method is called, it will first
* attempt sending the requests in the queue (or time them out) and then attempt sending the newly added requests.
* @param requestInfos the list of {@link RequestInfo} representing the requests that need to be sent out. This
* could be empty.
* @param pollTimeoutMs the poll timeout.
* @return a list of {@link ResponseInfo} representing the responses received for any requests that were sent out
* so far.
* @throws IllegalStateException if the NetworkClient is closed.
*/
public List<ResponseInfo> sendAndPoll(List<RequestInfo> requestInfos, int pollTimeoutMs) {
if (closed || !selector.isOpen()) {
throw new IllegalStateException("The NetworkClient is closed.");
}
long startTime = time.milliseconds();
List<ResponseInfo> responseInfoList = new ArrayList<>();
try {
for (RequestInfo requestInfo : requestInfos) {
ClientNetworkRequestMetrics clientNetworkRequestMetrics =
new ClientNetworkRequestMetrics(networkMetrics.requestQueueTime, networkMetrics.requestSendTime,
networkMetrics.requestSendTotalTime, 0);
pendingRequests.add(new RequestMetadata(time.milliseconds(), requestInfo, clientNetworkRequestMetrics));
}
List<NetworkSend> sends = prepareSends(responseInfoList);
selector.poll(pollTimeoutMs, sends);
handleSelectorEvents(responseInfoList);
} catch (Exception e) {
logger.error("Received an unexpected error during sendAndPoll(): ", e);
networkMetrics.networkClientException.inc();
} finally {
numPendingRequests.set(pendingRequests.size());
networkMetrics.networkClientSendAndPollTime.update(time.milliseconds() - startTime);
}
return responseInfoList;
}
/**
* Process the requests in the pendingRequestsQueue. Create {@link ResponseInfo} for those requests that have timed
* out while waiting in the queue. Then, attempt to prepare {@link NetworkSend}s by checking out connections for
* the rest of the requests in the queue.
* @param responseInfoList the list to populate with responseInfos for requests that timed out waiting for
* connections.
* @return the list of {@link NetworkSend} objects to hand over to the Selector.
*/
private List<NetworkSend> prepareSends(List<ResponseInfo> responseInfoList) {
List<NetworkSend> sends = new ArrayList<>();
ListIterator<RequestMetadata> iter = pendingRequests.listIterator();
/* Drop requests that have waited too long */
while (iter.hasNext()) {
RequestMetadata requestMetadata = iter.next();
if (time.milliseconds() - requestMetadata.requestQueuedAtMs > checkoutTimeoutMs) {
responseInfoList.add(
new ResponseInfo(requestMetadata.requestInfo, NetworkClientErrorCode.ConnectionUnavailable, null));
logger.trace("Failing request to host {} port {} due to connection unavailability",
requestMetadata.requestInfo.getHost(), requestMetadata.requestInfo.getPort());
iter.remove();
if (requestMetadata.pendingConnectionId != null) {
pendingConnectionsToAssociatedRequests.remove(requestMetadata.pendingConnectionId);
requestMetadata.pendingConnectionId = null;
}
networkMetrics.connectionTimeOutError.inc();
} else {
// Since requests are ordered by time, once the first request that cannot be dropped is found,
// we let that and the rest be iterated over in the next while loop. Just move the cursor backwards as this
// element needs to be processed.
iter.previous();
break;
}
}
while (iter.hasNext()) {
RequestMetadata requestMetadata = iter.next();
try {
String host = requestMetadata.requestInfo.getHost();
Port port = requestMetadata.requestInfo.getPort();
String connId = connectionTracker.checkOutConnection(host, port);
if (connId == null) {
if (requestMetadata.pendingConnectionId == null && connectionTracker.mayCreateNewConnection(host, port)) {
connId = selector.connect(new InetSocketAddress(host, port.getPort()), networkConfig.socketSendBufferBytes,
networkConfig.socketReceiveBufferBytes, port.getPortType());
connectionTracker.startTrackingInitiatedConnection(host, port, connId);
requestMetadata.pendingConnectionId = connId;
pendingConnectionsToAssociatedRequests.put(connId, requestMetadata);
logger.trace("Initiated a connection to host {} port {} ", host, port);
}
} else {
if (requestMetadata.pendingConnectionId != null) {
pendingConnectionsToAssociatedRequests.remove(requestMetadata.pendingConnectionId);
requestMetadata.pendingConnectionId = null;
}
logger.trace("Connection checkout succeeded for {}:{} with connectionId {} ", host, port, connId);
sends.add(new NetworkSend(connId, requestMetadata.requestInfo.getRequest(),
requestMetadata.clientNetworkRequestMetrics, time));
connectionIdToRequestInFlight.put(connId, requestMetadata);
iter.remove();
requestMetadata.onRequestDequeue();
}
} catch (IOException e) {
networkMetrics.networkClientIOError.inc();
logger.error("Received exception while checking out a connection", e);
}
}
return sends;
}
/**
* Handle Selector events after a poll: newly established connections, new disconnections, newly completed sends and
* receives.
* @param responseInfoList the list to populate with {@link ResponseInfo} objects for responses created based on
* the selector events.
*/
private void handleSelectorEvents(List<ResponseInfo> responseInfoList) {
for (String connId : selector.connected()) {
logger.trace("Checking in connection back to connection tracker for connectionId {} ", connId);
connectionTracker.checkInConnection(connId);
pendingConnectionsToAssociatedRequests.remove(connId);
}
for (String connId : selector.disconnected()) {
logger.trace("ConnectionId {} disconnected, removing it from connection tracker", connId);
connectionTracker.removeConnection(connId);
// If this was a pending connection and if there is a request that initiated this connection,
// mark the corresponding request as failed.
RequestMetadata requestMetadata = pendingConnectionsToAssociatedRequests.remove(connId);
if (requestMetadata != null) {
logger.trace("Pending connectionId {} disconnected", connId);
pendingRequests.remove(requestMetadata);
requestMetadata.pendingConnectionId = null;
responseInfoList.add(new ResponseInfo(requestMetadata.requestInfo, NetworkClientErrorCode.NetworkError, null));
} else {
// If this was an established connection and if there is a request in flight on this connection,
// mark the corresponding request as failed.
requestMetadata = connectionIdToRequestInFlight.remove(connId);
if (requestMetadata != null) {
logger.trace("ConnectionId {} with request in flight disconnected", connId);
responseInfoList.add(
new ResponseInfo(requestMetadata.requestInfo, NetworkClientErrorCode.NetworkError, null));
}
}
}
for (NetworkReceive recv : selector.completedReceives()) {
String connId = recv.getConnectionId();
logger.trace("Receive completed for connectionId {} and checking in the connection back to connection tracker",
connId);
connectionTracker.checkInConnection(connId);
RequestMetadata requestMetadata = connectionIdToRequestInFlight.remove(connId);
responseInfoList.add(new ResponseInfo(requestMetadata.requestInfo, null, recv.getReceivedBytes().getPayload()));
requestMetadata.onResponseReceive();
}
}
/**
* Close the NetworkClient and cleanup.
*/
@Override
public void close() {
logger.trace("Closing the NetworkClient");
selector.close();
closed = true;
}
/**
* Wake up the NetworkClient if it is within a {@link #sendAndPoll(List, int)} sleep. This wakes
* up the {@link Selector}, which in turn wakes up the {@link java.nio.channels.Selector}.
* <br>
* @see java.nio.channels.Selector#wakeup()
*/
public void wakeup() {
selector.wakeup();
}
/**
* A class that consists of a {@link RequestInfo} and some metadata related to the request
*/
private class RequestMetadata {
// to track network request related metrics
ClientNetworkRequestMetrics clientNetworkRequestMetrics;
// the RequestInfo associated with the request.
RequestInfo requestInfo;
// the time at which this request was queued.
private long requestQueuedAtMs;
// the time at which this request was sent(or moved from queue to in flight state)
private long requestDequeuedAtMs;
// if non-null, this is the connection that was initiated (and not established) on behalf of this request. This
// information is kept so that the NetworkClient does not keep initiating new connections for the same request, and
// so that in case this connection establishment fails, the request is failed immediately.
// Note that this is not necessarily the connection on which this request is sent eventually. This is because
// if another connection to the same destination becomes available before this pending connection is established,
// then the request will be sent on it. Similarly, if this connection gets established before a previously
// initiated connection for an earlier request in the queue, then in the next iteration, the earlier request could
// check out this connection. This, however, does not affect correctness.
private String pendingConnectionId;
RequestMetadata(long requestQueuedAtMs, RequestInfo requestInfo,
ClientNetworkRequestMetrics clientNetworkRequestMetrics) {
this.requestInfo = requestInfo;
this.requestQueuedAtMs = requestQueuedAtMs;
this.clientNetworkRequestMetrics = clientNetworkRequestMetrics;
this.pendingConnectionId = null;
}
/**
* Actions to be done on dequeue of this request and ready to be sent
*/
void onRequestDequeue() {
requestDequeuedAtMs = System.currentTimeMillis();
clientNetworkRequestMetrics.updateQueueTime(requestDequeuedAtMs - requestQueuedAtMs);
}
/**
* Actions to be done on receiving response for the request sent
*/
void onResponseReceive() {
networkMetrics.requestResponseRoundTripTime.update(System.currentTimeMillis() - requestDequeuedAtMs);
networkMetrics.requestResponseTotalTime.update(System.currentTimeMillis() - requestQueuedAtMs);
}
}
}