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

com.couchbase.client.core.io.netty.kv.FeatureNegotiatingHandler Maven / Gradle / Ivy

There is a newer version: 2.7.0
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.io.netty.kv;

import com.couchbase.client.core.annotation.Stability;
import com.couchbase.client.core.cnc.events.io.FeaturesNegotiatedEvent;
import com.couchbase.client.core.cnc.events.io.FeaturesNegotiationFailedEvent;
import com.couchbase.client.core.cnc.events.io.UnsolicitedFeaturesReturnedEvent;
import com.couchbase.client.core.deps.io.netty.buffer.ByteBuf;
import com.couchbase.client.core.deps.io.netty.channel.ChannelDuplexHandler;
import com.couchbase.client.core.deps.io.netty.channel.ChannelHandlerContext;
import com.couchbase.client.core.deps.io.netty.channel.ChannelPromise;
import com.couchbase.client.core.deps.io.netty.util.ReferenceCountUtil;
import com.couchbase.client.core.endpoint.EndpointContext;
import com.couchbase.client.core.error.CouchbaseException;
import com.couchbase.client.core.io.IoContext;
import com.couchbase.client.core.json.Mapper;
import com.couchbase.client.core.msg.kv.BaseKeyValueRequest;

import java.net.SocketAddress;
import java.time.Duration;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

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.noPartition;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.status;
import static com.couchbase.client.core.io.netty.kv.MemcacheProtocol.successful;

/**
 * The {@link FeatureNegotiatingHandler} is responsible for sending the KV "hello" command
 * and to handshake enabled features on both sides.
 *
 * 

If we get any response from the server, we'll take it. If the server returns a * non-successful response we will report that, but move on with no negotiated features. If * the server returns more features than we asked for, we'll only use the subset and not * more (and report the abnormal condition).

* * @since 2.0.0 */ @Stability.Internal public class FeatureNegotiatingHandler extends ChannelDuplexHandler { /** * Holds the timeout for the full feature negotiation phase. */ private final Duration timeout; /** * Holds all features the client requested to the server. */ private final Set features; /** * Holds the core context as reference to event bus and more. */ private final EndpointContext endpointContext; /** * Once connected, holds the io context for more debug information. */ private IoContext ioContext; /** * Holds the intercepted promise from up the pipeline which is either * completed or failed depending on the downstream components or the * result of the hello negotiation. */ private ChannelPromise interceptedConnectPromise; /** * Creates a new {@link FeatureNegotiatingHandler}. * * @param endpointContext the core context used to refer to values like the core id. * @param features the list of features that should be negotiated from the client side. */ public FeatureNegotiatingHandler(final EndpointContext endpointContext, final Set features) { this.endpointContext = endpointContext; this.timeout = endpointContext.environment().timeoutConfig().connectTimeout(); this.features = features; } /** * Intercepts the connect process inside the pipeline to only propagate either * success or failure if the hello process is completed either way. * *

Note that if no feature is to negotiate we can bail out right away.

* * @param ctx the {@link ChannelHandlerContext} for which the connect operation is made. * @param remoteAddress the {@link SocketAddress} to which it should connect. * @param localAddress the {@link SocketAddress} which is used as source on connect. * @param promise the {@link ChannelPromise} to notify once the operation completes. */ @Override public void connect(final ChannelHandlerContext ctx, final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { if (features.isEmpty()) { ConnectTimings.record(ctx.channel(), this.getClass()); ctx.pipeline().remove(this); ctx.connect(remoteAddress, localAddress, promise); } else { interceptedConnectPromise = promise; ChannelPromise downstream = ctx.newPromise(); downstream.addListener(f -> { if (!f.isSuccess() && !interceptedConnectPromise.isDone()) { ConnectTimings.record(ctx.channel(), this.getClass()); interceptedConnectPromise.tryFailure(f.cause()); } }); ctx.connect(remoteAddress, localAddress, downstream); } } /** * As soon as the channel is active start sending the hello request but also schedule * a timeout properly. * * @param ctx the {@link ChannelHandlerContext} for which the channel active operation is made. */ @Override public void channelActive(final ChannelHandlerContext ctx) { ioContext = new IoContext( endpointContext, ctx.channel().localAddress(), ctx.channel().remoteAddress(), endpointContext.bucket() ); ctx.executor().schedule(() -> { if (!interceptedConnectPromise.isDone()) { ConnectTimings.stop(ctx.channel(), this.getClass(), true); interceptedConnectPromise.tryFailure( new TimeoutException("KV Feature Negotiation timed out after " + timeout.toMillis() + "ms") ); } }, timeout.toNanos(), TimeUnit.NANOSECONDS); ConnectTimings.start(ctx.channel(), this.getClass()); ctx.writeAndFlush(buildHelloRequest(ctx)); // Fire the channel active immediately so the upper handler in the pipeline gets a chance to // pipeline its request before the response of this one arrives. This helps speeding up the // bootstrap sequence. ctx.fireChannelActive(); } /** * As soon as we get a response, turn it into a list of negotiated server features. * *

Since the server might respond with a non-success status code, this case is handled * and we would move on without any negotiated features but make sure the proper event * is raised.

* * @param ctx the {@link ChannelHandlerContext} for which the channel read operation is made. * @param msg the incoming msg that needs to be parsed. */ @Override public void channelRead(final ChannelHandlerContext ctx, final Object msg) { if (msg instanceof ByteBuf) { Optional latency = ConnectTimings.stop(ctx.channel(), this.getClass(), false); if (!successful((ByteBuf) msg)) { endpointContext.environment().eventBus().publish( new FeaturesNegotiationFailedEvent(ioContext, status((ByteBuf) msg)) ); } Set negotiated = extractFeaturesFromBody((ByteBuf) msg); ctx.channel().attr(ChannelAttributes.SERVER_FEATURE_KEY).set(negotiated); endpointContext.environment().eventBus().publish( new FeaturesNegotiatedEvent(ioContext, latency.orElse(Duration.ZERO), new ArrayList<>(negotiated)) ); interceptedConnectPromise.trySuccess(); ctx.pipeline().remove(this); } else { interceptedConnectPromise.tryFailure(new CouchbaseException("Unexpected response " + "type on channel read, this is a bug - please report. " + msg)); } ReferenceCountUtil.release(msg); } /** * Helper method to safely extract the negotiated server features from the * body of the memcache payload. * * @param response the response to extract from. * @return the list of server features, may be empty but never null. */ private Set extractFeaturesFromBody(final ByteBuf response) { Optional body = MemcacheProtocol.body(response); Set negotiated = EnumSet.noneOf(ServerFeature.class); List unsolicited = new ArrayList<>(); if (!body.isPresent()) { return negotiated; } while (body.get().isReadable()) { try { short featureRaw = body.get().readShort(); ServerFeature feature = ServerFeature.from(featureRaw); if (features.contains(feature)) { negotiated.add(feature); } else { unsolicited.add(feature); } } catch (Exception ex) { interceptedConnectPromise.tryFailure(new CouchbaseException( "Error while parsing negotiated server features.", ex )); } } if (!unsolicited.isEmpty()) { endpointContext.environment().eventBus().publish( new UnsolicitedFeaturesReturnedEvent(ioContext, unsolicited) ); } return negotiated; } /** * Helper method to build the HELLO request which will be sent to the server. * * @param ctx the {@link ChannelHandlerContext} for which the channel active operation is made. * @return the created request as a {@link ByteBuf}. */ private ByteBuf buildHelloRequest(final ChannelHandlerContext ctx) { ByteBuf key = buildHelloKey(ctx); ByteBuf body = ctx.alloc().buffer(features.size() * 2); for (ServerFeature feature : features) { body.writeShort(feature.value()); } ByteBuf request = MemcacheProtocol.request( ctx.alloc(), MemcacheProtocol.Opcode.HELLO, noDatatype(), noPartition(), BaseKeyValueRequest.nextOpaque(), noCas(), noExtras(), key, body ); key.release(); body.release(); return request; } /** * Helper method which builds the "key" of the HELLO command. The key is made up * of the user agent as well as a pair of IDs that uniquely identify a socket. * *

In the unlikely event of the agent not being present, a dummy value is sent which * should at least help to distinguish the SDK language.

* * @param ctx the {@link ChannelHandlerContext} for which the channel active operation is made. * @return a {@link ByteBuf} with the full request to send. */ private ByteBuf buildHelloKey(final ChannelHandlerContext ctx) { TreeMap result = new TreeMap<>(); String agent = endpointContext.environment().userAgent().formattedShort(); if (agent == null || agent.isEmpty()) { agent = "couchbase-java-core/0.0.0"; } else if (agent.length() > 200) { agent = agent.substring(0, 200); } result.put("a", agent); String channelId = "0x" + ctx.channel().id().asShortText(); long convertedChannelId; try { convertedChannelId = channelId.equals("0xembedded") ? 1L : Long.decode(channelId); } catch (NumberFormatException ex) { // This is just a safeguard in place should the netty channel ID // format ever change and trigger a failure of decoding the channel ID into a long convertedChannelId = new Random().nextInt(); } String paddedChannelId = paddedHex(convertedChannelId); String fullId = paddedHex(endpointContext.id()) + "/" + paddedChannelId; result.put("i", fullId); ctx.channel().attr(ChannelAttributes.CHANNEL_ID_KEY).set(fullId); return ctx.alloc().buffer().writeBytes(Mapper.encodeAsBytes(result)); } /** * Pad the long input into a string encoded hex value. * * @param input the number to format. * @return the padded long hex value. */ private static String paddedHex(long input) { return String.format("%016X", input); } /** * If there is an exception raised while we are waiting for our connect phase to complete, the error * should be propagated as a cause up the pipeline. * *

One reason for example could be TLS problems that need to be surfaced up the stack properly.

* * @param ctx the channel handler context. * @param cause the cause of the problem. */ @Override public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) { if (!interceptedConnectPromise.isDone()) { interceptedConnectPromise.tryFailure(cause); } ctx.fireExceptionCaught(cause); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy