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