/** * 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.camel.component.netty; import java.io.IOException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketTimeoutException; import java.util.Deque; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ScheduledExecutorService; import org.apache.camel.CamelContext; import org.apache.camel.EndpointInject; import org.apache.camel.LoggingLevel; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.mock.MockEndpoint; import org.apache.camel.impl.DefaultCamelContext; import org.apache.camel.test.junit4.CamelTestSupport; import org.junit.Test; /** * Test the effect of redelivery in association with netty component. */ public class NettyRedeliveryTest extends CamelTestSupport { /** * Body of sufficient size such that it doesn't fit into the TCP buffer and has to be read. */ private static final byte[] LARGE_BUFFER_BODY = new byte[1000000]; /** * Failure will occur with 2 redeliveries however is increasingly more likely the more it retries. */ private static final int REDELIVERY_COUNT = 100; private ExecutorService listener = Executors.newSingleThreadExecutor(); @EndpointInject(uri = "mock:exception") private MockEndpoint exception; @EndpointInject(uri = "mock:downstream") private MockEndpoint downstream; private Deque<Callable<?>> tasks = new LinkedBlockingDeque<Callable<?>>(); private int port; private boolean alive = true; @Override protected void doPreSetup() throws Exception { // Create a server to attempt to connect to port = createServerSocket(0); } @Override protected RouteBuilder createRouteBuilder() throws Exception { return new RouteBuilder() { @Override public void configure() throws Exception { onException(Exception.class) .maximumRedeliveries(REDELIVERY_COUNT) .retryAttemptedLogLevel(LoggingLevel.INFO) .retriesExhaustedLogLevel(LoggingLevel.ERROR) // lets have a little delay so we do async redelivery .redeliveryDelay(10) .to("mock:exception") .handled(true); from("direct:start") .routeId("start") .to("netty:tcp://localhost:" + port) .to("log:downstream") .to("mock:downstream"); } }; } @Override public void tearDown() throws Exception { super.tearDown(); alive = false; listener.shutdown(); } @Test public void testExceptionHandler() throws Exception { /* * We should have 0 for this as it should never be successful however it is usual that this actually returns 1. * * This is because two or more threads run concurrently and will setException(null) which is checked during * redelivery to ascertain whether the delivery was successful, this leads to multiple downstream invocations being * possible. */ downstream.setExpectedMessageCount(0); downstream.setAssertPeriod(1000); exception.setExpectedMessageCount(1); sendBody("direct:start", LARGE_BUFFER_BODY); exception.assertIsSatisfied(); // given 100 retries usually yields somewhere around -95 // assertEquals(0, context.getInflightRepository().size("start")); // Verify the number of tasks submitted - sometimes both callbacks add a task assertEquals(REDELIVERY_COUNT, tasks.size()); // Verify the downstream completed messages - othertimes one callback gets treated as done downstream.assertIsSatisfied(); } @Override protected CamelContext createCamelContext() throws Exception { // Override the error handler executor service such that we can track the tasks created CamelContext context = new DefaultCamelContext(createRegistry()) { @Override public ScheduledExecutorService getErrorHandlerExecutorService() { return getScheduledExecutorService(); } }; return context; } private ScheduledExecutorService getScheduledExecutorService() { final ScheduledExecutorService delegate = Executors.newScheduledThreadPool(10); return newProxy(ScheduledExecutorService.class, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("submit".equals(method.getName()) || "schedule".equals(method.getName())) { tasks.add((Callable<?>) args[0]); } return method.invoke(delegate, args); } }); } private int createServerSocket(int port) throws IOException { final ServerSocket listen = new ServerSocket(port); listen.setSoTimeout(100); listener.execute(new Runnable() { private ExecutorService pool = Executors.newCachedThreadPool(); @Override public void run() { try { while (alive) { try { pool.execute(new ClosingClientRunnable(listen.accept())); } catch (SocketTimeoutException ignored) { // Allow the server socket to terminate in a timely fashion } } } catch (IOException e) { throw new RuntimeException(e); } finally { try { listen.close(); } catch (IOException ignored) { } } } }); return listen.getLocalPort(); } private static <T> T newProxy(Class<T> interfaceType, InvocationHandler handler) { Object object = Proxy.newProxyInstance(interfaceType.getClassLoader(), new Class<?>[]{interfaceType}, handler); return interfaceType.cast(object); } /** * Handler for client connection. */ private class ClosingClientRunnable implements Runnable { private final Socket socket; ClosingClientRunnable(Socket socket) { this.socket = socket; } @Override public void run() { try { Thread.sleep(10); socket.close(); } catch (Throwable e) { throw new RuntimeException(e); } finally { try { socket.close(); } catch (IOException ignored) { } } } } }