001package ca.uhn.fhir.util;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2019 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import com.google.common.annotations.VisibleForTesting;
024import org.slf4j.Logger;
025import org.slf4j.LoggerFactory;
026
027import java.io.IOException;
028import java.net.DatagramSocket;
029import java.net.InetSocketAddress;
030import java.net.ServerSocket;
031import java.util.ArrayList;
032import java.util.Arrays;
033import java.util.List;
034
035/**
036 * Provides server ports that are free, in order for tests to use them
037 *
038 * <p><b>
039 * This class is ONLY designed for unit-testing usage, as it holds on to server ports
040 * for a long time (potentially lots of them!) and will leave your system low on
041 * ports if you put it into production.
042 * </b></p>
043 * <p>
044 * How it works:
045 * <p>
046 * We have lots of tests that need a free port because they want to open up
047 * a server, and need the port to be unique and unused so that the tests can
048 * run multithreaded. This turns out to just be an awful problem to solve for
049 * lots of reasons:
050 * <p>
051 * 1. You can request a free port from the OS by calling <code>new ServerSocket(0);</code>
052 * and this seems to work 99% of the time, but occasionally on a heavily loaded
053 * server if two processes ask at the exact same time they will receive the
054 * same port assignment, and one will fail.
055 * 2. Tests run in separate processes, so we can't just rely on keeping a collection
056 * of assigned ports or anything like that.
057 * <p>
058 * So we solve this like this:
059 * <p>
060 * At random, this class will pick a "control port" and bind it. A control port
061 * is just a randomly chosen port that is a multiple of 100. If we can bind
062 * successfully to that port, we now own the range of "n+1 to n+99". If we can't
063 * bind that port, it means some other process has probably taken it so
064 * we'll just try again until we find an available control port.
065 * <p>
066 * Assuming we successfully bind a control port, we'll give out any available
067 * ports in the range "n+1 to n+99" until we've exhausted the whole set, and
068 * then we'll pick another control port (if we actually get asked for over
069 * 100 ports.. this should be a rare event).
070 * <p>
071 * This mechanism has the benefit of (fingers crossed) being bulletproof
072 * in terms of its ability to give out ports that are actually free, thereby
073 * preventing random test failures.
074 * <p>
075 * This mechanism has the drawback of never giving up a control port once
076 * it has assigned one. To be clear, this class is deliberately leaking
077 * resources. Again, no production use!
078 */
079public class PortUtil {
080        private static final int SPACE_SIZE = 100;
081        private static final Logger ourLog = LoggerFactory.getLogger(PortUtil.class);
082        private static final PortUtil INSTANCE = new PortUtil();
083        private static int ourPortDelay = 500;
084        private List<ServerSocket> myControlSockets = new ArrayList<>();
085        private Integer myCurrentControlSocketPort = null;
086        private int myCurrentOffset = 0;
087        /**
088         * Constructor -
089         */
090        PortUtil() {
091                // nothing
092        }
093
094        /**
095         * Clear and release all control sockets
096         */
097        synchronized void clearInstance() {
098                for (ServerSocket next : myControlSockets) {
099                        ourLog.info("Releasing control port: {}", next.getLocalPort());
100                        try {
101                                next.close();
102                        } catch (IOException theE) {
103                                // ignore
104                        }
105                }
106                myControlSockets.clear();
107                myCurrentControlSocketPort = null;
108        }
109
110        /**
111         * Clear and release all control sockets
112         */
113        synchronized int getNextFreePort() {
114
115                while (true) {
116
117                        // Acquire a control port
118                        while (myCurrentControlSocketPort == null) {
119                                int nextCandidate = (int) (Math.random() * 65000.0);
120                                nextCandidate = nextCandidate - (nextCandidate % SPACE_SIZE);
121
122                                if (nextCandidate < 10000) {
123                                        continue;
124                                }
125
126                                try {
127                                        ServerSocket server = new ServerSocket();
128                                        server.setReuseAddress(true);
129                                        server.bind(new InetSocketAddress("localhost", nextCandidate));
130                                        myControlSockets.add(server);
131                                        ourLog.info("Acquired control socket on port {}", nextCandidate);
132                                        myCurrentControlSocketPort = nextCandidate;
133                                        myCurrentOffset = 0;
134                                } catch (IOException theE) {
135                                        ourLog.info("Candidate control socket {} is already taken", nextCandidate);
136                                        continue;
137                                }
138                        }
139
140                        // Find a free port within the allowable range
141                        while (true) {
142                                myCurrentOffset++;
143
144                                if (myCurrentOffset == SPACE_SIZE) {
145                                        // Current space is exhausted
146                                        myCurrentControlSocketPort = null;
147                                        break;
148                                }
149
150                                int nextCandidatePort = myCurrentControlSocketPort + myCurrentOffset;
151
152                                // Try to open a port on this socket and use it
153                                if (!isAvailable(nextCandidatePort)) {
154                                        continue;
155                                }
156
157                                // Log who asked for the port, just in case that's useful
158                                StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
159                                StackTraceElement previousElement = Arrays.stream(stackTraceElements)
160                                        .filter(t -> !t.toString().contains("PortUtil.") && !t.toString().contains("getStackTrace"))
161                                        .findFirst()
162                                        .orElse(stackTraceElements[2]);
163                                ourLog.info("Returned available port {} for: {}", nextCandidatePort, previousElement.toString());
164
165                                try {
166                                        Thread.sleep(ourPortDelay);
167                                } catch (InterruptedException theE) {
168                                        // ignore
169                                }
170
171                                return nextCandidatePort;
172
173                        }
174
175
176                }
177        }
178
179        @VisibleForTesting
180        public static void setPortDelay(Integer thePortDelay) {
181                if (thePortDelay == null) {
182                        thePortDelay = 500;
183                } else {
184                        ourPortDelay = thePortDelay;
185                }
186        }
187
188        /**
189         * This method checks if we are able to bind a given port to both
190         * 0.0.0.0 and localhost in order to be sure it's truly available.
191         */
192        private static boolean isAvailable(int thePort) {
193                ourLog.info("Testing a bind on thePort {}", thePort);
194                try (ServerSocket ss = new ServerSocket()) {
195                        ss.setReuseAddress(true);
196                        ss.bind(new InetSocketAddress("0.0.0.0", thePort));
197                        try (DatagramSocket ds = new DatagramSocket()) {
198                                ds.setReuseAddress(true);
199                                ds.connect(new InetSocketAddress("127.0.0.1", thePort));
200                                ourLog.info("Successfully bound thePort {}", thePort);
201                        } catch (IOException e) {
202                                ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
203                                return false;
204                        }
205                } catch (IOException e) {
206                        ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
207                        return false;
208                }
209
210                try (ServerSocket ss = new ServerSocket()) {
211                        ss.setReuseAddress(true);
212                        ss.bind(new InetSocketAddress("localhost", thePort));
213                } catch (IOException e) {
214                        ourLog.info("Failed to bind thePort {}: {}", thePort, e.toString());
215                        return false;
216                }
217
218                return true;
219        }
220
221        /**
222         * The entire purpose here is to find an available port that can then be
223         * bound for by server in a unit test without conflicting with other tests.
224         * <p>
225         * This is really only used for unit tests but is included in the library
226         * so it can be reused across modules. Use with caution.
227         */
228        public static int findFreePort() {
229                return INSTANCE.getNextFreePort();
230        }
231
232}
233