diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/AIMDBackoffManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/AIMDBackoffManager.java
index d8f592c60..5573278ae 100644
--- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/AIMDBackoffManager.java
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/AIMDBackoffManager.java
@@ -26,21 +26,11 @@
*/
package org.apache.hc.client5.http.impl.classic;
-import java.time.Instant;
-import java.time.temporal.ChronoUnit;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.atomic.AtomicReference;
import org.apache.hc.client5.http.HttpRoute;
-import org.apache.hc.client5.http.classic.BackoffManager;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.pool.ConnPoolControl;
import org.apache.hc.core5.util.Args;
-import org.apache.hc.core5.util.TimeValue;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
*
The {@code AIMDBackoffManager} applies an additive increase,
@@ -63,53 +53,7 @@ import org.slf4j.LoggerFactory;
* @since 4.2
*/
@Contract(threading = ThreadingBehavior.SAFE)
-public class AIMDBackoffManager implements BackoffManager {
-
- private static final Logger LOG = LoggerFactory.getLogger(AIMDBackoffManager.class);
-
-
- /**
- * Represents the connection pool control object for managing
- * the maximum number of connections allowed per HTTP route.
- */
- private final ConnPoolControl connPerRoute;
-
- /**
- * A map that keeps track of the last time a successful
- * connection was made for each HTTP route. Used for
- * adjusting the pool size when probing.
- */
- private final Map lastRouteProbes;
-
- /**
- * A map that keeps track of the last time a connection
- * failure occurred for each HTTP route. Used for
- * adjusting the pool size when backing off.
- */
- private final Map lastRouteBackoffs;
-
- /**
- * The cool-down time between adjustments in pool sizes for a given host.
- * This time value allows enough time for the adjustments to take effect.
- * Defaults to 5 seconds.
- */
- private final AtomicReference coolDown = new AtomicReference<>(TimeValue.ofSeconds(5L));
-
- /**
- * The factor to use when backing off; the new per-host limit will be
- * roughly the current max times this factor. {@code Math.floor} is
- * applied in the case of non-integer outcomes to ensure we actually
- * decrease the pool size. Pool sizes are never decreased below 1, however.
- * Defaults to 0.5.
- */
- private final AtomicReference backoffFactor = new AtomicReference<>(0.5);
-
- /**
- * The absolute maximum per-host connection pool size to probe up to.
- * Defaults to 2 (the default per-host max as per RFC 2616 section 8.1.4).
- */
- private final AtomicInteger cap = new AtomicInteger(2); // Per RFC 2616 sec 8.1.4
-
+public class AIMDBackoffManager extends AbstractBackoff {
/**
* Constructs an {@code AIMDBackoffManager} with the specified
@@ -122,29 +66,9 @@ public class AIMDBackoffManager implements BackoffManager {
* per-host routing maximums
*/
public AIMDBackoffManager(final ConnPoolControl connPerRoute) {
- this.connPerRoute = Args.notNull(connPerRoute, "Connection pool control");
- this.lastRouteProbes = new ConcurrentHashMap<>();
- this.lastRouteBackoffs = new ConcurrentHashMap<>();
+ super(connPerRoute);
}
- @Override
- public void backOff(final HttpRoute route) {
- final int curr = connPerRoute.getMaxPerRoute(route);
- final Instant now = Instant.now();
-
- lastRouteBackoffs.compute(route, (r, lastUpdate) -> {
- if (lastUpdate == null || now.isAfter(lastUpdate.plus(coolDown.get().toMilliseconds(), ChronoUnit.MILLIS))) {
- if (LOG.isDebugEnabled()) {
- LOG.debug("Backoff applied for route: {}, new max connections: {}", route, connPerRoute.getMaxPerRoute(route));
- }
- connPerRoute.setMaxPerRoute(route, getBackedOffPoolSize(curr));
- return now;
- }
- return lastUpdate;
- });
- }
-
-
/**
* Returns the backed-off pool size based on the current pool size.
* The new pool size is calculated as the floor of (backoffFactor * curr).
@@ -152,46 +76,14 @@ public class AIMDBackoffManager implements BackoffManager {
* @param curr the current pool size
* @return the backed-off pool size, with a minimum value of 1
*/
- private int getBackedOffPoolSize(final int curr) {
+ protected int getBackedOffPoolSize(final int curr) {
if (curr <= 1) {
return 1;
}
- return (int) (Math.floor(backoffFactor.get() * curr));
+ return (int) (Math.floor(getBackoffFactor().get() * curr));
}
- @Override
- public void probe(final HttpRoute route) {
- final int curr = connPerRoute.getMaxPerRoute(route);
- final int max = (curr >= cap.get()) ? cap.get() : curr + 1;
- final Instant now = Instant.now();
-
- lastRouteProbes.compute(route, (r, lastProbe) -> {
- if (lastProbe == null || now.isAfter(lastProbe.plus(coolDown.get().toMilliseconds(), ChronoUnit.MILLIS))) {
- final Instant lastBackoff = lastRouteBackoffs.get(r);
- if (lastBackoff == null || now.isAfter(lastBackoff.plus(coolDown.get().toMilliseconds(), ChronoUnit.MILLIS))) {
- connPerRoute.setMaxPerRoute(route, max);
- if (LOG.isDebugEnabled()){
- LOG.info("Probe applied for route: {}, new max connections: {}", route, connPerRoute.getMaxPerRoute(route));
- }
- return now;
- }
- }
- return lastProbe;
- });
- }
-
- /**
- * Returns the last update time of a specific route from the provided updates map.
- *
- * @param updates the map containing the last update times
- * @param route the HttpRoute whose last update time is required
- * @return the last update time or 0L if the route is not present in the map
- */
- private long getLastUpdate(final Map updates, final HttpRoute route) {
- return updates.getOrDefault(route, 0L);
- }
-
/**
* Sets the factor to use when backing off; the new
* per-host limit will be roughly the current max times
@@ -201,30 +93,10 @@ public class AIMDBackoffManager implements BackoffManager {
* below 1, however. Defaults to 0.5.
* @param d must be between 0.0 and 1.0, exclusive.
*/
+ @Override
public void setBackoffFactor(final double d) {
Args.check(d > 0.0 && d < 1.0, "Backoff factor must be 0.0 < f < 1.0");
- backoffFactor.set(d);
+ getBackoffFactor().set(d);
}
- /**
- * Sets the amount of time to wait between adjustments in
- * pool sizes for a given host, to allow enough time for
- * the adjustments to take effect. Defaults to 5 seconds.
- * @param coolDown must be positive
- */
- public void setCoolDown(final TimeValue coolDown) {
- Args.notNull(coolDown, "Cool down time value cannot be null");
- Args.positive(coolDown.getDuration(), "coolDown");
- this.coolDown.set(coolDown);
- }
-
- /**
- * Sets the absolute maximum per-host connection pool size to
- * probe up to; defaults to 2 (the default per-host max).
- * @param cap must be >= 1
- */
- public void setPerHostConnectionCap(final int cap) {
- Args.positive(cap, "Per host connection cap");
- this.cap.set(cap);
- }
}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/AbstractBackoff.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/AbstractBackoff.java
new file mode 100644
index 000000000..2ce1c49f6
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/AbstractBackoff.java
@@ -0,0 +1,281 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.classic;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.classic.BackoffManager;
+import org.apache.hc.core5.pool.ConnPoolControl;
+import org.apache.hc.core5.util.Args;
+import org.apache.hc.core5.util.TimeValue;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * AbstractBackoff is an abstract class that provides a common implementation for managing
+ * backoff behavior in HttpClient connection pool. Subclasses should implement the specific
+ * backoff algorithms by overriding the abstract methods.
+ *
+ * This class provides common functionality for maintaining the route-wise backoff and probe
+ * timestamps, as well as the cool-down period for each backoff attempt.
+ *
+ * It also contains the basic structure of the backOff and probe methods, which use the route-wise
+ * timestamps to determine if the backoff or probe should be applied, and then call the specific
+ * algorithm implementation for calculating the new pool size.
+ *
+ * @since 5.3
+ */
+public abstract class AbstractBackoff implements BackoffManager {
+
+
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractBackoff.class);
+
+ /**
+ * Connection pool control responsible for managing the maximum number of connections per HTTP route.
+ */
+ private final ConnPoolControl connPerRoute;
+
+ /**
+ * A map that stores the last probe timestamp for each HTTP route.
+ */
+ private final Map lastRouteProbes;
+
+ /**
+ * A map that stores the last backoff timestamp for each HTTP route.
+ */
+ private final Map lastRouteBackoffs;
+
+ /**
+ * The cool-down period after which the backoff or probe process can be performed again.
+ */
+ private final AtomicReference coolDown = new AtomicReference<>(TimeValue.ofSeconds(5L));
+
+ /**
+ * The growth rate used in the exponential backoff algorithm.
+ */
+ private final AtomicReference backoffFactor = new AtomicReference<>(0.5);
+
+ /**
+ * The per-host connection cap, as defined in RFC 2616 sec 8.1.4.
+ */
+ private final AtomicInteger cap = new AtomicInteger(2);
+
+ /**
+ * The number of time intervals used in the exponential backoff algorithm.
+ */
+ private final AtomicInteger timeInterval = new AtomicInteger(0);
+
+ /**
+ * Constructs a new ExponentialBackoffManager with the specified connection pool control.
+ *
+ * @param connPerRoute the connection pool control to be used for managing connections
+ * @throws IllegalArgumentException if connPerRoute is null
+ */
+ public AbstractBackoff(final ConnPoolControl connPerRoute) {
+ this.connPerRoute = Args.notNull(connPerRoute, "Connection pool control");
+ this.lastRouteProbes = new ConcurrentHashMap<>();
+ this.lastRouteBackoffs = new ConcurrentHashMap<>();
+ }
+
+ /**
+ * Reduces the number of maximum allowed connections for the specified route based on the exponential backoff algorithm.
+ *
+ * @param route the HttpRoute for which the backoff needs to be applied
+ */
+ @Override
+ public void backOff(final HttpRoute route) {
+ final int curr = connPerRoute.getMaxPerRoute(route);
+ final Instant now = Instant.now();
+
+ lastRouteBackoffs.compute(route, (r, lastUpdate) -> {
+ if (lastUpdate == null || now.isAfter(lastUpdate.plus(coolDown.get().toMilliseconds(), ChronoUnit.MILLIS))) {
+ final int backedOffPoolSize = getBackedOffPoolSize(curr); // Exponential backoff
+ connPerRoute.setMaxPerRoute(route, backedOffPoolSize);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Backoff applied for route: {}, new max connections: {}", route, connPerRoute.getMaxPerRoute(route));
+ }
+ return now;
+ }
+ return lastUpdate;
+ });
+ }
+
+
+ /**
+ * Calculates the new pool size after applying the exponential backoff algorithm.
+ * The new pool size is calculated using the formula: floor(curr / (1 + growthRate) ^ t),
+ * where curr is the current pool size, growthRate is the exponential growth rate, and t is the time interval.
+ *
+ * @param curr the current pool size
+ * @return the new pool size after applying the backoff
+ */
+ protected abstract int getBackedOffPoolSize(int curr);
+
+
+ /**
+ * Increases the number of maximum allowed connections for the specified route after a successful connection has been established.
+ *
+ * @param route the HttpRoute for which the probe needs to be applied
+ */
+ @Override
+ public void probe(final HttpRoute route) {
+ final int curr = connPerRoute.getMaxPerRoute(route);
+ final int max = (curr >= cap.get()) ? cap.get() : curr + 1;
+ final Instant now = Instant.now();
+
+ lastRouteProbes.compute(route, (r, lastProbe) -> {
+ if (lastProbe == null || now.isAfter(lastProbe.plus(coolDown.get().toMilliseconds(), ChronoUnit.MILLIS))) {
+ final Instant lastBackoff = lastRouteBackoffs.get(r);
+ if (lastBackoff == null || now.isAfter(lastBackoff.plus(coolDown.get().toMilliseconds(), ChronoUnit.MILLIS))) {
+ connPerRoute.setMaxPerRoute(route, max);
+ if (LOG.isDebugEnabled()) {
+ LOG.info("Probe applied for route: {}, new max connections: {}", route, connPerRoute.getMaxPerRoute(route));
+ }
+ timeInterval.set(0); // Reset the time interval
+ return now;
+ }
+ }
+ return lastProbe;
+ });
+ }
+
+ /**
+ * Retrieves the last update timestamp for the specified route from the provided updates map.
+ *
+ * @param updates the map containing update timestamps for HttpRoutes
+ * @param route the HttpRoute for which the last update timestamp is needed
+ * @return the last update timestamp for the specified route or 0L if not present in the map
+ */
+ public long getLastUpdate(final Map updates, final HttpRoute route) {
+ return updates.getOrDefault(route, 0L);
+ }
+
+ /**
+ * Sets the per-host connection cap.
+ *
+ * @param cap the per-host connection cap to be set
+ * @throws IllegalArgumentException if the cap is not positive
+ */
+ public void setPerHostConnectionCap(final int cap) {
+ Args.positive(cap, "Per host connection cap");
+ this.cap.set(cap);
+ }
+
+ /**
+ * Sets the backoff factor for the backoff algorithm.
+ * The backoff factor should be a value between 0.0 and 1.0.
+ * The specific implementation of how the backoff factor is used should be provided by subclasses.
+ *
+ * @param d the backoff factor to be set
+ */
+ abstract void setBackoffFactor(final double d);
+
+
+ /**
+ * Sets the cool-down time value for adjustments in pool sizes for a given host. This time value
+ * allows enough time for the adjustments to take effect before further adjustments are made.
+ * The cool-down time value must be positive and not null.
+ *
+ * @param coolDown the TimeValue representing the cool-down period between adjustments
+ * @throws IllegalArgumentException if the provided cool-down time value is null or non-positive
+ */
+
+ public void setCoolDown(final TimeValue coolDown) {
+ Args.notNull(coolDown, "Cool down time value cannot be null");
+ Args.positive(coolDown.getDuration(), "coolDown");
+ this.coolDown.set(coolDown);
+ }
+
+ /**
+ * Returns the connection pool control for managing the maximum number of connections per route.
+ *
+ * @return the connection pool control instance
+ */
+ protected ConnPoolControl getConnPerRoute() {
+ return connPerRoute;
+ }
+
+ /**
+ * Returns the map containing the last probe times for each HttpRoute.
+ *
+ * @return the map of HttpRoute to Instant representing the last probe times
+ */
+ protected Map getLastRouteProbes() {
+ return lastRouteProbes;
+ }
+
+ /**
+ * Returns the map containing the last backoff times for each HttpRoute.
+ *
+ * @return the map of HttpRoute to Instant representing the last backoff times
+ */
+ protected Map getLastRouteBackoffs() {
+ return lastRouteBackoffs;
+ }
+
+ /**
+ * Returns the cool down period between backoff and probe operations as an AtomicReference of TimeValue.
+ *
+ * @return the AtomicReference containing the cool down period
+ */
+ protected AtomicReference getCoolDown() {
+ return coolDown;
+ }
+
+ /**
+ * Returns the backoff factor as an AtomicReference of Double.
+ *
+ * @return the AtomicReference containing the backoff factor
+ */
+ protected AtomicReference getBackoffFactor() {
+ return backoffFactor;
+ }
+
+ /**
+ * Returns the cap on the maximum number of connections per route as an AtomicInteger.
+ *
+ * @return the AtomicInteger containing the cap value
+ */
+ protected AtomicInteger getCap() {
+ return cap;
+ }
+
+ /**
+ * Returns the time interval between backoff and probe operations as an AtomicInteger.
+ *
+ * @return the AtomicInteger containing the time interval
+ */
+ protected AtomicInteger getTimeInterval() {
+ return timeInterval;
+ }
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ExponentialBackoffManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ExponentialBackoffManager.java
new file mode 100644
index 000000000..d33fdef7c
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/ExponentialBackoffManager.java
@@ -0,0 +1,92 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.classic;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.core5.pool.ConnPoolControl;
+import org.apache.hc.core5.util.Args;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A backoff manager implementation that uses an exponential backoff algorithm to adjust the maximum
+ * number of connections per HTTP route. The algorithm reduces the number of connections in response
+ * to adverse events, such as connection failures, and gradually increases the number of connections
+ * when the route is operating without issues.
+ *
+ * This implementation is specifically designed for managing connections in an HTTP route context
+ * and provides methods for probing and backing off connections based on the performance of the route.
+ *
+ *
The exponential backoff algorithm is primarily implemented in the {@code getBackedOffPoolSize}
+ * method, which calculates the new connection pool size based on the current pool size, growth rate,
+ * and the number of time intervals.
+ *
+ * @since 5.3
+ */
+public class ExponentialBackoffManager extends AbstractBackoff {
+
+ private static final Logger LOG = LoggerFactory.getLogger(ExponentialBackoffManager.class);
+
+
+ /**
+ * Constructs a new ExponentialBackoffManager with the specified connection pool control.
+ *
+ * @param connPerRoute the connection pool control to be used for managing connections
+ * @throws IllegalArgumentException if connPerRoute is null
+ */
+ public ExponentialBackoffManager(final ConnPoolControl connPerRoute) {
+ super(connPerRoute);
+
+ }
+
+ /**
+ * Calculates the new pool size after applying the exponential backoff algorithm.
+ * The new pool size is calculated using the formula: floor(curr / (1 + growthRate) ^ t),
+ * where curr is the current pool size, growthRate is the exponential growth rate, and t is the time interval.
+ *
+ * @param curr the current pool size
+ * @return the new pool size after applying the backoff
+ */
+ protected int getBackedOffPoolSize(final int curr) {
+ if (curr <= 1) {
+ return 1;
+ }
+ final int t = getTimeInterval().incrementAndGet();
+ final int result = Math.max(1, (int) Math.floor(curr / Math.pow(1 + getBackoffFactor().get(), t)));
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("curr={}, t={}, growthRate={}, result={}", curr, t, getBackoffFactor().get(), result);
+ }
+ return result;
+ }
+
+ public void setBackoffFactor(final double rate) {
+ Args.check(rate > 0.0, "Growth rate must be greater than 0.0");
+ this.getBackoffFactor().set(rate);
+ }
+
+}
diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/LinearBackoffManager.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/LinearBackoffManager.java
new file mode 100644
index 000000000..fa7d52711
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/classic/LinearBackoffManager.java
@@ -0,0 +1,221 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.classic;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.client5.http.classic.BackoffManager;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+import org.apache.hc.core5.pool.ConnPoolControl;
+import org.apache.hc.core5.util.Args;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * An implementation of {@link BackoffManager} that uses a linear backoff strategy to adjust the maximum number
+ * of connections per route in an {@link org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager}.
+ * This class is designed to be thread-safe and can be used in multi-threaded environments.
+ *
+ * The linear backoff strategy increases or decreases the maximum number of connections per route by a fixed increment
+ * when backing off or probing, respectively. The adjustments are made based on a cool-down period, during which no
+ * further adjustments will be made.
+ *
+ * The {@code LinearBackoffManager} is intended to be used with a {@link org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager},
+ * which provides the {@link ConnPoolControl} interface. This class interacts with the {@code PoolingHttpClientConnectionManager}
+ * to adjust the maximum number of connections per route.
+ *
+ * Example usage:
+ *
+ * PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+ * LinearBackoffManager backoffManager = new LinearBackoffManager(connectionManager, 1);
+ * // Use the backoffManager with the connectionManager in your application
+ *
+ *
+ * @see BackoffManager
+ * @see ConnPoolControl
+ * @see org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager
+ * @since 5.3
+ */
+@Contract(threading = ThreadingBehavior.SAFE)
+public class LinearBackoffManager extends AbstractBackoff {
+
+
+ private static final Logger LOG = LoggerFactory.getLogger(org.slf4j.LoggerFactory.class);
+
+ /**
+ * The backoff increment used when adjusting connection pool sizes.
+ * The pool size will be increased or decreased by this value during the backoff process.
+ * The increment must be positive.
+ */
+ private final int increment;
+
+ private final ConcurrentHashMap routeAttempts;
+
+ /**
+ * Constructs a new LinearBackoffManager with the specified connection pool control.
+ * The backoff increment is set to {@code 1} by default.
+ *
+ * @param connPoolControl the connection pool control to be used by this LinearBackoffManager
+ */
+ public LinearBackoffManager(final ConnPoolControl connPoolControl) {
+ this(connPoolControl, 1);
+ }
+
+ /**
+ * Constructs a new LinearBackoffManager with the specified connection pool control and backoff increment.
+ *
+ * @param connPoolControl the connection pool control to be used by this LinearBackoffManager
+ * @param increment the backoff increment to be used when adjusting connection pool sizes
+ * @throws IllegalArgumentException if connPoolControl is {@code null} or increment is not positive
+ */
+ public LinearBackoffManager(final ConnPoolControl connPoolControl, final int increment) {
+ super(connPoolControl);
+ this.increment = Args.positive(increment, "Increment");
+ routeAttempts = new ConcurrentHashMap<>();
+ }
+
+
+ @Override
+ public void backOff(final HttpRoute route) {
+ final Instant now = Instant.now();
+
+ if (shouldSkip(route, now)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("BackOff not applied for route: {}, cool-down period not elapsed", route);
+ }
+ return;
+ }
+
+ final AtomicInteger attempt = routeAttempts.compute(route, (r, oldValue) -> {
+ if (oldValue == null) {
+ return new AtomicInteger(1);
+ }
+ oldValue.incrementAndGet();
+ return oldValue;
+ });
+
+ getLastRouteBackoffs().put(route, now);
+
+ final int currentMax = getConnPerRoute().getMaxPerRoute(route);
+ getConnPerRoute().setMaxPerRoute(route, getBackedOffPoolSize(currentMax));
+
+ attempt.incrementAndGet();
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Backoff applied for route: {}, new max connections: {}", route, getConnPerRoute().getMaxPerRoute(route));
+ }
+ }
+
+ /**
+ * Adjusts the maximum number of connections for the specified route, decreasing it by the increment value.
+ * The method ensures that adjustments only happen after the cool-down period has passed since the last adjustment.
+ *
+ * @param route the HttpRoute for which the maximum number of connections will be decreased
+ */
+ @Override
+ public void probe(final HttpRoute route) {
+ final Instant now = Instant.now();
+
+ if (shouldSkip(route, now)) {
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Probe not applied for route: {}, cool-down period not elapsed", route);
+ }
+ return;
+ }
+
+ routeAttempts.compute(route, (r, oldValue) -> {
+ if (oldValue == null || oldValue.get() <= 1) {
+ return null;
+ }
+ oldValue.decrementAndGet();
+ return oldValue;
+ });
+
+ getLastRouteProbes().put(route, now);
+
+ final int currentMax = getConnPerRoute().getMaxPerRoute(route);
+ final int newMax = Math.max(currentMax - increment, getCap().get()); // Ensure the new max does not go below the cap
+
+ getConnPerRoute().setMaxPerRoute(route, newMax);
+
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Probe applied for route: {}, new max connections: {}", route, getConnPerRoute().getMaxPerRoute(route));
+ }
+ }
+
+ /**
+ * Determines whether an adjustment action (backoff or probe) should be skipped for the given HttpRoute based on the cool-down period.
+ * If the time elapsed since the last successful probe or backoff for the given route is less than the cool-down
+ * period, the method returns true. Otherwise, it returns false.
+ *
+ * This method is used by both backOff() and probe() methods to enforce the cool-down period before making adjustments
+ * to the connection pool size.
+ *
+ * @param route the {@link HttpRoute} to check
+ * @param now the current {@link Instant} used to calculate the time since the last probe or backoff
+ * @return true if the cool-down period has not elapsed since the last probe or backoff, false otherwise
+ */
+ private boolean shouldSkip(final HttpRoute route, final Instant now) {
+ final Instant lastProbe = getLastRouteProbes().getOrDefault(route, Instant.EPOCH);
+ final Instant lastBackoff = getLastRouteBackoffs().getOrDefault(route, Instant.EPOCH);
+
+ return Duration.between(lastProbe, now).compareTo(getCoolDown().get().toDuration()) < 0 ||
+ Duration.between(lastBackoff, now).compareTo(getCoolDown().get().toDuration()) < 0;
+ }
+
+
+ /**
+ * Returns the new pool size after applying the linear backoff algorithm.
+ * The new pool size is calculated by adding the increment value to the current pool size.
+ *
+ * @param curr the current pool size
+ * @return the new pool size after applying the linear backoff
+ */
+ @Override
+ protected int getBackedOffPoolSize(final int curr) {
+ return curr + increment;
+ }
+
+
+ /**
+ * This method is not used in LinearBackoffManager's implementation.
+ * It is provided to fulfill the interface requirement and for potential future extensions or modifications
+ * of LinearBackoffManager that may use the backoff factor.
+ *
+ * @param d the backoff factor, not used in the current implementation
+ */
+ @Override
+ public void setBackoffFactor(final double d) {
+ // Intentionally empty, as the backoff factor is not used in LinearBackoffManager
+ }
+
+}
\ No newline at end of file
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestExponentialBackoffManager.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestExponentialBackoffManager.java
new file mode 100644
index 000000000..c45b9f0e7
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestExponentialBackoffManager.java
@@ -0,0 +1,157 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.classic;
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.util.TimeValue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestExponentialBackoffManager {
+
+ private ExponentialBackoffManager impl;
+ private MockConnPoolControl connPerRoute;
+ private HttpRoute route;
+ private static final long DEFAULT_COOL_DOWN_MS = 5000; // Adjust this value to match the default cooldown period in ExponentialBackoffManager
+
+ @BeforeEach
+ public void setUp() {
+ connPerRoute = new MockConnPoolControl();
+ route = new HttpRoute(new HttpHost("localhost", 80));
+ impl = new ExponentialBackoffManager(connPerRoute);
+ impl.setPerHostConnectionCap(10);
+ impl.setCoolDown(TimeValue.ofMilliseconds(DEFAULT_COOL_DOWN_MS));
+ impl.setBackoffFactor(1.75); // Adjust this value to match the default growth rate in ExponentialBackoffManager
+ }
+
+ @Test
+ public void exponentialBackoffApplied() {
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.setBackoffFactor(2); // Sets the growth rate to 2 for this test
+ impl.backOff(route);
+ assertEquals(1, connPerRoute.getMaxPerRoute(route)); // Corrected expected value
+ }
+
+ @Test
+ public void exponentialGrowthRateIsConfigurable() throws InterruptedException {
+ final int customCoolDownMs = 500;
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.setBackoffFactor(0.5);
+ impl.setCoolDown(TimeValue.ofMilliseconds(customCoolDownMs));
+ impl.backOff(route);
+ assertEquals(2, connPerRoute.getMaxPerRoute(route));
+ Thread.sleep(customCoolDownMs + 100); // Sleep for a slightly longer than the custom cooldown period
+ impl.backOff(route);
+ assertEquals(1, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void doesNotIncreaseBeyondPerHostMaxOnProbe() {
+ connPerRoute.setDefaultMaxPerRoute(5);
+ connPerRoute.setMaxPerRoute(route, 5);
+ impl.setPerHostConnectionCap(5);
+ impl.probe(route);
+ assertEquals(5, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void backoffDoesNotAdjustDuringCoolDownPeriod() throws InterruptedException {
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.backOff(route);
+ final long max = connPerRoute.getMaxPerRoute(route);
+ Thread.sleep(1); // Sleep for 1 ms
+ impl.backOff(route);
+ assertEquals(max, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void backoffStillAdjustsAfterCoolDownPeriod() throws InterruptedException {
+ connPerRoute.setMaxPerRoute(route, 8);
+ impl.backOff(route);
+ final long max = connPerRoute.getMaxPerRoute(route);
+ Thread.sleep(DEFAULT_COOL_DOWN_MS + 1); // Sleep for cooldown period + 1 ms
+ impl.backOff(route);
+ assertTrue(max == 1 || max > connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void probeDoesNotAdjustDuringCooldownPeriod() throws InterruptedException {
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.probe(route);
+ final long max = connPerRoute.getMaxPerRoute(route);
+ Thread.sleep(1); // Sleep for 1 ms
+ impl.probe(route);
+ assertEquals(max, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void probeStillAdjustsAfterCoolDownPeriod() throws InterruptedException {
+ connPerRoute.setMaxPerRoute(route, 8);
+ impl.probe(route);
+ final long max = connPerRoute.getMaxPerRoute(route);
+ Thread.sleep(DEFAULT_COOL_DOWN_MS + 1); // Sleep for cooldown period + 1 ms
+ impl.probe(route);
+ assertTrue(max < connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void willBackoffImmediatelyEvenAfterAProbe() {
+ connPerRoute.setMaxPerRoute(route, 8);
+ final long now = System.currentTimeMillis();
+ impl.probe(route);
+ final long max = connPerRoute.getMaxPerRoute(route);
+ impl.backOff(route);
+ assertTrue(connPerRoute.getMaxPerRoute(route) < max);
+ }
+
+ @Test
+ public void coolDownPeriodIsConfigurable() throws InterruptedException {
+ final long cd = 500; // Fixed cooldown period of 500 milliseconds
+ impl.setCoolDown(TimeValue.ofMilliseconds(cd));
+
+ // Sleep for a short duration before starting the test to reduce potential timing issues
+ Thread.sleep(100);
+
+ // Probe and check if the connection count remains the same during the cooldown period
+ impl.probe(route);
+ final int max0 = connPerRoute.getMaxPerRoute(route);
+ Thread.sleep(cd / 2); // Sleep for half the cooldown period
+ impl.probe(route);
+ assertEquals(max0, connPerRoute.getMaxPerRoute(route));
+
+ // Probe and check if the connection count increases after the cooldown period
+ Thread.sleep(cd / 2 + 1); // Sleep for the remaining half of the cooldown period + 1 ms
+ impl.probe(route);
+ assertTrue(max0 < connPerRoute.getMaxPerRoute(route));
+ }
+
+}
\ No newline at end of file
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestLinearBackoffManager.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestLinearBackoffManager.java
new file mode 100644
index 000000000..23c28e161
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/classic/TestLinearBackoffManager.java
@@ -0,0 +1,162 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.client5.http.impl.classic;
+
+
+import org.apache.hc.client5.http.HttpRoute;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.util.TimeValue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class TestLinearBackoffManager {
+
+ private LinearBackoffManager impl;
+ private MockConnPoolControl connPerRoute;
+ private HttpRoute route;
+ private static final long DEFAULT_COOL_DOWN_MS = 5000;
+
+ @BeforeEach
+ public void setUp() {
+ connPerRoute = new MockConnPoolControl();
+ route = new HttpRoute(new HttpHost("localhost", 80));
+ impl = new LinearBackoffManager(connPerRoute);
+ impl.setCoolDown(TimeValue.ofMilliseconds(DEFAULT_COOL_DOWN_MS));
+ }
+
+ @Test
+ public void incrementsConnectionsOnBackoff() {
+ final LinearBackoffManager impl = new LinearBackoffManager(connPerRoute);
+ impl.setCoolDown(TimeValue.ofMilliseconds(DEFAULT_COOL_DOWN_MS)); // Set the cool-down period
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.backOff(route);
+ assertEquals(5, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void decrementsConnectionsOnProbe() {
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.probe(route);
+ assertEquals(3, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void backoffDoesNotAdjustDuringCoolDownPeriod() throws InterruptedException {
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.backOff(route);
+ final long max = connPerRoute.getMaxPerRoute(route);
+ Thread.sleep(1); // Sleep for 1 ms
+ impl.backOff(route);
+ assertEquals(max, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void backoffStillAdjustsAfterCoolDownPeriod() throws InterruptedException {
+ final LinearBackoffManager impl = new LinearBackoffManager(connPerRoute);
+ impl.setCoolDown(TimeValue.ofMilliseconds(DEFAULT_COOL_DOWN_MS)); // Set the cool-down period
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.backOff(route);
+ final int max1 = connPerRoute.getMaxPerRoute(route);
+
+ Thread.sleep(DEFAULT_COOL_DOWN_MS + 1); // Sleep for cooldown period + 1 ms
+
+ impl.backOff(route);
+ final int max2 = connPerRoute.getMaxPerRoute(route);
+
+ assertTrue(max2 > max1);
+ }
+
+
+ @Test
+ public void probeDoesNotAdjustDuringCooldownPeriod() throws InterruptedException {
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.probe(route);
+ final long max = connPerRoute.getMaxPerRoute(route);
+ Thread.sleep(1); // Sleep for 1 ms
+ impl.probe(route);
+ assertEquals(max, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void probeStillAdjustsAfterCoolDownPeriod() throws InterruptedException {
+ connPerRoute.setMaxPerRoute(route, 4);
+ impl.probe(route);
+ Thread.sleep(DEFAULT_COOL_DOWN_MS + 1); // Sleep for cooldown period + 1 ms
+ impl.probe(route);
+ final long newMax = connPerRoute.getMaxPerRoute(route);
+ assertEquals(2, newMax); // The cap is set to 2 by default
+ }
+
+ @Test
+ public void testSetPerHostConnectionCap() {
+ connPerRoute.setMaxPerRoute(route, 5);
+ impl.setPerHostConnectionCap(10); // Set the cap to a higher value
+ impl.backOff(route);
+ assertEquals(6, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void probeUpdatesRemainingAttemptsIndirectly() throws InterruptedException {
+ // Set initial max per route
+ connPerRoute.setMaxPerRoute(route, 4);
+
+ // Apply backOff twice
+ impl.backOff(route);
+ Thread.sleep(DEFAULT_COOL_DOWN_MS + 1);
+ impl.backOff(route);
+
+ // Ensure that connection pool size has increased
+ assertEquals(6, connPerRoute.getMaxPerRoute(route));
+
+ // Apply probe once
+ impl.probe(route);
+
+ // Wait for a longer cool down period
+ Thread.sleep(DEFAULT_COOL_DOWN_MS * 2);
+
+ // Apply probe once more
+ impl.probe(route);
+
+ // Check that connection pool size has decreased once, indicating that the remaining attempts were updated
+ assertEquals(5, connPerRoute.getMaxPerRoute(route));
+ }
+
+ @Test
+ public void linearIncrementTest() throws InterruptedException {
+ final int initialMax = 4;
+ connPerRoute.setMaxPerRoute(route, initialMax);
+ for (int i = 1; i <= 5; i++) {
+ impl.backOff(route);
+ assertEquals(initialMax + i, connPerRoute.getMaxPerRoute(route));
+ Thread.sleep(DEFAULT_COOL_DOWN_MS + 1);
+
+ }
+ }
+}