All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.couchbase.client.core.service.PooledService Maven / Gradle / Ivy

There is a newer version: 2.7.0
Show newest version
/*
 * Copyright (c) 2018 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.cnc.events.service.IdleEndpointRemovedEvent;
import com.couchbase.client.core.cnc.events.service.ServiceConnectInitiatedEvent;
import com.couchbase.client.core.cnc.events.service.ServiceDisconnectInitiatedEvent;
import com.couchbase.client.core.cnc.events.service.ServiceStateChangedEvent;
import com.couchbase.client.core.diagnostics.EndpointDiagnostics;
import com.couchbase.client.core.endpoint.Endpoint;
import com.couchbase.client.core.endpoint.EndpointContext;
import com.couchbase.client.core.endpoint.EndpointState;
import com.couchbase.client.core.msg.Request;
import com.couchbase.client.core.msg.Response;
import com.couchbase.client.core.retry.RetryOrchestrator;
import com.couchbase.client.core.retry.RetryReason;
import com.couchbase.client.core.util.CompositeStateful;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;

/**
 * The {@link PooledService} is a flexible implementation to pool endpoints based on the
 * given configuration.
 *
 * 

This implementation is closely related to the older PooledService part of the 1.x series, * but has been adapted to the slightly new semantics of the endpoints and their behaviors. The pool * now has more authority on the lifetime of the endpoint since it also has more knowledge of * the related ones.

* * @since 2.0.0 */ abstract class PooledService implements Service { /** * The interval when to check if idle sockets are to be cleaned up. */ public static final Duration DEFAULT_IDLE_TIME_CHECK_INTERVAL = Duration.ofMillis(100); /** * Holds the config for this service. */ private final ServiceConfig serviceConfig; /** * Holds all currently tracked endpoints in this pool. */ private final List endpoints; /** * Holds the endpoint states and as a result the internal service state. */ private final CompositeStateful endpointStates; /** * The context for this service. */ private final ServiceContext serviceContext; /** * If the pool cannot grow because min and max are the same. */ private final boolean fixedPool; /** * If disconnect called by a caller, set to true. */ private final AtomicBoolean disconnected; /** * Tracked endpoints which are currently connecting but are reserved for a request waiting for it. */ private final List reservedEndpoints; /** * Creates a new {@link PooledService}. * * @param serviceConfig the underlying service config. * @param serviceContext the service context. */ PooledService(final ServiceConfig serviceConfig, final ServiceContext serviceContext) { this.serviceConfig = serviceConfig; this.endpoints = new CopyOnWriteArrayList<>(); this.reservedEndpoints = new CopyOnWriteArrayList<>(); final ServiceState initialState = serviceConfig.minEndpoints() > 0 ? ServiceState.DISCONNECTED : ServiceState.IDLE; this.endpointStates = CompositeStateful.create(initialState, endpointStates -> { if (endpointStates.isEmpty()) { return initialState; } ServiceState state = ServiceState.DISCONNECTED; int connected = 0; int connecting = 0; int disconnecting = 0; for (EndpointState endpointState : endpointStates) { switch (endpointState) { case CONNECTED: connected++; break; case CONNECTING: connecting++; break; case DISCONNECTING: disconnecting++; break; default: // ignore } } if (endpointStates.size() == connected) { state = ServiceState.CONNECTED; } else if (connected > 0) { state = ServiceState.DEGRADED; } else if (connecting > 0) { state = ServiceState.CONNECTING; } else if (disconnecting > 0) { state = ServiceState.DISCONNECTING; } return state; }, (from, to) -> serviceContext.environment().eventBus().publish(new ServiceStateChangedEvent(serviceContext, from, to)) ); this.disconnected = new AtomicBoolean(false); this.serviceContext = serviceContext; this.fixedPool = serviceConfig.minEndpoints() == serviceConfig.maxEndpoints(); scheduleCleanIdleConnections(); } /** * Returns the created {@link ServiceContext} for implementations to use. */ protected ServiceContext serviceContext() { return serviceContext; } /** * Helper method to schedule cleaning up idle connections per interval. */ private void scheduleCleanIdleConnections() { final Duration idleTime = serviceConfig.idleTime(); if (idleTime != null && !idleTime.isZero()) { serviceContext.environment().timer().schedule(this::cleanIdleConnections, idleTimeCheckInterval()); } } /** * Can be overridden for unit tests. */ protected Duration idleTimeCheckInterval() { return DEFAULT_IDLE_TIME_CHECK_INTERVAL; } /** * Go through the connections and clean up all the idle connections. *

* Note that we explicitly do not make any clean up attempts on the {@link #reservedEndpoints}. They will either come * into our endpoint pool when connected, or fall out of the pool when they are disconnected immediately. We only need * to take them into account when checking how many endpoints we have flying around to clean up at max. */ private synchronized void cleanIdleConnections() { if (disconnected.get()) { return; } final List endpoints = new ArrayList<>(this.endpoints); Collections.shuffle(endpoints); for (Endpoint endpoint : endpoints) { if ((this.endpoints.size() + this.reservedEndpoints.size()) == serviceConfig.minEndpoints()) { break; } long lastResponseReceived = endpoint.lastResponseReceived(); long actualIdleTime; if (lastResponseReceived != 0) { actualIdleTime = System.nanoTime() - endpoint.lastResponseReceived(); } else { // If we did not receive a last response timestamp, it could be the case that a socket is // connected but no request has been sent into it yet. If this is the case, take the timestamp // when the socket got last connected as a reference point to determine if it is idle. long lastConnected = endpoint.lastConnectedAt(); if (lastConnected != 0) { actualIdleTime = System.nanoTime() - lastConnected; } else { // No last connected timestamp, so the endpoint isn't even fully connected yet continue; } } // we also check if an endpoint received a hard disconnect signal and is still lingering around boolean receivedDisconnect = endpoint.receivedDisconnectSignal(); boolean idleTooLong = endpoint.outstandingRequests() == 0 && actualIdleTime >= serviceConfig.idleTime().toNanos(); if (receivedDisconnect || idleTooLong) { this.endpoints.remove(endpoint); endpointStates.deregister(endpoint); if (!receivedDisconnect) { endpoint.disconnect(); } publishIdleEndpointRemovedEvent(endpoint, actualIdleTime); } } scheduleCleanIdleConnections(); } /** * Helper method to publish an event with enriched context when an idle endpoint has been removed. * * @param endpoint the endpoint that got removed. * @param actualIdleTime the actual idle time of that endpoint. */ private void publishIdleEndpointRemovedEvent(final Endpoint endpoint, final long actualIdleTime) { if (endpoint.context() != null) { final EndpointContext enrichedContext = new EndpointContext(endpoint.context()) { @Override public void injectExportableParams(final Map input) { super.injectExportableParams(input); Map serviceInfo = new HashMap<>(); input.put("actualIdleTimeMillis", TimeUnit.NANOSECONDS.toMillis(actualIdleTime)); serviceInfo.put("remainingEndpoints", endpoints.size()); serviceInfo.put("reservedEndpoints", reservedEndpoints.size()); serviceInfo.put("state", state()); input.put("service", serviceInfo); } }; serviceContext.environment().eventBus().publish(new IdleEndpointRemovedEvent(enrichedContext)); } } /** * Subclass implements this method to create new endpoints. * * @return the created endpoint. */ protected abstract Endpoint createEndpoint(); /** * Subclass implements this method to pick their selection strategy of choice. * * @return the selection strategy. */ protected abstract EndpointSelectionStrategy selectionStrategy(); @Override public > void send(final R request) { if (request.completed()) { return; } Endpoint found = endpoints.isEmpty() ? null : selectionStrategy().select(request, endpoints); if (found != null) { found.send(request); return; } if (!fixedPool && (endpoints.size() + reservedEndpoints.size()) < serviceConfig.maxEndpoints()) { connectReservedEndpoint(request); } else { RetryOrchestrator.maybeRetry(serviceContext, request, RetryReason.ENDPOINT_NOT_AVAILABLE); } } /** * Connect the reserved endpoint and dispatch the request into it if possible. *

* Note that there are two synchronized sections in this method, because the subscription callback works on * a different thread. * * @param request the request that needs to bee dispatched. */ private synchronized > void connectReservedEndpoint(final R request) { if (!disconnected.get()) { Endpoint endpoint = createEndpoint(); endpointStates.register(endpoint, endpoint); endpoint .states() // The endpoint always starts DISCONNECT, so wait for first CONNECTING .skipUntil(s -> s == EndpointState.CONNECTING) // We only care about CONNECTED and DISCONNECTED events .filter(s -> s == EndpointState.CONNECTED || s == EndpointState.DISCONNECTED) // Once we see the first CONNECTED or DISCONNECTED, unsubscribe .takeUntil(s -> s == EndpointState.CONNECTED || s == EndpointState.DISCONNECTED) // We MUST move it to another scheduler, or the netty IO thread is blocked of the send below .publishOn(context().environment().scheduler()) .subscribe(s -> { synchronized (PooledService.this) { reservedEndpoints.remove(endpoint); if (disconnected.get()) { endpoint.disconnect(); endpointStates.deregister(endpoint); RetryOrchestrator.maybeRetry(serviceContext, request, RetryReason.ENDPOINT_NOT_AVAILABLE); } else { endpoints.add(endpoint); if (s == EndpointState.CONNECTED) { endpoint.send(request); } else if (s == EndpointState.DISCONNECTED) { RetryOrchestrator.maybeRetry(serviceContext, request, RetryReason.ENDPOINT_NOT_AVAILABLE); } } } }); endpoint.connect(); reservedEndpoints.add(endpoint); } } @Override public synchronized void connect() { if (state() == ServiceState.DISCONNECTED && !disconnected.get()) { serviceContext.environment().eventBus().publish(new ServiceConnectInitiatedEvent( serviceContext, serviceConfig.minEndpoints() )); for (int i = 0; i < serviceConfig.minEndpoints(); i++) { Endpoint endpoint = createEndpoint(); endpointStates.register(endpoint, endpoint); endpoint.connect(); endpoints.add(endpoint); } } } @Override public synchronized void disconnect() { if (disconnected.compareAndSet(false, true)) { serviceContext.environment().eventBus().publish(new ServiceDisconnectInitiatedEvent( serviceContext, endpoints.size() + reservedEndpoints.size() )); for (Endpoint endpoint : endpoints) { endpoint.disconnect(); endpointStates.deregister(endpoint); } for (Endpoint endpoint : reservedEndpoints) { endpoint.disconnect();; endpointStates.deregister(endpoint); } endpoints.clear(); reservedEndpoints.clear(); } } @Override public ServiceContext context() { return serviceContext; } @Override public ServiceState state() { return endpointStates.state(); } @Override public Flux states() { return endpointStates.states(); } @Override public Stream diagnostics() { return Stream .concat(endpoints.stream(), reservedEndpoints.stream()) .map(Endpoint::diagnostics); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy