/*
* 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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.HashMap;
import java.util.Map;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.protocol.SecurityProtocol;
import org.apache.kafka.common.utils.MockTime;
import org.apache.kafka.common.utils.Time;
import org.apache.kafka.common.utils.Utils;
import org.apache.kafka.test.TestUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* A set of tests for the selector. These use a test harness that runs a simple socket server that echos back responses.
*/
public class SelectorTest {
protected static final int BUFFER_SIZE = 4 * 1024;
protected EchoServer server;
protected Time time;
protected Selector selector;
protected ChannelBuilder channelBuilder;
private Metrics metrics;
@Before
public void setUp() throws Exception {
Map<String, Object> configs = new HashMap<>();
this.server = new EchoServer(SecurityProtocol.PLAINTEXT, configs);
this.server.start();
this.time = new MockTime();
this.channelBuilder = new PlaintextChannelBuilder();
this.channelBuilder.configure(configs);
this.metrics = new Metrics();
this.selector = new Selector(5000, this.metrics, time, "MetricGroup", channelBuilder);
}
@After
public void tearDown() throws Exception {
this.selector.close();
this.server.close();
this.metrics.close();
}
/**
* Validate that when the server disconnects, a client send ends up with that node in the disconnected list.
*/
@Test
public void testServerDisconnect() throws Exception {
String node = "0";
// connect and do a simple request
blockingConnect(node);
assertEquals("hello", blockingRequest(node, "hello"));
// disconnect
this.server.closeConnections();
while (!selector.disconnected().containsKey(node))
selector.poll(1000L);
// reconnect and do another request
blockingConnect(node);
assertEquals("hello", blockingRequest(node, "hello"));
}
/**
* Sending a request with one already in flight should result in an exception
*/
@Test(expected = IllegalStateException.class)
public void testCantSendWithInProgress() throws Exception {
String node = "0";
blockingConnect(node);
selector.send(createSend(node, "test1"));
selector.send(createSend(node, "test2"));
selector.poll(1000L);
}
/**
* Sending a request to a node without an existing connection should result in an exception
*/
@Test(expected = IllegalStateException.class)
public void testCantSendWithoutConnecting() throws Exception {
selector.send(createSend("0", "test"));
selector.poll(1000L);
}
/**
* Sending a request to a node with a bad hostname should result in an exception during connect
*/
@Test(expected = IOException.class)
public void testNoRouteToHost() throws Exception {
selector.connect("0", new InetSocketAddress("some.invalid.hostname.foo.bar.local", server.port), BUFFER_SIZE, BUFFER_SIZE);
}
/**
* Sending a request to a node not listening on that port should result in disconnection
*/
@Test
public void testConnectionRefused() throws Exception {
String node = "0";
ServerSocket nonListeningSocket = new ServerSocket(0);
int nonListeningPort = nonListeningSocket.getLocalPort();
selector.connect(node, new InetSocketAddress("localhost", nonListeningPort), BUFFER_SIZE, BUFFER_SIZE);
while (selector.disconnected().containsKey(node)) {
assertEquals(ChannelState.NOT_CONNECTED, selector.disconnected().get(node));
selector.poll(1000L);
}
nonListeningSocket.close();
}
/**
* Send multiple requests to several connections in parallel. Validate that responses are received in the order that
* requests were sent.
*/
@Test
public void testNormalOperation() throws Exception {
int conns = 5;
int reqs = 500;
// create connections
InetSocketAddress addr = new InetSocketAddress("localhost", server.port);
for (int i = 0; i < conns; i++)
connect(Integer.toString(i), addr);
// send echo requests and receive responses
Map<String, Integer> requests = new HashMap<String, Integer>();
Map<String, Integer> responses = new HashMap<String, Integer>();
int responseCount = 0;
for (int i = 0; i < conns; i++) {
String node = Integer.toString(i);
selector.send(createSend(node, node + "-0"));
}
// loop until we complete all requests
while (responseCount < conns * reqs) {
// do the i/o
selector.poll(0L);
assertEquals("No disconnects should have occurred.", 0, selector.disconnected().size());
// handle any responses we may have gotten
for (NetworkReceive receive : selector.completedReceives()) {
String[] pieces = asString(receive).split("-");
assertEquals("Should be in the form 'conn-counter'", 2, pieces.length);
assertEquals("Check the source", receive.source(), pieces[0]);
assertEquals("Check that the receive has kindly been rewound", 0, receive.payload().position());
if (responses.containsKey(receive.source())) {
assertEquals("Check the request counter", (int) responses.get(receive.source()), Integer.parseInt(pieces[1]));
responses.put(receive.source(), responses.get(receive.source()) + 1);
} else {
assertEquals("Check the request counter", 0, Integer.parseInt(pieces[1]));
responses.put(receive.source(), 1);
}
responseCount++;
}
// prepare new sends for the next round
for (Send send : selector.completedSends()) {
String dest = send.destination();
if (requests.containsKey(dest))
requests.put(dest, requests.get(dest) + 1);
else
requests.put(dest, 1);
if (requests.get(dest) < reqs)
selector.send(createSend(dest, dest + "-" + requests.get(dest)));
}
}
}
/**
* Validate that we can send and receive a message larger than the receive and send buffer size
*/
@Test
public void testSendLargeRequest() throws Exception {
String node = "0";
blockingConnect(node);
String big = TestUtils.randomString(10 * BUFFER_SIZE);
assertEquals(big, blockingRequest(node, big));
}
@Test
public void testLargeMessageSequence() throws Exception {
int bufferSize = 512 * 1024;
String node = "0";
int reqs = 50;
InetSocketAddress addr = new InetSocketAddress("localhost", server.port);
connect(node, addr);
String requestPrefix = TestUtils.randomString(bufferSize);
sendAndReceive(node, requestPrefix, 0, reqs);
}
/**
* Test sending an empty string
*/
@Test
public void testEmptyRequest() throws Exception {
String node = "0";
blockingConnect(node);
assertEquals("", blockingRequest(node, ""));
}
@Test(expected = IllegalStateException.class)
public void testExistingConnectionId() throws IOException {
blockingConnect("0");
blockingConnect("0");
}
@Test
public void testMute() throws Exception {
blockingConnect("0");
blockingConnect("1");
selector.send(createSend("0", "hello"));
selector.send(createSend("1", "hi"));
selector.mute("1");
while (selector.completedReceives().isEmpty())
selector.poll(5);
assertEquals("We should have only one response", 1, selector.completedReceives().size());
assertEquals("The response should not be from the muted node", "0", selector.completedReceives().get(0).source());
selector.unmute("1");
do {
selector.poll(5);
} while (selector.completedReceives().isEmpty());
assertEquals("We should have only one response", 1, selector.completedReceives().size());
assertEquals("The response should be from the previously muted node", "1", selector.completedReceives().get(0).source());
}
@Test
public void testCloseOldestConnection() throws Exception {
String id = "0";
blockingConnect(id);
time.sleep(6000); // The max idle time is 5000ms
selector.poll(0);
assertTrue("The idle connection should have been closed", selector.disconnected().containsKey(id));
assertEquals(ChannelState.EXPIRED, selector.disconnected().get(id));
}
private String blockingRequest(String node, String s) throws IOException {
selector.send(createSend(node, s));
selector.poll(1000L);
while (true) {
selector.poll(1000L);
for (NetworkReceive receive : selector.completedReceives())
if (receive.source().equals(node))
return asString(receive);
}
}
protected void connect(String node, InetSocketAddress serverAddr) throws IOException {
selector.connect(node, serverAddr, BUFFER_SIZE, BUFFER_SIZE);
}
/* connect and wait for the connection to complete */
private void blockingConnect(String node) throws IOException {
blockingConnect(node, new InetSocketAddress("localhost", server.port));
}
protected void blockingConnect(String node, InetSocketAddress serverAddr) throws IOException {
selector.connect(node, serverAddr, BUFFER_SIZE, BUFFER_SIZE);
while (!selector.connected().contains(node))
selector.poll(10000L);
while (!selector.isChannelReady(node))
selector.poll(10000L);
}
protected NetworkSend createSend(String node, String s) {
return new NetworkSend(node, ByteBuffer.wrap(s.getBytes()));
}
protected String asString(NetworkReceive receive) {
return new String(Utils.toArray(receive.payload()));
}
private void sendAndReceive(String node, String requestPrefix, int startIndex, int endIndex) throws Exception {
int requests = startIndex;
int responses = startIndex;
selector.send(createSend(node, requestPrefix + "-" + startIndex));
requests++;
while (responses < endIndex) {
// do the i/o
selector.poll(0L);
assertEquals("No disconnects should have occurred.", 0, selector.disconnected().size());
// handle requests and responses of the fast node
for (NetworkReceive receive : selector.completedReceives()) {
assertEquals(requestPrefix + "-" + responses, asString(receive));
responses++;
}
for (int i = 0; i < selector.completedSends().size() && requests < endIndex; i++, requests++) {
selector.send(createSend(node, requestPrefix + "-" + requests));
}
}
}
}