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 72 private static final Logger LOG = LoggerFactory.getLogger(org.slf4j.LoggerFactory.class); 73 74 /** 75 * The backoff increment used when adjusting connection pool sizes. 76 * The pool size will be increased or decreased by this value during the backoff process. 77 * The increment must be positive. 78 */ 79 private final int increment; 80 81 private final ConcurrentHashMap<HttpRoute, AtomicInteger> routeAttempts; 82 83 /** 84 * Constructs a new LinearBackoffManager with the specified connection pool control. 85 * The backoff increment is set to {@code 1} by default. 86 * 87 * @param connPoolControl the connection pool control to be used by this LinearBackoffManager 88 */ 89 public LinearBackoffManager(final ConnPoolControl<HttpRoute> connPoolControl) { 90 this(connPoolControl, 1); 91 } 92 93 /** 94 * Constructs a new LinearBackoffManager with the specified connection pool control and backoff increment. 95 * 96 * @param connPoolControl the connection pool control to be used by this LinearBackoffManager 97 * @param increment the backoff increment to be used when adjusting connection pool sizes 98 * @throws IllegalArgumentException if connPoolControl is {@code null} or increment is not positive 99 */ 100 public LinearBackoffManager(final ConnPoolControl<HttpRoute> connPoolControl, final int increment) { 101 super(connPoolControl); 102 this.increment = Args.positive(increment, "Increment"); 103 routeAttempts = new ConcurrentHashMap<>(); 104 } 105 106 107 @Override 108 public void backOff(final HttpRoute route) { 109 final Instant now = Instant.now(); 110 111 if (shouldSkip(route, now)) { 112 if (LOG.isDebugEnabled()) { 113 LOG.debug("BackOff not applied for route: {}, cool-down period not elapsed", route); 114 } 115 return; 116 } 117 118 final AtomicInteger attempt = routeAttempts.compute(route, (r, oldValue) -> { 119 if (oldValue == null) { 120 return new AtomicInteger(1); 121 } 122 oldValue.incrementAndGet(); 123 return oldValue; 124 }); 125 126 getLastRouteBackoffs().put(route, now); 127 128 final int currentMax = getConnPerRoute().getMaxPerRoute(route); 129 getConnPerRoute().setMaxPerRoute(route, getBackedOffPoolSize(currentMax)); 130 131 attempt.incrementAndGet(); 132 133 if (LOG.isDebugEnabled()) { 134 LOG.debug("Backoff applied for route: {}, new max connections: {}", route, getConnPerRoute().getMaxPerRoute(route)); 135 } 136 } 137 138 /** 139 * Adjusts the maximum number of connections for the specified route, decreasing it by the increment value. 140 * The method ensures that adjustments only happen after the cool-down period has passed since the last adjustment. 141 * 142 * @param route the HttpRoute for which the maximum number of connections will be decreased 143 */ 144 @Override 145 public void probe(final HttpRoute route) { 146 final Instant now = Instant.now(); 147 148 if (shouldSkip(route, now)) { 149 if (LOG.isDebugEnabled()) { 150 LOG.debug("Probe not applied for route: {}, cool-down period not elapsed", route); 151 } 152 return; 153 } 154 155 routeAttempts.compute(route, (r, oldValue) -> { 156 if (oldValue == null || oldValue.get() <= 1) { 157 return null; 158 } 159 oldValue.decrementAndGet(); 160 return oldValue; 161 }); 162 163 getLastRouteProbes().put(route, now); 164 165 final int currentMax = getConnPerRoute().getMaxPerRoute(route); 166 final int newMax = Math.max(currentMax - increment, getCap().get()); // Ensure the new max does not go below the cap 167 168 getConnPerRoute().setMaxPerRoute(route, newMax); 169 170 if (LOG.isDebugEnabled()) { 171 LOG.debug("Probe applied for route: {}, new max connections: {}", route, getConnPerRoute().getMaxPerRoute(route)); 172 } 173 } 174 175 /** 176 * Determines whether an adjustment action (backoff or probe) should be skipped for the given HttpRoute based on the cool-down period. 177 * If the time elapsed since the last successful probe or backoff for the given route is less than the cool-down 178 * period, the method returns true. Otherwise, it returns false. 179 * <p> 180 * This method is used by both backOff() and probe() methods to enforce the cool-down period before making adjustments 181 * to the connection pool size. 182 * 183 * @param route the {@link HttpRoute} to check 184 * @param now the current {@link Instant} used to calculate the time since the last probe or backoff 185 * @return true if the cool-down period has not elapsed since the last probe or backoff, false otherwise 186 */ 187 private boolean shouldSkip(final HttpRoute route, final Instant now) { 188 final Instant lastProbe = getLastRouteProbes().getOrDefault(route, Instant.EPOCH); 189 final Instant lastBackoff = getLastRouteBackoffs().getOrDefault(route, Instant.EPOCH); 190 191 return Duration.between(lastProbe, now).compareTo(getCoolDown().get().toDuration()) < 0 || 192 Duration.between(lastBackoff, now).compareTo(getCoolDown().get().toDuration()) < 0; 193 } 194 195 196 /** 197 * Returns the new pool size after applying the linear backoff algorithm. 198 * The new pool size is calculated by adding the increment value to the current pool size. 199 * 200 * @param curr the current pool size 201 * @return the new pool size after applying the linear backoff 202 */ 203 @Override 204 protected int getBackedOffPoolSize(final int curr) { 205 return curr + increment; 206 } 207 208 209 /** 210 * This method is not used in LinearBackoffManager's implementation. 211 * It is provided to fulfill the interface requirement and for potential future extensions or modifications 212 * of LinearBackoffManager that may use the backoff factor. 213 * 214 * @param d the backoff factor, not used in the current implementation 215 */ 216 @Override 217 public void setBackoffFactor(final double d) { 218 // Intentionally empty, as the backoff factor is not used in LinearBackoffManager 219 } 220 221 }