View Javadoc
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 }