1 /* 2 * ==================================================================== 3 * Licensed to the Apache Software Foundation (ASF) under one 4 * or more contributor license agreements. See the NOTICE file 5 * distributed with this work for additional information 6 * regarding copyright ownership. The ASF licenses this file 7 * to you under the Apache License, Version 2.0 (the 8 * "License"); you may not use this file except in compliance 9 * with the License. You may obtain a copy of the License at 10 * 11 * http://www.apache.org/licenses/LICENSE-2.0 12 * 13 * Unless required by applicable law or agreed to in writing, 14 * software distributed under the License is distributed on an 15 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 * KIND, either express or implied. See the License for the 17 * specific language governing permissions and limitations 18 * under the License. 19 * ==================================================================== 20 * 21 * This software consists of voluntary contributions made by many 22 * individuals on behalf of the Apache Software Foundation. For more 23 * information on the Apache Software Foundation, please see 24 * <http://www.apache.org/>. 25 * 26 */ 27 package org.apache.hc.client5.http.impl.classic; 28 29 import org.apache.hc.client5.http.HttpRoute; 30 import org.apache.hc.client5.http.classic.BackoffManager; 31 import org.apache.hc.core5.annotation.Contract; 32 import org.apache.hc.core5.annotation.ThreadingBehavior; 33 import org.apache.hc.core5.pool.ConnPoolControl; 34 import org.apache.hc.core5.util.Args; 35 import org.slf4j.Logger; 36 import org.slf4j.LoggerFactory; 37 38 import java.time.Duration; 39 import java.time.Instant; 40 import java.util.concurrent.ConcurrentHashMap; 41 import java.util.concurrent.atomic.AtomicInteger; 42 43 /** 44 * An implementation of {@link BackoffManager} that uses a linear backoff strategy to adjust the maximum number 45 * of connections per route in an {@link org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager}. 46 * This class is designed to be thread-safe and can be used in multi-threaded environments. 47 * <p> 48 * The linear backoff strategy increases or decreases the maximum number of connections per route by a fixed increment 49 * when backing off or probing, respectively. The adjustments are made based on a cool-down period, during which no 50 * further adjustments will be made. 51 * <p> 52 * The {@code LinearBackoffManager} is intended to be used with a {@link org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager}, 53 * which provides the {@link ConnPoolControl} interface. This class interacts with the {@code PoolingHttpClientConnectionManager} 54 * to adjust the maximum number of connections per route. 55 * <p> 56 * Example usage: 57 * <pre> 58 * PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); 59 * LinearBackoffManager backoffManager = new LinearBackoffManager(connectionManager, 1); 60 * // Use the backoffManager with the connectionManager in your application 61 * </pre> 62 * 63 * @see BackoffManager 64 * @see ConnPoolControl 65 * @see org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager 66 * @since 5.3 67 */ 68 @Contract(threading = ThreadingBehavior.SAFE) 69 public class LinearBackoffManager extends AbstractBackoff { 70 71 private static final Logger LOG = LoggerFactory.getLogger(LinearBackoffManager.class); 72 73 /** 74 * The backoff increment used when adjusting connection pool sizes. 75 * The pool size will be increased or decreased by this value during the backoff process. 76 * The increment must be positive. 77 */ 78 private final int increment; 79 80 private final ConcurrentHashMap<HttpRoute, AtomicInteger> routeAttempts; 81 82 /** 83 * Constructs a new LinearBackoffManager with the specified connection pool control. 84 * The backoff increment is set to {@code 1} by default. 85 * 86 * @param connPoolControl the connection pool control to be used by this LinearBackoffManager 87 */ 88 public LinearBackoffManager(final ConnPoolControl<HttpRoute> connPoolControl) { 89 this(connPoolControl, 1); 90 } 91 92 /** 93 * Constructs a new LinearBackoffManager with the specified connection pool control and backoff increment. 94 * 95 * @param connPoolControl the connection pool control to be used by this LinearBackoffManager 96 * @param increment the backoff increment to be used when adjusting connection pool sizes 97 * @throws IllegalArgumentException if connPoolControl is {@code null} or increment is not positive 98 */ 99 public LinearBackoffManager(final ConnPoolControl<HttpRoute> connPoolControl, final int increment) { 100 super(connPoolControl); 101 this.increment = Args.positive(increment, "Increment"); 102 routeAttempts = new ConcurrentHashMap<>(); 103 } 104 105 106 @Override 107 public void backOff(final HttpRoute route) { 108 final Instant now = Instant.now(); 109 110 if (shouldSkip(route, now)) { 111 if (LOG.isDebugEnabled()) { 112 LOG.debug("BackOff not applied for route: {}, cool-down period not elapsed", route); 113 } 114 return; 115 } 116 117 final AtomicInteger attempt = routeAttempts.compute(route, (r, oldValue) -> { 118 if (oldValue == null) { 119 return new AtomicInteger(1); 120 } 121 oldValue.incrementAndGet(); 122 return oldValue; 123 }); 124 125 getLastRouteBackoffs().put(route, now); 126 127 final int currentMax = getConnPerRoute().getMaxPerRoute(route); 128 getConnPerRoute().setMaxPerRoute(route, getBackedOffPoolSize(currentMax)); 129 130 attempt.incrementAndGet(); 131 132 if (LOG.isDebugEnabled()) { 133 LOG.debug("Backoff applied for route: {}, new max connections: {}", route, getConnPerRoute().getMaxPerRoute(route)); 134 } 135 } 136 137 /** 138 * Adjusts the maximum number of connections for the specified route, decreasing it by the increment value. 139 * The method ensures that adjustments only happen after the cool-down period has passed since the last adjustment. 140 * 141 * @param route the HttpRoute for which the maximum number of connections will be decreased 142 */ 143 @Override 144 public void probe(final HttpRoute route) { 145 final Instant now = Instant.now(); 146 147 if (shouldSkip(route, now)) { 148 if (LOG.isDebugEnabled()) { 149 LOG.debug("Probe not applied for route: {}, cool-down period not elapsed", route); 150 } 151 return; 152 } 153 154 routeAttempts.compute(route, (r, oldValue) -> { 155 if (oldValue == null || oldValue.get() <= 1) { 156 return null; 157 } 158 oldValue.decrementAndGet(); 159 return oldValue; 160 }); 161 162 getLastRouteProbes().put(route, now); 163 164 final int currentMax = getConnPerRoute().getMaxPerRoute(route); 165 final int newMax = Math.max(currentMax - increment, getCap().get()); // Ensure the new max does not go below the cap 166 167 getConnPerRoute().setMaxPerRoute(route, newMax); 168 169 if (LOG.isDebugEnabled()) { 170 LOG.debug("Probe applied for route: {}, new max connections: {}", route, getConnPerRoute().getMaxPerRoute(route)); 171 } 172 } 173 174 /** 175 * Determines whether an adjustment action (backoff or probe) should be skipped for the given HttpRoute based on the cool-down period. 176 * If the time elapsed since the last successful probe or backoff for the given route is less than the cool-down 177 * period, the method returns true. Otherwise, it returns false. 178 * <p> 179 * This method is used by both backOff() and probe() methods to enforce the cool-down period before making adjustments 180 * to the connection pool size. 181 * 182 * @param route the {@link HttpRoute} to check 183 * @param now the current {@link Instant} used to calculate the time since the last probe or backoff 184 * @return true if the cool-down period has not elapsed since the last probe or backoff, false otherwise 185 */ 186 private boolean shouldSkip(final HttpRoute route, final Instant now) { 187 final Instant lastProbe = getLastRouteProbes().getOrDefault(route, Instant.EPOCH); 188 final Instant lastBackoff = getLastRouteBackoffs().getOrDefault(route, Instant.EPOCH); 189 190 return Duration.between(lastProbe, now).compareTo(getCoolDown().get().toDuration()) < 0 || 191 Duration.between(lastBackoff, now).compareTo(getCoolDown().get().toDuration()) < 0; 192 } 193 194 195 /** 196 * Returns the new pool size after applying the linear backoff algorithm. 197 * The new pool size is calculated by adding the increment value to the current pool size. 198 * 199 * @param curr the current pool size 200 * @return the new pool size after applying the linear backoff 201 */ 202 @Override 203 protected int getBackedOffPoolSize(final int curr) { 204 return curr + increment; 205 } 206 207 208 /** 209 * This method is not used in LinearBackoffManager's implementation. 210 * It is provided to fulfill the interface requirement and for potential future extensions or modifications 211 * of LinearBackoffManager that may use the backoff factor. 212 * 213 * @param d the backoff factor, not used in the current implementation 214 */ 215 @Override 216 public void setBackoffFactor(final double d) { 217 // Intentionally empty, as the backoff factor is not used in LinearBackoffManager 218 } 219 220 }