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

com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdClientChannelPool Maven / Gradle / Ivy

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.cosmos.implementation.directconnectivity.rntbd;

import com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdEndpoint.Config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocatorMetric;
import io.netty.channel.Channel;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.pool.ChannelPool;
import io.netty.channel.pool.SimpleChannelPool;
import io.netty.util.concurrent.EventExecutor;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.concurrent.GlobalEventExecutor;
import io.netty.util.concurrent.Promise;
import io.netty.util.internal.ThrowableUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.time.Duration;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static com.azure.cosmos.implementation.directconnectivity.rntbd.RntbdReporter.reportIssueUnless;
import static com.google.common.base.Preconditions.checkState;

/**
 * {@link ChannelPool} implementation that enforces a maximum number of concurrent direct TCP Cosmos connections
 */
@JsonSerialize(using = RntbdClientChannelPool.JsonSerializer.class)
public final class RntbdClientChannelPool extends SimpleChannelPool {

    private static final TimeoutException ACQUISITION_TIMEOUT = ThrowableUtil.unknownStackTrace(
        new TimeoutException("Acquisition took longer than the configured maximum time"),
        RntbdClientChannelPool.class, "");

    private static final ClosedChannelException CHANNEL_CLOSED_ON_ACQUIRE = ThrowableUtil.unknownStackTrace(
        new ClosedChannelException(), RntbdClientChannelPool.class, "acquire0(...)");

    private static final IllegalStateException POOL_CLOSED_ON_ACQUIRE = ThrowableUtil.unknownStackTrace(
        new IllegalStateException("RntbdClientChannelPool was closed"),
        RntbdClientChannelPool.class, "acquire0");

    private static final IllegalStateException POOL_CLOSED_ON_RELEASE = ThrowableUtil.unknownStackTrace(
        new IllegalStateException("RntbdClientChannelPool was closed"),
        RntbdClientChannelPool.class, "release");

    private static final IllegalStateException TOO_MANY_PENDING_ACQUISITIONS = ThrowableUtil.unknownStackTrace(
        new IllegalStateException("Too many outstanding acquire operations"),
        RntbdClientChannelPool.class, "acquire0");

    private static final Logger logger = LoggerFactory.getLogger(RntbdClientChannelPool.class);

    private final long acquisitionTimeoutNanos;
    private final PooledByteBufAllocatorMetric allocatorMetric;
    private final EventExecutor executor;
    private final ScheduledFuture idleStateDetectionScheduledFuture;
    private final int maxChannels;
    private final int maxPendingAcquisitions;
    private final int maxRequestsPerChannel;

    // No need to worry about synchronization as everything that modified the queue or counts is done by this.executor

    private final Queue pendingAcquisitionQueue = new ArrayDeque();
    private final Runnable acquisitionTimeoutTask;

    // Because these values can be requested on any thread...

    private final AtomicInteger acquiredChannelCount = new AtomicInteger();
    private final AtomicInteger availableChannelCount = new AtomicInteger();
    private final AtomicBoolean closed = new AtomicBoolean();

    /**
     * Initializes a newly created {@link RntbdClientChannelPool} object
     *  @param bootstrap the {@link Bootstrap} that is used for connections
     * @param config    the {@link Config} that is used for the channel pool instance created
     */
    RntbdClientChannelPool(final RntbdServiceEndpoint endpoint, final Bootstrap bootstrap, final Config config) {
        this(endpoint, bootstrap, config, new RntbdClientChannelHealthChecker(config));
    }

    private RntbdClientChannelPool(
        final RntbdServiceEndpoint endpoint, final Bootstrap bootstrap, final Config config,
        final RntbdClientChannelHealthChecker healthChecker
    ) {

        super(bootstrap, new RntbdClientChannelHandler(config, healthChecker), healthChecker, true, true);

        this.allocatorMetric = config.allocator().metric();
        this.executor = bootstrap.config().group().next();
        this.maxChannels = config.maxChannelsPerEndpoint();
        this.maxPendingAcquisitions = Integer.MAX_VALUE;
        this.maxRequestsPerChannel = config.maxRequestsPerChannel();

        // TODO: DANOBLE: Add RntbdEndpoint.Config settings for acquisition timeout and acquisition timeout action
        //  Alternatively: drop acquisition timeout and acquisition timeout action
        //  Decision should be based on performance, reliability, and usability considerations

        final AcquisitionTimeoutAction acquisitionTimeoutAction = null;
        final long acquisitionTimeoutNanos = -1L;

        if (acquisitionTimeoutAction == null) {

            this.acquisitionTimeoutNanos = -1L;
            this.acquisitionTimeoutTask = null;

        } else {

            this.acquisitionTimeoutNanos = acquisitionTimeoutNanos;

            switch (acquisitionTimeoutAction) {
                case FAIL:
                    this.acquisitionTimeoutTask = new AcquireTimeoutTask(this) {
                        @Override
                        public void onTimeout(AcquireTask task) {
                            task.promise.setFailure(ACQUISITION_TIMEOUT);
                        }
                    };
                    break;
                case NEW:
                    this.acquisitionTimeoutTask = new AcquireTimeoutTask(this) {
                        @Override
                        public void onTimeout(AcquireTask task) {
                            // Increment the acquire count and get a new Channel by delegating to super.acquire
                            task.acquired();
                            RntbdClientChannelPool.super.acquire(task.promise);
                        }
                    };
                    break;
                default:
                    throw new Error();
            }
        }

        final long idleEndpointTimeout = config.idleEndpointTimeoutInNanos();

        this.idleStateDetectionScheduledFuture = this.executor.scheduleAtFixedRate(
            () -> {

                final long currentTime = System.nanoTime();
                final long lastRequestTime = endpoint.lastRequestTime();
                final long elapsedTime = currentTime - lastRequestTime;

                if (elapsedTime > idleEndpointTimeout) {

                    if (logger.isDebugEnabled()) {
                        logger.debug(
                            "{} closing due to inactivity (time elapsed since last request: {} > idleEndpointTimeout: {})",
                            endpoint, Duration.ofNanos(elapsedTime), Duration.ofNanos(idleEndpointTimeout));
                    }

                    endpoint.close();
                }

            }, idleEndpointTimeout, idleEndpointTimeout, TimeUnit.NANOSECONDS);
    }

    // region Accessors

    public int channelsAcquired() {
        return this.acquiredChannelCount.get();
    }

    public int channelsAvailable() {
        return this.availableChannelCount.get();
    }

    public int maxChannels() {
        return this.maxChannels;
    }

    public int maxRequestsPerChannel() {
        return this.maxRequestsPerChannel;
    }

    public SocketAddress remoteAddress() {
        return this.bootstrap().config().remoteAddress();
    }

    public int requestQueueLength() {
        return this.pendingAcquisitionQueue.size();
    }

    public long usedDirectMemory() {
        return this.allocatorMetric.usedDirectMemory();
    }

    public long usedHeapMemory() {
        return this.allocatorMetric.usedHeapMemory();
    }

    // endregion

    // region Methods

    public boolean isClosed() {
        return this.closed.get();
    }

    @Override
    public Future acquire(final Promise promise) {

        this.throwIfClosed();

        try {
            if (this.executor.inEventLoop()) {
                this.acquire0(promise);
            } else {
                this.executor.execute(() -> this.acquire0(promise));
            }
        } catch (Throwable cause) {
            promise.setFailure(cause);
        }

        return promise;
    }

    @Override
    public void close() {
        if (this.closed.compareAndSet(false, true)) {
            if (this.executor.inEventLoop()) {
                this.close0();
            } else {
                this.executor.submit(this::close0).awaitUninterruptibly();
            }
        }
    }

    @Override
    public Future release(final Channel channel, final Promise promise) {

        // We do not call this.throwIfClosed because a channel may be released back to the pool during close

        super.release(channel, this.executor.newPromise().addListener((FutureListener)future -> {

            checkState(this.executor.inEventLoop());

            if (this.isClosed()) {
                // Since the pool is closed, we have no choice but to close the channel
                promise.setFailure(POOL_CLOSED_ON_RELEASE);
                channel.close();
                return;
            }

            if (future.isSuccess()) {

                this.decrementAndRunTaskQueue();
                promise.setSuccess(null);

            } else {

                final Throwable cause = future.cause();

                if (!(cause instanceof IllegalArgumentException)) {
                    this.decrementAndRunTaskQueue();
                }

                promise.setFailure(cause);
            }
        }));

        return promise;
    }

    @Override
    public String toString() {
        return RntbdObjectMapper.toString(this);
    }

    /**
     * Offer a {@link Channel} back to the internal storage
     * 

* Maintainers: Implementations of this method must be thread-safe. * * @param channel the {@link Channel} to return to internal storage * @return {@code true}, if the {@link Channel} could be added to internal storage; otherwise {@code false} */ @Override protected boolean offerChannel(final Channel channel) { if (super.offerChannel(channel)) { this.availableChannelCount.incrementAndGet(); return true; } return false; } /** * Poll a {@link Channel} out of internal storage to reuse it *

* Maintainers: Implementations of this method must be thread-safe and this type ensures thread safety by calling * this method serially on a single-threaded EventExecutor. As a result this method need not (and should not) be * synchronized. * * @return a value of {@code null}, if no {@link Channel} is ready to be reused * @see #acquire(Promise) */ @Override protected Channel pollChannel() { final Channel first = super.pollChannel(); if (first == null) { return null; } if (this.isClosed()) { return first; // because this.close -> this.close0 -> super.close -> this.pollChannel } if (this.isServiceableOrInactiveChannel(first)) { return this.decrementAvailableChannelCountAndAccept(first); } super.offerChannel(first); // because we need a non-null sentinel to stop the search for a channel for (Channel next = super.pollChannel(); next != first; super.offerChannel(next), next = super.pollChannel()) { if (this.isServiceableOrInactiveChannel(next)) { return this.decrementAvailableChannelCountAndAccept(next); } } super.offerChannel(first); // because we choose not to check any channel more than once in a single call return null; } // endregion // region Privates private void acquire0(final Promise promise) { checkState(this.executor.inEventLoop()); if (this.isClosed()) { promise.setFailure(POOL_CLOSED_ON_ACQUIRE); return; } if (this.acquiredChannelCount.get() < this.maxChannels) { // We need to create a new promise to ensure the AcquireListener runs in the correct event loop checkState(this.acquiredChannelCount.get() >= 0); final AcquireListener l = new AcquireListener(this, promise); l.acquired(); final Promise p = this.executor.newPromise(); p.addListener(l); super.acquire(p); // acquire an existing channel or create and acquire a new channel } else { if (this.pendingAcquisitionQueue.size() >= this.maxPendingAcquisitions) { promise.setFailure(TOO_MANY_PENDING_ACQUISITIONS); } else { // Add a task to the pending acquisition queue and we'll satisfy the request later AcquireTask task = new AcquireTask(this, promise); if (this.pendingAcquisitionQueue.offer(task)) { if (acquisitionTimeoutTask != null) { task.timeoutFuture = executor.schedule(acquisitionTimeoutTask, acquisitionTimeoutNanos, TimeUnit.NANOSECONDS); } } else { promise.setFailure(TOO_MANY_PENDING_ACQUISITIONS); } } checkState(this.pendingAcquisitionQueue.size() > 0); } } private void close0() { checkState(this.executor.inEventLoop()); this.idleStateDetectionScheduledFuture.cancel(false); this.acquiredChannelCount.set(0); this.availableChannelCount.set(0); for (; ; ) { final AcquireTask task = this.pendingAcquisitionQueue.poll(); if (task == null) { break; } final ScheduledFuture timeoutFuture = task.timeoutFuture; if (timeoutFuture != null) { timeoutFuture.cancel(false); } task.promise.setFailure(new ClosedChannelException()); } // Ensure we dispatch this on another Thread as close0 will be called from the EventExecutor and we need // to ensure we will not block in an EventExecutor GlobalEventExecutor.INSTANCE.execute(RntbdClientChannelPool.super::close); } private void decrementAndRunTaskQueue() { checkState(acquiredChannelCount.decrementAndGet() >= 0); // Run the pending acquisition tasks before notifying the original promise so that if the user tries to // acquire again from the ChannelFutureListener and the pendingChannelAcquisitionCount is greater than // maxPendingAcquisitions we may be able to run some pending tasks first and so allow to add more runTaskQueue(); } private Channel decrementAvailableChannelCountAndAccept(final Channel first) { this.availableChannelCount.decrementAndGet(); return first; } private boolean isServiceableOrInactiveChannel(final Channel channel) { if (!channel.isActive()) { return true; } final RntbdRequestManager requestManager = channel.pipeline().get(RntbdRequestManager.class); if (requestManager == null) { reportIssueUnless(logger, !channel.isActive(), channel, "active with no request manager"); return true; // inactive } return requestManager.isServiceable(1 /* this.maxRequestsPerChannel */); } private void runTaskQueue() { while (this.acquiredChannelCount.get() < this.maxChannels) { final AcquireTask task = this.pendingAcquisitionQueue.poll(); if (task == null) { break; } final ScheduledFuture timeoutFuture = task.timeoutFuture; if (timeoutFuture != null) { timeoutFuture.cancel(false); } task.acquired(); super.acquire(task.promise); } checkState(this.acquiredChannelCount.get() >= 0); // we should never have negative values } private void throwIfClosed() { checkState(!this.isClosed(), "%s is closed", this); } // endregion // region Types private enum AcquisitionTimeoutAction { /** * Create a new connection when the timeout is detected. */ NEW, /** * Fail the {@link Future} of the acquire call with a {@link TimeoutException}. */ FAIL } private static class AcquireListener implements FutureListener { private final Promise originalPromise; private final RntbdClientChannelPool pool; private boolean acquired; AcquireListener(RntbdClientChannelPool pool, Promise originalPromise) { this.originalPromise = originalPromise; this.pool = pool; } public void acquired() { if (this.acquired) { return; } this.pool.acquiredChannelCount.incrementAndGet(); this.acquired = true; } @Override public void operationComplete(Future future) { checkState(this.pool.executor.inEventLoop()); if (this.pool.isClosed()) { if (future.isSuccess()) { // Since the pool is closed, we have no choice but to close the channel future.getNow().close(); } this.originalPromise.setFailure(POOL_CLOSED_ON_ACQUIRE); return; } if (future.isSuccess()) { // Ensure that the channel is active and ready to receive requests // A Direct TCP channel is ready to receive requests when it: // * is active and // * has an RntbdContext // We send a health check request on a channel without an RntbdContext to force: // 1. SSL negotiation // 2. RntbdContextRequest -> RntbdContext // 3. RntbdHealthCheckRequest -> receive acknowledgement final Channel channel = future.getNow(); channel.eventLoop().execute(() -> { if (!channel.isActive()) { this.fail(CHANNEL_CLOSED_ON_ACQUIRE); return; } final ChannelPipeline pipeline = channel.pipeline(); checkState(pipeline != null); final RntbdRequestManager requestManager = pipeline.get(RntbdRequestManager.class); checkState(requestManager != null); if (requestManager.hasRequestedRntbdContext()) { this.originalPromise.setSuccess(channel); } else { channel.writeAndFlush(RntbdHealthCheckRequest.MESSAGE).addListener(completed -> { if (completed.isSuccess()) { reportIssueUnless(logger, this.acquired && requestManager.hasRntbdContext(), channel,"acquired: {}, rntbdContext: {}", this.acquired, requestManager.rntbdContext()); this.originalPromise.setSuccess(channel); } else { logger.warn("Channel({}) health check request failed due to:", channel, completed.cause()); this.fail(completed.cause()); } }); } }); } else { logger.warn("channel acquisition failed due to:", future.cause()); this.fail(future.cause()); } } private void fail(Throwable cause) { if (this.acquired) { this.pool.decrementAndRunTaskQueue(); } else { this.pool.runTaskQueue(); } this.originalPromise.setFailure(cause); } } private static final class AcquireTask extends AcquireListener { // AcquireTask extends AcquireListener to reduce object creations and so GC pressure final long expireNanoTime; final Promise promise; ScheduledFuture timeoutFuture; AcquireTask(RntbdClientChannelPool pool, Promise promise) { // We need to create a new promise to ensure the AcquireListener runs in the correct event loop super(pool, promise); this.promise = pool.executor.newPromise().addListener(this); this.expireNanoTime = System.nanoTime() + pool.acquisitionTimeoutNanos; } } private static abstract class AcquireTimeoutTask implements Runnable { final RntbdClientChannelPool pool; public AcquireTimeoutTask(RntbdClientChannelPool pool) { this.pool = pool; } public abstract void onTimeout(AcquireTask task); @Override public final void run() { checkState(this.pool.executor.inEventLoop()); final long nanoTime = System.nanoTime(); for (; ; ) { AcquireTask task = this.pool.pendingAcquisitionQueue.peek(); // Compare nanoTime as described in the System.nanoTime documentation // See: // * https://docs.oracle.com/javase/7/docs/api/java/lang/System.html#nanoTime() // * https://github.com/netty/netty/issues/3705 if (task == null || nanoTime - task.expireNanoTime < 0) { break; } this.pool.pendingAcquisitionQueue.remove(); this.onTimeout(task); } } } static final class JsonSerializer extends StdSerializer { JsonSerializer() { super(RntbdClientChannelPool.class); } @Override public void serialize(final RntbdClientChannelPool value, final JsonGenerator generator, final SerializerProvider provider) throws IOException { RntbdClientChannelHealthChecker healthChecker = (RntbdClientChannelHealthChecker)value.healthChecker(); generator.writeStartObject(); generator.writeBooleanField("isClosed", value.isClosed()); generator.writeObjectFieldStart("configuration"); generator.writeNumberField("maxChannels", value.maxChannels()); generator.writeNumberField("maxRequestsPerChannel", value.maxRequestsPerChannel()); generator.writeNumberField("idleConnectionTimeout", healthChecker.idleConnectionTimeoutInNanos()); generator.writeNumberField("readDelayLimit", healthChecker.readDelayLimitInNanos()); generator.writeNumberField("writeDelayLimit", healthChecker.writeDelayLimitInNanos()); generator.writeEndObject(); generator.writeObjectFieldStart("state"); generator.writeNumberField("channelsAcquired", value.channelsAcquired()); generator.writeNumberField("channelsAvailable", value.channelsAvailable()); generator.writeNumberField("requestQueueLength", value.requestQueueLength()); generator.writeNumberField("usedDirectMemory", value.usedDirectMemory()); generator.writeNumberField("usedHeapMemory", value.usedHeapMemory()); generator.writeEndObject(); generator.writeEndObject(); } } // endregion }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy