/*
* 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.node.locate;
import com.couchbase.client.core.ReplicaNotAvailableException;
import com.couchbase.client.core.ReplicaNotConfiguredException;
import com.couchbase.client.core.ResponseEvent;
import com.couchbase.client.core.config.BucketConfig;
import com.couchbase.client.core.config.ClusterConfig;
import com.couchbase.client.core.config.CouchbaseBucketConfig;
import com.couchbase.client.core.config.DefaultCouchbaseBucketConfig;
import com.couchbase.client.core.config.MemcachedBucketConfig;
import com.couchbase.client.core.config.NodeInfo;
import com.couchbase.client.core.env.CoreEnvironment;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.CouchbaseRequest;
import com.couchbase.client.core.message.kv.BinaryRequest;
import com.couchbase.client.core.message.kv.GetAllMutationTokensRequest;
import com.couchbase.client.core.message.kv.GetBucketConfigRequest;
import com.couchbase.client.core.message.kv.ObserveRequest;
import com.couchbase.client.core.message.kv.ObserveSeqnoRequest;
import com.couchbase.client.core.message.kv.ReplicaGetRequest;
import com.couchbase.client.core.message.kv.StatRequest;
import com.couchbase.client.core.node.Node;
import com.couchbase.client.core.retry.RetryHelper;
import com.couchbase.client.core.state.LifecycleState;
import com.lmax.disruptor.RingBuffer;
import java.net.InetAddress;
import java.util.List;
import java.util.zip.CRC32;
/**
* This {@link Locator} finds the proper {@link Node}s for every incoming {@link BinaryRequest}.
*
* Depending on the bucket type used, it either uses partition/vbucket (couchbase) or ketama (memcache) hashing. For
* broadcast-type operations, it will return all suitable nodes without hashing by key.
*
* @since 1.0.0
* @author Michael Nitschinger
* @author Simon Baslé
*/
public class KeyValueLocator implements Locator {
/**
* The Logger used.
*/
private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(KeyValueLocator.class);
private static final int MIN_KEY_BYTES = 1;
private static final int MAX_KEY_BYTES = 250;
@Override
public void locateAndDispatch(final CouchbaseRequest request, final List<Node> nodes, final ClusterConfig cluster,
CoreEnvironment env, RingBuffer<ResponseEvent> responseBuffer) {
if (request instanceof GetBucketConfigRequest) {
locateByHostname(request, ((GetBucketConfigRequest) request).hostname(), nodes, env, responseBuffer);
return;
}
if (request instanceof StatRequest) {
locateByHostname(request, ((StatRequest) request).hostname(), nodes, env, responseBuffer);
return;
}
if (request instanceof GetAllMutationTokensRequest) {
locateByHostname(request, ((GetAllMutationTokensRequest) request).hostname(), nodes, env, responseBuffer);
return;
}
BucketConfig bucket = cluster.bucketConfig(request.bucket());
if (bucket instanceof CouchbaseBucketConfig) {
locateForCouchbaseBucket((BinaryRequest) request, nodes, (CouchbaseBucketConfig) bucket, env, responseBuffer);
} else if (bucket instanceof MemcachedBucketConfig) {
locateForMemcacheBucket((BinaryRequest) request, nodes, (MemcachedBucketConfig) bucket, env, responseBuffer);
} else {
throw new IllegalStateException("Unsupported Bucket Type: " + bucket + " for request " + request);
}
}
private static void locateByHostname(final CouchbaseRequest request, final InetAddress hostname, List<Node> nodes,
CoreEnvironment env, RingBuffer<ResponseEvent> responseBuffer) {
for (Node node : nodes) {
if (node.isState(LifecycleState.CONNECTED) || node.isState(LifecycleState.DEGRADED)) {
if (!hostname.equals(node.hostname())) {
continue;
}
node.send(request);
return;
}
}
RetryHelper.retryOrCancel(env, request, responseBuffer);
}
/**
* Locates the proper {@link Node}s for a Couchbase bucket.
*
* @param request the request.
* @param nodes the managed nodes.
* @param config the bucket configuration.
*/
private static void locateForCouchbaseBucket(final BinaryRequest request, final List<Node> nodes,
final CouchbaseBucketConfig config, CoreEnvironment env, RingBuffer<ResponseEvent> responseBuffer) {
if (!keyIsValid(request)) {
return;
}
int partitionId = partitionForKey(request.keyBytes(), config.numberOfPartitions());
request.partition((short) partitionId);
int nodeId = calculateNodeId(partitionId, request, config);
if (nodeId < 0) {
errorObservables(nodeId, request, config.name(), env, responseBuffer);
return;
}
NodeInfo nodeInfo = config.nodeAtIndex(nodeId);
for (Node node : nodes) {
if (node.hostname().equals(nodeInfo.hostname())) {
node.send(request);
return;
}
}
if(handleNotEqualNodeSizes(config.nodes().size(), nodes.size())) {
RetryHelper.retryOrCancel(env, request, responseBuffer);
return;
}
throw new IllegalStateException("Node not found for request" + request);
}
/**
* Helper method to calculate the node if for the given partition and request type.
*
* @param partitionId the partition id.
* @param request the request used.
* @param config the current bucket configuration.
* @return the calculated node id.
*/
private static int calculateNodeId(int partitionId, BinaryRequest request, CouchbaseBucketConfig config) {
boolean useFastForward = request.retryCount() > 0 && config.hasFastForwardMap();
if (request instanceof ReplicaGetRequest) {
return config.nodeIndexForReplica(partitionId, ((ReplicaGetRequest) request).replica() - 1, useFastForward);
} else if (request instanceof ObserveRequest && ((ObserveRequest) request).replica() > 0) {
return config.nodeIndexForReplica(partitionId, ((ObserveRequest) request).replica() - 1, useFastForward);
} else if (request instanceof ObserveSeqnoRequest && ((ObserveSeqnoRequest) request).replica() > 0) {
return config.nodeIndexForReplica(partitionId, ((ObserveSeqnoRequest) request).replica() - 1, useFastForward);
} else {
return config.nodeIndexForMaster(partitionId, useFastForward);
}
}
/**
* Fail observables because the partitions do not match up.
*
* If the replica is not even available in the configuration (identified by a -2 node index),
* it is clear that this replica is not configured. If a -1 is returned it is configured, but
* currently not available (not enough nodes in the cluster, for example if a node is seen down,
* after a failover, or during rebalance. Replica partitions in general take longer to heal than
* active partitions, since they are sacrificed for application availability.
*
* @param nodeId the current node id of the partition
* @param request the request to error
* @param name the name of the bucket
*/
private static void errorObservables(int nodeId, BinaryRequest request, String name, CoreEnvironment env,
RingBuffer<ResponseEvent> responseBuffer) {
if (nodeId == DefaultCouchbaseBucketConfig.PARTITION_NOT_EXISTENT) {
if (request instanceof ReplicaGetRequest) {
request.observable().onError(new ReplicaNotConfiguredException("Replica number "
+ ((ReplicaGetRequest) request).replica() + " not configured for bucket " + name));
return;
} else if (request instanceof ObserveRequest) {
request.observable().onError(new ReplicaNotConfiguredException("Replica number "
+ ((ObserveRequest) request).replica() + " not configured for bucket " + name));
return;
} else if (request instanceof ObserveSeqnoRequest) {
request.observable().onError(new ReplicaNotConfiguredException("Replica number "
+ ((ObserveSeqnoRequest) request).replica() + " not configured for bucket " + name));
return;
}
RetryHelper.retryOrCancel(env, request, responseBuffer);
return;
}
if (nodeId == -1) {
if (request instanceof ObserveRequest) {
request.observable().onError(new ReplicaNotAvailableException("Replica number "
+ ((ObserveRequest) request).replica() + " not available for bucket " + name));
return;
} else if (request instanceof ReplicaGetRequest) {
request.observable().onError(new ReplicaNotAvailableException("Replica number "
+ ((ReplicaGetRequest) request).replica() + " not available for bucket " + name));
return;
} else if (request instanceof ObserveSeqnoRequest) {
request.observable().onError(new ReplicaNotAvailableException("Replica number "
+ ((ObserveSeqnoRequest) request).replica() + " not available for bucket " + name));
return;
}
RetryHelper.retryOrCancel(env, request, responseBuffer);
return;
}
throw new IllegalStateException("Unknown NodeId: " + nodeId + ", request: " + request);
}
/**
* Calculate the vbucket for the given key.
*
* @param key the key to calculate from.
* @param numPartitions the number of partitions in the bucket.
* @return the calculated partition.
*/
private static int partitionForKey(byte[] key, int numPartitions) {
CRC32 crc32 = new CRC32();
crc32.update(key);
long rv = (crc32.getValue() >> 16) & 0x7fff;
return (int) rv &numPartitions - 1;
}
/**
* Locates the proper {@link Node}s for a Memcache bucket.
*
* @param request the request.
* @param nodes the managed nodes.
* @param config the bucket configuration.
*/
private static void locateForMemcacheBucket(final BinaryRequest request, final List<Node> nodes,
final MemcachedBucketConfig config, CoreEnvironment env, RingBuffer<ResponseEvent> responseBuffer) {
if (!keyIsValid(request)) {
return;
}
InetAddress hostname = config.nodeForId(request.keyBytes());
request.partition((short) 0);
for (Node node : nodes) {
if (node.hostname().equals(hostname)) {
node.send(request);
return;
}
}
if(handleNotEqualNodeSizes(config.nodes().size(), nodes.size())) {
RetryHelper.retryOrCancel(env, request, responseBuffer);
return;
}
throw new IllegalStateException("Node not found for request" + request);
}
/**
* Helper method to handle potentially different node sizes in the actual list and in the config.
*
* @return true if they are not equal, false if they are.
*/
private static boolean handleNotEqualNodeSizes(int configNodeSize, int actualNodeSize) {
if (configNodeSize != actualNodeSize) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Node list and configuration's partition hosts sizes : {} <> {}, rescheduling",
actualNodeSize, configNodeSize);
}
return true;
}
return false;
}
/**
* Helper method to check if the given request key is valid.
*
* If false is returned, the request observable is already failed.
*
* @param request the request to extract and validate the key from.
* @return true if valid, false otherwise.
*/
private static boolean keyIsValid(final BinaryRequest request) {
if (request.keyBytes() == null || request.keyBytes().length < MIN_KEY_BYTES) {
request.observable().onError(new IllegalArgumentException("The Document ID must not be null or empty."));
return false;
}
if (request.keyBytes().length > MAX_KEY_BYTES) {
request.observable().onError(new IllegalArgumentException(
"The Document ID must not be longer than 250 bytes."));
return false;
}
return true;
}
}