/*
* Copyright (c) 2017 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.endpoint.search;
import com.couchbase.client.core.RequestCancelledException;
import com.couchbase.client.core.ResponseEvent;
import com.couchbase.client.core.endpoint.AbstractEndpoint;
import com.couchbase.client.core.env.CoreEnvironment;
import com.couchbase.client.core.message.CouchbaseMessage;
import com.couchbase.client.core.message.CouchbaseRequest;
import com.couchbase.client.core.message.CouchbaseResponse;
import com.couchbase.client.core.message.ResponseStatus;
import com.couchbase.client.core.message.search.SearchQueryRequest;
import com.couchbase.client.core.message.search.SearchRequest;
import com.couchbase.client.core.retry.FailFastRetryStrategy;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lmax.disruptor.EventFactory;
import com.lmax.disruptor.EventHandler;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.DefaultLastHttpContent;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.timeout.IdleStateEvent;
import org.junit.Before;
import org.junit.Test;
import rx.observers.TestSubscriber;
import rx.schedulers.Schedulers;
import rx.subjects.AsyncSubject;
import rx.subjects.Subject;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Verifies the correct functionality of the {@link SearchHandler}.
*
* @author Michael Nitschinger
* @since 1.4
*/
public class SearchHandlerTest {
private ObjectMapper mapper = new ObjectMapper();
private Queue<SearchRequest> queue;
private EmbeddedChannel channel;
private Disruptor<ResponseEvent> responseBuffer;
private RingBuffer<ResponseEvent> responseRingBuffer;
private List<CouchbaseMessage> firedEvents;
private CountDownLatch latch;
private SearchHandler handler;
private AbstractEndpoint endpoint;
@Before
@SuppressWarnings("unchecked")
public void setup() {
responseBuffer = new Disruptor<ResponseEvent>(new EventFactory<ResponseEvent>() {
@Override
public ResponseEvent newInstance() {
return new ResponseEvent();
}
}, 1024, Executors.newCachedThreadPool());
firedEvents = Collections.synchronizedList(new ArrayList<CouchbaseMessage>());
latch = new CountDownLatch(1);
responseBuffer.handleEventsWith(new EventHandler<ResponseEvent>() {
@Override
public void onEvent(ResponseEvent event, long sequence, boolean endOfBatch) throws Exception {
firedEvents.add(event.getMessage());
latch.countDown();
}
});
responseRingBuffer = responseBuffer.start();
CoreEnvironment environment = mock(CoreEnvironment.class);
when(environment.scheduler()).thenReturn(Schedulers.computation());
when(environment.maxRequestLifetime()).thenReturn(10000L); // 10 seconds
when(environment.autoreleaseAfter()).thenReturn(2000L);
when(environment.retryStrategy()).thenReturn(FailFastRetryStrategy.INSTANCE);
endpoint = mock(AbstractEndpoint.class);
when(endpoint.environment()).thenReturn(environment);
when(environment.userAgent()).thenReturn("Couchbase Client Mock");
queue = new ArrayDeque<SearchRequest>();
handler = new SearchHandler(endpoint, responseRingBuffer, queue, false, false);
channel = new EmbeddedChannel(handler);
}
@Test
public void shouldHavePipeliningDisabled() {
Subject<CouchbaseResponse,CouchbaseResponse> obs1 = AsyncSubject.create();
SearchQueryRequest requestMock1 = mock(SearchQueryRequest.class);
when(requestMock1.path()).thenReturn("");
when(requestMock1.payload()).thenReturn("");
when(requestMock1.bucket()).thenReturn("foo");
when(requestMock1.username()).thenReturn("foo");
when(requestMock1.password()).thenReturn("");
when(requestMock1.observable()).thenReturn(obs1);
Subject<CouchbaseResponse,CouchbaseResponse> obs2 = AsyncSubject.create();
SearchQueryRequest requestMock2 = mock(SearchQueryRequest.class);
when(requestMock2.path()).thenReturn("");
when(requestMock2.payload()).thenReturn("");
when(requestMock2.bucket()).thenReturn("foo");
when(requestMock2.username()).thenReturn("foo");
when(requestMock2.password()).thenReturn("");
when(requestMock2.observable()).thenReturn(obs2);
TestSubscriber<CouchbaseResponse> t1 = TestSubscriber.create();
TestSubscriber<CouchbaseResponse> t2 = TestSubscriber.create();
obs1.subscribe(t1);
obs2.subscribe(t2);
channel.writeOutbound(requestMock1, requestMock2);
t1.assertNotCompleted();
t2.assertError(RequestCancelledException.class);
}
@Test
public void shouldFireKeepAlive() throws Exception {
//similar test to query
final AtomicInteger keepAliveEventCounter = new AtomicInteger();
final AtomicReference<ChannelHandlerContext> ctxRef = new AtomicReference();
SearchHandler searchKeepAliveHandler = new SearchHandler(endpoint, responseRingBuffer, queue, false, false) {
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
super.channelRegistered(ctx);
ctxRef.compareAndSet(null, ctx);
}
@Override
protected void onKeepAliveFired(ChannelHandlerContext ctx, CouchbaseRequest keepAliveRequest) {
assertEquals(1, keepAliveEventCounter.incrementAndGet());
}
@Override
protected void onKeepAliveResponse(ChannelHandlerContext ctx, CouchbaseResponse keepAliveResponse) {
assertEquals(2, keepAliveEventCounter.incrementAndGet());
}
};
EmbeddedChannel channel = new EmbeddedChannel(searchKeepAliveHandler);
//test idle event triggers a query keepAlive request and hook is called
searchKeepAliveHandler.userEventTriggered(ctxRef.get(), IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT);
assertEquals(1, keepAliveEventCounter.get());
assertTrue(queue.peek() instanceof SearchHandler.KeepAliveRequest);
SearchHandler.KeepAliveRequest keepAliveRequest = (SearchHandler.KeepAliveRequest) queue.peek();
//test responding to the request with http response is interpreted into a KeepAliveResponse and hook is called
HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND);
LastHttpContent responseEnd = new DefaultLastHttpContent();
channel.writeInbound(response, responseEnd);
SearchHandler.KeepAliveResponse keepAliveResponse = keepAliveRequest.observable()
.cast(SearchHandler.KeepAliveResponse.class)
.timeout(1, TimeUnit.SECONDS).toBlocking().single();
channel.pipeline().remove(searchKeepAliveHandler);
assertEquals(2, keepAliveEventCounter.get());
assertEquals(ResponseStatus.NOT_EXISTS, keepAliveResponse.status());
assertEquals(0, responseEnd.refCnt());
}
}