blob: 57125e22c5bd96e06f470a2513e2cff358aef47a [file]
/*
* 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.flume.http.sink;
import com.github.tomakehurst.wiremock.global.RequestDelaySpec;
import com.github.tomakehurst.wiremock.http.Fault;
import com.github.tomakehurst.wiremock.http.Request;
import com.github.tomakehurst.wiremock.http.RequestListener;
import com.github.tomakehurst.wiremock.http.Response;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import org.apache.flume.Context;
import org.apache.flume.EventDeliveryException;
import org.apache.flume.Sink;
import org.apache.flume.Transaction;
import org.apache.flume.channel.MemoryChannel;
import org.apache.flume.event.SimpleEvent;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.runners.MockitoJUnitRunner;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
import static org.apache.flume.Sink.Status;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
/**
* Runs a set of tests against a mocked HTTP endpoint.
*/
@RunWith(MockitoJUnitRunner.class)
public class TestHttpSinkIT {
private static final int RESPONSE_TIMEOUT = 4000;
private static final int CONNECT_TIMEOUT = 2500;
private MemoryChannel channel;
private HttpSink httpSink;
private static int findFreePort() {
try (ServerSocket socket = new ServerSocket(0)) {
return socket.getLocalPort();
} catch (IOException e) {
throw new AssertionError("Can not find free port.", e);
}
}
private final int port = findFreePort();
@Before
public void setupSink() {
if (httpSink == null) {
Context httpSinkContext = new Context();
httpSinkContext.put("endpoint", "http://localhost:" + port + "/endpoint");
httpSinkContext.put("requestTimeout", "2000");
httpSinkContext.put("connectTimeout", "1500");
httpSinkContext.put("acceptHeader", "application/json");
httpSinkContext.put("contentTypeHeader", "application/json");
httpSinkContext.put("backoff.200", "false");
httpSinkContext.put("rollback.200", "false");
httpSinkContext.put("backoff.401", "false");
httpSinkContext.put("rollback.401", "false");
httpSinkContext.put("incrementMetrics.200", "true");
Context memoryChannelContext = new Context();
channel = new MemoryChannel();
channel.configure(memoryChannelContext);
channel.start();
httpSink = new HttpSink();
httpSink.configure(httpSinkContext);
httpSink.setChannel(channel);
httpSink.start();
}
}
@After
public void waitForShutdown() throws InterruptedException {
httpSink.stop();
Thread.sleep(500);
}
@Rule
public WireMockRule service = new WireMockRule(wireMockConfig().port(port));
@Test
public void ensureSuccessfulMessageDelivery() throws Exception {
service.stubFor(post(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("SUCCESS")))
.willReturn(aResponse().withStatus(200)));
addEventToChannel(event("SUCCESS"));
service.verify(1, postRequestedFor(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("SUCCESS"))));
}
@Test
public void ensureEventsResentOn503Failure() throws Exception {
String errorScenario = "Error Scenario";
service.stubFor(post(urlEqualTo("/endpoint"))
.inScenario(errorScenario)
.whenScenarioStateIs(STARTED)
.withRequestBody(equalToJson(event("TRANSIENT_ERROR")))
.willReturn(aResponse().withStatus(503))
.willSetStateTo("Error Sent"));
service.stubFor(post(urlEqualTo("/endpoint"))
.inScenario(errorScenario)
.whenScenarioStateIs("Error Sent")
.withRequestBody(equalToJson(event("TRANSIENT_ERROR")))
.willReturn(aResponse().withStatus(200)));
addEventToChannel(event("TRANSIENT_ERROR"), Status.BACKOFF);
addEventToChannel(event("TRANSIENT_ERROR"), Status.READY);
service.verify(2, postRequestedFor(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("TRANSIENT_ERROR"))));
}
@Test
public void ensureEventsNotResentOn401Failure() throws Exception {
String errorScenario = "Error skip scenario";
service.stubFor(post(urlEqualTo("/endpoint"))
.inScenario(errorScenario)
.whenScenarioStateIs(STARTED)
.withRequestBody(equalToJson(event("UNAUTHORIZED REQUEST")))
.willReturn(aResponse().withStatus(401)
.withHeader("Content-Type", "text/plain")
.withBody("Not allowed!"))
.willSetStateTo("Error Sent"));
service.stubFor(post(urlEqualTo("/endpoint"))
.inScenario(errorScenario)
.whenScenarioStateIs("Error Sent")
.withRequestBody(equalToJson(event("NEXT EVENT")))
.willReturn(aResponse().withStatus(200)));
addEventToChannel(event("UNAUTHORIZED REQUEST"), Status.READY);
addEventToChannel(event("NEXT EVENT"), Status.READY);
service.verify(1, postRequestedFor(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("UNAUTHORIZED REQUEST"))));
service.verify(1, postRequestedFor(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("NEXT EVENT"))));
}
@Test
public void ensureEventsResentOnNetworkFailure() throws Exception {
String errorScenario = "Error Scenario";
service.stubFor(post(urlEqualTo("/endpoint"))
.inScenario(errorScenario)
.whenScenarioStateIs(STARTED)
.withRequestBody(equalToJson(event("NETWORK_ERROR")))
.willReturn(aResponse().withFault(Fault.RANDOM_DATA_THEN_CLOSE))
.willSetStateTo("Error Sent"));
service.stubFor(post(urlEqualTo("/endpoint"))
.inScenario(errorScenario)
.whenScenarioStateIs("Error Sent")
.withRequestBody(equalToJson(event("NETWORK_ERROR")))
.willReturn(aResponse().withStatus(200)));
addEventToChannel(event("NETWORK_ERROR"), Status.BACKOFF);
addEventToChannel(event("NETWORK_ERROR"), Status.READY);
service.verify(2, postRequestedFor(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("NETWORK_ERROR"))));
}
@Test
public void ensureEventsResentOnConnectionTimeout() throws Exception {
final CountDownLatch firstRequestReceived = new CountDownLatch(1);
service.addSocketAcceptDelay(new RequestDelaySpec(CONNECT_TIMEOUT));
service.addMockServiceRequestListener(new RequestListener() {
@Override
public void requestReceived(Request request, Response response) {
service.addSocketAcceptDelay(new RequestDelaySpec(0));
firstRequestReceived.countDown();
}
});
service.stubFor(post(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("SLOW_SOCKET")))
.willReturn(aResponse().withStatus(200)));
addEventToChannel(event("SLOW_SOCKET"), Status.BACKOFF);
// wait until the socket is connected
firstRequestReceived.await(2000, TimeUnit.MILLISECONDS);
addEventToChannel(event("SLOW_SOCKET"), Status.READY);
service.verify(2, postRequestedFor(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("SLOW_SOCKET"))));
}
@Test
public void ensureEventsResentOnRequestTimeout() throws Exception {
String errorScenario = "Error Scenario";
service.stubFor(post(urlEqualTo("/endpoint"))
.inScenario(errorScenario)
.whenScenarioStateIs(STARTED)
.withRequestBody(equalToJson(event("SLOW_RESPONSE")))
.willReturn(aResponse().withFixedDelay(RESPONSE_TIMEOUT).withStatus(200))
.willSetStateTo("Slow Response Sent"));
service.stubFor(post(urlEqualTo("/endpoint"))
.inScenario(errorScenario)
.whenScenarioStateIs("Slow Response Sent")
.withRequestBody(equalToJson(event("SLOW_RESPONSE")))
.willReturn(aResponse().withStatus(200)));
addEventToChannel(event("SLOW_RESPONSE"), Status.BACKOFF);
addEventToChannel(event("SLOW_RESPONSE"), Status.READY);
service.verify(2, postRequestedFor(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("SLOW_RESPONSE"))));
}
@Test
public void ensureHttpConnectionReusedForSuccessfulRequests() throws Exception {
// we should only get one delay when establishing a connection
service.addSocketAcceptDelay(new RequestDelaySpec(1000));
service.stubFor(post(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("SUCCESS")))
.willReturn(aResponse().withStatus(200)));
long startTime = System.currentTimeMillis();
addEventToChannel(event("SUCCESS"), Status.READY);
addEventToChannel(event("SUCCESS"), Status.READY);
addEventToChannel(event("SUCCESS"), Status.READY);
long endTime = System.currentTimeMillis();
assertTrue("Test should have completed faster", endTime - startTime < 2500);
service.verify(3, postRequestedFor(urlEqualTo("/endpoint"))
.withRequestBody(equalToJson(event("SUCCESS"))));
}
private void addEventToChannel(String line) throws EventDeliveryException {
addEventToChannel(line, Status.READY);
}
private void addEventToChannel(String line, Status expectedStatus)
throws EventDeliveryException {
SimpleEvent event = new SimpleEvent();
event.setBody(line.getBytes());
Transaction channelTransaction = channel.getTransaction();
channelTransaction.begin();
channel.put(event);
channelTransaction.commit();
channelTransaction.close();
Sink.Status status = httpSink.process();
assertEquals(expectedStatus, status);
}
private String event(String id) {
return "{'id':'" + id + "'}";
}
}