com.couchbase.client.core.RequestHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-io Show documentation
Show all versions of core-io Show documentation
The official Couchbase JVM Core IO Library
/*
* 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;
import com.couchbase.client.core.config.BucketConfig;
import com.couchbase.client.core.config.ClusterConfig;
import com.couchbase.client.core.config.NodeInfo;
import com.couchbase.client.core.env.CoreEnvironment;
import com.couchbase.client.core.event.EventBus;
import com.couchbase.client.core.event.system.ConfigUpdatedEvent;
import com.couchbase.client.core.logging.CouchbaseLogger;
import com.couchbase.client.core.logging.CouchbaseLoggerFactory;
import com.couchbase.client.core.message.BootstrapMessage;
import com.couchbase.client.core.message.CouchbaseRequest;
import com.couchbase.client.core.message.config.ConfigRequest;
import com.couchbase.client.core.message.dcp.DCPRequest;
import com.couchbase.client.core.message.internal.AddServiceRequest;
import com.couchbase.client.core.message.internal.RemoveServiceRequest;
import com.couchbase.client.core.message.internal.SignalFlush;
import com.couchbase.client.core.message.kv.BinaryRequest;
import com.couchbase.client.core.message.query.QueryRequest;
import com.couchbase.client.core.message.search.SearchRequest;
import com.couchbase.client.core.message.view.ViewRequest;
import com.couchbase.client.core.node.CouchbaseNode;
import com.couchbase.client.core.node.Node;
import com.couchbase.client.core.node.locate.ConfigLocator;
import com.couchbase.client.core.node.locate.DCPLocator;
import com.couchbase.client.core.node.locate.KeyValueLocator;
import com.couchbase.client.core.node.locate.Locator;
import com.couchbase.client.core.node.locate.QueryLocator;
import com.couchbase.client.core.node.locate.SearchLocator;
import com.couchbase.client.core.node.locate.ViewLocator;
import com.couchbase.client.core.service.Service;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.state.LifecycleState;
import com.couchbase.client.deps.com.lmax.disruptor.EventHandler;
import com.couchbase.client.deps.com.lmax.disruptor.RingBuffer;
import rx.Observable;
import rx.Subscriber;
import rx.functions.Action1;
import rx.functions.Func1;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import static com.couchbase.client.core.utils.Observables.failSafe;
/**
* The {@link RequestHandler} is responsible for adding and removing {@link Node}s as well as dispatching
* {@link Service} management operations. Its main purpose though is to receive incoming {@link CouchbaseRequest}s
* and dispatch them to the appropriate {@link Node}s.
*
* @author Michael Nitschinger
* @since 1.0
*/
public class RequestHandler implements EventHandler {
/**
* The logger used.
*/
private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(RequestHandler.class);
/**
* The node locator for the binary service.
*/
private final Locator binaryLocator = new KeyValueLocator();
/**
* The node locator for the view service.
*/
private final Locator viewLocator = new ViewLocator();
/**
* The node locator for the query service.
*/
private final Locator queryLocator = new QueryLocator();
/**
* The node locator for the config service.
*/
private final Locator configLocator = new ConfigLocator();
/**
* The node locator for DCP service.
*/
private final Locator dcpLocator = new DCPLocator();
/**
* The node locator for the query service.
*/
private final Locator searchLocator = new SearchLocator();
/**
* The list of currently managed nodes against the cluster.
*/
private final CopyOnWriteArrayList nodes;
/**
* The shared couchbase environment.
*/
private final CoreEnvironment environment;
/**
* The {@link ResponseEvent} {@link RingBuffer}.
*/
private final RingBuffer responseBuffer;
/**
* The event bus to publish events onto.
*/
private final EventBus eventBus;
/**
* Contains the current cluster configuration.
*/
private volatile ClusterConfig configuration;
/**
* Create a new {@link RequestHandler}.
*/
public RequestHandler(final CoreEnvironment environment, final Observable configObservable,
final RingBuffer responseBuffer) {
this(new CopyOnWriteArrayList(), environment, configObservable, responseBuffer);
}
/**
* Create a new {@link RequestHandler} with a custom node list.
*
* This constructor should only be used for testing purposes.
* @param nodes the node list to start with.
*/
RequestHandler(final CopyOnWriteArrayList nodes, final CoreEnvironment environment,
final Observable configObservable, final RingBuffer responseBuffer) {
this.nodes = nodes;
this.environment = environment;
this.responseBuffer = responseBuffer;
this.eventBus = environment.eventBus();
configuration = null;
configObservable.subscribe(new Action1() {
@Override
public void call(final ClusterConfig config) {
try {
LOGGER.debug("Got notified of a new configuration arriving.");
configuration = config;
reconfigure(config).subscribe(new Subscriber() {
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {
LOGGER.warn("Received Error during Reconfiguration.", e);
}
@Override
public void onNext(final ClusterConfig clusterConfig) {
int logicalNodes = nodes.size();
int configNodes = clusterConfig.allNodeAddresses().size();
if (logicalNodes != configNodes) {
LOGGER.debug("Number of logical Nodes does not match the number of nodes in the "
+ "current configuration! logical: {}, config: {}", logicalNodes, configNodes);
}
}
});
if (eventBus != null) {
eventBus.publish(new ConfigUpdatedEvent(config));
}
} catch (Exception ex) {
LOGGER.error("Error while subscribing to bucket config stream.", ex);
}
}
});
}
@Override
public void onEvent(final RequestEvent event, final long sequence, final boolean endOfBatch) throws Exception {
try {
dispatchRequest(event.getRequest());
} finally {
event.setRequest(null);
if (endOfBatch && nodes != null) {
flush();
}
}
}
/**
* Helper method to send the flush signal to all of the nodes available.
*/
private void flush() {
for (Node node : nodes) {
node.send(SignalFlush.INSTANCE);
}
}
/**
* Helper method to dispatch the incoming {@link CouchbaseRequest} to one or more nodes.
*
* @param request the request to dispatch.
*/
private void dispatchRequest(final CouchbaseRequest request) {
ClusterConfig config = configuration;
//prevent non-bootstrap requests to go through if bucket not part of config
if (!(request instanceof BootstrapMessage)) {
BucketConfig bucketConfig = config == null ? null : config.bucketConfig(request.bucket());
if (config == null || (request.bucket() != null && bucketConfig == null)) {
failSafe(environment.scheduler(), true, request.observable(),
new BucketClosedException(request.bucket() + " has been closed"));
return;
}
//short-circuit some kind of requests for which we know there won't be any handler to respond.
try {
checkFeaturesForRequest(request, bucketConfig);
} catch (ServiceNotAvailableException e) {
failSafe(environment.scheduler(), true, request.observable(), e);
return;
}
}
locator(request).locateAndDispatch(request, nodes, config, environment, responseBuffer);
}
/**
* Checks, for a sub-set of {@link CouchbaseRequest}, if the current environment has
* the necessary feature activated. If not, throws an {@link ServiceNotAvailableException}.
*
* @param request the request to check.
* @throws ServiceNotAvailableException if the request type needs a particular feature which isn't activated.
*/
protected void checkFeaturesForRequest(final CouchbaseRequest request, final BucketConfig config) {
if (request instanceof BinaryRequest && !config.serviceEnabled(ServiceType.BINARY)) {
throw new ServiceNotAvailableException("The KeyValue service is not enabled or no node in the cluster "
+ "supports it.");
} else if (request instanceof ViewRequest && !config.serviceEnabled(ServiceType.VIEW)) {
throw new ServiceNotAvailableException("The View service is not enabled or no node in the cluster "
+ "supports it.");
} else if (request instanceof QueryRequest && !config.serviceEnabled(ServiceType.QUERY)) {
throw new ServiceNotAvailableException("The Query service is not enabled or no node in the "
+ "cluster supports it.");
} else if (request instanceof SearchRequest && !config.serviceEnabled(ServiceType.SEARCH)) {
throw new ServiceNotAvailableException("The Search service is not enabled or no node in the "
+ "cluster supports it.");
} else if (request instanceof DCPRequest && !(environment.dcpEnabled()
|| config.serviceEnabled(ServiceType.DCP))) {
throw new ServiceNotAvailableException("The DCP service is not enabled or no node in the cluster "
+ "supports it.");
}
}
/**
* Add a {@link Node} identified by its hostname.
*
* @param hostname the hostname of the node.
* @return the states of the node (most probably {@link LifecycleState#CONNECTED}).
*/
public Observable addNode(final InetAddress hostname) {
Node node = nodeBy(hostname);
if (node != null) {
LOGGER.debug("Node {} already registered, skipping.", hostname);
return Observable.just(node.state());
}
return addNode(new CouchbaseNode(hostname, environment, responseBuffer));
}
/**
* Adds a {@link Node} to the cluster.
*
* The code first initiates a asynchronous connect and then eventually adds it to the node list once it has been
* connected successfully.
*/
Observable addNode(final Node node) {
LOGGER.debug("Got instructed to add Node {}", node.hostname());
if (nodes.contains(node)) {
LOGGER.debug("Node {} already registered, skipping.", node.hostname());
return Observable.just(node.state());
}
LOGGER.debug("Connecting Node " + node.hostname());
return node.connect().map(new Func1() {
@Override
public LifecycleState call(LifecycleState lifecycleState) {
LOGGER.debug("Connect finished, registering for use.");
nodes.addIfAbsent(node);
return lifecycleState;
}
});
}
/**
* Remove a {@link Node} identified by its hostname.
*
* @param hostname the hostname of the node.
* @return the states of the node (most probably {@link LifecycleState#DISCONNECTED}).
*/
public Observable removeNode(final InetAddress hostname) {
return removeNode(nodeBy(hostname));
}
/**
* Removes a {@link Node} from the cluster.
*
* The node first gets removed from the list and then is disconnected afterwards, so that outstanding
* operations can be handled gracefully.
*/
Observable removeNode(final Node node) {
LOGGER.debug("Got instructed to remove Node {}", node.hostname());
nodes.remove(node);
return node.disconnect();
}
/**
* Add the service to the node.
*
* @param request the request which contains infos about the service and node to add.
* @return an observable which contains the newly created service.
*/
public Observable addService(final AddServiceRequest request) {
LOGGER.debug("Got instructed to add Service {}, to Node {}", request.type(), request.hostname());
return nodeBy(request.hostname()).addService(request);
}
/**
* Remove a service from a node.
*
* @param request the request which contains infos about the service and node to remove.
* @return an observable which contains the removed service.
*/
public Observable removeService(final RemoveServiceRequest request) {
LOGGER.debug("Got instructed to remove Service {}, from Node {}", request.type(), request.hostname());
return nodeBy(request.hostname()).removeService(request);
}
/**
* Returns the node by its hostname.
*
* @param hostname the hostname of the node.
* @return the node or null if no hostname for that ip address.
*/
public Node nodeBy(final InetAddress hostname) {
if (hostname == null) {
return null;
}
for (Node node : nodes) {
if (node.hostname().equals(hostname)) {
return node;
}
}
return null;
}
/**
* Helper method to detect the correct locator for the given request type.
*
* @return the locator for the given request type.
*/
protected Locator locator(final CouchbaseRequest request) {
if (request instanceof BinaryRequest) {
return binaryLocator;
} else if (request instanceof ViewRequest) {
return viewLocator;
} else if (request instanceof QueryRequest) {
return queryLocator;
} else if (request instanceof ConfigRequest) {
return configLocator;
} else if (request instanceof DCPRequest) {
return dcpLocator;
} else if (request instanceof SearchRequest) {
return searchLocator;
} else {
throw new IllegalArgumentException("Unknown Request Type: " + request);
}
}
/**
* Helper method which grabs the current configuration and checks if the node setup is out of sync.
*
* This method is always called when a new configuration arrives and it will try to sync the actual node
* and service setup with the one proposed by the configuration.
*/
public Observable reconfigure(final ClusterConfig config) {
LOGGER.debug("Starting reconfiguration.");
if (config.bucketConfigs().values().isEmpty()) {
LOGGER.debug("No open bucket found in config, disconnecting all nodes.");
//JVMCBC-231: a race condition can happen where the nodes set is seen as
// not empty, while the subsequent Observable.from is not, failing in calling last()
List snapshotNodes;
synchronized (nodes) {
snapshotNodes = new ArrayList(nodes);
}
if (snapshotNodes.isEmpty()) {
return Observable.just(config);
}
return Observable.from(snapshotNodes).doOnNext(new Action1() {
@Override
public void call(Node node) {
removeNode(node);
node.disconnect().subscribe(new Subscriber() {
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {
LOGGER.warn("Got error during node disconnect.", e);
}
@Override
public void onNext(LifecycleState lifecycleState) {}
});
}
}).last().map(new Func1() {
@Override
public ClusterConfig call(Node node) {
return config;
}
});
}
return Observable
.just(config)
.flatMap(new Func1>() {
@Override
public Observable call(final ClusterConfig clusterConfig) {
return Observable.from(clusterConfig.bucketConfigs().values());
}
}).flatMap(new Func1>() {
@Override
public Observable call(BucketConfig bucketConfig) {
return reconfigureBucket(bucketConfig);
}
})
.last()
.doOnNext(new Action1() {
@Override
public void call(Boolean aBoolean) {
Set configNodes = config.allNodeAddresses();
for (Node node : nodes) {
if (!configNodes.contains(node.hostname())) {
LOGGER.debug("Removing and disconnecting node {}.", node.hostname());
removeNode(node);
node.disconnect().subscribe(new Subscriber() {
@Override
public void onCompleted() {}
@Override
public void onError(Throwable e) {
LOGGER.warn("Got error during node disconnect.", e);
}
@Override
public void onNext(LifecycleState lifecycleState) {}
});
}
}
}
})
.map(new Func1() {
@Override
public ClusterConfig call(Boolean aBoolean) {
return config;
}
});
}
/**
* For every bucket that is open, apply the reconfiguration.
*
* @param config the config for this bucket.
*/
private Observable reconfigureBucket(final BucketConfig config) {
LOGGER.debug("Starting reconfiguration for bucket {}", config.name());
List> observables = new ArrayList>();
for (final NodeInfo nodeInfo : config.nodes()) {
Observable obs = addNode(nodeInfo.hostname())
.flatMap(new Func1>>() {
@Override
public Observable