/*
* 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.DefaultClusterConfig;
import com.couchbase.client.core.env.CoreEnvironment;
import com.couchbase.client.core.env.DefaultCoreEnvironment;
import com.couchbase.client.core.message.CouchbaseRequest;
import com.couchbase.client.core.message.CouchbaseResponse;
import com.couchbase.client.core.message.dcp.DCPRequest;
import com.couchbase.client.core.message.internal.SignalFlush;
import com.couchbase.client.core.message.kv.GetRequest;
import com.couchbase.client.core.message.query.QueryRequest;
import com.couchbase.client.core.node.Node;
import com.couchbase.client.core.node.locate.Locator;
import com.couchbase.client.core.retry.FailFastRetryStrategy;
import com.couchbase.client.core.retry.RetryHelper;
import com.couchbase.client.core.service.ServiceType;
import com.couchbase.client.core.state.LifecycleState;
import com.lmax.disruptor.RingBuffer;
import org.junit.Test;
import org.mockito.Mockito;
import rx.Observable;
import rx.subjects.AsyncSubject;
import rx.subjects.PublishSubject;
import rx.subjects.Subject;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.CopyOnWriteArrayList;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* Verifies the functionality of {@link RequestHandler}.
*/
public class RequestHandlerTest {
private static final CoreEnvironment environment = DefaultCoreEnvironment.create();
private static final Observable<ClusterConfig> configObservable = Observable.empty();
@Test
public void shouldAddNodes() {
CopyOnWriteArrayList<Node> nodes = new CopyOnWriteArrayList<Node>();
RequestHandler handler = new RequestHandler(nodes, environment, configObservable, null);
assertEquals(0, nodes.size());
Node nodeMock = mock(Node.class);
when(nodeMock.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
handler.addNode(nodeMock).toBlocking().single();
assertEquals(1, nodes.size());
}
@Test
public void shouldIgnoreAlreadyAddedNode() throws Exception {
CopyOnWriteArrayList<Node> nodes = new CopyOnWriteArrayList<Node>();
RequestHandler handler = new RequestHandler(nodes, environment, configObservable, null);
assertEquals(0, nodes.size());
Node nodeMock = mock(Node.class);
when(nodeMock.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
handler.addNode(nodeMock).toBlocking().single();
assertEquals(1, nodes.size());
handler.addNode(nodeMock).toBlocking().single();
assertEquals(1, nodes.size());
}
@Test
public void shouldRemoveNodes() {
CopyOnWriteArrayList<Node> nodes = new CopyOnWriteArrayList<Node>();
RequestHandler handler = new RequestHandler(nodes, environment, configObservable, null);
Node node1 = mock(Node.class);
when(node1.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
when(node1.disconnect()).thenReturn(Observable.just(LifecycleState.DISCONNECTED));
Node node2 = mock(Node.class);
when(node2.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
when(node2.disconnect()).thenReturn(Observable.just(LifecycleState.DISCONNECTED));
Node node3 = mock(Node.class);
when(node3.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
when(node3.disconnect()).thenReturn(Observable.just(LifecycleState.DISCONNECTED));
handler.addNode(node1).toBlocking().single();
handler.addNode(node2).toBlocking().single();
handler.addNode(node3).toBlocking().single();
assertEquals(3, nodes.size());
handler.removeNode(node2).toBlocking().single();
assertEquals(2, nodes.size());
assertTrue(nodes.contains(node1));
assertTrue(nodes.contains(node3));
assertFalse(nodes.contains(node2));
handler.removeNode(node1).toBlocking().single();
assertEquals(1, nodes.size());
assertTrue(nodes.contains(node3));
assertFalse(nodes.contains(node2));
assertFalse(nodes.contains(node1));
handler.removeNode(node3).toBlocking().single();
assertEquals(0, nodes.size());
}
@Test
public void shouldRemoveNodeEvenIfNotDisconnected() throws Exception {
CopyOnWriteArrayList<Node> nodes = new CopyOnWriteArrayList<Node>();
RequestHandler handler = new RequestHandler(nodes, environment, configObservable, null);
Node node1 = mock(Node.class);
when(node1.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
when(node1.disconnect()).thenReturn(Observable.just(LifecycleState.DISCONNECTING));
handler.addNode(node1).toBlocking().single();
assertEquals(1, nodes.size());
handler.removeNode(node1).toBlocking().single();
assertEquals(0, nodes.size());
}
@Test
public void shouldRouteEventToNode() throws Exception {
ClusterConfig mockClusterConfig = mock(ClusterConfig.class);
when(mockClusterConfig.hasBucket(anyString())).thenReturn(Boolean.TRUE);
Observable<ClusterConfig> mockConfigObservable = Observable.just(mockClusterConfig);
RequestHandler handler = new DummyLocatorClusterNodeHandler(environment, mockConfigObservable);
Node mockNode = mock(Node.class);
when(mockNode.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
when(mockNode.state()).thenReturn(LifecycleState.CONNECTED);
handler.addNode(mockNode).toBlocking().single();
RequestEvent mockEvent = mock(RequestEvent.class);
CouchbaseRequest mockRequest = mock(CouchbaseRequest.class);
when(mockEvent.getRequest()).thenReturn(mockRequest);
handler.onEvent(mockEvent, 0, true);
verify(mockNode).send(mockRequest);
verify(mockNode).send(SignalFlush.INSTANCE);
verify(mockEvent).setRequest(null);
}
private void assertFeatureForRequest(RequestHandler handler, CouchbaseRequest request, boolean expectedOk) {
BucketConfig mockConfig = mock(BucketConfig.class);
when(mockConfig.serviceEnabled(ServiceType.BINARY)).thenReturn(true);
try {
handler.checkFeaturesForRequest(request, mockConfig);
if (!expectedOk) {
fail();
}
} catch (ServiceNotAvailableException e) {
if (expectedOk) {
fail();
}
assertTrue(e.getMessage().endsWith("service is not enabled or no node in the cluster supports it."));
}
}
@Test
public void shouldPreventFeatureDependentRequestsWhenFeatureDisabled() {
String dcpWasEnabled = System.getProperty("com.couchbase.dcpEnabled");
try {
System.getProperties().remove("com.couchbase.dcpEnabled");
DCPRequest mockDcpRequest = mock(DCPRequest.class);
CouchbaseRequest mockKeyValueRequest = mock(GetRequest.class);
CoreEnvironment env = DefaultCoreEnvironment.builder()
.dcpEnabled(false)
.build();
RequestHandler handler = new DummyLocatorClusterNodeHandler(env);
assertFeatureForRequest(handler, mockDcpRequest, false);
assertFeatureForRequest(handler, mockKeyValueRequest, true);
} finally {
if (dcpWasEnabled != null) {
System.setProperty("com.couchbase.dcpEnabled", dcpWasEnabled);
}
}
}
@Test
public void shouldAllowDcpWhenDcpFeatureEnabled() {
String dcpWasEnabled = System.getProperty("com.couchbase.dcpEnabled");
try {
System.getProperties().remove("com.couchbase.dcpEnabled");
DCPRequest mockDcpRequest = mock(DCPRequest.class);
CouchbaseRequest mockKeyValueRequest = mock(GetRequest.class);
CoreEnvironment env = DefaultCoreEnvironment
.builder()
.dcpEnabled(true)
.build();
RequestHandler handler = new DummyLocatorClusterNodeHandler(env);
assertFeatureForRequest(handler, mockDcpRequest, true);
assertFeatureForRequest(handler, mockKeyValueRequest, true);
} finally {
if (dcpWasEnabled != null) {
System.setProperty("com.couchbase.dcpEnabled", dcpWasEnabled);
}
}
}
@Test(expected = RequestCancelledException.class)
public void shouldCancelOnRetryPolicyFailFast() throws Exception {
CoreEnvironment env = mock(CoreEnvironment.class);
when(env.retryStrategy()).thenReturn(FailFastRetryStrategy.INSTANCE);
ClusterConfig mockClusterConfig = mock(ClusterConfig.class);
when(mockClusterConfig.hasBucket(anyString())).thenReturn(Boolean.TRUE);
Observable<ClusterConfig> mockConfigObservable = Observable.just(mockClusterConfig);
RequestHandler handler = new DummyLocatorClusterNodeHandler(env, mockConfigObservable);
Node mockNode = mock(Node.class);
when(mockNode.connect()).thenReturn(Observable.just(LifecycleState.DISCONNECTED));
when(mockNode.state()).thenReturn(LifecycleState.DISCONNECTED);
handler.addNode(mockNode).toBlocking().single();
RequestEvent mockEvent = mock(RequestEvent.class);
CouchbaseRequest mockRequest = mock(CouchbaseRequest.class);
AsyncSubject<CouchbaseResponse> response = AsyncSubject.create();
when(mockEvent.getRequest()).thenReturn(mockRequest);
when(mockRequest.observable()).thenReturn(response);
handler.onEvent(mockEvent, 0, true);
verify(mockNode, times(1)).send(SignalFlush.INSTANCE);
verify(mockNode, never()).send(mockRequest);
verify(mockEvent).setRequest(null);
response.toBlocking().single();
}
@Test
public void shouldNotFailReconfigureOnRemoveAllRaceCondition() throws InterruptedException {
final ClusterConfig config = mock(DefaultClusterConfig.class);
when(config.bucketConfigs()).thenReturn(Collections.<String, BucketConfig>emptyMap());
final Subject<ClusterConfig, ClusterConfig> configObservable = PublishSubject.<ClusterConfig>create();
//this simulates the race condition in JVMCBC-231, otherwise calls all methods of HashSet
CopyOnWriteArrayList<Node> nodes = Mockito.spy(new CopyOnWriteArrayList<Node>());
when(nodes.isEmpty()).thenReturn(false);
final RequestHandler handler = new RequestHandler(nodes, environment, configObservable, null);
//mock and add the nodes
final Node node1 = mock(Node.class);
when(node1.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
when(node1.disconnect()).thenReturn(Observable.just(LifecycleState.DISCONNECTED));
final Node node2 = mock(Node.class);
when(node2.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
when(node2.disconnect()).thenReturn(Observable.just(LifecycleState.DISCONNECTED));
final Node node3 = mock(Node.class);
when(node3.connect()).thenReturn(Observable.just(LifecycleState.CONNECTED));
when(node3.disconnect()).thenReturn(Observable.just(LifecycleState.DISCONNECTED));
handler.addNode(node1).toBlocking().single();
handler.addNode(node2).toBlocking().single();
handler.addNode(node3).toBlocking().single();
//first reconfiguration with empty node list triggers the removal of all in the nodes set...
try {
handler.reconfigure(config).toBlocking().single();
} catch (NoSuchElementException e) {
fail("failed to remove all nodes on first pass - " + e);
}
//... yet second empty node list configuration will still go into the branch where the nodes set is
//seen as empty (race condition previously encountered)
try {
handler.reconfigure(config).toBlocking().single();
} catch (NoSuchElementException e) {
fail("race condition on removing all during reconfigure - " + e);
}
//OK - the effect of the race condition should have been avoided there by dong a snapshot
}
/**
* Helper class which implements a dummy locator for testing purposes.
*/
class DummyLocatorClusterNodeHandler extends RequestHandler {
private Locator LOCATOR = new DummyLocator();
DummyLocatorClusterNodeHandler(CoreEnvironment environment) {
super(environment, configObservable, null);
}
DummyLocatorClusterNodeHandler(CoreEnvironment environment,
Observable<ClusterConfig> specificConfigObservable) {
super(environment, specificConfigObservable, null);
}
@Override
protected Locator locator(CouchbaseRequest request) {
return LOCATOR;
}
class DummyLocator implements Locator {
@Override
public void locateAndDispatch(CouchbaseRequest request, List<Node> nodes, ClusterConfig config,
CoreEnvironment env, RingBuffer<ResponseEvent> responseBuffer) {
for (Node node : nodes) {
if (node.state() == LifecycleState.CONNECTED) {
node.send(request);
return;
}
}
RetryHelper.retryOrCancel(env, request, responseBuffer);
}
}
}
}