diff --git a/server/src/main/java/io/druid/client/BrokerServerView.java b/server/src/main/java/io/druid/client/BrokerServerView.java index 95826bb60ca5..ff5514545d3b 100644 --- a/server/src/main/java/io/druid/client/BrokerServerView.java +++ b/server/src/main/java/io/druid/client/BrokerServerView.java @@ -44,7 +44,6 @@ import io.druid.timeline.VersionedIntervalTimeline; import io.druid.timeline.partition.PartitionChunk; -import javax.annotation.Nullable; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentMap; @@ -72,6 +71,8 @@ public class BrokerServerView implements TimelineServerView private final ServiceEmitter emitter; private final Predicate> segmentFilter; + private final DirectDruidClientConfig directDruidClientConfig; + private volatile boolean initialized = false; @Inject @@ -83,7 +84,8 @@ public BrokerServerView( FilteredServerInventoryView baseView, TierSelectorStrategy tierSelectorStrategy, ServiceEmitter emitter, - final BrokerSegmentWatcherConfig segmentWatcherConfig + final BrokerSegmentWatcherConfig segmentWatcherConfig, + final DirectDruidClientConfig directDruidClientConfig ) { this.warehouse = warehouse; @@ -93,6 +95,7 @@ public BrokerServerView( this.baseView = baseView; this.tierSelectorStrategy = tierSelectorStrategy; this.emitter = emitter; + this.directDruidClientConfig = directDruidClientConfig; this.clients = Maps.newConcurrentMap(); this.selectors = Maps.newHashMap(); this.timelines = Maps.newHashMap(); @@ -200,7 +203,15 @@ private QueryableDruidServer addServer(DruidServer server) private DirectDruidClient makeDirectClient(DruidServer server) { - return new DirectDruidClient(warehouse, queryWatcher, smileMapper, httpClient, server.getHost(), emitter); + return new DirectDruidClient( + warehouse, + queryWatcher, + smileMapper, + httpClient, + server.getHost(), + emitter, + directDruidClientConfig + ); } private QueryableDruidServer removeServer(DruidServer server) diff --git a/server/src/main/java/io/druid/client/DirectDruidClient.java b/server/src/main/java/io/druid/client/DirectDruidClient.java index efceff75fb29..67710a7d6135 100644 --- a/server/src/main/java/io/druid/client/DirectDruidClient.java +++ b/server/src/main/java/io/druid/client/DirectDruidClient.java @@ -19,9 +19,6 @@ package io.druid.client; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonToken; -import com.fasterxml.jackson.core.ObjectCodec; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; @@ -29,14 +26,12 @@ import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.fasterxml.jackson.jaxrs.smile.SmileMediaTypes; import com.google.common.base.Charsets; -import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.collect.Maps; import com.google.common.io.ByteSource; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.metamx.common.IAE; import com.metamx.common.Pair; import com.metamx.common.RE; import com.metamx.common.guava.BaseSequence; @@ -54,9 +49,7 @@ import com.metamx.http.client.response.StatusResponseHolder; import io.druid.query.BaseQuery; import io.druid.query.BySegmentResultValueClass; -import io.druid.query.DruidMetrics; import io.druid.query.Query; -import io.druid.query.QueryInterruptedException; import io.druid.query.QueryRunner; import io.druid.query.QueryToolChest; import io.druid.query.QueryToolChestWarehouse; @@ -71,18 +64,14 @@ import org.jboss.netty.handler.codec.http.HttpResponse; import javax.ws.rs.core.MediaType; -import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.io.SequenceInputStream; import java.net.URL; import java.util.Enumeration; -import java.util.Iterator; import java.util.Map; import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -106,13 +95,16 @@ public class DirectDruidClient implements QueryRunner private final AtomicInteger openConnections; private final boolean isSmile; + private final DirectDruidClientConfig config; + public DirectDruidClient( QueryToolChestWarehouse warehouse, QueryWatcher queryWatcher, ObjectMapper objectMapper, HttpClient httpClient, String host, - ServiceEmitter emitter + ServiceEmitter emitter, + DirectDruidClientConfig config ) { this.warehouse = warehouse; @@ -121,6 +113,7 @@ public DirectDruidClient( this.httpClient = httpClient; this.host = host; this.emitter = emitter; + this.config = config; this.isSmile = this.objectMapper.getFactory() instanceof SmileFactory; this.openConnections = new AtomicInteger(); @@ -156,7 +149,7 @@ public Sequence run(final Query query, final Map context) } final ListenableFuture future; - final String url = String.format("http://%s/druid/v2/", host); + final String url = String.format("http://%s/%s", host, config.getQueryResponseIteratorFactory().getQueryUrlPath()); final String cancelUrl = String.format("http://%s/druid/v2/%s", host, query.getId()); try { @@ -387,16 +380,16 @@ public void onFailure(Throwable t) } Sequence retVal = new BaseSequence<>( - new BaseSequence.IteratorMaker>() + new BaseSequence.IteratorMaker>() { @Override - public JsonParserIterator make() + public QueryResponseIterator make() { - return new JsonParserIterator(typeRef, future, url); + return config.getQueryResponseIteratorFactory().make(typeRef, future, url, objectMapper, host, context); } @Override - public void cleanup(JsonParserIterator iterFromMake) + public void cleanup(QueryResponseIterator iterFromMake) { CloseQuietly.close(iterFromMake); } @@ -417,90 +410,4 @@ public void cleanup(JsonParserIterator iterFromMake) return retVal; } - - private class JsonParserIterator implements Iterator, Closeable - { - private JsonParser jp; - private ObjectCodec objectCodec; - private final JavaType typeRef; - private final Future future; - private final String url; - - public JsonParserIterator(JavaType typeRef, Future future, String url) - { - this.typeRef = typeRef; - this.future = future; - this.url = url; - jp = null; - } - - @Override - public boolean hasNext() - { - init(); - - if (jp.isClosed()) { - return false; - } - if (jp.getCurrentToken() == JsonToken.END_ARRAY) { - CloseQuietly.close(jp); - return false; - } - - return true; - } - - @Override - public T next() - { - init(); - try { - final T retVal = objectCodec.readValue(jp, typeRef); - jp.nextToken(); - return retVal; - } - catch (IOException e) { - throw Throwables.propagate(e); - } - } - - @Override - public void remove() - { - throw new UnsupportedOperationException(); - } - - private void init() - { - if (jp == null) { - try { - jp = objectMapper.getFactory().createParser(future.get()); - final JsonToken nextToken = jp.nextToken(); - if (nextToken == JsonToken.START_OBJECT) { - QueryInterruptedException cause = jp.getCodec().readValue(jp, QueryInterruptedException.class); - throw new QueryInterruptedException(cause, host); - } else if (nextToken != JsonToken.START_ARRAY) { - throw new IAE("Next token wasn't a START_ARRAY, was[%s] from url [%s]", jp.getCurrentToken(), url); - } else { - jp.nextToken(); - objectCodec = jp.getCodec(); - } - } - catch (IOException | InterruptedException | ExecutionException e) { - throw new RE(e, "Failure getting results from[%s] because of [%s]", url, e.getMessage()); - } - catch (CancellationException e) { - throw new QueryInterruptedException(e, host); - } - } - } - - @Override - public void close() throws IOException - { - if (jp != null) { - jp.close(); - } - } - } } diff --git a/server/src/main/java/io/druid/client/DirectDruidClientConfig.java b/server/src/main/java/io/druid/client/DirectDruidClientConfig.java new file mode 100644 index 000000000000..9190dc2d31db --- /dev/null +++ b/server/src/main/java/io/druid/client/DirectDruidClientConfig.java @@ -0,0 +1,43 @@ +/* + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets 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 io.druid.client; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + */ +public class DirectDruidClientConfig +{ + @JsonProperty("queryResponseIterator") + private QueryResponseIteratorFactory queryResponseIteratorFactory = new V2QueryResponseIteratorFactory(); + + @JsonProperty + private String stuff; + + public QueryResponseIteratorFactory getQueryResponseIteratorFactory() + { + return queryResponseIteratorFactory; + } + + public String getStuff() + { + return stuff; + } +} diff --git a/server/src/main/java/io/druid/client/QueryResponseIteratorFactory.java b/server/src/main/java/io/druid/client/QueryResponseIteratorFactory.java new file mode 100644 index 000000000000..1904c607ef58 --- /dev/null +++ b/server/src/main/java/io/druid/client/QueryResponseIteratorFactory.java @@ -0,0 +1,56 @@ +/* +* Licensed to Metamarkets Group Inc. (Metamarkets) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. Metamarkets 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 io.druid.client; + +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.Closeable; +import java.io.InputStream; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.Future; + +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type", defaultImpl = V2QueryResponseIteratorFactory.class) +@JsonSubTypes(value = { + @JsonSubTypes.Type(name = "v2", value = V2QueryResponseIteratorFactory.class), + @JsonSubTypes.Type(name = "v3", value = V3QueryResponseIteratorFactory.class) +}) +interface QueryResponseIteratorFactory +{ + String getQueryUrlPath(); + + QueryResponseIterator make( + JavaType typeRef, + Future future, + String url, + ObjectMapper objectMapper, + String host, + Map responseContext + ); +} + +// marker interface for QueryResponseIterator for the response received from historicals/realtime-tasks +interface QueryResponseIterator extends Iterator, Closeable +{ + +} diff --git a/server/src/main/java/io/druid/client/V2QueryResponseIterator.java b/server/src/main/java/io/druid/client/V2QueryResponseIterator.java new file mode 100644 index 000000000000..5066f880fa25 --- /dev/null +++ b/server/src/main/java/io/druid/client/V2QueryResponseIterator.java @@ -0,0 +1,160 @@ +/* +* Licensed to Metamarkets Group Inc. (Metamarkets) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. Metamarkets 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 io.druid.client; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Throwables; +import com.metamx.common.IAE; +import com.metamx.common.RE; +import com.metamx.common.guava.CloseQuietly; +import io.druid.query.QueryInterruptedException; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +class V2QueryResponseIterator implements QueryResponseIterator +{ + private JsonParser jp; + private ObjectCodec objectCodec; + private final JavaType typeRef; + private final Future future; + private final String url; + + private final ObjectMapper objectMapper; + private final String host; + private final Map responseContext; + + V2QueryResponseIterator( + JavaType typeRef, + Future future, + String url, + ObjectMapper objectMapper, + String host, + Map responseContext + ) + { + this.typeRef = typeRef; + this.future = future; + this.url = url; + jp = null; + this.objectMapper = objectMapper; + this.host = host; + this.responseContext = responseContext; + } + + @Override + public boolean hasNext() + { + init(); + + if (jp.isClosed()) { + return false; + } + if (jp.getCurrentToken() == JsonToken.END_ARRAY) { + CloseQuietly.close(jp); + return false; + } + + return true; + } + + @Override + public T next() + { + init(); + try { + final T retVal = objectCodec.readValue(jp, typeRef); + jp.nextToken(); + return retVal; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void remove() + { + throw new UnsupportedOperationException(); + } + + private void init() + { + if (jp == null) { + try { + jp = objectMapper.getFactory().createParser(future.get()); + final JsonToken nextToken = jp.nextToken(); + if (nextToken == JsonToken.START_OBJECT) { + QueryInterruptedException cause = jp.getCodec().readValue(jp, QueryInterruptedException.class); + throw new QueryInterruptedException(cause, host); + } else if (nextToken != JsonToken.START_ARRAY) { + throw new IAE("Next token wasn't a START_ARRAY, was[%s] from url [%s]", jp.getCurrentToken(), url); + } else { + jp.nextToken(); + objectCodec = jp.getCodec(); + } + } + catch (IOException | InterruptedException | ExecutionException e) { + throw new RE(e, "Failure getting results from[%s] because of [%s]", url, e.getMessage()); + } + catch (CancellationException e) { + throw new QueryInterruptedException(e, host); + } + } + } + + @Override + public void close() throws IOException + { + if (jp != null) { + jp.close(); + } + } +} + +class V2QueryResponseIteratorFactory implements QueryResponseIteratorFactory +{ + @Override + public String getQueryUrlPath() + { + return "/druid/v2"; + } + + @Override + public QueryResponseIterator make( + JavaType typeRef, + Future future, + String url, + ObjectMapper objectMapper, + String host, + Map responseContext + ) + { + return new V3QueryResponseIterator(typeRef, future, url, objectMapper, host, responseContext); + } +} diff --git a/server/src/main/java/io/druid/client/V3QueryResponseIterator.java b/server/src/main/java/io/druid/client/V3QueryResponseIterator.java new file mode 100644 index 000000000000..5e161ecf7969 --- /dev/null +++ b/server/src/main/java/io/druid/client/V3QueryResponseIterator.java @@ -0,0 +1,186 @@ +/* +* Licensed to Metamarkets Group Inc. (Metamarkets) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. Metamarkets 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 io.druid.client; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Throwables; +import com.metamx.common.IAE; +import com.metamx.common.RE; +import io.druid.query.QueryInterruptedException; +import io.druid.server.QueryResourceV3; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + */ +class V3QueryResponseIterator implements QueryResponseIterator +{ + private JsonParser jp; + private ObjectCodec objectCodec; + private final JavaType typeRef; + private final Future future; + private final String url; + + private final ObjectMapper objectMapper; + private final String host; + private final Map responseContext; + + V3QueryResponseIterator( + JavaType typeRef, + Future future, + String url, + ObjectMapper objectMapper, + String host, + Map responseContext + ) + { + this.typeRef = typeRef; + this.future = future; + this.url = url; + jp = null; + this.objectMapper = objectMapper; + this.host = host; + this.responseContext = responseContext; + } + + @Override + public boolean hasNext() + { + init(); + + if (jp.isClosed()) { + return false; + } + + if (jp.getCurrentToken() == JsonToken.END_ARRAY) { + // now we are finised reading all the result values. + try { + jp.nextToken(); //read off FIELD_NAME token for "context" + jp.nextToken(); + Map ctx = objectCodec.readValue(jp, Map.class); + if (ctx.size() > 0) { + responseContext.put(host, ctx); + } + jp.nextToken(); //read off END_OBJECT token + } catch (IOException ex) { + throw Throwables.propagate(ex); + } + + return false; + } + + + return true; + } + + @Override + public T next() + { + init(); + try { + final T retVal = objectCodec.readValue(jp, typeRef); + jp.nextToken(); + return retVal; + } + catch (IOException e) { + throw Throwables.propagate(e); + } + } + + @Override + public void remove() + { + throw new UnsupportedOperationException(); + } + + private void init() + { + if (jp == null) { + try { + jp = objectMapper.getFactory().createParser(future.get()); + if (jp.nextToken() == JsonToken.START_OBJECT) { + if (jp.nextToken() == JsonToken.FIELD_NAME) { + if (QueryResourceV3.KEY_RESULT.equals(jp.getCurrentName())) { + if (jp.nextToken() == JsonToken.START_ARRAY) { + jp.nextToken(); + objectCodec = jp.getCodec(); + } else { + throw new IAE("result must be array, token was[%s] from url [%s]", jp.getCurrentToken(), url); + } + } else { + QueryInterruptedException cause = jp.getCodec().readValue(jp, QueryInterruptedException.class); + throw new QueryInterruptedException(cause, host); + } + } else { + throw new IAE("Next token wasn't a FIELD_NAME, was[%s] from url [%s]", jp.getCurrentToken(), url); + } + } else { + throw new IAE("expecting json object, was[%s] from url [%s]", jp.getCurrentToken(), url); + } + } + catch (IOException | InterruptedException | ExecutionException e) { + throw new RE(e, "Failure getting results from[%s] because of [%s]", url, e.getMessage()); + } + catch (CancellationException e) { + throw new QueryInterruptedException(e, host); + } + } + } + + @Override + public void close() throws IOException + { + if (jp != null) { + jp.close(); + } + } +} + +class V3QueryResponseIteratorFactory implements QueryResponseIteratorFactory +{ + @Override + public String getQueryUrlPath() + { + return "/druid/v3"; + } + + @Override + public QueryResponseIterator make( + JavaType typeRef, + Future future, + String url, + ObjectMapper objectMapper, + String host, + Map responseContext + ) + { + return null; + } +} + diff --git a/server/src/main/java/io/druid/server/AsyncQueryForwardingServlet.java b/server/src/main/java/io/druid/server/AsyncQueryForwardingServlet.java index c8d00f7b7069..a29741b217ec 100644 --- a/server/src/main/java/io/druid/server/AsyncQueryForwardingServlet.java +++ b/server/src/main/java/io/druid/server/AsyncQueryForwardingServlet.java @@ -62,6 +62,9 @@ */ public class AsyncQueryForwardingServlet extends AsyncProxyServlet { + public static final String QUERY_URL_V2 = "/druid/v2"; + public static final String QUERY_URL_V3 = "/druid/v3"; + private static final EmittingLogger log = new EmittingLogger(AsyncQueryForwardingServlet.class); @Deprecated // use SmileMediaTypes.APPLICATION_JACKSON_SMILE private static final String APPLICATION_SMILE = "application/smile"; @@ -161,9 +164,9 @@ protected void service(HttpServletRequest request, HttpServletResponse response) final String defaultHost = hostFinder.getDefaultHost(); request.setAttribute(HOST_ATTRIBUTE, defaultHost); - final boolean isQueryEndpoint = request.getRequestURI().startsWith("/druid/v2"); + final String requestURI = request.getRequestURI(); - if (isQueryEndpoint && HttpMethod.DELETE.is(request.getMethod())) { + if (requestURI.startsWith(QUERY_URL_V2) && HttpMethod.DELETE.is(request.getMethod())) { // query cancellation request for (final String host : hostFinder.getAllHosts()) { // send query cancellation to all brokers this query may have gone to @@ -192,7 +195,8 @@ public void onComplete(Result result) ); } } - } else if (isQueryEndpoint && HttpMethod.POST.is(request.getMethod())) { + } else if ((requestURI.startsWith(QUERY_URL_V2) || requestURI.startsWith(QUERY_URL_V3)) && + HttpMethod.POST.is(request.getMethod())) { // query request try { Query inputQuery = objectMapper.readValue(request.getInputStream(), Query.class); diff --git a/server/src/main/java/io/druid/server/QueryResource.java b/server/src/main/java/io/druid/server/QueryResource.java index 06681e658115..5ccc074130b8 100644 --- a/server/src/main/java/io/druid/server/QueryResource.java +++ b/server/src/main/java/io/druid/server/QueryResource.java @@ -241,76 +241,7 @@ public Object accumulate(Object accumulated, Object in) } ); - try { - final Query theQuery = query; - final QueryToolChest theToolChest = toolChest; - Response.ResponseBuilder builder = Response - .ok( - new StreamingOutput() - { - @Override - public void write(OutputStream outputStream) throws IOException, WebApplicationException - { - // json serializer will always close the yielder - CountingOutputStream os = new CountingOutputStream(outputStream); - jsonWriter.writeValue(os, yielder); - - os.flush(); // Some types of OutputStream suppress flush errors in the .close() method. - os.close(); - - final long queryTime = System.currentTimeMillis() - start; - emitter.emit( - DruidMetrics.makeQueryTimeMetric(theToolChest, jsonMapper, theQuery, req.getRemoteAddr()) - .setDimension("success", "true") - .build("query/time", queryTime) - ); - emitter.emit( - DruidMetrics.makeQueryTimeMetric(theToolChest, jsonMapper, theQuery, req.getRemoteAddr()) - .build("query/bytes", os.getCount()) - ); - - requestLogger.log( - new RequestLogLine( - new DateTime(start), - req.getRemoteAddr(), - theQuery, - new QueryStats( - ImmutableMap.of( - "query/time", queryTime, - "query/bytes", os.getCount(), - "success", true - ) - ) - ) - ); - } - }, - contentType - ) - .header("X-Druid-Query-Id", queryId); - - //Limit the response-context header, see https://github.com/druid-io/druid/issues/2331 - //Note that Response.ResponseBuilder.header(String key,Object value).build() calls value.toString() - //and encodes the string using ASCII, so 1 char is = 1 byte - String responseCtxString = jsonMapper.writeValueAsString(responseContext); - if (responseCtxString.length() > RESPONSE_CTX_HEADER_LEN_LIMIT) { - log.warn("Response Context truncated for id [%s] . Full context is [%s].", queryId, responseCtxString); - responseCtxString = responseCtxString.substring(0, RESPONSE_CTX_HEADER_LEN_LIMIT); - } - - return builder - .header("X-Druid-Response-Context", responseCtxString) - .build(); - } - catch (Exception e) { - // make sure to close yielder if anything happened before starting to serialize the response. - yielder.close(); - throw Throwables.propagate(e); - } - finally { - // do not close yielder here, since we do not want to close the yielder prior to - // StreamingOutput having iterated over all the results - } + return buildQueryResponse(req, query, toolChest, jsonWriter, contentType, start, yielder, responseContext); } catch (QueryInterruptedException e) { try { @@ -398,4 +329,89 @@ public void write(OutputStream outputStream) throws IOException, WebApplicationE Thread.currentThread().setName(currThreadName); } } + + protected Response buildQueryResponse( + final HttpServletRequest req, + final Query query, + final QueryToolChest toolChest, + final ObjectWriter jsonWriter, + final String contentType, + final long startTime, + final Yielder yielder, + final Map responseContext + ) + throws IOException + { + try { + Response.ResponseBuilder builder = Response + .ok( + new StreamingOutput() + { + @Override + public void write(OutputStream outputStream) throws IOException, WebApplicationException + { + // json serializer will always close the yielder + CountingOutputStream os = new CountingOutputStream(outputStream); + + try { + jsonWriter.writeValue(os, yielder); + } finally { + os.flush(); // Some types of OutputStream suppress flush errors in the .close() method. + os.close(); + } + + final long queryTime = System.currentTimeMillis() - startTime; + emitter.emit( + DruidMetrics.makeQueryTimeMetric(toolChest, jsonMapper, query, req.getRemoteAddr()) + .setDimension("success", "true") + .build("query/time", queryTime) + ); + emitter.emit( + DruidMetrics.makeQueryTimeMetric(toolChest, jsonMapper, query, req.getRemoteAddr()) + .build("query/bytes", os.getCount()) + ); + + requestLogger.log( + new RequestLogLine( + new DateTime(startTime), + req.getRemoteAddr(), + query, + new QueryStats( + ImmutableMap.of( + "query/time", queryTime, + "query/bytes", os.getCount(), + "success", true + ) + ) + ) + ); + } + }, + contentType + ) + .header("X-Druid-Query-Id", query.getId()); + + //Limit the response-context header, see https://github.com/druid-io/druid/issues/2331 + //Note that Response.ResponseBuilder.header(String key,Object value).build() calls value.toString() + //and encodes the string using ASCII, so 1 char is = 1 byte + String responseCtxString = jsonMapper.writeValueAsString(responseContext); + if (responseCtxString.length() > RESPONSE_CTX_HEADER_LEN_LIMIT) { + log.warn("Response Context truncated for id [%s] . Full context is [%s].", query.getId(), responseCtxString); + responseCtxString = responseCtxString.substring(0, RESPONSE_CTX_HEADER_LEN_LIMIT); + } + + return builder + .header("X-Druid-Response-Context", responseCtxString) + .build(); + } + catch (Exception e) { + // make sure to close yielder if anything happened before starting to serialize the response. + yielder.close(); + throw Throwables.propagate(e); + } + finally { + // do not close yielder here, since we do not want to close the yielder prior to + // StreamingOutput having iterated over all the results + } + } } diff --git a/server/src/main/java/io/druid/server/QueryResourceV3.java b/server/src/main/java/io/druid/server/QueryResourceV3.java new file mode 100644 index 000000000000..f143d521409e --- /dev/null +++ b/server/src/main/java/io/druid/server/QueryResourceV3.java @@ -0,0 +1,164 @@ +/* + * + * Licensed to Metamarkets Group Inc. (Metamarkets) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. Metamarkets 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 io.druid.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.common.io.CountingOutputStream; +import com.google.inject.Inject; +import com.metamx.common.guava.Yielder; +import com.metamx.emitter.service.ServiceEmitter; +import io.druid.guice.annotations.Json; +import io.druid.guice.annotations.Smile; +import io.druid.query.DruidMetrics; +import io.druid.query.Query; +import io.druid.query.QuerySegmentWalker; +import io.druid.query.QueryToolChest; +import io.druid.query.QueryToolChestWarehouse; +import io.druid.server.initialization.ServerConfig; +import io.druid.server.log.RequestLogger; +import io.druid.server.security.AuthConfig; +import org.joda.time.DateTime; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.Path; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.StreamingOutput; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Map; + +/** + */ +@Path("/druid/v3/") +public class QueryResourceV3 extends QueryResource +{ + public static final String KEY_RESULT = "result"; + public static final String KEY_CONTEXT = "context"; + + private final ServiceEmitter emitter; + private final ObjectMapper jsonMapper; + private final RequestLogger requestLogger; + + @Inject + public QueryResourceV3( + QueryToolChestWarehouse warehouse, + ServerConfig config, + @Json ObjectMapper jsonMapper, + @Smile ObjectMapper smileMapper, + QuerySegmentWalker texasRanger, + ServiceEmitter emitter, + RequestLogger requestLogger, + QueryManager queryManager, + AuthConfig authConfig + ) + { + super(warehouse, config, jsonMapper, smileMapper, texasRanger, emitter, requestLogger, queryManager, authConfig); + this.emitter = emitter; + this.jsonMapper = jsonMapper; + this.requestLogger = requestLogger; + } + + @Override + protected Response buildQueryResponse( + final HttpServletRequest req, + final Query query, + final QueryToolChest toolChest, + final ObjectWriter jsonWriter, + final String contentType, + final long startTime, + final Yielder yielder, + final Map responseContext + ) + throws IOException + { + try { + return Response + .ok( + new StreamingOutput() + { + @Override + public void write(OutputStream outputStream) throws IOException, WebApplicationException + { + CountingOutputStream os = new CountingOutputStream(outputStream); + + //jsonSerializer would close the yielder. + try { + jsonWriter.writeValue( + os, + ImmutableMap.of( + KEY_RESULT, yielder, + KEY_CONTEXT, responseContext + ) + ); + } finally { + os.flush(); // Some types of OutputStream suppress flush errors in the .close() method. + os.close(); + } + + final long queryTime = System.currentTimeMillis() - startTime; + emitter.emit( + DruidMetrics.makeQueryTimeMetric(toolChest, jsonMapper, query, req.getRemoteAddr()) + .setDimension("success", "true") + .build("query/time", queryTime) + ); + emitter.emit( + DruidMetrics.makeQueryTimeMetric(toolChest, jsonMapper, query, req.getRemoteAddr()) + .build("query/bytes", os.getCount()) + ); + + requestLogger.log( + new RequestLogLine( + new DateTime(startTime), + req.getRemoteAddr(), + query, + new QueryStats( + ImmutableMap.of( + "query/time", queryTime, + "query/bytes", os.getCount(), + "success", true + ) + ) + ) + ); + } + }, + contentType + ) + .header("X-Druid-Query-Id", query.getId()) + .build(); + } + catch (Exception e) { + // make sure to close yielder if anything happened before starting to serialize the response. + yielder.close(); + throw Throwables.propagate(e); + } + finally { + // do not close yielder here, since we do not want to close the yielder prior to + // StreamingOutput having iterated over all the results + } + } +} diff --git a/server/src/test/java/io/druid/client/BrokerServerViewTest.java b/server/src/test/java/io/druid/client/BrokerServerViewTest.java index 8e2282439350..11989eeaaeb6 100644 --- a/server/src/test/java/io/druid/client/BrokerServerViewTest.java +++ b/server/src/test/java/io/druid/client/BrokerServerViewTest.java @@ -336,7 +336,8 @@ public CallbackAction segmentViewInitialized() baseView, new HighestPriorityTierSelectorStrategy(new RandomServerSelectorStrategy()), new NoopServiceEmitter(), - new BrokerSegmentWatcherConfig() + new BrokerSegmentWatcherConfig(), + new DirectDruidClientConfig() ); baseView.start(); diff --git a/server/src/test/java/io/druid/client/DirectDruidClientConfigTest.java b/server/src/test/java/io/druid/client/DirectDruidClientConfigTest.java new file mode 100644 index 000000000000..b1fe8a21e9ee --- /dev/null +++ b/server/src/test/java/io/druid/client/DirectDruidClientConfigTest.java @@ -0,0 +1,69 @@ +/* +* Licensed to Metamarkets Group Inc. (Metamarkets) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. Metamarkets 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 io.druid.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.druid.segment.TestHelper; +import org.junit.Assert; +import org.junit.Test; + +/** + */ +public class DirectDruidClientConfigTest +{ + @Test + public void testSerdeDefault() throws Exception + { + String json = "{}"; + + ObjectMapper mapper = TestHelper.JSON_MAPPER; + + DirectDruidClientConfig config = mapper.readValue( + mapper.writeValueAsString( + mapper.readValue( + json, + DirectDruidClientConfig.class + ) + ), DirectDruidClientConfig.class + ); + + Assert.assertTrue(config.getQueryResponseIteratorFactory() instanceof V2QueryResponseIteratorFactory); + } + + @Test + public void testSerdeWithV3Iterator() throws Exception + { + String json = "{\n" + + " \"queryResponseIterator\": { \"type\": \"v3\" }\n" + + "}"; + + ObjectMapper mapper = TestHelper.JSON_MAPPER; + DirectDruidClientConfig config = mapper.readValue( + mapper.writeValueAsString( + mapper.readValue( + json, + DirectDruidClientConfig.class + ) + ), DirectDruidClientConfig.class + ); + + Assert.assertTrue(config.getQueryResponseIteratorFactory() instanceof V3QueryResponseIteratorFactory); + } +} diff --git a/server/src/test/java/io/druid/client/DirectDruidClientTest.java b/server/src/test/java/io/druid/client/DirectDruidClientTest.java index 1b222539f060..932eed877b91 100644 --- a/server/src/test/java/io/druid/client/DirectDruidClientTest.java +++ b/server/src/test/java/io/druid/client/DirectDruidClientTest.java @@ -122,7 +122,8 @@ public void testRun() throws Exception new DefaultObjectMapper(), httpClient, "foo", - new NoopServiceEmitter() + new NoopServiceEmitter(), + new DirectDruidClientConfig() ); DirectDruidClient client2 = new DirectDruidClient( new ReflectionQueryToolChestWarehouse(), @@ -130,7 +131,8 @@ public void testRun() throws Exception new DefaultObjectMapper(), httpClient, "foo2", - new NoopServiceEmitter() + new NoopServiceEmitter(), + new DirectDruidClientConfig() ); QueryableDruidServer queryableDruidServer1 = new QueryableDruidServer( @@ -232,7 +234,8 @@ public void testCancel() throws Exception new DefaultObjectMapper(), httpClient, "foo", - new NoopServiceEmitter() + new NoopServiceEmitter(), + new DirectDruidClientConfig() ); QueryableDruidServer queryableDruidServer1 = new QueryableDruidServer( @@ -301,7 +304,8 @@ public void testQueryInterruptionExceptionLogMessage() throws JsonProcessingExce new DefaultObjectMapper(), httpClient, hostName, - new NoopServiceEmitter() + new NoopServiceEmitter(), + new DirectDruidClientConfig() ); QueryableDruidServer queryableDruidServer = new QueryableDruidServer( diff --git a/server/src/test/java/io/druid/client/V3QueryResponseIteratorTest.java b/server/src/test/java/io/druid/client/V3QueryResponseIteratorTest.java new file mode 100644 index 000000000000..cdf73774a5a9 --- /dev/null +++ b/server/src/test/java/io/druid/client/V3QueryResponseIteratorTest.java @@ -0,0 +1,117 @@ +/* +* Licensed to Metamarkets Group Inc. (Metamarkets) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. Metamarkets 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 io.druid.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.util.concurrent.Futures; +import io.druid.query.QueryInterruptedException; +import io.druid.segment.TestHelper; +import org.apache.commons.io.IOUtils; +import org.junit.Assert; +import org.junit.Test; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Future; + +/** + */ +public class V3QueryResponseIteratorTest +{ + @Test + public void testSimpleResponse() throws Exception + { + String response = "{\n" + + " \"result\": [\"v1\", \"v2\"],\n" + + " \"context\": {\n" + + " \"k\": \"v\"\n" + + " }\n" + + "}"; + + doTest( + response, + ImmutableList.of("v1", "v2"), + ImmutableMap.of("host1", ImmutableMap.of("k", "v")) + ); + } + + @Test + public void testEmptyResponse() throws Exception + { + String response = "{\n" + + " \"result\": [],\n" + + " \"context\": { }\n" + + "}"; + + doTest(response, Collections.emptyList(), ImmutableMap.of()); + } + + @Test (expected = QueryInterruptedException.class) + public void testErrorResponse() throws Exception + { + String response = "{\n" + + " \"error\": \"Query timeout\"\n" + + "}"; + doTest(response, null, null); + } + + private void doTest(String response, List expectedValues, Map expectedContext) + throws Exception + { + ObjectMapper objectMapper = TestHelper.JSON_MAPPER; + + final TypeFactory typeFactory = objectMapper.getTypeFactory(); + JavaType baseType = typeFactory.constructType(new TypeReference(){}); + + Future responseFuture = Futures.immediateFuture( + IOUtils.toInputStream(response) + ); + + Map context = new HashMap<>(); + + V3QueryResponseIterator parser = new V3QueryResponseIterator<>( + baseType, + responseFuture, + "http://dummy/", + objectMapper, + "host1", + context + ); + + List resultValues = new ArrayList<>(); + while (parser.hasNext()) { + resultValues.add(parser.next()); + } + + Assert.assertEquals(resultValues, expectedValues); + Assert.assertEquals(context, expectedContext); + + Assert.assertEquals(responseFuture.get().available(), 0); + } +} diff --git a/services/src/main/java/io/druid/cli/CliBroker.java b/services/src/main/java/io/druid/cli/CliBroker.java index 3748c832bdaa..62c2ff334760 100644 --- a/services/src/main/java/io/druid/cli/CliBroker.java +++ b/services/src/main/java/io/druid/cli/CliBroker.java @@ -28,6 +28,7 @@ import io.druid.client.BrokerSegmentWatcherConfig; import io.druid.client.BrokerServerView; import io.druid.client.CachingClusteredClient; +import io.druid.client.DirectDruidClientConfig; import io.druid.client.TimelineServerView; import io.druid.client.cache.CacheConfig; import io.druid.client.cache.CacheMonitor; @@ -39,14 +40,13 @@ import io.druid.guice.JsonConfigProvider; import io.druid.guice.LazySingleton; import io.druid.guice.LifecycleModule; -import io.druid.query.MapQueryToolChestWarehouse; import io.druid.query.QuerySegmentWalker; -import io.druid.query.QueryToolChestWarehouse; import io.druid.query.RetryQueryRunnerConfig; import io.druid.query.lookup.LookupModule; import io.druid.server.ClientInfoResource; import io.druid.server.ClientQuerySegmentWalker; import io.druid.server.QueryResource; +import io.druid.server.QueryResourceV3; import io.druid.server.coordination.broker.DruidBroker; import io.druid.server.http.BrokerResource; import io.druid.server.initialization.jetty.JettyServerInitializer; @@ -97,11 +97,13 @@ public void configure(Binder binder) JsonConfigProvider.bind(binder, "druid.broker.balancer", ServerSelectorStrategy.class); JsonConfigProvider.bind(binder, "druid.broker.retryPolicy", RetryQueryRunnerConfig.class); JsonConfigProvider.bind(binder, "druid.broker.segment", BrokerSegmentWatcherConfig.class); + JsonConfigProvider.bind(binder, "druid.broker.client", DirectDruidClientConfig.class); binder.bind(QuerySegmentWalker.class).to(ClientQuerySegmentWalker.class).in(LazySingleton.class); binder.bind(JettyServerInitializer.class).to(QueryJettyServerInitializer.class).in(LazySingleton.class); Jerseys.addResource(binder, QueryResource.class); + Jerseys.addResource(binder, QueryResourceV3.class); Jerseys.addResource(binder, BrokerResource.class); Jerseys.addResource(binder, ClientInfoResource.class); LifecycleModule.register(binder, QueryResource.class); diff --git a/services/src/main/java/io/druid/cli/CliHistorical.java b/services/src/main/java/io/druid/cli/CliHistorical.java index b6b6ad34f644..7d62ef9b9229 100644 --- a/services/src/main/java/io/druid/cli/CliHistorical.java +++ b/services/src/main/java/io/druid/cli/CliHistorical.java @@ -37,6 +37,7 @@ import io.druid.query.QuerySegmentWalker; import io.druid.query.lookup.LookupModule; import io.druid.server.QueryResource; +import io.druid.server.QueryResourceV3; import io.druid.server.coordination.ServerManager; import io.druid.server.coordination.ZkCoordinator; import io.druid.server.http.HistoricalResource; @@ -82,8 +83,10 @@ public void configure(Binder binder) binder.bind(NodeTypeConfig.class).toInstance(new NodeTypeConfig("historical")); binder.bind(JettyServerInitializer.class).to(QueryJettyServerInitializer.class).in(LazySingleton.class); Jerseys.addResource(binder, QueryResource.class); + Jerseys.addResource(binder, QueryResourceV3.class); Jerseys.addResource(binder, HistoricalResource.class); LifecycleModule.register(binder, QueryResource.class); + LifecycleModule.register(binder, QueryResourceV3.class); LifecycleModule.register(binder, ZkCoordinator.class); JsonConfigProvider.bind(binder, "druid.historical.cache", CacheConfig.class); diff --git a/services/src/main/java/io/druid/cli/CliPeon.java b/services/src/main/java/io/druid/cli/CliPeon.java index 7d421bdd5834..6b575d62737c 100644 --- a/services/src/main/java/io/druid/cli/CliPeon.java +++ b/services/src/main/java/io/druid/cli/CliPeon.java @@ -84,6 +84,7 @@ import io.druid.segment.realtime.plumber.CoordinatorBasedSegmentHandoffNotifierFactory; import io.druid.segment.realtime.plumber.SegmentHandoffNotifierFactory; import io.druid.server.QueryResource; +import io.druid.server.QueryResourceV3; import io.druid.server.initialization.jetty.ChatHandlerServerModule; import io.druid.server.initialization.jetty.JettyServerInitializer; import io.druid.server.metrics.DataSourceTaskIdHolder; @@ -202,6 +203,8 @@ public void configure(Binder binder) binder.bind(JettyServerInitializer.class).to(QueryJettyServerInitializer.class); Jerseys.addResource(binder, QueryResource.class); LifecycleModule.register(binder, QueryResource.class); + Jerseys.addResource(binder, QueryResourceV3.class); + LifecycleModule.register(binder, QueryResourceV3.class); binder.bind(NodeTypeConfig.class).toInstance(new NodeTypeConfig(nodeType)); LifecycleModule.register(binder, Server.class); } diff --git a/services/src/main/java/io/druid/cli/RouterJettyServerInitializer.java b/services/src/main/java/io/druid/cli/RouterJettyServerInitializer.java index a179a440a3d4..e3e1c1af4c4b 100644 --- a/services/src/main/java/io/druid/cli/RouterJettyServerInitializer.java +++ b/services/src/main/java/io/druid/cli/RouterJettyServerInitializer.java @@ -41,7 +41,9 @@ import org.eclipse.jetty.server.handler.HandlerList; import org.eclipse.jetty.servlet.DefaultServlet; import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHandler; import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.servlet.ServletMapping; /** */ @@ -100,7 +102,13 @@ public void initialize(Server server, Injector injector) //NOTE: explicit maxThreads to workaround https://tickets.puppetlabs.com/browse/TK-152 sh.setInitParameter("maxThreads", Integer.toString(httpClientConfig.getNumMaxThreads())); - root.addServlet(sh, "/druid/v2/*"); + ServletHandler servletHandler = root.getServletHandler(); + servletHandler.addServlet(sh); + ServletMapping mapping = new ServletMapping(); + mapping.setServletName(sh.getName()); + mapping.setPathSpecs(new String[] {"/druid/v2/*", "/druid/v3/*"}); + servletHandler.addServletMapping(mapping); + JettyServerInitUtils.addExtensionFilters(root, injector); root.addFilter(JettyServerInitUtils.defaultAsyncGzipFilterHolder(), "/*", null); // Can't use '/*' here because of Guice conflicts with AsyncQueryForwardingServlet path diff --git a/services/src/main/java/io/druid/guice/RealtimeModule.java b/services/src/main/java/io/druid/guice/RealtimeModule.java index e164f538323d..658d9e04922a 100644 --- a/services/src/main/java/io/druid/guice/RealtimeModule.java +++ b/services/src/main/java/io/druid/guice/RealtimeModule.java @@ -40,6 +40,7 @@ import io.druid.segment.realtime.plumber.CoordinatorBasedSegmentHandoffNotifierFactory; import io.druid.segment.realtime.plumber.SegmentHandoffNotifierFactory; import io.druid.server.QueryResource; +import io.druid.server.QueryResourceV3; import io.druid.server.initialization.jetty.JettyServerInitializer; import org.eclipse.jetty.server.Server; @@ -103,7 +104,9 @@ public void configure(Binder binder) binder.bind(NodeTypeConfig.class).toInstance(new NodeTypeConfig("realtime")); binder.bind(JettyServerInitializer.class).to(QueryJettyServerInitializer.class).in(LazySingleton.class); Jerseys.addResource(binder, QueryResource.class); + Jerseys.addResource(binder, QueryResourceV3.class); LifecycleModule.register(binder, QueryResource.class); + LifecycleModule.register(binder, QueryResourceV3.class); LifecycleModule.register(binder, Server.class); } }