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 }