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.routing;
28
29 import org.apache.hc.core5.annotation.Contract;
30 import org.apache.hc.core5.annotation.ThreadingBehavior;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 import java.io.IOException;
35 import java.net.Proxy;
36 import java.net.ProxySelector;
37 import java.net.SocketAddress;
38 import java.net.URI;
39 import java.util.ArrayList;
40 import java.util.Collections;
41 import java.util.List;
42 import java.util.concurrent.atomic.AtomicInteger;
43
44 /**
45 * A DistributedProxySelector is a custom {@link ProxySelector} implementation that
46 * delegates proxy selection to a list of underlying ProxySelectors in a
47 * distributed manner. It ensures that proxy selection is load-balanced
48 * across the available ProxySelectors, and provides thread safety by
49 * maintaining separate states for each thread.
50 *
51 * <p>The DistributedProxySelector class maintains a list of ProxySelectors,
52 * a {@link ThreadLocal} variable for the current {@link ProxySelector}, and an {@link AtomicInteger}
53 * to keep track of the shared index across all threads. When the select()
54 * method is called, it delegates the proxy selection to the current
55 * ProxySelector or the next available one in the list if the current one
56 * returns an empty proxy list. Any exceptions that occur during proxy
57 * selection are caught and ignored, and the next ProxySelector is tried.
58 *
59 * <p>The connectFailed() method notifies the active {@link ProxySelector} of a
60 * connection failure, allowing the underlying ProxySelector to handle
61 * connection failures according to its own logic.
62 *
63 * @since 5.3
64 */
65 @Contract(threading = ThreadingBehavior.SAFE)
66 public class DistributedProxySelector extends ProxySelector {
67
68 private static final Logger LOG = LoggerFactory.getLogger(DistributedProxySelector.class);
69
70 /**
71 * A list of {@link ProxySelector} instances to be used by the DistributedProxySelector
72 * for selecting proxies.
73 */
74 private final List<ProxySelector> selectors;
75
76 /**
77 * A {@link ThreadLocal} variable holding the current {@link ProxySelector} for each thread,
78 * ensuring thread safety when accessing the current {@link ProxySelector}.
79 */
80 private final ThreadLocal<ProxySelector> currentSelector;
81
82 /**
83 * An {@link AtomicInteger} representing the shared index across all threads for
84 * maintaining the current position in the list of ProxySelectors, ensuring
85 * proper distribution of {@link ProxySelector} usage.
86 */
87 private final AtomicInteger sharedIndex;
88
89
90 /**
91 * Constructs a DistributedProxySelector with the given list of {@link ProxySelector}.
92 * The constructor initializes the currentSelector as a {@link ThreadLocal}, and
93 * the sharedIndex as an {@link AtomicInteger}.
94 *
95 * @param selectors the list of ProxySelectors to use.
96 * @throws IllegalArgumentException if the list is null or empty.
97 */
98 public DistributedProxySelector(final List<ProxySelector> selectors) {
99 if (selectors == null || selectors.isEmpty()) {
100 throw new IllegalArgumentException("At least one ProxySelector is required");
101 }
102 this.selectors = new ArrayList<>(selectors);
103 this.currentSelector = new ThreadLocal<>();
104 this.sharedIndex = new AtomicInteger();
105 }
106
107 /**
108 * Selects a list of proxies for the given {@link URI} by delegating to the current
109 * {@link ProxySelector} or the next available {@link ProxySelector} in the list if the current
110 * one returns an empty proxy list. If an {@link Exception} occurs, it will be caught
111 * and ignored, and the next {@link ProxySelector} will be tried.
112 *
113 * @param uri the {@link URI} to select a proxy for.
114 * @return a list of proxies for the given {@link URI}.
115 */
116 @Override
117 public List<Proxy> select(final URI uri) {
118 List<Proxy> result = Collections.emptyList();
119 ProxySelector selector;
120
121 for (int i = 0; i < selectors.size(); i++) {
122 selector = nextSelector();
123 if (LOG.isDebugEnabled()) {
124 LOG.debug("Selecting next proxy selector for URI {}: {}", uri, selector);
125 }
126
127 try {
128 currentSelector.set(selector);
129 result = currentSelector.get().select(uri);
130 if (!result.isEmpty()) {
131 break;
132 }
133 } catch (final Exception e) {
134 // ignore and try the next selector
135 if (LOG.isDebugEnabled()) {
136 LOG.debug("Exception caught while selecting proxy for URI {}: {}", uri, e.getMessage());
137 }
138 } finally {
139 currentSelector.remove();
140 }
141 }
142 return result;
143 }
144
145 /**
146 * Notifies the active {@link ProxySelector} of a connection failure. This method
147 * retrieves the current {@link ProxySelector} from the {@link ThreadLocal} variable and
148 * delegates the handling of the connection failure to the underlying
149 * ProxySelector's connectFailed() method. After handling the connection
150 * failure, the current ProxySelector is removed from the {@link ThreadLocal} variable.
151 *
152 * @param uri the {@link URI} that failed to connect.
153 * @param sa the {@link SocketAddress} of the proxy that failed to connect.
154 * @param ioe the {@link IOException} that resulted from the failed connection.
155 */
156 @Override
157 public void connectFailed(final URI uri, final SocketAddress sa, final IOException ioe) {
158 final ProxySelector selector = currentSelector.get();
159 if (selector != null) {
160 selector.connectFailed(uri, sa, ioe);
161 currentSelector.remove();
162 if (LOG.isDebugEnabled()) {
163 LOG.debug("Removed the current ProxySelector for URI {}: {}", uri, selector);
164 }
165 }
166 }
167
168 /**
169 * Retrieves the next available {@link ProxySelector} in the list of selectors,
170 * incrementing the shared index atomically to ensure proper distribution
171 * across different threads.
172 *
173 * @return the next {@link ProxySelector} in the list.
174 */
175 private ProxySelector nextSelector() {
176 final int nextIndex = sharedIndex.getAndUpdate(i -> (i + 1) % selectors.size());
177 return selectors.get(nextIndex);
178 }
179 }