/* * Copyright (c) 2017 Couchbase, Inc. * * Licensed 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.couchbase.client.core.endpoint.analytics; import com.couchbase.client.core.ResponseEvent; import com.couchbase.client.core.endpoint.AbstractEndpoint; import com.couchbase.client.core.endpoint.AbstractGenericHandler; import com.couchbase.client.core.endpoint.ResponseStatusConverter; import com.couchbase.client.core.endpoint.util.ClosingPositionBufProcessor; import com.couchbase.client.core.endpoint.util.StringClosingPositionBufProcessor; import com.couchbase.client.core.endpoint.util.WhitespaceSkipper; import com.couchbase.client.core.logging.CouchbaseLogger; import com.couchbase.client.core.logging.CouchbaseLoggerFactory; import com.couchbase.client.core.message.AbstractCouchbaseRequest; import com.couchbase.client.core.message.AbstractCouchbaseResponse; import com.couchbase.client.core.message.CouchbaseRequest; import com.couchbase.client.core.message.CouchbaseResponse; import com.couchbase.client.core.message.KeepAlive; import com.couchbase.client.core.message.ResponseStatus; import com.couchbase.client.core.message.analytics.AnalyticsRequest; import com.couchbase.client.core.message.analytics.GenericAnalyticsRequest; import com.couchbase.client.core.message.analytics.GenericAnalyticsResponse; import com.couchbase.client.core.message.analytics.RawAnalyticsRequest; import com.couchbase.client.core.message.analytics.RawAnalyticsResponse; import com.couchbase.client.core.service.ServiceType; import com.couchbase.client.core.utils.UnicastAutoReleaseSubject; import com.lmax.disruptor.RingBuffer; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufProcessor; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpRequest; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpObject; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import rx.Scheduler; import rx.subjects.AsyncSubject; import java.util.Queue; import java.util.concurrent.TimeUnit; import static com.couchbase.client.core.endpoint.util.ByteBufJsonHelper.findNextChar; import static com.couchbase.client.core.endpoint.util.ByteBufJsonHelper.findNextCharNotPrefixedBy; import static com.couchbase.client.core.endpoint.util.ByteBufJsonHelper.findSectionClosingPosition; import static com.couchbase.client.core.endpoint.util.ByteBufJsonHelper.findSplitPosition; /** * The {@link AnalyticsHandler} is responsible for encoding {@link AnalyticsRequest}s into lower level * {@link HttpRequest}s as well as decoding {@link HttpObject}s into * {@link CouchbaseResponse}s. * * @author Michael Nitschinger * @since 1.4.3 */ public class AnalyticsHandler extends AbstractGenericHandler<HttpObject, HttpRequest, AnalyticsRequest> { private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(AnalyticsHandler.class); protected static final byte QUERY_STATE_INITIAL = 0; protected static final byte QUERY_STATE_SIGNATURE = 1; protected static final byte QUERY_STATE_ROWS = 2; protected static final byte QUERY_STATE_ROWS_RAW = 20; protected static final byte QUERY_STATE_ROWS_DECIDE = 29; protected static final byte QUERY_STATE_ERROR = 3; protected static final byte QUERY_STATE_WARNING = 4; protected static final byte QUERY_STATE_STATUS = 5; protected static final byte QUERY_STATE_INFO = 6; protected static final byte QUERY_STATE_NO_INFO = 7; //alternate case where there's nothing after status protected static final byte QUERY_STATE_DONE = 8; /** * This is the number of characters expected to be present to be able to read * the beginning of the JSON, including the "requestID" token and its value * (currently expected to be 36 chars, but the code is adaptative). */ private static final int MINIMUM_WINDOW_FOR_REQUESTID = 55; /** * This is a window of characters allowing to detect the clientContextID token * (including room for JSON separators, etc...). */ public static final int MINIMUM_WINDOW_FOR_CLIENTID_TOKEN = 27; /** * Contains the current pending response header if set. */ private HttpResponse responseHeader; /** * Contains the accumulating buffer for the response content. */ private ByteBuf responseContent; /** * Represents an observable that sends result chunks. */ private UnicastAutoReleaseSubject<ByteBuf> queryRowObservable; /** * Represents an observable that has the signature of the Analytics results if there are any. */ private UnicastAutoReleaseSubject<ByteBuf> querySignatureObservable; /** * Represents an observable that sends errors and warnings if any during query execution. */ private UnicastAutoReleaseSubject<ByteBuf> queryErrorObservable; /** * Represent an observable that has the final execution status of the query, once all result rows and/or * errors/warnings have been sent. */ private AsyncSubject<String> queryStatusObservable; /** * Represents an observable containing metrics on a terminated query. */ private UnicastAutoReleaseSubject<ByteBuf> queryInfoObservable; /** * Represents the current query parsing state. */ private byte queryParsingState = QUERY_STATE_INITIAL; /** * In case of chunked processing, allows to detect we are still parsing a section. */ private boolean sectionDone = false; /** * Creates a new {@link AnalyticsHandler} with the default queue for requests. * * @param endpoint the {@link AbstractEndpoint} to coordinate with. * @param responseBuffer the {@link RingBuffer} to push responses into. */ public AnalyticsHandler(AbstractEndpoint endpoint, RingBuffer<ResponseEvent> responseBuffer, boolean isTransient, final boolean pipeline) { super(endpoint, responseBuffer, isTransient, pipeline); } /** * Creates a new {@link AnalyticsHandler} with a custom queue for requests (suitable for tests). * * @param endpoint the {@link AbstractEndpoint} to coordinate with. * @param responseBuffer the {@link RingBuffer} to push responses into. * @param queue the queue which holds all outstanding open requests. */ AnalyticsHandler(AbstractEndpoint endpoint, RingBuffer<ResponseEvent> responseBuffer, Queue<AnalyticsRequest> queue, boolean isTransient, final boolean pipeline) { super(endpoint, responseBuffer, queue, isTransient, pipeline); } @Override protected HttpRequest encodeRequest(final ChannelHandlerContext ctx, final AnalyticsRequest msg) throws Exception { FullHttpRequest request; if (msg instanceof GenericAnalyticsRequest) { GenericAnalyticsRequest queryRequest = (GenericAnalyticsRequest) msg; request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, "/query/service"); request.headers().set(HttpHeaders.Names.USER_AGENT, env().userAgent()); if (queryRequest.isJsonFormat()) { request.headers().set(HttpHeaders.Names.CONTENT_TYPE, "application/json"); } ByteBuf query = ctx.alloc().buffer(((GenericAnalyticsRequest) msg).query().length()); query.writeBytes(((GenericAnalyticsRequest) msg).query().getBytes(CHARSET)); request.headers().add(HttpHeaders.Names.CONTENT_LENGTH, query.readableBytes()); request.headers().set(HttpHeaders.Names.HOST, remoteHttpHost(ctx)); request.content().writeBytes(query); query.release(); } else if (msg instanceof KeepAliveRequest) { request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/analytics/version"); request.headers().set(HttpHeaders.Names.USER_AGENT, env().userAgent()); request.headers().set(HttpHeaders.Names.HOST, remoteHttpHost(ctx)); return request; } else { throw new IllegalArgumentException("Unknown incoming AnalyticsRequest type " + msg.getClass()); } addHttpBasicAuth(ctx, request, msg.username(), msg.password()); return request; } @Override protected CouchbaseResponse decodeResponse(final ChannelHandlerContext ctx, final HttpObject msg) throws Exception { CouchbaseResponse response = null; if (msg instanceof HttpResponse) { responseHeader = (HttpResponse) msg; if (responseContent != null) { responseContent.clear(); } else { responseContent = ctx.alloc().buffer(); } } if (currentRequest() instanceof KeepAliveRequest) { if (msg instanceof LastHttpContent) { response = new KeepAliveResponse(ResponseStatusConverter.fromHttp(responseHeader.getStatus().code()), currentRequest()); responseContent.clear(); responseContent.discardReadBytes(); finishedDecoding(); } } else if (msg instanceof HttpContent) { responseContent.writeBytes(((HttpContent) msg).content()); boolean lastChunk = msg instanceof LastHttpContent; //important to place the RawQueryRequest test before, as it extends GenericQueryRequest if (currentRequest() instanceof RawAnalyticsRequest) { response = handleRawAnalyticsResponse(lastChunk, ctx); } else if (currentRequest() instanceof GenericAnalyticsRequest) { if (queryRowObservable == null) { //still in initial parsing response = handleGenericAnalyticsResponse(lastChunk); //null response indicates need for more data before continuing parsing if (response != null) { parseQueryResponse(lastChunk); } } else { parseQueryResponse(lastChunk); } } } return response; } private RawAnalyticsResponse handleRawAnalyticsResponse(boolean lastChunk, ChannelHandlerContext ctx) { if (!lastChunk) { return null; } ResponseStatus status = ResponseStatusConverter.fromHttp(responseHeader.getStatus().code()); ByteBuf responseCopy = ctx.alloc().buffer(responseContent.readableBytes(), responseContent.readableBytes()); responseCopy.writeBytes(responseContent); cleanupQueryStates(); return new RawAnalyticsResponse(status, currentRequest(), responseCopy, responseHeader.getStatus().code(), responseHeader.getStatus().reasonPhrase()); } /** * Checks if there's not another section opened before the current one, * which starts at openBracketPos * * @param openBracketPos the position of the current section's opening bracket * @return true if transition to next state should be made because there's a new * section opening. */ private boolean isEmptySection(int openBracketPos) { int nextColon = findNextChar(responseContent, ':'); return nextColon > -1 && nextColon < openBracketPos; } /** * Base method to handle the response for the generic analytics request. * * It waits for the first few bytes on the actual response to determine if an error is raised or if a successful * response can be expected. The actual error and/or chunk parsing is deferred to other parts of this handler. * * @return a {@link CouchbaseResponse} if eligible. */ private CouchbaseResponse handleGenericAnalyticsResponse(boolean lastChunk) { String requestId; String clientId = ""; if (responseContent.readableBytes() < MINIMUM_WINDOW_FOR_REQUESTID + MINIMUM_WINDOW_FOR_CLIENTID_TOKEN && !lastChunk) { return null; //wait for more data } int startIndex = responseContent.readerIndex(); if (responseContent.readableBytes() >= MINIMUM_WINDOW_FOR_REQUESTID) { responseContent.skipBytes(findNextChar(responseContent, ':')); responseContent.skipBytes(findNextChar(responseContent, '"') + 1); int endOfId = findNextChar(responseContent, '"'); ByteBuf slice = responseContent.readSlice(endOfId); requestId = slice.toString(CHARSET); } else { return null; } //IMPORTANT: from there on, before returning null to get more data you need to reset //the cursor, since following code will consume data from the buffer. if (responseContent.readableBytes() >= MINIMUM_WINDOW_FOR_CLIENTID_TOKEN && findNextChar(responseContent, ':') < MINIMUM_WINDOW_FOR_CLIENTID_TOKEN) { responseContent.markReaderIndex(); ByteBuf slice = responseContent.readSlice(findNextChar(responseContent, ':')); if (slice.toString(CHARSET).contains("clientContextID")) { //find the size of the client id responseContent.skipBytes(findNextChar(responseContent, '"') + 1); //opening of clientId int clientIdSize = findNextCharNotPrefixedBy(responseContent, '"', '\\'); if (clientIdSize < 0) { //reset the cursor way back before requestID, there was not enough data to get the whole id responseContent.readerIndex(startIndex); //wait for more data return null; } //read it clientId = responseContent.readSlice(clientIdSize).toString(CHARSET); //advance to next token if possible //closing quote boolean hasClosingQuote = responseContent.readableBytes() > 0; if (hasClosingQuote) { responseContent.skipBytes(1); } //next token's quote int openingNextToken = findNextChar(responseContent, '"'); if (openingNextToken > -1) { responseContent.skipBytes(openingNextToken); } } else { //reset the cursor, there was no client id responseContent.resetReaderIndex(); } } boolean success = true; if (responseContent.readableBytes() >= 20) { ByteBuf peekForErrors = responseContent.slice(responseContent.readerIndex(), 20); if (peekForErrors.toString(CHARSET).contains("errors")) { success = false; } } else { //it is important to reset the readerIndex if returning null, in order to allow for complete retry responseContent.readerIndex(startIndex); return null; } ResponseStatus status = ResponseStatusConverter.fromHttp(responseHeader.getStatus().code()); if (!success) { status = ResponseStatus.FAILURE; } Scheduler scheduler = env().scheduler(); long ttl = env().autoreleaseAfter(); queryRowObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); queryErrorObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); queryStatusObservable = AsyncSubject.create(); queryInfoObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); querySignatureObservable = UnicastAutoReleaseSubject.create(ttl, TimeUnit.MILLISECONDS, scheduler); //set up trace ids on all these UnicastAutoReleaseSubjects, so that if they get in a bad state // (multiple subscribers or subscriber coming in too late) we can trace back to here String rid = clientId == null ? requestId : clientId + " / " + requestId; queryRowObservable.withTraceIdentifier("queryRow." + rid).onBackpressureBuffer(); queryErrorObservable.withTraceIdentifier("queryError." + rid).onBackpressureBuffer(); queryInfoObservable.withTraceIdentifier("queryInfo." + rid).onBackpressureBuffer(); querySignatureObservable.withTraceIdentifier("querySignature." + rid).onBackpressureBuffer(); queryStatusObservable.onBackpressureBuffer(); if (!env().callbacksOnIoPool()) { queryErrorObservable.observeOn(scheduler); queryRowObservable.observeOn(scheduler); querySignatureObservable.observeOn(scheduler); queryStatusObservable.observeOn(scheduler); queryInfoObservable.observeOn(scheduler); } return new GenericAnalyticsResponse( queryErrorObservable, queryRowObservable, querySignatureObservable, queryStatusObservable, queryInfoObservable, currentRequest(), status, requestId, clientId ); } /** * Generic dispatch method to parse the query response chunks. * * Depending on the state the parser is currently in, several different sub-methods are called * which do the actual handling. * * @param lastChunk if the current emitted content body is the last one. */ private void parseQueryResponse(boolean lastChunk) { if (sectionDone || queryParsingState == QUERY_STATE_INITIAL) { queryParsingState = transitionToNextToken(lastChunk); } if (queryParsingState == QUERY_STATE_SIGNATURE) { parseQuerySignature(lastChunk); } if (queryParsingState == QUERY_STATE_ROWS_DECIDE) { decideBetweenRawAndObjects(lastChunk); } if (queryParsingState == QUERY_STATE_ROWS) { parseQueryRows(lastChunk); } else if (queryParsingState == QUERY_STATE_ROWS_RAW) { parseQueryRowsRaw(lastChunk); } if (queryParsingState == QUERY_STATE_ERROR) { parseQueryError(lastChunk); } if (queryParsingState == QUERY_STATE_WARNING) { parseQueryError(lastChunk); //warning are treated the same as errors -> sent to errorObservable } if (queryParsingState == QUERY_STATE_STATUS) { parseQueryStatus(lastChunk); } if (queryParsingState == QUERY_STATE_INFO) { parseQueryInfo(lastChunk); } else if (queryParsingState == QUERY_STATE_NO_INFO) { finishInfo(); } if (queryParsingState == QUERY_STATE_DONE) { //final state, but there could still be a small chunk with closing brackets //only finalize and reset if this is the last chunk sectionDone = lastChunk; //if false this will allow next iteration to skip non-relevant automatic //transition to next token (which is desirable since there is no more token). if (sectionDone) { cleanupQueryStates(); } } } /** * Peek the next token, returning the QUERY_STATE corresponding to it and placing the readerIndex just after * the token's ':'. Must be at the end of the previous token. * * @param lastChunk true if this is the last chunk * @return the next QUERY_STATE */ private byte transitionToNextToken(boolean lastChunk) { int endNextToken = findNextChar(responseContent, ':'); if (endNextToken < 0 && !lastChunk) { return queryParsingState; } if (endNextToken < 0 && lastChunk && queryParsingState >= QUERY_STATE_STATUS) { return QUERY_STATE_NO_INFO; } byte newState; ByteBuf peekSlice = responseContent.readSlice(endNextToken + 1); String peek = peekSlice.toString(CHARSET); if (peek.contains("\"signature\":")) { newState = QUERY_STATE_SIGNATURE; } else if (peek.endsWith("\"results\":")) { newState = QUERY_STATE_ROWS_DECIDE; } else if (peek.endsWith("\"status\":")) { newState = QUERY_STATE_STATUS; } else if (peek.endsWith("\"errors\":")) { newState = QUERY_STATE_ERROR; } else if (peek.endsWith("\"warnings\":")) { newState = QUERY_STATE_WARNING; } else if (peek.endsWith("\"metrics\":")) { newState = QUERY_STATE_INFO; } else { if (lastChunk) { IllegalStateException e = new IllegalStateException("Error parsing query response (in TRANSITION) at \"" + peek + "\", enable trace to see response content"); if (LOGGER.isTraceEnabled()) { LOGGER.trace(responseContent.toString(CHARSET), e); } throw e; } else { //we need more data return queryParsingState; } } sectionDone = false; return newState; } private void decideBetweenRawAndObjects(boolean lastChunk) { responseContent.markReaderIndex(); int openArrayPos = findNextChar(responseContent, '['); if (openArrayPos > -1) { responseContent.skipBytes(openArrayPos + 1); } else { responseContent.resetReaderIndex(); if (lastChunk == true) { throw new IllegalStateException("Unable to decide between raw and objects with content " + responseContent.toString(CHARSET)); } return; //more data } int spaceToSkip = responseContent.forEachByte(new WhitespaceSkipper()); if (spaceToSkip > -1) { responseContent.readerIndex(spaceToSkip); } else { //there's only whitespace! need more info responseContent.resetReaderIndex(); return; } if (responseContent.isReadable()) { byte first = responseContent.getByte(responseContent.readerIndex()); if (first == '{') { queryParsingState = QUERY_STATE_ROWS; } else if (first == ']') { //empty result section! sectionDone(); queryParsingState = transitionToNextToken(lastChunk); } else { queryParsingState = QUERY_STATE_ROWS_RAW; } } else { responseContent.resetReaderIndex(); } } private void sectionDone() { this.sectionDone = true; responseContent.discardReadBytes(); } /** * Parse the signature section in the Analytics response. */ private void parseQuerySignature(boolean lastChunk) { ByteBufProcessor processor = null; //signature can be any valid JSON item, which get tricky to detect //let's try to find out what's the boundary character int openPos = responseContent.forEachByte(new WhitespaceSkipper()) - responseContent.readerIndex(); if (openPos < 0) { //only whitespace left in the buffer, need more data return; } char openChar = (char) responseContent.getByte(responseContent.readerIndex() + openPos); if (openChar == '{') { processor = new ClosingPositionBufProcessor('{', '}', true); } else if (openChar == '[') { processor = new ClosingPositionBufProcessor('[', ']', true); } else if (openChar == '"') { processor = new StringClosingPositionBufProcessor(); } //else this should be a scalar, skip processor int closePos; if (processor != null) { closePos = responseContent.forEachByte(processor) - responseContent.readerIndex(); } else { closePos = findNextChar(responseContent, ',') - 1; } if (closePos > 0) { responseContent.skipBytes(openPos); int length = closePos - openPos + 1; ByteBuf signature = responseContent.readSlice(length); querySignatureObservable.onNext(signature.copy()); } else { //wait for more data return; } //note: the signature section could be absent, so we'll make sure to complete the observable // when receiving status since this is in every well-formed response. sectionDone(); queryParsingState = transitionToNextToken(lastChunk); } /** * Parses the query rows from the content stream as long as there is data to be found. */ private void parseQueryRows(boolean lastChunk) { while (true) { int openBracketPos = findNextChar(responseContent, '{'); if (isEmptySection(openBracketPos) || (lastChunk && openBracketPos < 0)) { sectionDone(); queryParsingState = transitionToNextToken(lastChunk); break; } int closeBracketPos = findSectionClosingPosition(responseContent, '{', '}'); if (closeBracketPos == -1) { break; } int length = closeBracketPos - openBracketPos - responseContent.readerIndex() + 1; responseContent.skipBytes(openBracketPos); ByteBuf resultSlice = responseContent.readSlice(length); queryRowObservable.onNext(resultSlice.copy()); responseContent.discardSomeReadBytes(); } } /** * Parses the query raw results from the content stream as long as there is data to be found. */ private void parseQueryRowsRaw(boolean lastChunk) { while (responseContent.isReadable()) { int splitPos = findSplitPosition(responseContent, ','); int arrayEndPos = findSplitPosition(responseContent, ']'); boolean doSectionDone = false; if (splitPos == -1 && arrayEndPos == -1) { //need more data break; } else if (arrayEndPos > 0 && (arrayEndPos < splitPos || splitPos == -1)) { splitPos = arrayEndPos; doSectionDone = true; } int length = splitPos - responseContent.readerIndex(); ByteBuf resultSlice = responseContent.readSlice(length); queryRowObservable.onNext(resultSlice.copy()); responseContent.skipBytes(1); responseContent.discardReadBytes(); if (doSectionDone) { sectionDone(); queryParsingState = transitionToNextToken(lastChunk); break; } } } /** * Parses the errors and warnings from the content stream as long as there are some to be found. */ private void parseQueryError(boolean lastChunk) { while (true) { int openBracketPos = findNextChar(responseContent, '{'); if (isEmptySection(openBracketPos) || (lastChunk && openBracketPos < 0)) { sectionDone(); queryParsingState = transitionToNextToken(lastChunk); //warnings or status break; } int closeBracketPos = findSectionClosingPosition(responseContent, '{', '}'); if (closeBracketPos == -1) { break; } int length = closeBracketPos - openBracketPos - responseContent.readerIndex() + 1; responseContent.skipBytes(openBracketPos); ByteBuf resultSlice = responseContent.readSlice(length); queryErrorObservable.onNext(resultSlice.copy()); } } /** * Last before the end of the stream, we can now parse the final result status * (including full execution of the query). */ private void parseQueryStatus(boolean lastChunk) { //some sections don't always come up, unlike status. Take this chance to close said sections' observables here. querySignatureObservable.onCompleted(); queryRowObservable.onCompleted(); queryErrorObservable.onCompleted(); responseContent.markReaderIndex(); responseContent.skipBytes(findNextChar(responseContent, '"') + 1); int endStatus = findNextChar(responseContent, '"'); if (endStatus > -1) { ByteBuf resultSlice = responseContent.readSlice(endStatus); queryStatusObservable.onNext(resultSlice.toString(CHARSET)); queryStatusObservable.onCompleted(); sectionDone(); queryParsingState = transitionToNextToken(lastChunk); } else { responseContent.resetReaderIndex(); return; //need more data } } /** * At the end of the response stream, parse out the info portion (metrics). * * For the sake of easiness, since we know it comes at the end, we wait until the full data is together and read * the info json objects off in one shot (but they are still emitted separately). * * @param last if this batch is the last one. */ private void parseQueryInfo(boolean last) { int openBracketPos = findNextChar(responseContent, '{'); int closeBracketPos = findSectionClosingPosition(responseContent, '{', '}'); if (closeBracketPos == -1) { if (last) { throw new IllegalStateException("Could not find metrics closing in last chunk"); } else { return; //wait for more data } } int from = responseContent.readerIndex() + openBracketPos; int to = closeBracketPos - openBracketPos - responseContent.readerIndex() + 1; queryInfoObservable.onNext(responseContent.slice(from, to).copy()); responseContent.readerIndex(to + openBracketPos); //has to be here rather than in parseQueryResponse, as when there is a split //(and thus not enough data) it could finish the metrics too early finishInfo(); } private void finishInfo() { queryInfoObservable.onCompleted(); sectionDone(); queryParsingState = QUERY_STATE_DONE; } /** * Clean up the query states after all rows have been consumed. */ private void cleanupQueryStates() { finishedDecoding(); queryInfoObservable = null; queryRowObservable = null; queryErrorObservable = null; queryStatusObservable = null; querySignatureObservable = null; queryParsingState = QUERY_STATE_INITIAL; } @Override public void handlerRemoved(final ChannelHandlerContext ctx) throws Exception { if (queryRowObservable != null) { queryRowObservable.onCompleted(); } if (queryInfoObservable != null) { queryInfoObservable.onCompleted(); } if (queryErrorObservable != null) { queryErrorObservable.onCompleted(); } if (queryStatusObservable != null) { queryStatusObservable.onCompleted(); } if (querySignatureObservable != null) { querySignatureObservable.onCompleted(); } cleanupQueryStates(); if (responseContent != null && responseContent.refCnt() > 0) { responseContent.release(); } super.handlerRemoved(ctx); } @Override protected CouchbaseRequest createKeepAliveRequest() { return new KeepAliveRequest(); } protected static class KeepAliveRequest extends AbstractCouchbaseRequest implements AnalyticsRequest, KeepAlive { protected KeepAliveRequest() { super(null, null); } } protected static class KeepAliveResponse extends AbstractCouchbaseResponse { protected KeepAliveResponse(ResponseStatus status, CouchbaseRequest request) { super(status, request); } } @Override protected ServiceType serviceType() { return ServiceType.ANALYTICS; } public int getQueryParsingState() { return this.queryParsingState; } }