All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.client.core.msg.kv.SubdocGetRequest Maven / Gradle / Ivy

There is a newer version: 3.7.2
Show newest version
/*
 * Copyright (c) 2018 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.msg.kv;

import com.couchbase.client.core.CoreContext;
import com.couchbase.client.core.api.kv.CoreSubdocGetCommand;
import com.couchbase.client.core.cnc.RequestSpan;
import com.couchbase.client.core.cnc.TracingIdentifiers;
import com.couchbase.client.core.deps.io.netty.buffer.ByteBuf;
import com.couchbase.client.core.deps.io.netty.buffer.ByteBufAllocator;
import com.couchbase.client.core.deps.io.netty.buffer.CompositeByteBuf;
import com.couchbase.client.core.deps.io.netty.util.ReferenceCountUtil;
import com.couchbase.client.core.error.CouchbaseException;
import com.couchbase.client.core.io.CollectionIdentifier;
import com.couchbase.client.core.io.netty.kv.KeyValueChannelContext;
import com.couchbase.client.core.io.netty.kv.MemcacheProtocol;
import com.couchbase.client.core.msg.ResponseStatus;
import com.couchbase.client.core.retry.RetryStrategy;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;

import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.Status;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.body;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.cas;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.datatype;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.decodeStatus;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.decodeSubDocumentStatus;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.mapSubDocumentError;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.noCas;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.noDatatype;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.noExtras;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.request;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.status;
import static com.couchbase.client.core.msg.kv.SubdocUtil.handleNonFieldLevelErrors;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Comparator.comparing;

public class SubdocGetRequest extends BaseKeyValueRequest {

  private static final byte SUBDOC_FLAG_XATTR_PATH = (byte) 0x04;

  private static final Comparator xattrsFirst = comparing(it -> !it.xattr());

  private final byte flags;
  private final List commands;
  private final String origKey;

  public static SubdocGetRequest create(final Duration timeout, final CoreContext ctx, CollectionIdentifier collectionIdentifier,
                                        final RetryStrategy retryStrategy, final String key,
                                        final byte flags, final List commands, final RequestSpan span) {
    return new SubdocGetRequest(timeout, ctx, collectionIdentifier, retryStrategy, key, flags, convertCommands(commands), span);
  }

  public SubdocGetRequest(final Duration timeout, final CoreContext ctx, CollectionIdentifier collectionIdentifier,
                          final RetryStrategy retryStrategy, final String key,
                          final byte flags, final List commands, final RequestSpan span) {
    super(timeout, ctx, retryStrategy, key, collectionIdentifier, span);
    this.flags = flags;
    this.commands = commands;
    this.origKey = key;

    if (span != null) {
      span.lowCardinalityAttribute(TracingIdentifiers.ATTR_OPERATION, TracingIdentifiers.SPAN_REQUEST_KV_LOOKUP_IN);
    }
  }

  static List convertCommands(List commands) {
    List result = new ArrayList<>(commands.size());
    for (int i = 0, len = commands.size(); i < len; i++) {
      CoreSubdocGetCommand core = commands.get(i);
      result.add(new SubdocGetRequest.Command(
        core.type(),
        core.path(),
        core.xattr(),
        i
      ));
    }

    // xattrs must come first. decode() puts the results back in original order.
    result.sort(xattrsFirst);
    return result;
  }

  @Override
  public ByteBuf encode(ByteBufAllocator alloc, int opaque, KeyValueChannelContext ctx) {
    ByteBuf key = null;
    ByteBuf extras = null;
    ByteBuf body = null;

    try {
      if (!ctx.vattrEnabled()) {
        // Server will not handle all vattrs perfectly: it will reject those it doesn't know by breaking the connection.
        // Do a check to see if all vattr commands meet a whitelist of vattrs.
        for (Command c: commands) {
          if (c.xattr()
                  && (c.path.length() > 0 && c.path.charAt(0) == '$')
                  && !(c.path.startsWith("$document") || c.path.startsWith("$XTOC"))) {
            throw mapSubDocumentError(this, SubDocumentOpResponseStatus.XATTR_UNKNOWN_VATTR, c.path, c.originalIndex(), null);
          }
        }
      }

      key = encodedKeyWithCollection(alloc, ctx);

      if (flags != 0) {
        extras = alloc.buffer(Byte.BYTES).writeByte(flags);
      }

      if (commands.size() == 1) {
        // Note currently the only subdoc error response handled is ERR_SUBDOC_MULTI_PATH_FAILURE.  Make sure to
        // add the others if do the single lookup optimisation.
        // Update: single subdoc optimization will not be supported.  It adds just 3 bytes to the package size and gives
        // minimal performance gains, in return for additional client complexity.
        body = commands.get(0).encode(alloc);
      } else {
        body = alloc.compositeBuffer(commands.size());
        for (Command command : commands) {
          ByteBuf commandBuffer = command.encode(alloc);
          try {
            ((CompositeByteBuf) body).addComponent(commandBuffer);
            body.writerIndex(body.writerIndex() + commandBuffer.readableBytes());
          } catch (Exception ex) {
            ReferenceCountUtil.release(commandBuffer);
            throw ex;
          }
        }
      }

      return request(
        alloc,
        MemcacheProtocol.Opcode.SUBDOC_MULTI_LOOKUP,
        noDatatype(),
        partition(),
        opaque,
        noCas(),
        extras == null ? noExtras() : extras,
        key,
        body
      );
    } finally {
      ReferenceCountUtil.release(key);
      ReferenceCountUtil.release(body);
      ReferenceCountUtil.release(extras);
    }
  }

  @Override
  public SubdocGetResponse decode(final ByteBuf response, KeyValueChannelContext ctx) {
    short rawStatus = status(response);
    boolean isJson = (datatype(response) & MemcacheProtocol.Datatype.JSON.datatype()) != 0;
    Optional maybeBody = body(response);
    MemcacheProtocol.FlexibleExtras flexibleExtras = MemcacheProtocol.flexibleExtras(response);
    ResponseStatus status = decodeStatus(response);

    SubDocumentField[] values = null;
    List errors = null;
    Optional error = Optional.empty();
    String bodyErrorMessage = null;

    // Sources for sub-doc error-handling:
    // https://github.com/couchbase/kv_engine/blob/master/docs/SubDocument.md
    // Internal discussion: https://couchbase.slack.com/archives/CFJDXSGUA/p1694617140032519
    // The body (if present) either contains a sub-doc response (can include errors), or a JSON error message.
    boolean isSuccess = rawStatus == Status.SUCCESS.status()
      || rawStatus == Status.SUBDOC_SUCCESS_DELETED_DOCUMENT.status()
      // With lookupIn, even if some operations fail, others can succeed and the op can be seen as successful.
      || rawStatus == Status.SUBDOC_MULTI_PATH_FAILURE.status()
      || rawStatus == Status.SUBDOC_MULTI_PATH_FAILURE_DELETED.status();

    if (maybeBody.isPresent()) {
      ByteBuf body = maybeBody.get();
      if (isSuccess) {
        values = new SubDocumentField[commands.size()];
        for (Command command : commands) {
          short statusRaw = body.readShort();
          SubDocumentOpResponseStatus fieldStatus = decodeSubDocumentStatus(statusRaw);
          Optional fieldError = Optional.empty();
          if (fieldStatus != SubDocumentOpResponseStatus.SUCCESS) {
            if (errors == null) errors = new ArrayList<>();
            CouchbaseException err = mapSubDocumentError(this, fieldStatus, command.path, command.originalIndex(), flexibleExtras);
            errors.add(err);
            fieldError = Optional.of(err);
          }
          int valueLength = body.readInt();
          byte[] value = new byte[valueLength];
          body.readBytes(value, 0, valueLength);
          SubDocumentField op = new SubDocumentField(fieldStatus, fieldError, value, command.path, command.type);
          values[command.originalIndex] = op;
        }
      } else if (isJson) {
        // Body contains an error message
        byte[] value = new byte[body.readableBytes()];
        body.readBytes(value, 0, body.readableBytes());
        bodyErrorMessage = new String(value, UTF_8);
      }
    }

    boolean isDeleted = rawStatus == Status.SUBDOC_MULTI_PATH_FAILURE_DELETED.status()
            || rawStatus == Status.SUBDOC_SUCCESS_DELETED_DOCUMENT.status();

    // Note that we send all subdoc requests as multi currently so always get this back on error
    if (rawStatus == Status.SUBDOC_MULTI_PATH_FAILURE.status()
        || rawStatus == Status.SUBDOC_MULTI_PATH_FAILURE_DELETED.status()) {
      // Special case logic for CMD_EXISTS
      if (commands.size() == 1 && commands.get(0).type == SubdocCommandType.EXISTS) {
        status = ResponseStatus.SUCCESS;
      } else {
        // Otherwise return success, as some of the operations have succeeded
        status = ResponseStatus.SUCCESS;
      }
    }

    CouchbaseException nonFieldError = handleNonFieldLevelErrors(this, rawStatus, flexibleExtras, bodyErrorMessage);
    if (nonFieldError != null) {
      error = Optional.of(nonFieldError);
    }

    return new SubdocGetResponse(status, error, values, cas(response), isDeleted, flexibleExtras);
  }

  public static class Command {
    private final SubdocCommandType type;
    private final String path;
    private final boolean xattr;
    private final int originalIndex;

    public Command(SubdocCommandType type, String path, boolean xattr, int originalIndex) {
      this.type = type;
      this.path = path;
      this.xattr = xattr;
      this.originalIndex = originalIndex;
    }

    public ByteBuf encode(ByteBufAllocator alloc) {
      byte[] path = this.path.getBytes(UTF_8);
      int pathLength = path.length;

      ByteBuf buffer = alloc.buffer(4 + pathLength);
      buffer.writeByte(type.opcode());
      if (xattr) {
        buffer.writeByte(SUBDOC_FLAG_XATTR_PATH);
      } else {
        buffer.writeByte(0);
      }
      buffer.writeShort(pathLength);
      buffer.writeBytes(path);
      return buffer;
    }

    public int originalIndex() {
      return originalIndex;
    }

    public boolean xattr() {
      return xattr;
    }
  }

  @Override
  public boolean idempotent() {
    return true;
  }

  @Override
  public String name() {
    return "lookup_in";
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy