package feign.vertx; import feign.VertxFeign; import feign.jackson.JacksonDecoder; import feign.jackson.JacksonEncoder; import feign.vertx.testcase.HelloServiceAPI; import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.http.HttpClientOptions; import io.vertx.core.http.HttpServer; import io.vertx.core.http.HttpServerOptions; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.List; /** * Verify that Feign-Vertx are resilient to a server disconnect event. */ @RunWith(VertxUnitRunner.class) public class VertxHttp11ClientReconnectTest { protected Vertx vertx = Vertx.vertx(); protected HttpServer httpServer = null; HelloServiceAPI client = null; @Rule public ExpectedException expectedException = ExpectedException.none(); /** * Create a Feign Vertx client that is built once and used several times * during positive and negative test cases. * @param context */ @Before public void before(TestContext context) { // for HTTP 1.1 test, set up the pool size to 3. Then in the server side, there should be only 3 connections created per client. HttpClientOptions options = new HttpClientOptions(); options.setMaxPoolSize(3); client = VertxFeign .builder() .vertx(this.vertx) .options(options) .encoder(new JacksonEncoder()) .decoder(new JacksonDecoder()) .target(HelloServiceAPI.class, "http://localhost:8091"); } /** * Shutdown the server * @param context */ @After public void after(TestContext context) { closeServer(); } /** * Create an HTTP Server and return a future for the startup result * @return Future for handling the serve open event */ protected Future<HttpServer> createServer() { Future<HttpServer> ret = Future.future(); HttpServerOptions serverOptions = new HttpServerOptions() .setLogActivity(true) .setPort(8091) .setSsl(false); httpServer = this.vertx.createHttpServer(serverOptions); // Simple 200 handler httpServer.requestHandler( req -> req.response().setStatusCode(200).end("Success!") ); // Listen! delegating to the future httpServer.listen( ret.completer() ); return ret; } /** * Verify Feign-Vertx's HttpClient automatically reconnects * to a server that severed connections temporarily. * @param context */ @Test public void testHttpReconnectTemporary(TestContext context) { // Async test context Async async = context.async(); Future<Void> futSuccess = Future.future(); // 1. Start the server // 2. Send a Volley of Requests to the server // 3. Close server! // 4. Start server again // 5. Send another Volley of Requests to the server // If all goes well the testFuture will signal success createServer() .compose( result -> requestVolley() ) .compose( result -> closeServer() ) .compose( result -> createServer() ) .compose( result -> requestVolley() ) .compose( result -> futSuccess.complete(), futSuccess); // This future will be triggered at the end of the test futSuccess.setHandler( result -> { if (result.succeeded()) { async.complete(); // success } else { context.fail(result.cause()); } }); } /** * Verify Feign-Vertx's HttpClient reconnects to a server * after an extended outage. * @param context */ @Test public void testHttpReconnectOutage(TestContext context) { // Async test context Async async = context.async(); Future<Void> futFailed = Future.future(); Future<Void> futSuccess = Future.future(); // 1. Start the server // 2. Send a Volley of Requests to the server // 3. Close server! // 4. Send another Volley of Requests to the server // Expect a failure and absorb it with futFailed createServer() .compose( result -> requestVolley() ) .compose( result -> closeServer() ) // do not restart the server, causing a failure on the next request .compose( result -> requestVolley() ) .compose( result -> futFailed.complete(), futFailed); // We expect a failure when this future is called. futFailed.setHandler( result -> { if ( result.failed() ) { // We expect HTTP/1.1 "Connection was refused" or HTTP/2 "Connection was closed" if ( result.cause().getMessage().startsWith("Connection ") ) { // This is what we expect for this future! // 1. Start server again // 2. Re-using the same Feign-Vertx client as the failure above // we send a new volley of requests // If all goes well futSuccess will succeed and end the unit test createServer() .compose( res2 -> requestVolley() ) .compose( res2 -> futSuccess.complete(), futSuccess ); } else { // Problem, we received an unexpected error context.fail("futFailed should have seen a \"Connection refused\" but received a \""+result.cause().getMessage()+"\""); } } else { // Problem, we unexpectedly succeeded! context.fail("futFailed should have received a failure but it actually succeeded!"); } }); // This is the final future that will be triggered upon a positive test case futSuccess.setHandler( result -> { if (result.succeeded()) { async.complete(); // success } else { context.fail(result.cause()); } }); } /** * This issues ten requests to the server via a shared * long lived Feign Vertx client. * @return */ private CompositeFuture requestVolley() { List<Future> requests = new ArrayList<>(10); // Initiate 10 requests for( int i=0; i<10; i++ ) { Future<feign.Response> request = Future.future(); client.hello().setHandler(request.completer()); requests.add(request); } // Composite future for all of the requests return CompositeFuture.all(requests); } /** * Close the server connection * @return future for close completion */ public Future<Void> closeServer() { // Close server Future<Void> fut = Future.future(); httpServer.close(fut.completer()); return fut; } }