/* * 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.service; import com.couchbase.client.core.ResponseEvent; import com.couchbase.client.core.endpoint.Endpoint; import com.couchbase.client.core.env.AbstractServiceConfig; 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.retry.RetryHelper; import com.couchbase.client.core.service.strategies.SelectionStrategy; import com.couchbase.client.core.state.AbstractStateMachine; import com.couchbase.client.core.state.LifecycleState; import com.lmax.disruptor.RingBuffer; import rx.Observable; import rx.Subscriber; import rx.Subscription; import rx.functions.Action1; import rx.functions.Func1; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; /** * A generic implementation of a service pool. * * @author Michael Nitschinger * @since 1.4.2 */ public abstract class PooledService extends AbstractStateMachine<LifecycleState> implements Service { private static final CouchbaseLogger LOGGER = CouchbaseLoggerFactory.getInstance(Service.class); 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 int minEndpoints; private final int maxEndpoints; private final boolean fixedEndpoints; private final EndpointStateZipper endpointStates; private final RingBuffer<ResponseEvent> responseBuffer; private final EndpointFactory endpointFactory; private final List<Endpoint> endpoints; private final LifecycleState initialState; private final SelectionStrategy selectionStrategy; private final Subscription idleSubscription; private final Object epMutex = new Object(); /** * Pending requests to account for requests waiting for a socket to be connected. */ private volatile int pendingRequests; /** * Full disconnect has been initiated. */ private volatile boolean disconnect; PooledService(final String hostname, final String bucket, final String username, final String password, final int port, final CoreEnvironment env, final AbstractServiceConfig serviceConfig, final RingBuffer<ResponseEvent> responseBuffer, final EndpointFactory endpointFactory, final SelectionStrategy selectionStrategy) { super(serviceConfig.minEndpoints() == 0 ? LifecycleState.IDLE : LifecycleState.DISCONNECTED); preCheckEndpointSettings(serviceConfig); this.initialState = serviceConfig.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 = serviceConfig.minEndpoints(); this.maxEndpoints = serviceConfig.maxEndpoints(); this.responseBuffer = responseBuffer; this.endpointFactory = endpointFactory; this.endpoints = new CopyOnWriteArrayList<Endpoint>(); this.fixedEndpoints = minEndpoints == maxEndpoints; this.selectionStrategy = selectionStrategy; this.pendingRequests = 0; this.disconnect = false; endpointStates = new EndpointStateZipper(initialState); endpointStates.states().subscribe(new Action1<LifecycleState>() { @Override public void call(LifecycleState lifecycleState) { transitionState(lifecycleState); } }); if (serviceConfig.idleTime() == 0) { idleSubscription = null; } else { idleSubscription = Observable .interval(serviceConfig.idleTime(), TimeUnit.SECONDS, env.scheduler()) .subscribe(new Subscriber<Long>() { @Override public void onCompleted() { LOGGER.trace("Completed Idle Timer Subscription"); } @Override public void onError(Throwable e) { LOGGER.warn("Error while subscribing to Idle Timer", e); } @Override public void onNext(Long aLong) { List<Endpoint> toDisconnect = new ArrayList<Endpoint>(); synchronized (epMutex) { boolean removed; do { removed = false; for (int i = 0; i < endpoints.size(); i++) { Endpoint e = endpoints.get(i); if (e != null) { long diffs = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - e.lastResponse()); if(e.isFree() && diffs >= serviceConfig.idleTime()) { LOGGER.debug(logIdent(hostname, PooledService.this) + "Endpoint {} idle for longer than {}s, disconnecting.", e, serviceConfig.idleTime()); endpoints.remove(i); endpointStates.deregister(e); removed = true; toDisconnect.add(e); LOGGER.debug(logIdent(hostname, PooledService.this) + "New number of endpoints is {}", endpoints.size()); } } } } while (removed); } for (Endpoint ep : toDisconnect) { ep.disconnect().subscribe(new Subscriber<LifecycleState>() { @Override public void onCompleted() { /* ignored on purpose */ } @Override public void onError(Throwable e) { LOGGER.warn("Got an error while disconnecting endpoint!", e); } @Override public void onNext(LifecycleState state) { /* ignored on purpose */ } }); } ensureMinimum(); } }); } } /** * Helper method to ensure a minimum number of endpoints is enabled. */ private void ensureMinimum() { int belowMin = minEndpoints - endpoints.size(); if (belowMin > 0) { LOGGER.debug(logIdent(hostname, this) + "Service is {} below minimum, filling up.", belowMin); synchronized (epMutex) { for (int i = 0; i < belowMin; i++) { Endpoint endpoint = endpointFactory.create(hostname, bucket, username, password, port, env, responseBuffer); endpoints.add(endpoint); endpointStates.register(endpoint, endpoint); endpoint.connect().subscribe(new Subscriber<LifecycleState>() { @Override public void onCompleted() { /* ignored on purpose */ } @Override public void onError(Throwable e) { LOGGER.warn("Got an error while connecting endpoint!", e); } @Override public void onNext(LifecycleState state) { /* ignored on purpose */ } }); } LOGGER.debug(logIdent(hostname, PooledService.this) + "New number of endpoints is {}", endpoints.size()); } } } private void preCheckEndpointSettings(final AbstractServiceConfig serviceConfig) { int minEndpoints = serviceConfig.minEndpoints(); int maxEndpoints = serviceConfig.maxEndpoints(); boolean pipelining = serviceConfig.isPipelined(); if (minEndpoints < 0 || maxEndpoints < 0) { throw new IllegalArgumentException("The minEndpoints and maxEndpoints must not be negative"); } if (maxEndpoints == 0) { throw new IllegalArgumentException("The maxEndpoints must be greater than 0"); } if (maxEndpoints < minEndpoints) { throw new IllegalArgumentException("The maxEndpoints must not be smaller than mindEndpoints"); } // temporary limitation: if (pipelining && (minEndpoints != maxEndpoints)) { throw new IllegalArgumentException("Pipelining and non-fixed size of endpoints is " + "currently not supported."); } } @Override public Observable<LifecycleState> connect() { if (state() == LifecycleState.CONNECTED || state() == LifecycleState.CONNECTING) { LOGGER.debug(logIdent(hostname, this) + "Already connected or connecting, skipping connect."); return Observable.just(state()); } LOGGER.debug(logIdent(hostname, this) + "Got instructed to connect."); synchronized (epMutex) { int numToConnect = minEndpoints - endpoints.size(); if (numToConnect == 0) { LOGGER.debug("No endpoints needed to connect, skipping."); return Observable.just(state()); } for (int i = 0; i < numToConnect; i++) { Endpoint endpoint = endpointFactory.create(hostname, bucket, username, password, port, env, responseBuffer); endpoints.add(endpoint); endpointStates.register(endpoint, endpoint); } LOGGER.debug(logIdent(hostname, PooledService.this) + "New number of endpoints is {}", endpoints.size()); } return Observable .from(endpoints) .flatMap(new Func1<Endpoint, Observable<LifecycleState>>() { @Override public Observable<LifecycleState> call(final Endpoint endpoint) { LOGGER.debug(logIdent(hostname, PooledService.this) + "Connecting Endpoint during Service connect."); return endpoint.connect(); } }) .lastOrDefault(initialState) .map(new Func1<LifecycleState, LifecycleState>() { @Override public LifecycleState call(final LifecycleState state) { return state(); } }); } @Override public Observable<LifecycleState> disconnect() { disconnect = true; if (state() == LifecycleState.DISCONNECTED || state() == LifecycleState.DISCONNECTING) { LOGGER.debug(logIdent(hostname, this) + "Already disconnected or disconnecting, skipping disconnect."); return Observable.just(state()); } LOGGER.debug(logIdent(hostname, this) + "Got instructed to disconnect."); List<Endpoint> endpoints; synchronized (epMutex) { endpoints = new ArrayList<Endpoint>(this.endpoints); this.endpoints.clear(); LOGGER.debug(logIdent(hostname, PooledService.this) + "New number of endpoints is {}", endpoints.size()); } return Observable .from(endpoints) .flatMap(new Func1<Endpoint, Observable<LifecycleState>>() { @Override public Observable<LifecycleState> call(Endpoint endpoint) { LOGGER.debug(logIdent(hostname, PooledService.this) + "Disconnecting Endpoint during Service disconnect."); return endpoint.disconnect(); } }) .lastOrDefault(initialState) .map(new Func1<LifecycleState, LifecycleState>() { @Override public LifecycleState call(final LifecycleState state) { endpointStates.terminate(); if (idleSubscription != null && !idleSubscription.isUnsubscribed()) { idleSubscription.unsubscribe(); } return state(); } }); } @Override public void send(final CouchbaseRequest request) { if (request instanceof SignalFlush) { sendFlush((SignalFlush) request); return; } Endpoint endpoint = endpoints.size() > 0 ? selectionStrategy.select(request, endpoints) : null; if (endpoint == null) { if (fixedEndpoints || ((endpoints.size() + pendingRequests) >= maxEndpoints)) { RetryHelper.retryOrCancel(env, request, responseBuffer); } else { maybeOpenAndSend(request); } } else { endpoint.send(request); } } /** * Helper method to try and open new endpoints as needed and correctly integrate * them into the state of the service. */ private void maybeOpenAndSend(final CouchbaseRequest request) { pendingRequests++; LOGGER.debug(logIdent(hostname, PooledService.this) + "Need to open a new Endpoint (size {}), pending requests {}", endpoints.size(), pendingRequests); final Endpoint endpoint = endpointFactory.create( hostname, bucket, username, password, port, env, responseBuffer ); final Subscription subscription = whenState(endpoint, LifecycleState.CONNECTED, new Action1<LifecycleState>() { @Override public void call(LifecycleState lifecycleState) { try { if (disconnect) { RetryHelper.retryOrCancel(env, request, responseBuffer); } else { endpoint.send(request); endpoint.send(SignalFlush.INSTANCE); synchronized (epMutex) { endpoints.add(endpoint); endpointStates.register(endpoint, endpoint); LOGGER.debug(logIdent(hostname, PooledService.this) + "New number of endpoints is {}", endpoints.size()); } } } finally { pendingRequests--; } } } ); endpoint.connect().subscribe(new Subscriber<LifecycleState>() { @Override public void onCompleted() { // ignored on purpose } @Override public void onError(Throwable e) { unsubscribeAndRetry(subscription, request); } @Override public void onNext(LifecycleState state) { if (state == LifecycleState.DISCONNECTING || state == LifecycleState.DISCONNECTED) { unsubscribeAndRetry(subscription, request); } } }); } /** * Helper method to unsubscribe from the subscription and send the request into retry. */ private void unsubscribeAndRetry(final Subscription subscription, final CouchbaseRequest request) { if (subscription != null && !subscription.isUnsubscribed()) { subscription.unsubscribe(); } pendingRequests--; RetryHelper.retryOrCancel(env, request, responseBuffer); } /** * Helper method to send the flush signal to all endpoints. * * @param signalFlush the flush signal to propagate. */ private void sendFlush(final SignalFlush signalFlush) { int length = endpoints.size(); for (int i = 0; i < length; i++) { Endpoint endpoint = endpoints.get(i); if (endpoint != null) { endpoint.send(signalFlush); } } } @Override public BucketServiceMapping mapping() { return type().mapping(); } /** * Simple log helper to give logs a common prefix. * * @param hostname the address. * @param service the service. * @return a prefix string for logs. */ static String logIdent(final String hostname, final Service service) { return "[" + hostname + "][" + service.getClass().getSimpleName() + "]: "; } /** * Helper method to register a specific action when a state is reached on an * endpoint for the first time after subscription. * * @param endpoint the endpoint to watch. * @param wanted the wanted state. * @param then the action to execute. */ private static Subscription whenState(final Endpoint endpoint, final LifecycleState wanted, final Action1<LifecycleState> then) { return endpoint .states() .filter(new Func1<LifecycleState, Boolean>() { @Override public Boolean call(LifecycleState state) { return state == wanted; } }) .take(1) .subscribe(then); } /** * Returns the current endpoint list, for testing verification purposes. */ protected List<Endpoint> endpoints() { return endpoints; } }