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