/* * Copyright 2016 LINE Corporation * * LINE Corporation 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 com.linecorp.armeria.client.thrift; import static com.linecorp.armeria.common.http.HttpSessionProtocols.HTTP; import static com.linecorp.armeria.common.http.HttpSessionProtocols.HTTPS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.nio.ByteBuffer; import java.util.AbstractMap; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.LinkedBlockingQueue; import java.util.function.Function; import javax.net.ssl.TrustManagerFactory; import org.apache.thrift.TApplicationException; import org.apache.thrift.async.AsyncMethodCallback; import org.apache.thrift.protocol.TMessageType; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import com.linecorp.armeria.client.ClientDecorationBuilder; import com.linecorp.armeria.client.ClientFactory; import com.linecorp.armeria.client.ClientOption; import com.linecorp.armeria.client.ClientOptionValue; import com.linecorp.armeria.client.ClientOptions; import com.linecorp.armeria.client.Clients; import com.linecorp.armeria.client.SessionOption; import com.linecorp.armeria.client.SessionOptionValue; import com.linecorp.armeria.client.SessionOptions; import com.linecorp.armeria.client.http.HttpClientFactory; import com.linecorp.armeria.client.logging.KeyedChannelPoolLoggingHandler; import com.linecorp.armeria.client.logging.LoggingClient; import com.linecorp.armeria.client.pool.KeyedChannelPoolHandler; import com.linecorp.armeria.client.pool.PoolKey; import com.linecorp.armeria.common.RequestContext; import com.linecorp.armeria.common.RpcRequest; import com.linecorp.armeria.common.RpcResponse; import com.linecorp.armeria.common.SerializationFormat; import com.linecorp.armeria.common.http.HttpHeaders; import com.linecorp.armeria.common.http.HttpRequest; import com.linecorp.armeria.common.http.HttpResponse; import com.linecorp.armeria.common.logging.RequestLog; import com.linecorp.armeria.common.logging.RequestLogAvailability; import com.linecorp.armeria.common.thrift.ThriftCall; import com.linecorp.armeria.common.thrift.ThriftReply; import com.linecorp.armeria.common.thrift.ThriftSerializationFormats; import com.linecorp.armeria.common.util.Exceptions; import com.linecorp.armeria.server.Server; import com.linecorp.armeria.server.ServerBuilder; import com.linecorp.armeria.server.Service; import com.linecorp.armeria.server.logging.LoggingService; import com.linecorp.armeria.server.thrift.THttpService; import com.linecorp.armeria.service.test.thrift.main.BinaryService; import com.linecorp.armeria.service.test.thrift.main.DevNullService; import com.linecorp.armeria.service.test.thrift.main.FileService; import com.linecorp.armeria.service.test.thrift.main.FileServiceException; import com.linecorp.armeria.service.test.thrift.main.HeaderService; import com.linecorp.armeria.service.test.thrift.main.HelloService; import com.linecorp.armeria.service.test.thrift.main.OnewayHelloService; import com.linecorp.armeria.service.test.thrift.main.TimeService; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.ssl.util.SelfSignedCertificate; import io.netty.util.AsciiString; @SuppressWarnings("unchecked") @RunWith(Parameterized.class) public class ThriftOverHttpClientTest { private static final boolean ENABLE_LOGGING_DECORATORS = false; private static final Server server; private static int httpPort; private static int httpsPort; private static ClientFactory clientFactoryWithUseHttp2Preface; private static ClientFactory clientFactoryWithoutUseHttp2Preface; private static ClientOptions clientOptions; private static final BlockingQueue<String> serverReceivedNames = new LinkedBlockingQueue<>(); private static volatile boolean recordMessageLogs; private static final BlockingQueue<RequestLog> requestLogs = new LinkedBlockingQueue<>(); private static final HelloService.AsyncIface helloHandler = (name, resultHandler) -> resultHandler.onComplete("Hello, " + name + '!'); private static final HelloService.AsyncIface exceptionThrowingHandler = (name, resultHandler) -> resultHandler.onError(new Exception(name)); private static final OnewayHelloService.AsyncIface onewayHelloHandler = (name, resultHandler) -> { resultHandler.onComplete(null); assertThat(serverReceivedNames.add(name)).isTrue(); }; private static final DevNullService.AsyncIface devNullHandler = (value, resultHandler) -> { resultHandler.onComplete(null); assertThat(serverReceivedNames.add(value)).isTrue(); }; private static final BinaryService.Iface binaryHandler = data -> { ByteBuffer result = ByteBuffer.allocate(data.remaining()); for (int i = data.position(), j = 0; i < data.limit(); i++, j++) { result.put(j, (byte) (data.get(i) + 1)); } return result; }; private static final TimeService.AsyncIface timeServiceHandler = resultHandler -> resultHandler.onComplete(System.currentTimeMillis()); private static final FileService.AsyncIface fileServiceHandler = (path, resultHandler) -> resultHandler.onError(Exceptions.clearTrace(new FileServiceException())); private static final HeaderService.AsyncIface headerServiceHandler = (name, resultHandler) -> { final HttpRequest req = RequestContext.current().request(); resultHandler.onComplete(req.headers().get(AsciiString.of(name), "")); }; private enum Handlers { HELLO(helloHandler, HelloService.Iface.class, HelloService.AsyncIface.class), EXCEPTION(exceptionThrowingHandler, HelloService.Iface.class, HelloService.AsyncIface.class), ONEWAYHELLO(onewayHelloHandler, OnewayHelloService.Iface.class, OnewayHelloService.AsyncIface.class), DEVNULL(devNullHandler, DevNullService.Iface.class, DevNullService.AsyncIface.class), BINARY(binaryHandler, BinaryService.Iface.class, BinaryService.AsyncIface.class), TIME(timeServiceHandler, TimeService.Iface.class, TimeService.AsyncIface.class), FILE(fileServiceHandler, FileService.Iface.class, FileService.AsyncIface.class), HEADER(headerServiceHandler, HeaderService.Iface.class, HeaderService.AsyncIface.class); private final Object handler; private final Class<?> iface; private final Class<?> asyncIface; Handlers(Object handler, Class<?> iface, Class<?> asyncIface) { this.handler = handler; this.iface = iface; this.asyncIface = asyncIface; } Object handler() { return handler; } <T> Class<T> iface() { return (Class<T>) iface; } <T> Class<T> asyncIface() { return (Class<T>) asyncIface; } String path(SerializationFormat serializationFormat) { return '/' + name() + '/' + serializationFormat.uriText(); } } static { final SelfSignedCertificate ssc; final ServerBuilder sb = new ServerBuilder(); try { sb.port(0, HTTP); sb.port(0, HTTPS); ssc = new SelfSignedCertificate("127.0.0.1"); sb.sslContext(HTTPS, ssc.certificate(), ssc.privateKey()); for (Handlers h : Handlers.values()) { for (SerializationFormat defaultSerializationFormat : ThriftSerializationFormats.values()) { Service<HttpRequest, HttpResponse> service = THttpService.of(h.handler(), defaultSerializationFormat); if (ENABLE_LOGGING_DECORATORS) { service = service.decorate(LoggingService::new); } sb.serviceAt(h.path(defaultSerializationFormat), service); } } } catch (Exception e) { throw new Error(e); } server = sb.build(); } @Parameterized.Parameters(name = "serFmt: {0}, sessProto: {1}, useHttp2Preface: {3}") public static Collection<Object[]> parameters() throws Exception { List<Object[]> parameters = new ArrayList<>(); for (SerializationFormat serializationFormat : ThriftSerializationFormats.values()) { parameters.add(new Object[] { serializationFormat, "http", false, true }); parameters.add(new Object[] { serializationFormat, "http", false, false }); parameters.add(new Object[] { serializationFormat, "https", true, false }); parameters.add(new Object[] { serializationFormat, "h1", true, false }); // HTTP/1 over TLS parameters.add(new Object[] { serializationFormat, "h1c", false, true }); // HTTP/1 cleartext parameters.add(new Object[] { serializationFormat, "h1c", false, false }); parameters.add(new Object[] { serializationFormat, "h2", true, false }); // HTTP/2 over TLS parameters.add(new Object[] { serializationFormat, "h2c", false, true }); // HTTP/2 cleartext parameters.add(new Object[] { serializationFormat, "h2c", false, false }); } return parameters; } private final SerializationFormat serializationFormat; private final String httpProtocol; private final boolean useTls; private final boolean useHttp2Preface; public ThriftOverHttpClientTest(SerializationFormat serializationFormat, String httpProtocol, boolean useTls, boolean useHttp2Preface) { assert !(useTls && useHttp2Preface); this.serializationFormat = serializationFormat; this.httpProtocol = httpProtocol; this.useTls = useTls; this.useHttp2Preface = useHttp2Preface; } @BeforeClass public static void init() throws Exception { server.start().get(); httpPort = server.activePorts().values().stream() .filter(p -> p.protocol() == HTTP).findAny().get().localAddress() .getPort(); httpsPort = server.activePorts().values().stream() .filter(p -> p.protocol() == HTTPS).findAny().get().localAddress() .getPort(); final SessionOptionValue<TrustManagerFactory> trustManagerFactoryOptVal = SessionOption.TRUST_MANAGER_FACTORY.newValue(InsecureTrustManagerFactory.INSTANCE); final SessionOptionValue<Function<KeyedChannelPoolHandler<PoolKey>, KeyedChannelPoolHandler<PoolKey>>> poolHandlerDecoratorOptVal = SessionOption.POOL_HANDLER_DECORATOR.newValue( ENABLE_LOGGING_DECORATORS ? KeyedChannelPoolLoggingHandler::new : Function.identity()); clientFactoryWithUseHttp2Preface = new THttpClientFactory( new HttpClientFactory(SessionOptions.of( trustManagerFactoryOptVal, poolHandlerDecoratorOptVal, SessionOption.USE_HTTP2_PREFACE.newValue(true)))); clientFactoryWithoutUseHttp2Preface = new THttpClientFactory( new HttpClientFactory(SessionOptions.of( trustManagerFactoryOptVal, poolHandlerDecoratorOptVal, SessionOption.USE_HTTP2_PREFACE.newValue(false)))); final ClientDecorationBuilder decoBuilder = new ClientDecorationBuilder(); decoBuilder.add(RpcRequest.class, RpcResponse.class, (delegate, ctx, req) -> { if (recordMessageLogs) { ctx.log().addListener(requestLogs::add, RequestLogAvailability.COMPLETE); } return delegate.execute(ctx, req); }); if (ENABLE_LOGGING_DECORATORS) { decoBuilder.add(RpcRequest.class, RpcResponse.class, LoggingClient::new); } clientOptions = ClientOptions.of(ClientOption.DECORATION.newValue(decoBuilder.build())); } @AfterClass public static void destroy() throws Exception { CompletableFuture.runAsync(() -> { clientFactoryWithUseHttp2Preface.close(); clientFactoryWithoutUseHttp2Preface.close(); server.stop(); }); } @Before public void beforeTest() { serverReceivedNames.clear(); recordMessageLogs = false; requestLogs.clear(); } private ClientFactory clientFactory() { return useHttp2Preface ? clientFactoryWithUseHttp2Preface : clientFactoryWithoutUseHttp2Preface; } @Test(timeout = 10000) public void testHelloServiceSync() throws Exception { HelloService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.HELLO), Handlers.HELLO.iface(), clientOptions); assertThat(client.hello("kukuman")).isEqualTo("Hello, kukuman!"); assertThat(client.hello(null)).isEqualTo("Hello, null!"); for (int i = 0; i < 10; i++) { assertThat(client.hello("kukuman" + i)).isEqualTo("Hello, kukuman" + i + '!'); } } @Test(timeout = 10000) public void testHelloServiceAsync() throws Exception { HelloService.AsyncIface client = Clients.newClient(clientFactory(), getURI(Handlers.HELLO), Handlers.HELLO.asyncIface(), clientOptions); final int testCount = 10; final BlockingQueue<AbstractMap.SimpleEntry<Integer, ?>> resultQueue = new LinkedBlockingDeque<>(testCount); for (int i = 0; i < testCount; i++) { final int num = i; client.hello("kukuman" + num, new AsyncMethodCallback<String>() { @Override public void onComplete(String response) { assertThat(resultQueue.add(new AbstractMap.SimpleEntry<>(num, response))).isTrue(); } @Override public void onError(Exception exception) { assertThat(resultQueue.add(new AbstractMap.SimpleEntry<>(num, exception))).isTrue(); } }); } for (int i = 0; i < testCount; i++) { AbstractMap.SimpleEntry<Integer, ?> pair = resultQueue.take(); assertThat(pair.getValue()).isEqualTo("Hello, kukuman" + pair.getKey() + '!'); } } @Test(timeout = 10000) public void testOnewayHelloServiceSync() throws Exception { OnewayHelloService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.ONEWAYHELLO), Handlers.ONEWAYHELLO.iface(), clientOptions); client.hello("kukuman"); client.hello("kukuman2"); assertThat(serverReceivedNames.take()).isEqualTo("kukuman"); assertThat(serverReceivedNames.take()).isEqualTo("kukuman2"); } @Test(timeout = 10000) public void testOnewayHelloServiceAsync() throws Exception { OnewayHelloService.AsyncIface client = Clients.newClient(clientFactory(), getURI(Handlers.ONEWAYHELLO), Handlers.ONEWAYHELLO.asyncIface(), clientOptions); BlockingQueue<Object> resQueue = new LinkedBlockingQueue<>(); String[] names = { "kukuman", "kukuman2" }; for (String name : names) { client.hello(name, new RequestQueuingCallback(resQueue)); } for (String ignored : names) { assertThat(resQueue.take()).isEqualTo("null"); } for (String ignored : names) { assertThat(serverReceivedNames.take()).isIn(names); } } @Test(timeout = 10000) public void testDevNullServiceSync() throws Exception { DevNullService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.DEVNULL), Handlers.DEVNULL.iface(), clientOptions); client.consume("kukuman"); client.consume("kukuman2"); assertThat(serverReceivedNames.take()).isEqualTo("kukuman"); assertThat(serverReceivedNames.take()).isEqualTo("kukuman2"); } @Test(timeout = 10000) public void testDevNullServiceAsync() throws Exception { DevNullService.AsyncIface client = Clients.newClient(clientFactory(), getURI(Handlers.DEVNULL), Handlers.DEVNULL.asyncIface(), clientOptions); BlockingQueue<Object> resQueue = new LinkedBlockingQueue<>(); String[] names = { "kukuman", "kukuman2" }; for (String name : names) { client.consume(name, new RequestQueuingCallback(resQueue)); } for (String ignored : names) { assertThat(resQueue.take()).isEqualTo("null"); } for (String ignored : names) { assertThat(serverReceivedNames.take()).isIn(names); } } @Test(timeout = 10000) public void testBinaryServiceSync() throws Exception { BinaryService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.BINARY), Handlers.BINARY.iface(), clientOptions); ByteBuffer result = client.process(ByteBuffer.wrap(new byte[] { 1, 2 })); List<Byte> out = new ArrayList<>(); for (int i = result.position(); i < result.limit(); i++) { out.add(result.get(i)); } assertThat(out).containsExactly((byte) 2, (byte) 3); } @Test(timeout = 10000) public void testTimeServiceSync() throws Exception { TimeService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.TIME), Handlers.TIME.iface(), clientOptions); long serverTime = client.getServerTime(); assertThat(serverTime).isLessThanOrEqualTo(System.currentTimeMillis()); } @Test(timeout = 10000) public void testTimeServiceAsync() throws Exception { TimeService.AsyncIface client = Clients.newClient(clientFactory(), getURI(Handlers.TIME), Handlers.TIME.asyncIface(), clientOptions); BlockingQueue<Object> resQueue = new LinkedBlockingQueue<>(); client.getServerTime(new RequestQueuingCallback(resQueue)); final Object result = resQueue.take(); assertThat(result).isInstanceOf(Long.class); assertThat((Long) result).isLessThanOrEqualTo(System.currentTimeMillis()); } @Test(timeout = 10000, expected = FileServiceException.class) public void testFileServiceSync() throws Exception { FileService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.FILE), Handlers.FILE.iface(), clientOptions); client.create("test"); } @Test(timeout = 10000) public void testFileServiceAsync() throws Exception { FileService.AsyncIface client = Clients.newClient(clientFactory(), getURI(Handlers.FILE), Handlers.FILE.asyncIface(), clientOptions); BlockingQueue<Object> resQueue = new LinkedBlockingQueue<>(); client.create("test", new RequestQueuingCallback(resQueue)); assertThat(resQueue.take()).isInstanceOf(FileServiceException.class); } @Test(timeout = 10000) public void testDerivedClient() throws Exception { final String AUTHORIZATION = "authorization"; final String NO_TOKEN = ""; final String TOKEN_A = "token 1234"; final String TOKEN_B = "token 5678"; final HeaderService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.HEADER), Handlers.HEADER.iface(), clientOptions); assertThat(client.header(AUTHORIZATION)).isEqualTo(NO_TOKEN); final HeaderService.Iface clientA = Clients.newDerivedClient(client, newHttpHeaderOption(AsciiString.of(AUTHORIZATION), TOKEN_A)); final HeaderService.Iface clientB = Clients.newDerivedClient(client, newHttpHeaderOption(AsciiString.of(AUTHORIZATION), TOKEN_B)); assertThat(clientA.header(AUTHORIZATION)).isEqualTo(TOKEN_A); assertThat(clientB.header(AUTHORIZATION)).isEqualTo(TOKEN_B); // Ensure that the parent client's HTTP_HEADERS option did not change: assertThat(client.header(AUTHORIZATION)).isEqualTo(NO_TOKEN); } @Test(timeout = 10000) public void testMessageLogsForCall() throws Exception { HelloService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.HELLO), Handlers.HELLO.iface(), clientOptions); recordMessageLogs = true; client.hello("trustin"); final RequestLog log = requestLogs.take(); assertThat(log.requestEnvelope()).isInstanceOf(HttpHeaders.class); assertThat(log.requestContent()).isInstanceOf(RpcRequest.class); assertThat(log.rawRequestContent()).isInstanceOf(ThriftCall.class); final RpcRequest request = (RpcRequest) log.requestContent(); assertThat(request.serviceType()).isEqualTo(HelloService.Iface.class); assertThat(request.method()).isEqualTo("hello"); assertThat(request.params()).containsExactly("trustin"); final ThriftCall rawRequest = (ThriftCall) log.rawRequestContent(); assertThat(rawRequest.header().type).isEqualTo(TMessageType.CALL); assertThat(rawRequest.header().name).isEqualTo("hello"); assertThat(rawRequest.args()).isInstanceOf(HelloService.hello_args.class); assertThat(((HelloService.hello_args) rawRequest.args()).getName()).isEqualTo("trustin"); assertThat(log.responseEnvelope()).isInstanceOf(HttpHeaders.class); assertThat(log.responseContent()).isInstanceOf(RpcResponse.class); assertThat(log.rawResponseContent()).isInstanceOf(ThriftReply.class); final RpcResponse response = (RpcResponse) log.responseContent(); assertThat(response.get()).isEqualTo("Hello, trustin!"); final ThriftReply rawResponse = (ThriftReply) log.rawResponseContent(); assertThat(rawResponse.header().type).isEqualTo(TMessageType.REPLY); assertThat(rawResponse.header().name).isEqualTo("hello"); assertThat(rawResponse.result()).isInstanceOf(HelloService.hello_result.class); assertThat(((HelloService.hello_result) rawResponse.result()).getSuccess()) .isEqualTo("Hello, trustin!"); } @Test(timeout = 10000) public void testMessageLogsForOneWay() throws Exception { OnewayHelloService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.HELLO), Handlers.ONEWAYHELLO.iface(), clientOptions); recordMessageLogs = true; client.hello("trustin"); final RequestLog log = requestLogs.take(); assertThat(log.requestEnvelope()).isInstanceOf(HttpHeaders.class); assertThat(log.requestContent()).isInstanceOf(RpcRequest.class); assertThat(log.rawRequestContent()).isInstanceOf(ThriftCall.class); final RpcRequest request = (RpcRequest) log.requestContent(); assertThat(request.serviceType()).isEqualTo(OnewayHelloService.Iface.class); assertThat(request.method()).isEqualTo("hello"); assertThat(request.params()).containsExactly("trustin"); final ThriftCall rawRequest = (ThriftCall) log.rawRequestContent(); assertThat(rawRequest.header().type).isEqualTo(TMessageType.ONEWAY); assertThat(rawRequest.header().name).isEqualTo("hello"); assertThat(rawRequest.args()).isInstanceOf(OnewayHelloService.hello_args.class); assertThat(((OnewayHelloService.hello_args) rawRequest.args()).getName()).isEqualTo("trustin"); assertThat(log.responseEnvelope()).isInstanceOf(HttpHeaders.class); assertThat(log.responseContent()).isInstanceOf(RpcResponse.class); assertThat(log.rawResponseContent()).isNull(); final RpcResponse response = (RpcResponse) log.responseContent(); assertThat(response.get()).isNull(); } @Test(timeout = 10000) public void testMessageLogsForException() throws Exception { HelloService.Iface client = Clients.newClient(clientFactory(), getURI(Handlers.EXCEPTION), Handlers.EXCEPTION.iface(), clientOptions); recordMessageLogs = true; assertThatThrownBy(() -> client.hello("trustin")).isInstanceOf(TApplicationException.class); final RequestLog log = requestLogs.take(); assertThat(log.requestEnvelope()).isInstanceOf(HttpHeaders.class); assertThat(log.requestContent()).isInstanceOf(RpcRequest.class); assertThat(log.rawRequestContent()).isInstanceOf(ThriftCall.class); final RpcRequest request = (RpcRequest) log.requestContent(); assertThat(request.serviceType()).isEqualTo(HelloService.Iface.class); assertThat(request.method()).isEqualTo("hello"); assertThat(request.params()).containsExactly("trustin"); final ThriftCall rawRequest = (ThriftCall) log.rawRequestContent(); assertThat(rawRequest.header().type).isEqualTo(TMessageType.CALL); assertThat(rawRequest.header().name).isEqualTo("hello"); assertThat(rawRequest.args()).isInstanceOf(HelloService.hello_args.class); assertThat(((HelloService.hello_args) rawRequest.args()).getName()).isEqualTo("trustin"); assertThat(log.responseEnvelope()).isInstanceOf(HttpHeaders.class); assertThat(log.responseContent()).isInstanceOf(RpcResponse.class); assertThat(log.rawResponseContent()).isInstanceOf(ThriftReply.class); final RpcResponse response = (RpcResponse) log.responseContent(); assertThat(response.cause()).isNotNull(); final ThriftReply rawResponse = (ThriftReply) log.rawResponseContent(); assertThat(rawResponse.header().type).isEqualTo(TMessageType.EXCEPTION); assertThat(rawResponse.header().name).isEqualTo("hello"); assertThat(rawResponse.exception()).isNotNull(); } private static ClientOptionValue<HttpHeaders> newHttpHeaderOption(AsciiString name, String value) { return ClientOption.HTTP_HEADERS.newValue(HttpHeaders.of(name, value)); } private String getURI(Handlers handler) { int port = useTls ? httpsPort : httpPort; return serializationFormat.uriText() + '+' + httpProtocol + "://127.0.0.1:" + port + handler.path( serializationFormat); } @SuppressWarnings("rawtypes") private static class RequestQueuingCallback implements AsyncMethodCallback { private final BlockingQueue<Object> resQueue; RequestQueuingCallback(BlockingQueue<Object> resQueue) { this.resQueue = resQueue; } @Override public void onComplete(Object response) { assertThat(resQueue.add(response == null ? "null" : response)).isTrue(); } @Override public void onError(Exception exception) { assertThat(resQueue.add(exception)).isTrue(); } } }