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 }