com.couchbase.client.core.endpoint.dcp.DCPHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.couchbase.client.core.endpoint.dcp.DCPHandler.java

Source

/*
 * Copyright (c) 2016 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.dcp;

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.kv.KeyValueStatus;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.CouchbaseResponse;
import com.couchbase.client.core.message.dcp.AbstractDCPRequest;
import com.couchbase.client.core.message.dcp.DCPRequest;
import com.couchbase.client.core.message.dcp.DCPResponse;
import com.couchbase.client.core.message.dcp.ExpirationMessage;
import com.couchbase.client.core.message.dcp.FailoverLogEntry;
import com.couchbase.client.core.message.dcp.GetFailoverLogRequest;
import com.couchbase.client.core.message.dcp.GetFailoverLogResponse;
import com.couchbase.client.core.message.dcp.GetLastCheckpointRequest;
import com.couchbase.client.core.message.dcp.GetLastCheckpointResponse;
import com.couchbase.client.core.message.dcp.MutationMessage;
import com.couchbase.client.core.message.dcp.RemoveMessage;
import com.couchbase.client.core.message.dcp.SnapshotMarkerMessage;
import com.couchbase.client.core.message.dcp.StreamCloseRequest;
import com.couchbase.client.core.message.dcp.StreamCloseResponse;
import com.couchbase.client.core.message.dcp.StreamEndMessage;
import com.couchbase.client.core.message.dcp.StreamRequestRequest;
import com.couchbase.client.core.message.dcp.StreamRequestResponse;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.deps.io.netty.handler.codec.memcache.binary.BinaryMemcacheRequest;
import com.couchbase.client.deps.io.netty.handler.codec.memcache.binary.DefaultBinaryMemcacheRequest;
import com.couchbase.client.deps.io.netty.handler.codec.memcache.binary.FullBinaryMemcacheResponse;
import com.lmax.disruptor.EventSink;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import rx.functions.Action1;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.Queue;

/**
 * @author Sergey Avseyev
 * @since 1.1.0
 */
public class DCPHandler
        extends AbstractGenericHandler<FullBinaryMemcacheResponse, BinaryMemcacheRequest, DCPRequest> {

    public static final byte OP_OPEN_CONNECTION = 0x50;
    public static final byte OP_STREAM_CLOSE = 0x52;
    public static final byte OP_STREAM_REQUEST = 0x53;
    public static final byte OP_STREAM_END = 0x55;
    public static final byte OP_SNAPSHOT_MARKER = 0x56;
    public static final byte OP_MUTATION = 0x57;
    public static final byte OP_REMOVE = 0x58;
    public static final byte OP_EXPIRATION = 0x59;
    public static final byte OP_CONTROL = 0x5e;
    public static final byte OP_BUFFER_ACK = 0x5d;
    public static final byte OP_GET_FAILOVER_LOG = 0x54;
    public static final byte OP_GET_LAST_CHECKPOINT = (byte) 0x97;
    /**
     * The Logger used in this handler.
     */
    private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(DCPHandler.class);
    private DCPConnection connection;

    /**
     * Creates a new {@link DCPHandler} with the default queue for requests.
     *
     * @param endpoint       the {@link AbstractEndpoint} to coordinate with.
     * @param responseBuffer the {@link EventSink} to push responses into.
     */
    public DCPHandler(AbstractEndpoint endpoint, EventSink<ResponseEvent> responseBuffer, boolean isTransient) {
        this(endpoint, responseBuffer, new ArrayDeque<DCPRequest>(), isTransient);
    }

    /**
     * Creates a new {@link DCPHandler} with a custom queue for requests (suitable for tests).
     *
     * @param endpoint       the {@link AbstractEndpoint} to coordinate with.
     * @param responseBuffer the {@link EventSink} to push responses into.
     * @param queue          the queue which holds all outstanding open requests.
     */
    public DCPHandler(AbstractEndpoint endpoint, EventSink<ResponseEvent> responseBuffer, Queue<DCPRequest> queue,
            boolean isTransient) {
        super(endpoint, responseBuffer, queue, isTransient);
    }

    @Override
    protected BinaryMemcacheRequest encodeRequest(ChannelHandlerContext ctx, DCPRequest msg) throws Exception {
        BinaryMemcacheRequest request;

        if (msg instanceof StreamRequestRequest) {
            StreamRequestRequest streamRequest = (StreamRequestRequest) msg;
            request = handleStreamRequestRequest(ctx, streamRequest);
            connection = streamRequest.connection();
        } else if (msg instanceof StreamCloseRequest) {
            request = handleStreamCloseRequest(ctx, (StreamCloseRequest) msg);
        } else if (msg instanceof GetFailoverLogRequest) {
            request = handleFailoverLogsRequest(ctx, (GetFailoverLogRequest) msg);
        } else if (msg instanceof GetLastCheckpointRequest) {
            request = handleGetLastCheckpointRequest(ctx, (GetLastCheckpointRequest) msg);
        } else {
            throw new IllegalArgumentException("Unknown incoming DCPRequest type " + msg.getClass());
        }

        if (msg.partition() >= 0) {
            request.setReserved(msg.partition());
        }

        return request;
    }

    @Override
    protected CouchbaseResponse decodeResponse(ChannelHandlerContext ctx, FullBinaryMemcacheResponse msg)
            throws Exception {
        DCPRequest request = currentRequest();
        DCPResponse response = null;

        if (msg.getOpcode() == OP_STREAM_REQUEST && request instanceof StreamRequestRequest) {
            StreamRequestRequest streamRequest = (StreamRequestRequest) request;
            ByteBuf content = msg.content();
            List<FailoverLogEntry> failoverLog = null;
            long rollbackToSequenceNumber = 0;
            KeyValueStatus status = KeyValueStatus.valueOf(msg.getStatus());
            switch (status) {
            case SUCCESS:
                failoverLog = readFailoverLogs(content);
                break;
            case ERR_ROLLBACK:
                rollbackToSequenceNumber = content.readLong();
                break;
            default:
                LOGGER.warn("Unexpected status of StreamRequestResponse: {} (0x{}, {})", status,
                        Integer.toHexString(status.code()), status.description());
            }
            response = new StreamRequestResponse(ResponseStatusConverter.fromBinary(msg.getStatus()), failoverLog,
                    rollbackToSequenceNumber, request);
            connection.registerContext(request.partition(), ctx);
            connection.consumed(request.partition(), msg.getTotalBodyLength());
        } else if (msg.getOpcode() == OP_STREAM_CLOSE && request instanceof StreamCloseRequest) {
            response = new StreamCloseResponse(ResponseStatusConverter.fromBinary(msg.getStatus()), request);
        } else if (msg.getOpcode() == OP_GET_FAILOVER_LOG && request instanceof GetFailoverLogRequest) {
            response = new GetFailoverLogResponse(ResponseStatusConverter.fromBinary(msg.getStatus()),
                    readFailoverLogs(msg.content()), request);
        } else if (msg.getOpcode() == OP_GET_LAST_CHECKPOINT && request instanceof GetLastCheckpointRequest) {
            long sequenceNumber = msg.content().readLong();
            response = new GetLastCheckpointResponse(ResponseStatusConverter.fromBinary(msg.getStatus()),
                    sequenceNumber, request);
        } else if (msg.getOpcode() == OP_BUFFER_ACK) {
            KeyValueStatus status = KeyValueStatus.valueOf(msg.getStatus());
            if (status != KeyValueStatus.SUCCESS) {
                LOGGER.warn("Unexpected status of service response (opcode={}): {} (0x{}, {})",
                        Integer.toHexString(msg.getOpcode()), status, Integer.toHexString(status.code()),
                        status.description());
            }
        } else {
            /**
             * FIXME
             * There are two cases:
             *
             * 1) Queue is empty:
             *    In this case outer method {@link AbstractGenericHandler#decode} picks 'null'
             *    as a current request, and it will might lead to NPEs because it does not cover
             *    scenario when we receive some packet without request in the queue.
             *
             * 2) Queue is not empty:
             *    In this case we might notify about errors wrong observable in the outer method,
             *    so we have to substitute it as soon as we detected that incoming message is not
             *    relevant to 'current request'
             */
            final DCPRequest oldRequest = currentRequest();
            final DCPRequest dummy = new AbstractDCPRequest(connection.bucket(), null) {
            };
            dummy.observable().subscribe(new Action1<CouchbaseResponse>() {
                @Override
                public void call(CouchbaseResponse couchbaseResponse) {
                    // skip
                }
            }, new Action1<Throwable>() {
                @Override
                public void call(Throwable throwable) {
                    connection.subject().onError(throwable);
                }
            });
            try {
                currentRequest(dummy);
                handleDCPRequest(ctx, msg);
            } finally {
                currentRequest(oldRequest);
            }
        }
        if (request != null && request.partition() >= 0 && response != null) {
            response.partition(request.partition());
        }

        if (response != null || request == null) {
            finishedDecoding();
        }
        return response;
    }

    /**
     * Handles incoming stream of DCP messages.
     */
    private void handleDCPRequest(final ChannelHandlerContext ctx, final FullBinaryMemcacheResponse msg) {
        DCPRequest request = null;
        int flags = 0;
        long bySeqno = 0;
        long revSeqno = 0;

        switch (msg.getOpcode()) {
        case OP_SNAPSHOT_MARKER:
            long startSequenceNumber = 0;
            long endSequenceNumber = 0;
            if (msg.getExtrasLength() > 0) {
                final ByteBuf extras = msg.getExtras();
                startSequenceNumber = extras.readLong();
                endSequenceNumber = extras.readLong();
                flags = extras.readInt();
            }
            request = new SnapshotMarkerMessage(msg.getTotalBodyLength(), msg.getStatus(), startSequenceNumber,
                    endSequenceNumber, flags, connection.bucket());
            break;

        case OP_MUTATION:
            int expiration = 0;
            int lockTime = 0;

            if (msg.getExtrasLength() > 0) {
                final ByteBuf extras = msg.getExtras();
                bySeqno = extras.readLong();
                revSeqno = extras.readLong();
                flags = extras.readInt();
                expiration = extras.readInt();
                lockTime = extras.readInt();
            }
            request = new MutationMessage(msg.getTotalBodyLength(), msg.getStatus(),
                    new String(msg.getKey(), CHARSET), msg.content().retain(), expiration, bySeqno, revSeqno, flags,
                    lockTime, msg.getCAS(), connection.bucket());
            break;

        case OP_REMOVE:
            if (msg.getExtrasLength() > 0) {
                final ByteBuf extras = msg.getExtras();
                bySeqno = extras.readLong();
                revSeqno = extras.readLong();
            }
            request = new RemoveMessage(msg.getTotalBodyLength(), msg.getStatus(),
                    new String(msg.getKey(), CHARSET), msg.getCAS(), bySeqno, revSeqno, connection.bucket());
            break;

        case OP_EXPIRATION:
            if (msg.getExtrasLength() > 0) {
                final ByteBuf extras = msg.getExtras();
                bySeqno = extras.readLong();
                revSeqno = extras.readLong();
            }
            request = new ExpirationMessage(msg.getTotalBodyLength(), msg.getStatus(),
                    new String(msg.getKey(), CHARSET), msg.getCAS(), bySeqno, revSeqno, connection.bucket());
            break;

        case OP_STREAM_END:
            final ByteBuf extras = msg.getExtras();
            StreamEndMessage.Reason reason = StreamEndMessage.Reason.valueOf(extras.readInt());
            request = new StreamEndMessage(msg.getTotalBodyLength(), msg.getStatus(), reason, connection.bucket());
            connection.streamClosed(request.partition(), reason);
            break;

        default:
            LOGGER.info("Unhandled DCP message: {}, {}", msg.getOpcode(), msg);
        }
        if (request != null) {
            connection.subject().onNext(request);
        }
    }

    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        super.handlerRemoved(ctx);
    }

    /**
     * Creates a DCP Stream Request.
     *
     * See the [Protocol Description](https://github.com/couchbaselabs/dcp-documentation/blob/master/documentation/
     * commands/stream-request.md) for more details.
     *
     * @param ctx the channel handler context.
     * @param msg the stream request message.
     * @return a converted {@link BinaryMemcacheRequest}.
     */
    private BinaryMemcacheRequest handleStreamRequestRequest(final ChannelHandlerContext ctx,
            final StreamRequestRequest msg) {
        ByteBuf extras = ctx.alloc().buffer(48);
        extras.writeInt(0) // flags
                .writeInt(0) // reserved
                .writeLong(msg.startSequenceNumber()) // start sequence number
                .writeLong(msg.endSequenceNumber()) // end sequence number
                .writeLong(msg.vbucketUUID()) // vbucket UUID
                .writeLong(msg.snapshotStartSequenceNumber()) // snapshot start sequence number
                .writeLong(msg.snapshotEndSequenceNumber()); // snapshot end sequence number

        byte extrasLength = (byte) extras.readableBytes();

        BinaryMemcacheRequest request = new DefaultBinaryMemcacheRequest(extras);
        request.setOpcode(OP_STREAM_REQUEST);
        request.setExtrasLength(extrasLength);
        request.setTotalBodyLength(extrasLength);

        return request;
    }

    private BinaryMemcacheRequest handleStreamCloseRequest(ChannelHandlerContext ctx, StreamCloseRequest msg) {
        BinaryMemcacheRequest request = new DefaultBinaryMemcacheRequest();
        request.setOpcode(OP_STREAM_CLOSE);

        return request;
    }

    private BinaryMemcacheRequest handleFailoverLogsRequest(ChannelHandlerContext ctx, GetFailoverLogRequest msg) {
        BinaryMemcacheRequest request = new DefaultBinaryMemcacheRequest();
        request.setOpcode(OP_GET_FAILOVER_LOG);
        request.setReserved(msg.partition());

        return request;
    }

    private List<FailoverLogEntry> readFailoverLogs(final ByteBuf content) {
        List<FailoverLogEntry> failoverLog = new ArrayList<FailoverLogEntry>(content.readableBytes() / 16);
        while (content.readableBytes() >= 16) {
            FailoverLogEntry entry = new FailoverLogEntry(content.readLong(), content.readLong());
            failoverLog.add(entry);
        }
        return failoverLog;
    }

    private BinaryMemcacheRequest handleGetLastCheckpointRequest(ChannelHandlerContext ctx,
            GetLastCheckpointRequest msg) {
        BinaryMemcacheRequest request = new DefaultBinaryMemcacheRequest();
        request.setOpcode(OP_GET_LAST_CHECKPOINT);
        request.setReserved(msg.partition());

        return request;
    }

    @Override
    protected ServiceType serviceType() {
        return ServiceType.DCP;
    }
}