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

io.micronaut.http.client.netty.PoolResizer Maven / Gradle / Ivy

There is a newer version: 4.7.6
Show newest version
/*
 * Copyright 2017-2022 original authors
 *
 * 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
 *
 * https://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 io.micronaut.http.client.netty;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.http.client.HttpClientConfiguration;
import io.micronaut.http.client.exceptions.HttpClientException;
import org.slf4j.Logger;
import reactor.core.publisher.Sinks;

import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;

/**
 * This class handles the sizing of a connection pool to conform to the configuration in
 * {@link io.micronaut.http.client.HttpClientConfiguration.ConnectionPoolConfiguration}.
 * 

* This class consists of various mutator methods (e.g. {@link #addPendingRequest}) that * may be called concurrently and in a reentrant fashion (e.g. inside {@link #openNewConnection}). * These mutator methods update their respective fields and then mark this class as * {@link #dirty()}. The state management logic ensures that {@link #doSomeWork()} is called in a * serialized fashion (no concurrency or reentrancy) at least once after each {@link #dirty()} * call. */ @Internal abstract class PoolResizer { private final Logger log; private final HttpClientConfiguration.ConnectionPoolConfiguration connectionPoolConfiguration; private final AtomicReference state = new AtomicReference<>(WorkState.IDLE); private final AtomicInteger pendingConnectionCount = new AtomicInteger(0); private final Deque> pendingRequests = new ConcurrentLinkedDeque<>(); private final List http1Connections = new CopyOnWriteArrayList<>(); private final List http2Connections = new CopyOnWriteArrayList<>(); PoolResizer(Logger log, HttpClientConfiguration.ConnectionPoolConfiguration connectionPoolConfiguration) { this.log = log; this.connectionPoolConfiguration = connectionPoolConfiguration; } private void dirty() { WorkState before = state.getAndUpdate(ws -> { if (ws == WorkState.IDLE) { return WorkState.ACTIVE_WITHOUT_PENDING_WORK; } else { return WorkState.ACTIVE_WITH_PENDING_WORK; } }); if (before != WorkState.IDLE) { // already in one of the active states, another thread will take care of our changes return; } // we were in idle state, this thread will handle the changes. while (true) { try { doSomeWork(); } catch (Throwable t) { // this is probably an irrecoverable failure, we need to bail immediately, but // avoid locking up the state. Another thread might be able to continue work. state.set(WorkState.IDLE); throw t; } WorkState endState = state.updateAndGet(ws -> { if (ws == WorkState.ACTIVE_WITH_PENDING_WORK) { return WorkState.ACTIVE_WITHOUT_PENDING_WORK; } else { return WorkState.IDLE; } }); if (endState == WorkState.IDLE) { // nothing else to do \o/ break; } } } private void doSomeWork() { BlockHint blockedPendingRequests = null; while (true) { PoolSink toDispatch = pendingRequests.pollFirst(); if (toDispatch == null) { break; } boolean dispatched = false; for (ResizerConnection c : http2Connections) { if (dispatchSafe(c, toDispatch)) { dispatched = true; break; } } if (!dispatched) { for (ResizerConnection c : http1Connections) { if (dispatchSafe(c, toDispatch)) { dispatched = true; break; } } } if (!dispatched) { pendingRequests.addFirst(toDispatch); blockedPendingRequests = BlockHint.combine(blockedPendingRequests, toDispatch.getBlockHint()); break; } } // snapshot our fields int pendingRequestCount = this.pendingRequests.size(); int pendingConnectionCount = this.pendingConnectionCount.get(); int http1ConnectionCount = this.http1Connections.size(); int http2ConnectionCount = this.http2Connections.size(); if (pendingRequestCount == 0) { // if there are no pending requests, there is nothing to do. return; } int connectionsToOpen = pendingRequestCount - pendingConnectionCount; // make sure we won't exceed our config setting for pending connections connectionsToOpen = Math.min(connectionsToOpen, connectionPoolConfiguration.getMaxPendingConnections() - pendingConnectionCount); // limit the connection count to the protocol-specific settings, but only if that protocol was seen for this pool. if (http1ConnectionCount > 0) { connectionsToOpen = Math.min(connectionsToOpen, connectionPoolConfiguration.getMaxConcurrentHttp1Connections() - http1ConnectionCount); } if (http2ConnectionCount > 0) { connectionsToOpen = Math.min(connectionsToOpen, connectionPoolConfiguration.getMaxConcurrentHttp2Connections() - http2ConnectionCount); } if (connectionsToOpen > 0) { this.pendingConnectionCount.addAndGet(connectionsToOpen); for (int i = 0; i < connectionsToOpen; i++) { try { openNewConnection(blockedPendingRequests); } catch (Exception e) { try { onNewConnectionFailure(e); } catch (Exception f) { log.error("Internal error", f); } } } dirty(); } } private boolean dispatchSafe(ResizerConnection connection, PoolSink toDispatch) { try { return connection.dispatch(toDispatch); } catch (Exception e) { try { if (toDispatch.tryEmitError(e) != Sinks.EmitResult.OK) { // this is probably fine, log it anyway log.debug("Failure during connection dispatch operation, but dispatch request was already complete.", e); } } catch (Exception f) { log.error("Internal error", f); } return true; } } abstract void openNewConnection(@Nullable BlockHint blockedPendingRequests) throws Exception; static boolean incrementWithLimit(AtomicInteger variable, int limit) { while (true) { int old = variable.get(); if (old >= limit) { return false; } if (variable.compareAndSet(old, old + 1)) { return true; } } } // can be overridden, so `throws Exception` ensures we handle any errors void onNewConnectionFailure(@Nullable Throwable error) throws Exception { // todo: implement a circuit breaker here? right now, we just fail one connection in the // subclass implementation, but maybe we should do more. pendingConnectionCount.decrementAndGet(); dirty(); } final void onNewConnectionEstablished1(ResizerConnection connection) { http1Connections.add(connection); pendingConnectionCount.decrementAndGet(); dirty(); } final void onNewConnectionEstablished2(ResizerConnection connection) { http2Connections.add(connection); pendingConnectionCount.decrementAndGet(); dirty(); } final void onConnectionInactive1(ResizerConnection connection) { http1Connections.remove(connection); dirty(); } final void onConnectionInactive2(ResizerConnection connection) { http2Connections.remove(connection); dirty(); } final void addPendingRequest(PoolSink sink) { int maxPendingAcquires = connectionPoolConfiguration.getMaxPendingAcquires(); if (maxPendingAcquires != Integer.MAX_VALUE && pendingRequests.size() >= maxPendingAcquires) { sink.tryEmitError(new HttpClientException("Cannot acquire connection, exceeded max pending acquires configuration")); return; } pendingRequests.addLast(sink); dirty(); } @Nullable final Sinks.One pollPendingRequest() { Sinks.One req = pendingRequests.pollFirst(); if (req != null) { dirty(); } return req; } final void markConnectionAvailable() { dirty(); } final void forEachConnection(Consumer c) { for (ResizerConnection http1Connection : http1Connections) { c.accept(http1Connection); } for (ResizerConnection http2Connection : http2Connections) { c.accept(http2Connection); } } private enum WorkState { /** * There are no pending changes, and nobody is currently executing {@link #doSomeWork()}. */ IDLE, /** * Someone is currently executing {@link #doSomeWork()}, but there were further changes * after {@link #doSomeWork()} was called, so it needs to be called again. */ ACTIVE_WITH_PENDING_WORK, /** * Someone is currently executing {@link #doSomeWork()}, and there were no other changes * since then. */ ACTIVE_WITHOUT_PENDING_WORK, } abstract static class ResizerConnection { /** * Attempt to dispatch a stream on this connection. * * @param sink The pending request that wants to acquire this connection * @return {@code true} if the acquisition may succeed (if it fails later, the pending * request must be readded), or {@code false} if it fails immediately */ abstract boolean dispatch(PoolSink sink) throws Exception; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy