/*
* 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.service;
import com.couchbase.client.core.ResponseEvent;
import com.couchbase.client.core.endpoint.Endpoint;
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.internal.SignalFlush;
import com.couchbase.client.core.state.AbstractStateMachine;
import com.couchbase.client.core.state.LifecycleState;
import com.lmax.disruptor.RingBuffer;
import rx.Observable;
import rx.functions.Action1;
import rx.functions.Func1;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Parent implementation of a dynamic {@link Service}.
*
* @author Michael Nitschinger
* @since 1.1.0
*/
public abstract class AbstractDynamicService extends AbstractStateMachine<LifecycleState> implements Service {
private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(Service.class);
private final Endpoint[] endpoints;
private final String hostname;
private final String bucket;
private final String username;
private final String password;
private final int port;
private final CoreEnvironment env;
private final RingBuffer<ResponseEvent> responseBuffer;
private final int minEndpoints;
private final EndpointFactory endpointFactory;
private final EndpointStateZipper endpointStates;
private final LifecycleState initialState;
protected AbstractDynamicService(final String hostname, final String bucket, final String username, final String password, final int port,
final CoreEnvironment env, final int minEndpoints,
final RingBuffer<ResponseEvent> responseBuffer, final EndpointFactory endpointFactory) {
super(minEndpoints == 0 ? LifecycleState.IDLE : LifecycleState.DISCONNECTED);
this.initialState = minEndpoints == 0 ? LifecycleState.IDLE : LifecycleState.DISCONNECTED;
this.hostname = hostname;
this.bucket = bucket;
this.username = username;
this.password = password;
this.port = port;
this.env = env;
this.minEndpoints = minEndpoints;
this.responseBuffer = responseBuffer;
this.endpointFactory = endpointFactory;
endpointStates = new EndpointStateZipper(initialState);
endpoints = new Endpoint[minEndpoints];
endpointStates.states().subscribe(new Action1<LifecycleState>() {
@Override
public void call(LifecycleState lifecycleState) {
transitionState(lifecycleState);
}
});
}
protected abstract void dispatch(CouchbaseRequest request);
@Override
public Observable<LifecycleState> connect() {
LOGGER.debug(logIdent(hostname, this) + "Got instructed to connect.");
if (state() == LifecycleState.CONNECTED || state() == LifecycleState.CONNECTING) {
LOGGER.debug(logIdent(hostname, this) + "Already connected or connecting, skipping connect.");
return Observable.just(state());
}
for (int i = 0; i < minEndpoints; i++) {
Endpoint endpoint = createEndpoint();
endpoints[i] = endpoint;
endpointStates.register(endpoint, endpoint);
}
return Observable
.from(endpoints)
.flatMap(new Func1<Endpoint, Observable<LifecycleState>>() {
@Override
public Observable<LifecycleState> call(final Endpoint endpoint) {
LOGGER.debug(logIdent(hostname, AbstractDynamicService.this)
+ "Initializing connect on Endpoint.");
return endpoint.connect();
}
})
.lastOrDefault(initialState)
.map(new Func1<LifecycleState, LifecycleState>() {
@Override
public LifecycleState call(final LifecycleState state) {
return state();
}
});
}
@Override
public void send(final CouchbaseRequest request) {
if (request instanceof SignalFlush) {
int length = endpoints.length;
for (int i = 0; i < length; i++) {
Endpoint endpoint = endpoints[i];
// Make sure to only send a flush event
// to the endpoint if it is actually set
// on the array
if (endpoint != null) {
endpoint.send(request);
}
}
return;
}
dispatch(request);
}
@Override
public Observable<LifecycleState> disconnect() {
LOGGER.debug(logIdent(hostname, this) + "Got instructed to disconnect.");
if (state() == LifecycleState.DISCONNECTED || state() == LifecycleState.DISCONNECTING) {
LOGGER.debug(logIdent(hostname, this) + "Already disconnected or disconnecting, skipping disconnect.");
return Observable.just(state());
}
return Observable
.from(endpoints)
.flatMap(new Func1<Endpoint, Observable<LifecycleState>>() {
@Override
public Observable<LifecycleState> call(Endpoint endpoint) {
LOGGER.debug(logIdent(hostname, AbstractDynamicService.this)
+ "Initializing disconnect on Endpoint.");
return endpoint.disconnect();
}
})
.lastOrDefault(initialState)
.map(new Func1<LifecycleState, LifecycleState>() {
@Override
public LifecycleState call(final LifecycleState state) {
endpointStates.terminate();
return state();
}
});
}
@Override
public BucketServiceMapping mapping() {
return type().mapping();
}
/**
* Helper method to create a new endpoint.
*
* @return the endpoint to create.
*/
protected Endpoint createEndpoint() {
return endpointFactory.create(hostname, bucket, username, password, port, env, responseBuffer);
}
/**
* Simple log helper to give logs a common prefix.
*
* @param hostname the address.
* @param service the service.
* @return a prefix string for logs.
*/
protected static String logIdent(final String hostname, final Service service) {
return "[" + hostname + "][" + service.getClass().getSimpleName() + "]: ";
}
/**
* Returns the current list of endpoints.
*
* @return the list of endpoints.
*/
protected List<Endpoint> endpoints() {
return Arrays.asList(endpoints);
}
/**
* Returns the underlying endpoint state zipper.
*
* @return the underlying state zipper.
*/
protected EndpointStateZipper endpointStates() {
return endpointStates;
}
/**
* Waits until the endpoint goes into the given state, calls the action and then unsubscribes.
*
* @param endpoint the endpoint to monitor.
* @param wanted the wanted state.
* @param then the action to execute when the state is reached.
*/
protected static void whenState(final Endpoint endpoint, final LifecycleState wanted, Action1<LifecycleState> then) {
endpoint
.states()
.filter(new Func1<LifecycleState, Boolean>() {
@Override
public Boolean call(LifecycleState state) {
return state == wanted;
}
})
.take(1)
.subscribe(then);
}
}