All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.kafka.common.network.Selector Maven / Gradle / Ivy

There is a newer version: 1.4.0
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF 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.apache.kafka.common.network;

import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.errors.AuthenticationException;
import org.apache.kafka.common.memory.MemoryPool;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.metrics.Sensor;
import org.apache.kafka.common.metrics.internals.IntGaugeSuite;
import org.apache.kafka.common.metrics.stats.Avg;
import org.apache.kafka.common.metrics.stats.CumulativeSum;
import org.apache.kafka.common.metrics.stats.Max;
import org.apache.kafka.common.metrics.stats.Meter;
import org.apache.kafka.common.metrics.stats.SampledStat;
import org.apache.kafka.common.metrics.stats.WindowedCount;
import org.apache.kafka.common.utils.LogContext;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;
import org.slf4j.Logger;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.UnresolvedAddressException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * A nioSelector interface for doing non-blocking multi-connection network I/O.
 * 

* This class works with {@link NetworkSend} and {@link NetworkReceive} to transmit size-delimited network requests and * responses. *

* A connection can be added to the nioSelector associated with an integer id by doing * *

 * nioSelector.connect("42", new InetSocketAddress("google.com", server.port), 64000, 64000);
 * 
* * The connect call does not block on the creation of the TCP connection, so the connect method only begins initiating * the connection. The successful invocation of this method does not mean a valid connection has been established. * * Sending requests, receiving responses, processing connection completions, and disconnections on the existing * connections are all done using the poll() call. * *
 * nioSelector.send(new NetworkSend(myDestination, myBytes));
 * nioSelector.send(new NetworkSend(myOtherDestination, myOtherBytes));
 * nioSelector.poll(TIMEOUT_MS);
 * 
* * The nioSelector maintains several lists that are reset by each call to poll() which are available via * various getters. These are reset by each call to poll(). * * This class is not thread safe! */ public class Selector implements Selectable, AutoCloseable { public static final long NO_IDLE_TIMEOUT_MS = -1; public static final int NO_FAILED_AUTHENTICATION_DELAY = 0; private enum CloseMode { GRACEFUL(true), // process outstanding buffered receives, notify disconnect NOTIFY_ONLY(true), // discard any outstanding receives, notify disconnect DISCARD_NO_NOTIFY(false); // discard any outstanding receives, no disconnect notification final boolean notifyDisconnect; CloseMode(boolean notifyDisconnect) { this.notifyDisconnect = notifyDisconnect; } } private final Logger log; private final java.nio.channels.Selector nioSelector; private final Map channels; private final Set explicitlyMutedChannels; private boolean outOfMemory; private final List completedSends; private final LinkedHashMap completedReceives; private final Set immediatelyConnectedKeys; private final Map closingChannels; private Set keysWithBufferedRead; private final Map disconnected; private final List connected; private final List failedSends; private final Time time; private final SelectorMetrics sensors; private final ChannelBuilder channelBuilder; private final int maxReceiveSize; private final boolean recordTimePerConnection; private final IdleExpiryManager idleExpiryManager; private final LinkedHashMap delayedClosingChannels; private final MemoryPool memoryPool; private final long lowMemThreshold; private final int failedAuthenticationDelayMs; //indicates if the previous call to poll was able to make progress in reading already-buffered data. //this is used to prevent tight loops when memory is not available to read any more data private boolean madeReadProgressLastPoll = true; /** * Create a new nioSelector * @param maxReceiveSize Max size in bytes of a single network receive (use {@link NetworkReceive#UNLIMITED} for no limit) * @param connectionMaxIdleMs Max idle connection time (use {@link #NO_IDLE_TIMEOUT_MS} to disable idle timeout) * @param failedAuthenticationDelayMs Minimum time by which failed authentication response and channel close should be delayed by. * Use {@link #NO_FAILED_AUTHENTICATION_DELAY} to disable this delay. * @param metrics Registry for Selector metrics * @param time Time implementation * @param metricGrpPrefix Prefix for the group of metrics registered by Selector * @param metricTags Additional tags to add to metrics registered by Selector * @param metricsPerConnection Whether or not to enable per-connection metrics * @param channelBuilder Channel builder for every new connection * @param logContext Context for logging with additional info */ public Selector(int maxReceiveSize, long connectionMaxIdleMs, int failedAuthenticationDelayMs, Metrics metrics, Time time, String metricGrpPrefix, Map metricTags, boolean metricsPerConnection, boolean recordTimePerConnection, ChannelBuilder channelBuilder, MemoryPool memoryPool, LogContext logContext) { try { this.nioSelector = java.nio.channels.Selector.open(); } catch (IOException e) { throw new KafkaException(e); } this.maxReceiveSize = maxReceiveSize; this.time = time; this.channels = new HashMap<>(); this.explicitlyMutedChannels = new HashSet<>(); this.outOfMemory = false; this.completedSends = new ArrayList<>(); this.completedReceives = new LinkedHashMap<>(); this.immediatelyConnectedKeys = new HashSet<>(); this.closingChannels = new HashMap<>(); this.keysWithBufferedRead = new HashSet<>(); this.connected = new ArrayList<>(); this.disconnected = new HashMap<>(); this.failedSends = new ArrayList<>(); this.log = logContext.logger(Selector.class); this.sensors = new SelectorMetrics(metrics, metricGrpPrefix, metricTags, metricsPerConnection); this.channelBuilder = channelBuilder; this.recordTimePerConnection = recordTimePerConnection; this.idleExpiryManager = connectionMaxIdleMs < 0 ? null : new IdleExpiryManager(time, connectionMaxIdleMs); this.memoryPool = memoryPool; this.lowMemThreshold = (long) (0.1 * this.memoryPool.size()); this.failedAuthenticationDelayMs = failedAuthenticationDelayMs; this.delayedClosingChannels = (failedAuthenticationDelayMs > NO_FAILED_AUTHENTICATION_DELAY) ? new LinkedHashMap<>() : null; } public Selector(int maxReceiveSize, long connectionMaxIdleMs, Metrics metrics, Time time, String metricGrpPrefix, Map metricTags, boolean metricsPerConnection, boolean recordTimePerConnection, ChannelBuilder channelBuilder, MemoryPool memoryPool, LogContext logContext) { this(maxReceiveSize, connectionMaxIdleMs, NO_FAILED_AUTHENTICATION_DELAY, metrics, time, metricGrpPrefix, metricTags, metricsPerConnection, recordTimePerConnection, channelBuilder, memoryPool, logContext); } public Selector(int maxReceiveSize, long connectionMaxIdleMs, int failedAuthenticationDelayMs, Metrics metrics, Time time, String metricGrpPrefix, Map metricTags, boolean metricsPerConnection, ChannelBuilder channelBuilder, LogContext logContext) { this(maxReceiveSize, connectionMaxIdleMs, failedAuthenticationDelayMs, metrics, time, metricGrpPrefix, metricTags, metricsPerConnection, false, channelBuilder, MemoryPool.NONE, logContext); } public Selector(int maxReceiveSize, long connectionMaxIdleMs, Metrics metrics, Time time, String metricGrpPrefix, Map metricTags, boolean metricsPerConnection, ChannelBuilder channelBuilder, LogContext logContext) { this(maxReceiveSize, connectionMaxIdleMs, NO_FAILED_AUTHENTICATION_DELAY, metrics, time, metricGrpPrefix, metricTags, metricsPerConnection, channelBuilder, logContext); } public Selector(long connectionMaxIdleMS, Metrics metrics, Time time, String metricGrpPrefix, ChannelBuilder channelBuilder, LogContext logContext) { this(NetworkReceive.UNLIMITED, connectionMaxIdleMS, metrics, time, metricGrpPrefix, Collections.emptyMap(), true, channelBuilder, logContext); } public Selector(long connectionMaxIdleMS, int failedAuthenticationDelayMs, Metrics metrics, Time time, String metricGrpPrefix, ChannelBuilder channelBuilder, LogContext logContext) { this(NetworkReceive.UNLIMITED, connectionMaxIdleMS, failedAuthenticationDelayMs, metrics, time, metricGrpPrefix, Collections.emptyMap(), true, channelBuilder, logContext); } /** * Begin connecting to the given address and add the connection to this nioSelector associated with the given id * number. *

* Note that this call only initiates the connection, which will be completed on a future {@link #poll(long)} * call. Check {@link #connected()} to see which (if any) connections have completed after a given poll call. * @param id The id for the new connection * @param address The address to connect to * @param sendBufferSize The send buffer for the new connection * @param receiveBufferSize The receive buffer for the new connection * @throws IllegalStateException if there is already a connection for that id * @throws IOException if DNS resolution fails on the hostname or if the broker is down */ @Override public void connect(String id, InetSocketAddress address, int sendBufferSize, int receiveBufferSize) throws IOException { ensureNotRegistered(id); SocketChannel socketChannel = SocketChannel.open(); SelectionKey key = null; try { configureSocketChannel(socketChannel, sendBufferSize, receiveBufferSize); boolean connected = doConnect(socketChannel, address); key = registerChannel(id, socketChannel, SelectionKey.OP_CONNECT); if (connected) { // OP_CONNECT won't trigger for immediately connected channels log.debug("Immediately connected to node {}", id); immediatelyConnectedKeys.add(key); key.interestOps(0); } } catch (IOException | RuntimeException e) { if (key != null) immediatelyConnectedKeys.remove(key); channels.remove(id); socketChannel.close(); throw e; } } // Visible to allow test cases to override. In particular, we use this to implement a blocking connect // in order to simulate "immediately connected" sockets. protected boolean doConnect(SocketChannel channel, InetSocketAddress address) throws IOException { try { return channel.connect(address); } catch (UnresolvedAddressException e) { throw new IOException("Can't resolve address: " + address, e); } } private void configureSocketChannel(SocketChannel socketChannel, int sendBufferSize, int receiveBufferSize) throws IOException { socketChannel.configureBlocking(false); Socket socket = socketChannel.socket(); socket.setKeepAlive(true); if (sendBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE) socket.setSendBufferSize(sendBufferSize); if (receiveBufferSize != Selectable.USE_DEFAULT_BUFFER_SIZE) socket.setReceiveBufferSize(receiveBufferSize); socket.setTcpNoDelay(true); } /** * Register the nioSelector with an existing channel * Use this on server-side, when a connection is accepted by a different thread but processed by the Selector *

* If a connection already exists with the same connection id in `channels` or `closingChannels`, * an exception is thrown. Connection ids must be chosen to avoid conflict when remote ports are reused. * Kafka brokers add an incrementing index to the connection id to avoid reuse in the timing window * where an existing connection may not yet have been closed by the broker when a new connection with * the same remote host:port is processed. *

* If a `KafkaChannel` cannot be created for this connection, the `socketChannel` is closed * and its selection key cancelled. *

*/ public void register(String id, SocketChannel socketChannel) throws IOException { ensureNotRegistered(id); registerChannel(id, socketChannel, SelectionKey.OP_READ); this.sensors.connectionCreated.record(); // Default to empty client information as the ApiVersionsRequest is not // mandatory. In this case, we still want to account for the connection. ChannelMetadataRegistry metadataRegistry = this.channel(id).channelMetadataRegistry(); if (metadataRegistry.clientInformation() == null) metadataRegistry.registerClientInformation(ClientInformation.EMPTY); } private void ensureNotRegistered(String id) { if (this.channels.containsKey(id)) throw new IllegalStateException("There is already a connection for id " + id); if (this.closingChannels.containsKey(id)) throw new IllegalStateException("There is already a connection for id " + id + " that is still being closed"); } protected SelectionKey registerChannel(String id, SocketChannel socketChannel, int interestedOps) throws IOException { SelectionKey key = socketChannel.register(nioSelector, interestedOps); KafkaChannel channel = buildAndAttachKafkaChannel(socketChannel, id, key); this.channels.put(id, channel); if (idleExpiryManager != null) idleExpiryManager.update(channel.id(), time.nanoseconds()); return key; } private KafkaChannel buildAndAttachKafkaChannel(SocketChannel socketChannel, String id, SelectionKey key) throws IOException { ChannelMetadataRegistry metadataRegistry = null; try { metadataRegistry = new SelectorChannelMetadataRegistry(); KafkaChannel channel = channelBuilder.buildChannel(id, key, maxReceiveSize, memoryPool, metadataRegistry); key.attach(channel); return channel; } catch (Exception e) { try { socketChannel.close(); } finally { key.cancel(); } // Ideally, these resources are closed by the KafkaChannel but if the KafkaChannel is not created to an // error, this builder should close the resources it has created instead. Utils.closeQuietly(metadataRegistry, "metadataRegistry"); throw new IOException("Channel could not be created for socket " + socketChannel, e); } } /** * Interrupt the nioSelector if it is blocked waiting to do I/O. */ @Override public void wakeup() { this.nioSelector.wakeup(); } /** * Close this selector and all associated connections */ @Override public void close() { List connections = new ArrayList<>(channels.keySet()); AtomicReference firstException = new AtomicReference<>(); Utils.closeAllQuietly(firstException, "release connections", connections.stream().map(id -> (AutoCloseable) () -> close(id)).toArray(AutoCloseable[]::new)); // If there is any exception thrown in close(id), we should still be able // to close the remaining objects, especially the sensors because keeping // the sensors may lead to failure to start up the ReplicaFetcherThread if // the old sensors with the same names has not yet been cleaned up. Utils.closeQuietly(nioSelector, "nioSelector", firstException); Utils.closeQuietly(sensors, "sensors", firstException); Utils.closeQuietly(channelBuilder, "channelBuilder", firstException); Throwable exception = firstException.get(); if (exception instanceof RuntimeException && !(exception instanceof SecurityException)) { throw (RuntimeException) exception; } } /** * Queue the given request for sending in the subsequent {@link #poll(long)} calls * @param send The request to send */ public void send(NetworkSend send) { String connectionId = send.destinationId(); KafkaChannel channel = openOrClosingChannelOrFail(connectionId); if (closingChannels.containsKey(connectionId)) { // ensure notification via `disconnected`, leave channel in the state in which closing was triggered this.failedSends.add(connectionId); } else { try { channel.setSend(send); } catch (Exception e) { // update the state for consistency, the channel will be discarded after `close` channel.state(ChannelState.FAILED_SEND); // ensure notification via `disconnected` when `failedSends` are processed in the next poll this.failedSends.add(connectionId); close(channel, CloseMode.DISCARD_NO_NOTIFY); if (!(e instanceof CancelledKeyException)) { log.error("Unexpected exception during send, closing connection {} and rethrowing exception.", connectionId, e); throw e; } } } } /** * Do whatever I/O can be done on each connection without blocking. This includes completing connections, completing * disconnections, initiating new sends, or making progress on in-progress sends or receives. * * When this call is completed the user can check for completed sends, receives, connections or disconnects using * {@link #completedSends()}, {@link #completedReceives()}, {@link #connected()}, {@link #disconnected()}. These * lists will be cleared at the beginning of each `poll` call and repopulated by the call if there is * any completed I/O. * * In the "Plaintext" setting, we are using socketChannel to read & write to the network. But for the "SSL" setting, * we encrypt the data before we use socketChannel to write data to the network, and decrypt before we return the responses. * This requires additional buffers to be maintained as we are reading from network, since the data on the wire is encrypted * we won't be able to read exact no.of bytes as kafka protocol requires. We read as many bytes as we can, up to SSLEngine's * application buffer size. This means we might be reading additional bytes than the requested size. * If there is no further data to read from socketChannel selector won't invoke that channel and we have additional bytes * in the buffer. To overcome this issue we added "keysWithBufferedRead" map which tracks channels which have data in the SSL * buffers. If there are channels with buffered data that can by processed, we set "timeout" to 0 and process the data even * if there is no more data to read from the socket. * * At most one entry is added to "completedReceives" for a channel in each poll. This is necessary to guarantee that * requests from a channel are processed on the broker in the order they are sent. Since outstanding requests added * by SocketServer to the request queue may be processed by different request handler threads, requests on each * channel must be processed one-at-a-time to guarantee ordering. * * @param timeout The amount of time to wait, in milliseconds, which must be non-negative * @throws IllegalArgumentException If `timeout` is negative * @throws IllegalStateException If a send is given for which we have no existing connection or for which there is * already an in-progress send */ @Override public void poll(long timeout) throws IOException { if (timeout < 0) throw new IllegalArgumentException("timeout should be >= 0"); boolean madeReadProgressLastCall = madeReadProgressLastPoll; clear(); boolean dataInBuffers = !keysWithBufferedRead.isEmpty(); if (!immediatelyConnectedKeys.isEmpty() || (madeReadProgressLastCall && dataInBuffers)) timeout = 0; if (!memoryPool.isOutOfMemory() && outOfMemory) { //we have recovered from memory pressure. unmute any channel not explicitly muted for other reasons log.trace("Broker no longer low on memory - unmuting incoming sockets"); for (KafkaChannel channel : channels.values()) { if (channel.isInMutableState() && !explicitlyMutedChannels.contains(channel)) { channel.maybeUnmute(); } } outOfMemory = false; } /* check ready keys */ long startSelect = time.nanoseconds(); int numReadyKeys = select(timeout); long endSelect = time.nanoseconds(); this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds(), false); if (numReadyKeys > 0 || !immediatelyConnectedKeys.isEmpty() || dataInBuffers) { Set readyKeys = this.nioSelector.selectedKeys(); // Poll from channels that have buffered data (but nothing more from the underlying socket) if (dataInBuffers) { keysWithBufferedRead.removeAll(readyKeys); //so no channel gets polled twice Set toPoll = keysWithBufferedRead; keysWithBufferedRead = new HashSet<>(); //poll() calls will repopulate if needed pollSelectionKeys(toPoll, false, endSelect); } // Poll from channels where the underlying socket has more data pollSelectionKeys(readyKeys, false, endSelect); // Clear all selected keys so that they are excluded from the ready count for the next select readyKeys.clear(); pollSelectionKeys(immediatelyConnectedKeys, true, endSelect); immediatelyConnectedKeys.clear(); } else { madeReadProgressLastPoll = true; //no work is also "progress" } long endIo = time.nanoseconds(); this.sensors.ioTime.record(endIo - endSelect, time.milliseconds(), false); // Close channels that were delayed and are now ready to be closed completeDelayedChannelClose(endIo); // we use the time at the end of select to ensure that we don't close any connections that // have just been processed in pollSelectionKeys maybeCloseOldestConnection(endSelect); } /** * handle any ready I/O on a set of selection keys * @param selectionKeys set of keys to handle * @param isImmediatelyConnected true if running over a set of keys for just-connected sockets * @param currentTimeNanos time at which set of keys was determined */ // package-private for testing void pollSelectionKeys(Set selectionKeys, boolean isImmediatelyConnected, long currentTimeNanos) { for (SelectionKey key : determineHandlingOrder(selectionKeys)) { KafkaChannel channel = channel(key); long channelStartTimeNanos = recordTimePerConnection ? time.nanoseconds() : 0; boolean sendFailed = false; String nodeId = channel.id(); // register all per-connection metrics at once sensors.maybeRegisterConnectionMetrics(nodeId); if (idleExpiryManager != null) idleExpiryManager.update(nodeId, currentTimeNanos); try { /* complete any connections that have finished their handshake (either normally or immediately) */ if (isImmediatelyConnected || key.isConnectable()) { if (channel.finishConnect()) { this.connected.add(nodeId); this.sensors.connectionCreated.record(); SocketChannel socketChannel = (SocketChannel) key.channel(); log.debug("Created socket with SO_RCVBUF = {}, SO_SNDBUF = {}, SO_TIMEOUT = {} to node {}", socketChannel.socket().getReceiveBufferSize(), socketChannel.socket().getSendBufferSize(), socketChannel.socket().getSoTimeout(), nodeId); } else { continue; } } /* if channel is not ready finish prepare */ if (channel.isConnected() && !channel.ready()) { channel.prepare(); if (channel.ready()) { long readyTimeMs = time.milliseconds(); boolean isReauthentication = channel.successfulAuthentications() > 1; if (isReauthentication) { sensors.successfulReauthentication.record(1.0, readyTimeMs); if (channel.reauthenticationLatencyMs() == null) log.warn( "Should never happen: re-authentication latency for a re-authenticated channel was null; continuing..."); else sensors.reauthenticationLatency .record(channel.reauthenticationLatencyMs().doubleValue(), readyTimeMs); } else { sensors.successfulAuthentication.record(1.0, readyTimeMs); if (!channel.connectedClientSupportsReauthentication()) sensors.successfulAuthenticationNoReauth.record(1.0, readyTimeMs); } log.debug("Successfully {}authenticated with {}", isReauthentication ? "re-" : "", channel.socketDescription()); } } if (channel.ready() && channel.state() == ChannelState.NOT_CONNECTED) channel.state(ChannelState.READY); Optional responseReceivedDuringReauthentication = channel.pollResponseReceivedDuringReauthentication(); responseReceivedDuringReauthentication.ifPresent(receive -> { long currentTimeMs = time.milliseconds(); addToCompletedReceives(channel, receive, currentTimeMs); }); //if channel is ready and has bytes to read from socket or buffer, and has no //previous completed receive then read from it if (channel.ready() && (key.isReadable() || channel.hasBytesBuffered()) && !hasCompletedReceive(channel) && !explicitlyMutedChannels.contains(channel)) { attemptRead(channel); } if (channel.hasBytesBuffered() && !explicitlyMutedChannels.contains(channel)) { //this channel has bytes enqueued in intermediary buffers that we could not read //(possibly because no memory). it may be the case that the underlying socket will //not come up in the next poll() and so we need to remember this channel for the //next poll call otherwise data may be stuck in said buffers forever. If we attempt //to process buffered data and no progress is made, the channel buffered status is //cleared to avoid the overhead of checking every time. keysWithBufferedRead.add(key); } /* if channel is ready write to any sockets that have space in their buffer and for which we have data */ long nowNanos = channelStartTimeNanos != 0 ? channelStartTimeNanos : currentTimeNanos; try { attemptWrite(key, channel, nowNanos); } catch (Exception e) { sendFailed = true; throw e; } /* cancel any defunct sockets */ if (!key.isValid()) close(channel, CloseMode.GRACEFUL); } catch (Exception e) { String desc = String.format("%s (channelId=%s)", channel.socketDescription(), channel.id()); if (e instanceof IOException) { log.debug("Connection with {} disconnected", desc, e); } else if (e instanceof AuthenticationException) { boolean isReauthentication = channel.successfulAuthentications() > 0; if (isReauthentication) sensors.failedReauthentication.record(); else sensors.failedAuthentication.record(); String exceptionMessage = e.getMessage(); if (e instanceof DelayedResponseAuthenticationException) exceptionMessage = e.getCause().getMessage(); log.info("Failed {}authentication with {} ({})", isReauthentication ? "re-" : "", desc, exceptionMessage); } else { log.warn("Unexpected error from {}; closing connection", desc, e); } if (e instanceof DelayedResponseAuthenticationException) maybeDelayCloseOnAuthenticationFailure(channel); else close(channel, sendFailed ? CloseMode.NOTIFY_ONLY : CloseMode.GRACEFUL); } finally { maybeRecordTimePerConnection(channel, channelStartTimeNanos); } } } private void attemptWrite(SelectionKey key, KafkaChannel channel, long nowNanos) throws IOException { if (channel.hasSend() && channel.ready() && key.isWritable() && !channel.maybeBeginClientReauthentication(() -> nowNanos)) { write(channel); } } // package-private for testing void write(KafkaChannel channel) throws IOException { String nodeId = channel.id(); long bytesSent = channel.write(); NetworkSend send = channel.maybeCompleteSend(); // We may complete the send with bytesSent < 1 if `TransportLayer.hasPendingWrites` was true and `channel.write()` // caused the pending writes to be written to the socket channel buffer if (bytesSent > 0 || send != null) { long currentTimeMs = time.milliseconds(); if (bytesSent > 0) this.sensors.recordBytesSent(nodeId, bytesSent, currentTimeMs); if (send != null) { this.completedSends.add(send); this.sensors.recordCompletedSend(nodeId, send.size(), currentTimeMs); } } } private Collection determineHandlingOrder(Set selectionKeys) { //it is possible that the iteration order over selectionKeys is the same every invocation. //this may cause starvation of reads when memory is low. to address this we shuffle the keys if memory is low. if (!outOfMemory && memoryPool.availableMemory() < lowMemThreshold) { List shuffledKeys = new ArrayList<>(selectionKeys); Collections.shuffle(shuffledKeys); return shuffledKeys; } else { return selectionKeys; } } private void attemptRead(KafkaChannel channel) throws IOException { String nodeId = channel.id(); long bytesReceived = channel.read(); if (bytesReceived != 0) { long currentTimeMs = time.milliseconds(); sensors.recordBytesReceived(nodeId, bytesReceived, currentTimeMs); madeReadProgressLastPoll = true; NetworkReceive receive = channel.maybeCompleteReceive(); if (receive != null) { addToCompletedReceives(channel, receive, currentTimeMs); } } if (channel.isMuted()) { outOfMemory = true; //channel has muted itself due to memory pressure. } else { madeReadProgressLastPoll = true; } } private boolean maybeReadFromClosingChannel(KafkaChannel channel) { boolean hasPending; if (channel.state().state() != ChannelState.State.READY) hasPending = false; else if (explicitlyMutedChannels.contains(channel) || hasCompletedReceive(channel)) hasPending = true; else { try { attemptRead(channel); hasPending = hasCompletedReceive(channel); } catch (Exception e) { log.trace("Read from closing channel failed, ignoring exception", e); hasPending = false; } } return hasPending; } // Record time spent in pollSelectionKeys for channel (moved into a method to keep checkstyle happy) private void maybeRecordTimePerConnection(KafkaChannel channel, long startTimeNanos) { if (recordTimePerConnection) channel.addNetworkThreadTimeNanos(time.nanoseconds() - startTimeNanos); } @Override public List completedSends() { return this.completedSends; } @Override public Collection completedReceives() { return this.completedReceives.values(); } @Override public Map disconnected() { return this.disconnected; } @Override public List connected() { return this.connected; } @Override public void mute(String id) { KafkaChannel channel = openOrClosingChannelOrFail(id); mute(channel); } private void mute(KafkaChannel channel) { channel.mute(); explicitlyMutedChannels.add(channel); keysWithBufferedRead.remove(channel.selectionKey()); } @Override public void unmute(String id) { KafkaChannel channel = openOrClosingChannelOrFail(id); unmute(channel); } private void unmute(KafkaChannel channel) { // Remove the channel from explicitlyMutedChannels only if the channel has been actually unmuted. if (channel.maybeUnmute()) { explicitlyMutedChannels.remove(channel); if (channel.hasBytesBuffered()) { keysWithBufferedRead.add(channel.selectionKey()); madeReadProgressLastPoll = true; } } } @Override public void muteAll() { for (KafkaChannel channel : this.channels.values()) mute(channel); } @Override public void unmuteAll() { for (KafkaChannel channel : this.channels.values()) unmute(channel); } // package-private for testing void completeDelayedChannelClose(long currentTimeNanos) { if (delayedClosingChannels == null) return; while (!delayedClosingChannels.isEmpty()) { DelayedAuthenticationFailureClose delayedClose = delayedClosingChannels.values().iterator().next(); if (!delayedClose.tryClose(currentTimeNanos)) break; } } private void maybeCloseOldestConnection(long currentTimeNanos) { if (idleExpiryManager == null) return; Map.Entry expiredConnection = idleExpiryManager.pollExpiredConnection(currentTimeNanos); if (expiredConnection != null) { String connectionId = expiredConnection.getKey(); KafkaChannel channel = this.channels.get(connectionId); if (channel != null) { if (log.isTraceEnabled()) log.trace("About to close the idle connection from {} due to being idle for {} millis", connectionId, (currentTimeNanos - expiredConnection.getValue()) / 1000 / 1000); channel.state(ChannelState.EXPIRED); close(channel, CloseMode.GRACEFUL); } } } /** * Clears completed receives. This is used by SocketServer to remove references to * receive buffers after processing completed receives, without waiting for the next * poll(). */ public void clearCompletedReceives() { this.completedReceives.clear(); } /** * Clears completed sends. This is used by SocketServer to remove references to * send buffers after processing completed sends, without waiting for the next * poll(). */ public void clearCompletedSends() { this.completedSends.clear(); } /** * Clears all the results from the previous poll. This is invoked by Selector at the start of * a poll() when all the results from the previous poll are expected to have been handled. *

* SocketServer uses {@link #clearCompletedSends()} and {@link #clearCompletedReceives()} to * clear `completedSends` and `completedReceives` as soon as they are processed to avoid * holding onto large request/response buffers from multiple connections longer than necessary. * Clients rely on Selector invoking {@link #clear()} at the start of each poll() since memory usage * is less critical and clearing once-per-poll provides the flexibility to process these results in * any order before the next poll. */ private void clear() { this.completedSends.clear(); this.completedReceives.clear(); this.connected.clear(); this.disconnected.clear(); // Remove closed channels after all their buffered receives have been processed or if a send was requested for (Iterator> it = closingChannels.entrySet().iterator(); it.hasNext(); ) { KafkaChannel channel = it.next().getValue(); boolean sendFailed = failedSends.remove(channel.id()); boolean hasPending = false; if (!sendFailed) hasPending = maybeReadFromClosingChannel(channel); if (!hasPending) { doClose(channel, true); it.remove(); } } for (String channel : this.failedSends) this.disconnected.put(channel, ChannelState.FAILED_SEND); this.failedSends.clear(); this.madeReadProgressLastPoll = false; } /** * Check for data, waiting up to the given timeout. * * @param timeoutMs Length of time to wait, in milliseconds, which must be non-negative * @return The number of keys ready */ private int select(long timeoutMs) throws IOException { if (timeoutMs < 0L) throw new IllegalArgumentException("timeout should be >= 0"); if (timeoutMs == 0L) return this.nioSelector.selectNow(); else return this.nioSelector.select(timeoutMs); } /** * Close the connection identified by the given id */ public void close(String id) { KafkaChannel channel = this.channels.get(id); if (channel != null) { // There is no disconnect notification for local close, but updating // channel state here anyway to avoid confusion. channel.state(ChannelState.LOCAL_CLOSE); close(channel, CloseMode.DISCARD_NO_NOTIFY); } else { KafkaChannel closingChannel = this.closingChannels.remove(id); // Close any closing channel, leave the channel in the state in which closing was triggered if (closingChannel != null) doClose(closingChannel, false); } } private void maybeDelayCloseOnAuthenticationFailure(KafkaChannel channel) { DelayedAuthenticationFailureClose delayedClose = new DelayedAuthenticationFailureClose(channel, failedAuthenticationDelayMs); if (delayedClosingChannels != null) delayedClosingChannels.put(channel.id(), delayedClose); else delayedClose.closeNow(); } private void handleCloseOnAuthenticationFailure(KafkaChannel channel) { try { channel.completeCloseOnAuthenticationFailure(); } catch (Exception e) { log.error("Exception handling close on authentication failure node {}", channel.id(), e); } finally { close(channel, CloseMode.GRACEFUL); } } /** * Begin closing this connection. * If 'closeMode' is `CloseMode.GRACEFUL`, the channel is disconnected here, but outstanding receives * are processed. The channel is closed when there are no outstanding receives or if a send is * requested. For other values of `closeMode`, outstanding receives are discarded and the channel * is closed immediately. * * The channel will be added to disconnect list when it is actually closed if `closeMode.notifyDisconnect` * is true. */ private void close(KafkaChannel channel, CloseMode closeMode) { channel.disconnect(); // Ensure that `connected` does not have closed channels. This could happen if `prepare` throws an exception // in the `poll` invocation when `finishConnect` succeeds connected.remove(channel.id()); // Keep track of closed channels with pending receives so that all received records // may be processed. For example, when producer with acks=0 sends some records and // closes its connections, a single poll() in the broker may receive records and // handle close(). When the remote end closes its connection, the channel is retained until // a send fails or all outstanding receives are processed. Mute state of disconnected channels // are tracked to ensure that requests are processed one-by-one by the broker to preserve ordering. if (closeMode == CloseMode.GRACEFUL && maybeReadFromClosingChannel(channel)) { closingChannels.put(channel.id(), channel); log.debug("Tracking closing connection {} to process outstanding requests", channel.id()); } else { doClose(channel, closeMode.notifyDisconnect); } this.channels.remove(channel.id()); if (delayedClosingChannels != null) delayedClosingChannels.remove(channel.id()); if (idleExpiryManager != null) idleExpiryManager.remove(channel.id()); } private void doClose(KafkaChannel channel, boolean notifyDisconnect) { SelectionKey key = channel.selectionKey(); try { immediatelyConnectedKeys.remove(key); keysWithBufferedRead.remove(key); channel.close(); } catch (IOException e) { log.error("Exception closing connection to node {}:", channel.id(), e); } finally { key.cancel(); key.attach(null); } this.sensors.connectionClosed.record(); this.explicitlyMutedChannels.remove(channel); if (notifyDisconnect) this.disconnected.put(channel.id(), channel.state()); } /** * check if channel is ready */ @Override public boolean isChannelReady(String id) { KafkaChannel channel = this.channels.get(id); return channel != null && channel.ready(); } private KafkaChannel openOrClosingChannelOrFail(String id) { KafkaChannel channel = this.channels.get(id); if (channel == null) channel = this.closingChannels.get(id); if (channel == null) throw new IllegalStateException("Attempt to retrieve channel for which there is no connection. Connection id " + id + " existing connections " + channels.keySet()); return channel; } /** * Return the selector channels. */ public List channels() { return new ArrayList<>(channels.values()); } /** * Return the channel associated with this connection or `null` if there is no channel associated with the * connection. */ public KafkaChannel channel(String id) { return this.channels.get(id); } /** * Return the channel with the specified id if it was disconnected, but not yet closed * since there are outstanding messages to be processed. */ public KafkaChannel closingChannel(String id) { return closingChannels.get(id); } /** * Returns the lowest priority channel chosen using the following sequence: * 1) If one or more channels are in closing state, return any one of them * 2) If idle expiry manager is enabled, return the least recently updated channel * 3) Otherwise return any of the channels * * This method is used to close a channel to accommodate a new channel on the inter-broker listener * when broker-wide `max.connections` limit is enabled. */ public KafkaChannel lowestPriorityChannel() { KafkaChannel channel = null; if (!closingChannels.isEmpty()) { channel = closingChannels.values().iterator().next(); } else if (idleExpiryManager != null && !idleExpiryManager.lruConnections.isEmpty()) { String channelId = idleExpiryManager.lruConnections.keySet().iterator().next(); channel = channel(channelId); } else if (!channels.isEmpty()) { channel = channels.values().iterator().next(); } return channel; } /** * Get the channel associated with selectionKey */ private KafkaChannel channel(SelectionKey key) { return (KafkaChannel) key.attachment(); } /** * Check if given channel has a completed receive */ private boolean hasCompletedReceive(KafkaChannel channel) { return completedReceives.containsKey(channel.id()); } /** * adds a receive to completed receives */ private void addToCompletedReceives(KafkaChannel channel, NetworkReceive networkReceive, long currentTimeMs) { if (hasCompletedReceive(channel)) throw new IllegalStateException("Attempting to add second completed receive to channel " + channel.id()); this.completedReceives.put(channel.id(), networkReceive); sensors.recordCompletedReceive(channel.id(), networkReceive.size(), currentTimeMs); } // only for testing public Set keys() { return new HashSet<>(nioSelector.keys()); } class SelectorChannelMetadataRegistry implements ChannelMetadataRegistry { private CipherInformation cipherInformation; private ClientInformation clientInformation; @Override public void registerCipherInformation(final CipherInformation cipherInformation) { if (this.cipherInformation != null) { if (this.cipherInformation.equals(cipherInformation)) return; sensors.connectionsByCipher.decrement(this.cipherInformation); } this.cipherInformation = cipherInformation; sensors.connectionsByCipher.increment(cipherInformation); } @Override public CipherInformation cipherInformation() { return cipherInformation; } @Override public void registerClientInformation(final ClientInformation clientInformation) { if (this.clientInformation != null) { if (this.clientInformation.equals(clientInformation)) return; sensors.connectionsByClient.decrement(this.clientInformation); } this.clientInformation = clientInformation; sensors.connectionsByClient.increment(clientInformation); } @Override public ClientInformation clientInformation() { return clientInformation; } @Override public void close() { if (this.cipherInformation != null) { sensors.connectionsByCipher.decrement(this.cipherInformation); this.cipherInformation = null; } if (this.clientInformation != null) { sensors.connectionsByClient.decrement(this.clientInformation); this.clientInformation = null; } } } class SelectorMetrics implements AutoCloseable { private final Metrics metrics; private final Map metricTags; private final boolean metricsPerConnection; private final String metricGrpName; private final String perConnectionMetricGrpName; public final Sensor connectionClosed; public final Sensor connectionCreated; public final Sensor successfulAuthentication; public final Sensor successfulReauthentication; public final Sensor successfulAuthenticationNoReauth; public final Sensor reauthenticationLatency; public final Sensor failedAuthentication; public final Sensor failedReauthentication; public final Sensor bytesTransferred; public final Sensor bytesSent; public final Sensor requestsSent; public final Sensor bytesReceived; public final Sensor responsesReceived; public final Sensor selectTime; public final Sensor ioTime; public final IntGaugeSuite connectionsByCipher; public final IntGaugeSuite connectionsByClient; /* Names of metrics that are not registered through sensors */ private final List topLevelMetricNames = new ArrayList<>(); private final List sensors = new ArrayList<>(); public SelectorMetrics(Metrics metrics, String metricGrpPrefix, Map metricTags, boolean metricsPerConnection) { this.metrics = metrics; this.metricTags = metricTags; this.metricsPerConnection = metricsPerConnection; this.metricGrpName = metricGrpPrefix + "-metrics"; this.perConnectionMetricGrpName = metricGrpPrefix + "-node-metrics"; StringBuilder tagsSuffix = new StringBuilder(); for (Map.Entry tag: metricTags.entrySet()) { tagsSuffix.append(tag.getKey()); tagsSuffix.append("-"); tagsSuffix.append(tag.getValue()); } this.connectionClosed = sensor("connections-closed:" + tagsSuffix); this.connectionClosed.add(createMeter(metrics, metricGrpName, metricTags, "connection-close", "connections closed")); this.connectionCreated = sensor("connections-created:" + tagsSuffix); this.connectionCreated.add(createMeter(metrics, metricGrpName, metricTags, "connection-creation", "new connections established")); this.successfulAuthentication = sensor("successful-authentication:" + tagsSuffix); this.successfulAuthentication.add(createMeter(metrics, metricGrpName, metricTags, "successful-authentication", "connections with successful authentication")); this.successfulReauthentication = sensor("successful-reauthentication:" + tagsSuffix); this.successfulReauthentication.add(createMeter(metrics, metricGrpName, metricTags, "successful-reauthentication", "successful re-authentication of connections")); this.successfulAuthenticationNoReauth = sensor("successful-authentication-no-reauth:" + tagsSuffix); MetricName successfulAuthenticationNoReauthMetricName = metrics.metricName( "successful-authentication-no-reauth-total", metricGrpName, "The total number of connections with successful authentication where the client does not support re-authentication", metricTags); this.successfulAuthenticationNoReauth.add(successfulAuthenticationNoReauthMetricName, new CumulativeSum()); this.failedAuthentication = sensor("failed-authentication:" + tagsSuffix); this.failedAuthentication.add(createMeter(metrics, metricGrpName, metricTags, "failed-authentication", "connections with failed authentication")); this.failedReauthentication = sensor("failed-reauthentication:" + tagsSuffix); this.failedReauthentication.add(createMeter(metrics, metricGrpName, metricTags, "failed-reauthentication", "failed re-authentication of connections")); this.reauthenticationLatency = sensor("reauthentication-latency:" + tagsSuffix); MetricName reauthenticationLatencyMaxMetricName = metrics.metricName("reauthentication-latency-max", metricGrpName, "The max latency observed due to re-authentication", metricTags); this.reauthenticationLatency.add(reauthenticationLatencyMaxMetricName, new Max()); MetricName reauthenticationLatencyAvgMetricName = metrics.metricName("reauthentication-latency-avg", metricGrpName, "The average latency observed due to re-authentication", metricTags); this.reauthenticationLatency.add(reauthenticationLatencyAvgMetricName, new Avg()); this.bytesTransferred = sensor("bytes-sent-received:" + tagsSuffix); bytesTransferred.add(createMeter(metrics, metricGrpName, metricTags, new WindowedCount(), "network-io", "network operations (reads or writes) on all connections")); this.bytesSent = sensor("bytes-sent:" + tagsSuffix, bytesTransferred); this.bytesSent.add(createMeter(metrics, metricGrpName, metricTags, "outgoing-byte", "outgoing bytes sent to all servers")); this.requestsSent = sensor("requests-sent:" + tagsSuffix); this.requestsSent.add(createMeter(metrics, metricGrpName, metricTags, new WindowedCount(), "request", "requests sent")); MetricName metricName = metrics.metricName("request-size-avg", metricGrpName, "The average size of requests sent.", metricTags); this.requestsSent.add(metricName, new Avg()); metricName = metrics.metricName("request-size-max", metricGrpName, "The maximum size of any request sent.", metricTags); this.requestsSent.add(metricName, new Max()); this.bytesReceived = sensor("bytes-received:" + tagsSuffix, bytesTransferred); this.bytesReceived.add(createMeter(metrics, metricGrpName, metricTags, "incoming-byte", "bytes read off all sockets")); this.responsesReceived = sensor("responses-received:" + tagsSuffix); this.responsesReceived.add(createMeter(metrics, metricGrpName, metricTags, new WindowedCount(), "response", "responses received")); this.selectTime = sensor("select-time:" + tagsSuffix); this.selectTime.add(createMeter(metrics, metricGrpName, metricTags, new WindowedCount(), "select", "times the I/O layer checked for new I/O to perform")); metricName = metrics.metricName("io-wait-time-ns-avg", metricGrpName, "The average length of time the I/O thread spent waiting for a socket ready for reads or writes in nanoseconds.", metricTags); this.selectTime.add(metricName, new Avg()); this.selectTime.add(createIOThreadRatioMeterLegacy(metrics, metricGrpName, metricTags, "io-wait", "waiting")); this.selectTime.add(createIOThreadRatioMeter(metrics, metricGrpName, metricTags, "io-wait", "waiting")); this.ioTime = sensor("io-time:" + tagsSuffix); metricName = metrics.metricName("io-time-ns-avg", metricGrpName, "The average length of time for I/O per select call in nanoseconds.", metricTags); this.ioTime.add(metricName, new Avg()); this.ioTime.add(createIOThreadRatioMeterLegacy(metrics, metricGrpName, metricTags, "io", "doing I/O")); this.ioTime.add(createIOThreadRatioMeter(metrics, metricGrpName, metricTags, "io", "doing I/O")); this.connectionsByCipher = new IntGaugeSuite<>(log, "sslCiphers", metrics, cipherInformation -> { Map tags = new LinkedHashMap<>(); tags.put("cipher", cipherInformation.cipher()); tags.put("protocol", cipherInformation.protocol()); tags.putAll(metricTags); return metrics.metricName("connections", metricGrpName, "The number of connections with this SSL cipher and protocol.", tags); }, 100); this.connectionsByClient = new IntGaugeSuite<>(log, "clients", metrics, clientInformation -> { Map tags = new LinkedHashMap<>(); tags.put("clientSoftwareName", clientInformation.softwareName()); tags.put("clientSoftwareVersion", clientInformation.softwareVersion()); tags.putAll(metricTags); return metrics.metricName("connections", metricGrpName, "The number of connections with this client and version.", tags); }, 100); metricName = metrics.metricName("connection-count", metricGrpName, "The current number of active connections.", metricTags); topLevelMetricNames.add(metricName); this.metrics.addMetric(metricName, (config, now) -> channels.size()); } private Meter createMeter(Metrics metrics, String groupName, Map metricTags, SampledStat stat, String baseName, String descriptiveName) { MetricName rateMetricName = metrics.metricName(baseName + "-rate", groupName, String.format("The number of %s per second", descriptiveName), metricTags); MetricName totalMetricName = metrics.metricName(baseName + "-total", groupName, String.format("The total number of %s", descriptiveName), metricTags); if (stat == null) return new Meter(rateMetricName, totalMetricName); else return new Meter(stat, rateMetricName, totalMetricName); } private Meter createMeter(Metrics metrics, String groupName, Map metricTags, String baseName, String descriptiveName) { return createMeter(metrics, groupName, metricTags, null, baseName, descriptiveName); } /** * This method generates `time-total` metrics but has a couple of deficiencies: no `-ns` suffix and no dash between basename * and `time-total` suffix. * @deprecated use {{@link #createIOThreadRatioMeter(Metrics, String, Map, String, String)}} for new metrics instead */ @Deprecated private Meter createIOThreadRatioMeterLegacy(Metrics metrics, String groupName, Map metricTags, String baseName, String action) { // this name remains relevant, non-deprecated descendant method uses the same MetricName rateMetricName = metrics.metricName(baseName + "-ratio", groupName, String.format("The fraction of time the I/O thread spent %s", action), metricTags); // this name is deprecated MetricName totalMetricName = metrics.metricName(baseName + "time-total", groupName, String.format("*Deprecated* The total time the I/O thread spent %s", action), metricTags); return new Meter(TimeUnit.NANOSECONDS, rateMetricName, totalMetricName); } private Meter createIOThreadRatioMeter(Metrics metrics, String groupName, Map metricTags, String baseName, String action) { MetricName rateMetricName = metrics.metricName(baseName + "-ratio", groupName, String.format("The fraction of time the I/O thread spent %s", action), metricTags); MetricName totalMetricName = metrics.metricName(baseName + "-time-ns-total", groupName, String.format("The total time the I/O thread spent %s", action), metricTags); return new Meter(TimeUnit.NANOSECONDS, rateMetricName, totalMetricName); } private Sensor sensor(String name, Sensor... parents) { Sensor sensor = metrics.sensor(name, parents); sensors.add(sensor); return sensor; } public void maybeRegisterConnectionMetrics(String connectionId) { if (!connectionId.isEmpty() && metricsPerConnection) { // if one sensor of the metrics has been registered for the connection, // then all other sensors should have been registered; and vice versa String nodeRequestName = "node-" + connectionId + ".requests-sent"; Sensor nodeRequest = this.metrics.getSensor(nodeRequestName); if (nodeRequest == null) { Map tags = new LinkedHashMap<>(metricTags); tags.put("node-id", "node-" + connectionId); nodeRequest = sensor(nodeRequestName); nodeRequest.add(createMeter(metrics, perConnectionMetricGrpName, tags, new WindowedCount(), "request", "requests sent")); MetricName metricName = metrics.metricName("request-size-avg", perConnectionMetricGrpName, "The average size of requests sent.", tags); nodeRequest.add(metricName, new Avg()); metricName = metrics.metricName("request-size-max", perConnectionMetricGrpName, "The maximum size of any request sent.", tags); nodeRequest.add(metricName, new Max()); String bytesSentName = "node-" + connectionId + ".bytes-sent"; Sensor bytesSent = sensor(bytesSentName); bytesSent.add(createMeter(metrics, perConnectionMetricGrpName, tags, "outgoing-byte", "outgoing bytes")); String nodeResponseName = "node-" + connectionId + ".responses-received"; Sensor nodeResponse = sensor(nodeResponseName); nodeResponse.add(createMeter(metrics, perConnectionMetricGrpName, tags, new WindowedCount(), "response", "responses received")); String bytesReceivedName = "node-" + connectionId + ".bytes-received"; Sensor bytesReceive = sensor(bytesReceivedName); bytesReceive.add(createMeter(metrics, perConnectionMetricGrpName, tags, "incoming-byte", "incoming bytes")); String nodeTimeName = "node-" + connectionId + ".latency"; Sensor nodeRequestTime = sensor(nodeTimeName); metricName = metrics.metricName("request-latency-avg", perConnectionMetricGrpName, tags); nodeRequestTime.add(metricName, new Avg()); metricName = metrics.metricName("request-latency-max", perConnectionMetricGrpName, tags); nodeRequestTime.add(metricName, new Max()); } } } public void recordBytesSent(String connectionId, long bytes, long currentTimeMs) { this.bytesSent.record(bytes, currentTimeMs, false); if (!connectionId.isEmpty()) { String bytesSentName = "node-" + connectionId + ".bytes-sent"; Sensor bytesSent = this.metrics.getSensor(bytesSentName); if (bytesSent != null) bytesSent.record(bytes, currentTimeMs); } } public void recordCompletedSend(String connectionId, long totalBytes, long currentTimeMs) { requestsSent.record(totalBytes, currentTimeMs, false); if (!connectionId.isEmpty()) { String nodeRequestName = "node-" + connectionId + ".requests-sent"; Sensor nodeRequest = this.metrics.getSensor(nodeRequestName); if (nodeRequest != null) nodeRequest.record(totalBytes, currentTimeMs); } } public void recordBytesReceived(String connectionId, long bytes, long currentTimeMs) { this.bytesReceived.record(bytes, currentTimeMs, false); if (!connectionId.isEmpty()) { String bytesReceivedName = "node-" + connectionId + ".bytes-received"; Sensor bytesReceived = this.metrics.getSensor(bytesReceivedName); if (bytesReceived != null) bytesReceived.record(bytes, currentTimeMs); } } public void recordCompletedReceive(String connectionId, long totalBytes, long currentTimeMs) { responsesReceived.record(totalBytes, currentTimeMs, false); if (!connectionId.isEmpty()) { String nodeRequestName = "node-" + connectionId + ".responses-received"; Sensor nodeRequest = this.metrics.getSensor(nodeRequestName); if (nodeRequest != null) nodeRequest.record(totalBytes, currentTimeMs); } } public void close() { for (MetricName metricName : topLevelMetricNames) metrics.removeMetric(metricName); for (Sensor sensor : sensors) metrics.removeSensor(sensor.name()); connectionsByCipher.close(); connectionsByClient.close(); } } /** * Encapsulate a channel that must be closed after a specific delay has elapsed due to authentication failure. */ private class DelayedAuthenticationFailureClose { private final KafkaChannel channel; private final long endTimeNanos; private boolean closed; /** * @param channel The channel whose close is being delayed * @param delayMs The amount of time by which the operation should be delayed */ public DelayedAuthenticationFailureClose(KafkaChannel channel, int delayMs) { this.channel = channel; this.endTimeNanos = time.nanoseconds() + (delayMs * 1000L * 1000L); this.closed = false; } /** * Try to close this channel if the delay has expired. * @param currentTimeNanos The current time * @return True if the delay has expired and the channel was closed; false otherwise */ public final boolean tryClose(long currentTimeNanos) { if (endTimeNanos <= currentTimeNanos) closeNow(); return closed; } /** * Close the channel now, regardless of whether the delay has expired or not. */ public final void closeNow() { if (closed) throw new IllegalStateException("Attempt to close a channel that has already been closed"); handleCloseOnAuthenticationFailure(channel); closed = true; } } // helper class for tracking least recently used connections to enable idle connection closing private static class IdleExpiryManager { private final Map lruConnections; private final long connectionsMaxIdleNanos; private long nextIdleCloseCheckTime; public IdleExpiryManager(Time time, long connectionsMaxIdleMs) { this.connectionsMaxIdleNanos = connectionsMaxIdleMs * 1000 * 1000; // initial capacity and load factor are default, we set them explicitly because we want to set accessOrder = true this.lruConnections = new LinkedHashMap<>(16, .75F, true); this.nextIdleCloseCheckTime = time.nanoseconds() + this.connectionsMaxIdleNanos; } public void update(String connectionId, long currentTimeNanos) { lruConnections.put(connectionId, currentTimeNanos); } public Map.Entry pollExpiredConnection(long currentTimeNanos) { if (currentTimeNanos <= nextIdleCloseCheckTime) return null; if (lruConnections.isEmpty()) { nextIdleCloseCheckTime = currentTimeNanos + connectionsMaxIdleNanos; return null; } Map.Entry oldestConnectionEntry = lruConnections.entrySet().iterator().next(); Long connectionLastActiveTime = oldestConnectionEntry.getValue(); nextIdleCloseCheckTime = connectionLastActiveTime + connectionsMaxIdleNanos; if (currentTimeNanos > nextIdleCloseCheckTime) return oldestConnectionEntry; else return null; } public void remove(String connectionId) { lruConnections.remove(connectionId); } } //package-private for testing boolean isOutOfMemory() { return outOfMemory; } //package-private for testing boolean isMadeReadProgressLastPoll() { return madeReadProgressLastPoll; } // package-private for testing Map delayedClosingChannels() { return delayedClosingChannels; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy