001    /*
002    Copyright (c) 1996-2012, Damon Hart-Davis
003    All rights reserved.
004    
005    Redistribution and use in source and binary forms, with or without
006    modification, are permitted provided that the following conditions are
007    met:
008    
009      * Redistributions of source code must retain the above copyright
010        notice, this list of conditions and the following disclaimer.
011    
012      * Redistributions in binary form must reproduce the above copyright
013        notice, this list of conditions and the following disclaimer in the
014        documentation and/or other materials provided with the
015        distribution.
016    
017    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
018    IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
019    TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
020    PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
021    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
022    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
023    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
024    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
025    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
026    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
027    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
028    */
029    
030    package org.hd.d.pg2k.svrCore.location;
031    
032    import java.io.IOException;
033    import java.net.HttpURLConnection;
034    import java.net.MalformedURLException;
035    import java.net.URL;
036    import java.util.ArrayList;
037    import java.util.Collections;
038    import java.util.Comparator;
039    import java.util.Date;
040    import java.util.Hashtable;
041    import java.util.Iterator;
042    import java.util.List;
043    import java.util.Map;
044    import java.util.SortedMap;
045    import java.util.TreeMap;
046    
047    import org.hd.d.pg2k.svrCore.CoreConsts;
048    import org.hd.d.pg2k.svrCore.GenUtils;
049    import org.hd.d.pg2k.svrCore.HostUtils;
050    import org.hd.d.pg2k.svrCore.Rnd;
051    import org.hd.d.pg2k.svrCore.VarTools;
052    import org.hd.d.pg2k.svrCore.vars.InstanceID;
053    import org.hd.d.pg2k.svrCore.vars.SimpleVariablePipelineIF;
054    import org.hd.d.pg2k.svrCore.vars.SimpleVariableValue;
055    import org.hd.d.pg2k.svrCore.vars.SystemVariables;
056    
057    
058    /**Load-balancing utility functions for stand-alone and Web apps.
059     */
060    public final class LoadBalancingUtils
061        {
062        /**Prevent construction of an instance. */
063        private LoadBalancingUtils() { }
064    
065    
066        /**If true then always log balancing decisions. */
067        public static final boolean LOG_BALANCING_DECISIONS = false;
068    
069        /**Private cache for getActiveMirrors().
070         * A map from mirror tag (String) to available bandwidth
071         * in bytes-per-second-per-new-client (Long).
072         * <p>
073         * A Hashtable is used for thread safety,
074         * but also a lock on this value is used
075         * as a lock by getActiveMirrors() where needed.
076         * <p>
077         * TODO: ensure that size is bounded and/or dead entries can be trimmed on memory shortage.
078         */
079        private static final Hashtable<String,Long> _cache_gAM = new Hashtable<String,Long>();
080    
081        /**Time _cache_gAM is valid until; initially zero marking the cache as invalid. */
082        private static long _cache_gAM_valid_until;
083    
084        /**Roughly how stale getActiveMirrors() is prepared for its answers to be, ms. */
085        private static final int _MAX_STALE_GET_ACTIVE_MIRRORS_MS = VarTools.MIN_AGE_MS;
086    
087        /**Get active mirrors listed by mirror tag; may be empty but never null.
088         * Retrieves the tags (cc-xxxxx) of active mirrors from system variables,
089         * filtering out stale entries and those without significant free bandwidth.
090         * <p>
091         * Returns a Map from the mirror tag to the per-client bandwidth
092         * that the mirror claims to have available.
093         * <p>
094         * This does <em>not</em> check names in DNS,
095         * or that mirrors are actually responding,
096         * just which mirrors claim to be up as recorded in the system variables.
097         * <p>
098         * If this cannot retrieve the variable values that it needs,
099         * then this routine returns an (immutable) empty result.
100         * <p>
101         * This does not re-compute its value on each call,
102         * but caches it for a while since it may be expensive to compute.
103         * <p>
104         * It is possible to optionally leave in "stale" entries
105         * so as to be able to "coast" for a little longer
106         * after loss of contact with the master.
107         *
108         * @param vars  the system variables; never null
109         * @param filterStale  if true, filter out stale entries;
110         *     this result is not cached
111         *
112         * @return  non-null immutable empty,
113         *     or new mutable non-empty Map that is private to the caller
114         */
115        public static final SortedMap<String,Long> getActiveMirrors(final SimpleVariablePipelineIF vars,
116                                                                    final boolean filterStale)
117            {
118            if(vars == null)
119                { throw new IllegalArgumentException(); }
120    
121            synchronized(_cache_gAM)
122                {
123                // If the cached value is still valid,
124                // then return a *copy* of it.
125                final long now = System.currentTimeMillis();
126                if(!filterStale)
127                    {
128                    // Not filtering, so we can use the cache.
129    
130                    // Returned cached content if still valid.
131                    if(now <= _cache_gAM_valid_until)
132                        { return(new TreeMap<String,Long>(_cache_gAM)); }
133                    else // clear and recompute stale values...
134                        {
135                        _cache_gAM.clear(); // Zap the now-invalid cache content...
136                        // We'll set a valid (short) cache life
137                        // so that we don't keep trying to recompute this
138                        // even when we encounter errors.
139                        _cache_gAM_valid_until = now + Math.min(17000, _MAX_STALE_GET_ACTIVE_MIRRORS_MS/4);
140                        }
141                    }
142    
143                try
144                    {
145                    // Get set of systems claiming to be alive...
146                    final SimpleVariableValue mirrorAvailableBandwidthRaw =
147                            vars.getVariable(SystemVariables.ThroughputMonitorFilter_AVAIL_BPS_PER_CLIENT);
148                    if(mirrorAvailableBandwidthRaw == null)
149                        { return(_NO_LIVE_MIRRORS); }
150                    final SimpleVariableValue mirrorAvailableBandwidth = (!filterStale) ? mirrorAvailableBandwidthRaw :
151                            mirrorAvailableBandwidthRaw.removeAllKeysOlder(System.currentTimeMillis() - _MAX_STALE_GET_ACTIVE_MIRRORS_MS);
152                    final Map<InstanceID,SimpleVariableValue> mABMap = mirrorAvailableBandwidth.getGlobalMap();
153                    if(mABMap == null)
154                        { return(_NO_LIVE_MIRRORS); }
155    
156                    // Get the map of mirror tags...
157                    final SimpleVariableValue mirrorTagsRaw =
158                            vars.getVariable(SystemVariables.ThroughputMonitorFilter_ACTIVE_MIRROR_NAME);
159                    if(mirrorTagsRaw == null)
160                        { return(_NO_LIVE_MIRRORS); }
161                    final SimpleVariableValue mirrorTags = (!filterStale) ? mirrorTagsRaw :
162                            mirrorTagsRaw.removeAllKeysOlder(System.currentTimeMillis() - _MAX_STALE_GET_ACTIVE_MIRRORS_MS);
163                    final Map<InstanceID,SimpleVariableValue> mTMap = mirrorTags.getGlobalMap();
164                    if(mTMap == null)
165                        { return(_NO_LIVE_MIRRORS); }
166    
167                    // First get a list of all active mirrors.
168                    // Create a map from system ID
169                    // to non-zero available per-client bandwidth.
170                    final SortedMap<String,Long> liveMirrors = new TreeMap<String,Long>();
171    
172                    // Scan the global map of available bandwidth,
173                    // and for every positive value,
174                    // note it with the mirror tag (assuming that there is one)
175                    // of the system concerned.
176                    for(final InstanceID id : mABMap.keySet())
177                        {
178                        final SimpleVariableValue svvAvailBandwidth = mABMap.get(id);
179                        if((svvAvailBandwidth != null) && (svvAvailBandwidth.getValue() != null) && (((Number) svvAvailBandwidth.getValue()).longValue() > 0))
180                            {
181                            final SimpleVariableValue svvTag = mTMap.get(id);
182    
183                            if((svvTag != null) && (svvTag.getValue() != null))
184                                {
185                                // Got a live mirror and its tag!
186                                final String tag = (String) svvTag.getValue();
187                                final Long bw = new Long(((Number) svvAvailBandwidth.getValue()).longValue());
188                                liveMirrors.put(tag, bw);
189                                }
190                            }
191                        }
192    
193                    // Memory use (heap-churn) optimisation:
194                    // return the single "no mirrors" value if none found...
195                    if(liveMirrors.isEmpty())
196                        { return(_NO_LIVE_MIRRORS); }
197    
198                    // Return details (and cache a copy) of one or more mirrors...
199                    // Don't cache filtered data.
200                    if(!filterStale)
201                        {
202                        // Cache the unfiltered data.
203                        _cache_gAM.putAll(liveMirrors);
204                        // Set a reasonable life on the non-empty cache...
205                        _cache_gAM_valid_until = now + _MAX_STALE_GET_ACTIVE_MIRRORS_MS/2;
206                        }
207                    return(liveMirrors);
208                    }
209                // If we have a problem getting the variable values that we need,
210                // then return a value indicating no currently-active mirrors.
211                catch(final IOException e)
212                    {
213                    e.printStackTrace(); // Whinge, but absorb the error.
214                    return(_NO_LIVE_MIRRORS);
215                    }
216                }
217            }
218    
219        /**Immutable empty Map to indicate no live mirrors. */
220        private static final SortedMap<String,Long> _NO_LIVE_MIRRORS =
221                Collections.unmodifiableSortedMap(new TreeMap<String,Long>());
222    
223    
224        /**Make full hostname of site given a mirror tag; never null/empty.
225         * The host name returned is relative to the given domain name.
226         * <p>
227         * No checking is done that the resulting name actually refers to a valid host.
228         * <p>
229         * Result is undefined if the tag is not a valid mirror tag.
230         */
231        public static String makeMirrorHostnameFromTag(final String mirrorTag,
232                                                       final String domainName)
233            {
234            final String baseName = HostUtils.normaliseVirtualHostName(domainName);
235            return("mirror-" + mirrorTag + "." + baseName);
236            }
237    
238    
239        /**Immutable info on interaction with one remote mirror.
240         */
241        private static final class MirrorInfo
242            {
243            /**Make a new instance with a suitable lifetime... */
244            MirrorInfo(final Boolean isUp,
245                       final Thread t)
246                {
247                // Set random cache expiry time with mean specified,
248                // plus a bit extra for positive results
249                // and when we're (temporarily) conserving energy.
250                validUntil = System.currentTimeMillis() +
251                        (Boolean.TRUE.equals(isUp) ?
252                            (MIRROR_RECHECK_MS) : (MIRROR_RECHECK_MS/2)) +
253                        (GenUtils.mustConservePower() ? MIRROR_RECHECK_MS/2 : 0) +
254                        Rnd.fastRnd.nextInt(MIRROR_RECHECK_MS);
255                this.isUp = isUp;
256                checker = t;
257                }
258    
259            /**Time that this info is valid until. */
260            final long validUntil;
261    
262            /**TRUE if the mirror seems to be up and working and accessible.
263             * FALSE if the mirror seems to be down.
264             * null if we do not yet know, because we are checking asynchronously.
265             */
266            final Boolean isUp;
267    
268            /**Non-null if being checked in background by thread; null if not being checked.
269             * If any referred-to thread is not alive then this entry is not being checked.
270             */
271            final Thread checker;
272    
273            /**If true then this entry can be expired.
274             * This is true if the validUntil time has passed
275             * and there is no thread working on this host/entry.
276             */
277            boolean canExpire()
278                {
279                if(System.currentTimeMillis() <= validUntil) { return(false); }
280                if(checker == null) { return(true); }
281                return(!checker.isAlive());
282                }
283    
284            /**If true then this entry will expire soon.
285             * This can be used to decide if we should start a new checker thread.
286             * <p>
287             * This is true if the validUntil time will pass soon
288             * and there is no thread working on this host/entry.
289             */
290            boolean expiresSoon()
291                {
292                if(System.currentTimeMillis() <= validUntil - (MIRROR_RECHECK_MS/4))
293                    { return(false); }
294                if(checker == null) { return(true); }
295                return(!checker.isAlive());
296                }
297            }
298    
299    
300        /**Target/normal/mean time before we (re)test that a mirror is working, in ms.
301         * Should be of the order of minutes to tens of minutes,
302         * (and the results may be effectively cached in pages' HTML for a while)
303         * but not so long as to make the system insensitive to extended outages
304         * or gross configuration errors.
305         * <p>
306         * We randomise this a little to help avoid (re)testing collisions
307         * between peers.
308         * <p>
309         * Testing may be slow and/or consume significant network resources.
310         * <p>
311         * We may asymmetrically retain positive results longer than negative ones.
312         */
313        private static final int MIRROR_RECHECK_MS = 13 * 60 * 1000 +
314            Rnd.fastRnd.nextInt(3 * 60 * 1000);
315    
316    
317        /**Construct a full mirror host name from the mirror tag.
318         * Result is undefined if the tag is invalid.
319         */
320        public static String makeMirrorNameFromTag(final String tag)
321            {
322            return("mirror-" + tag + "." + CoreConsts.MAIN_DATA_HOST);
323            }
324    
325        /**Try the requested mirrors in turn, returning the full hostname of the first that is up, or null if none is available.
326         * The mirrors are tried in the order in the list.
327         * <p>
328         * The cache of mirror state is used, and updated if need be.
329         * <p>
330         * This routine may take considerable time to complete
331         * if this has to actually test the mirrors
332         * and they are down and/or network connectivity is poor.
333         *
334         * @param orderedMirrorTags  valid mirror tags in the order the mirrors should be tried
335         *
336         * @return the full hostname of the first working mirror found,
337         *     or null if none of the mirrors specified appear to be up
338         */
339        public static String findFirstWorkingMirror(final List<String> orderedMirrorTags)
340            {
341            // Try the tags in order, asynchronously.
342            for(final String tag : orderedMirrorTags)
343                {
344                final String fullHostname = makeMirrorNameFromTag(tag);
345    
346                // If the server is up then our search is over.
347                // Don't block to find out though.
348                if(Boolean.TRUE.equals(testIfHTTPServerIsUp(fullHostname, true)))
349                    { return(fullHostname); }
350                }
351    
352            return(null); // No available mirror.
353            }
354    
355    //  /**If true then we automatically filter out mirrors with too little bandwidth in orderMirrorTagsBestBandwidthFirst(). */
356    //  private static final boolean _OMTBF_DROP_MIRRORS_BW_TOO_LOW = true;
357    
358        /**Orders mirrors into "fastest first" order, returning a List of mirror tags and dropping any known unusable; never null.
359         * This accepts as input a set of mirror details as returned from getActiveMirrors()
360         * (a mapping from mirror tag to claimed available bandwidth per client).
361         * <p>
362         * The results will be ordered to be highest-bandwidth first, and then by name.
363         * <p>
364         * This will drop mirrors known from the cache to be dead/unreachable,
365         * and any mirrors that are <em>much</em> slower than the best available.
366         * <p>
367         * The result will be smaller when inappropriate/slow/dead mirrors
368         * are dropped from the result.
369         * <p>
370         * This does <em>not</em> synchronously attempt
371         * to verify the accuracy of the data that it has been passed,
372         * eg it does not do blocking DNS lookups nor make HTTP connections,
373         * so should always be reasonably fast.
374         * (It does however check the status of remote mirrors asynchronously,
375         * and won't return a candidate unless verified to be working.)
376         *
377         * @param availableMirrors  mapping of mirror tags to available per-client bandwidth; never null
378         * @return private, mutable-unless-empty, best-first list of mirrors;
379         *     may be empty and/or smaller than the input set but never null
380         */
381        public static List<String> orderMirrorTagsBestBandwidthFirst(final Map<String, Long> availableMirrors)
382            {
383            if(null == availableMirrors) { throw new IllegalArgumentException(); }
384    
385            // Assume that all candidates will usually make it to the result.
386            final List<String> result = new ArrayList<String>(availableMirrors.size());
387    
388            // First filter out any unsuitable mirrors.
389            // This may be any with non-positive bandwidth,
390            // or bandwidth too small to sustain a good user experience,
391            // or that a geographically inappropriate for the user.
392            for(final String tag : availableMirrors.keySet())
393                {
394                final Long bwL = availableMirrors.get(tag);
395                // Reject malformed or non-positive bandwidth entries...
396                if(bwL == null) { continue; }
397                if(bwL.longValue() <= 0) { continue; }
398    
399    //            // Reject mirrors with far too little bandwidth to be likely to be of use.
400    //            // We filter for much lower than typical per-user bandwidth,
401    //            // in case our number is somewhat wrong.
402    //            if(_OMTBF_DROP_MIRRORS_BW_TOO_LOW &&
403    //                    (bwL.longValue() < WebConsts.DEFAULT_EXPECTED_USER_BW_BYTESPERSEC/10))
404    //                { continue; }
405    
406                final String fullHostname = makeMirrorNameFromTag(tag);
407    
408                // If we don't know the mirror to be up
409                // then move on to the next possible mirror.
410                if(!Boolean.TRUE.equals(testIfHTTPServerIsUp(fullHostname, true)))
411                    { continue; }
412    
413                result.add(tag);
414                }
415    
416            // If there are no remaining results then return immutable empty List to save space.
417            if(result.isEmpty())
418                { return(Collections.emptyList()); }
419    
420            // If more than one remaining candidate
421            // then sort the results into bandwidth order
422            // and trim any VERY slow hosts...
423            if(result.size() > 1)
424                {
425                Collections.sort(result, new BWOrder(availableMirrors));
426    
427                // Get the bandwidth of the best entry...
428                final Long bestBW = availableMirrors.get(result.get(0));
429                if(bestBW == null) { throw new IllegalStateException(); }
430    
431                // Compute minimum bandwidth to remain in the result.
432                // In the first instance a fraction of the fastest mirror's bandwidth
433                // that is about the same as the maximum as the maximum "closeness" benefit.
434                // The reasoning is that even being very close does not compensate for being very slow.
435                final long thresholdBW = bestBW.longValue() / (GeoProximity.VCLOSE.getCloseness() << 1);
436    
437                // Nibble away entries from the end of the list that don't make the grade.
438                // Working backwards, as soon as we find one good enough then we can stop.
439                for(int i = result.size(); --i >= 1; )
440                    {
441                    final Long bw = availableMirrors.get(result.get(i));
442                    if(bw == null) { throw new IllegalStateException(); }
443                    if(bw.longValue() >= thresholdBW) { break; }
444                    result.remove(i);
445                    }
446                }
447    
448            // Return the ordered results.
449            return(result);
450            }
451    
452        /**Our simple sorting comparator for mirrors sorts by bandwidth (or other Long metric) then name. */
453        public static final class BWOrder implements Comparator<String>
454            {
455            /**Create an instance with the tag info map behind it. */
456            public BWOrder(final Map<String,Long> mirrorInfo)
457                { this.mirrorInfo = mirrorInfo; }
458            /**The map from tag name to bandwidth. */
459            private final Map<String,Long> mirrorInfo;
460    
461            /**Sort in order by bandwidth (highest first), then by name to give a total ordering. */
462            public final int compare(final String s1, final String s2)
463                {
464                final long b1 = mirrorInfo.get(s1).longValue();
465                final long b2 = mirrorInfo.get(s2).longValue();
466    
467                // Sort for largest-bandwidth first...
468                if(b1 > b2) { return(-1); }
469                if(b1 < b2) { return(+1); }
470    
471                // Sort by name to break ties and thus to provide a total ordering.
472                return(s1.compareTo(s2));
473                }
474            }
475    
476        /**Connection establishment timeout allowed by testIfHTTPServerIsUp(), in ms.
477         * Fairly short to make sure that a server is responding reasonably quickly,
478         * but long enough to allow for DNS lookup and the odd lost packet
479         * and some intermittent Net congestion.
480         * <p>
481         * Should be of the order of a few seconds to a few tens of seconds.
482         */
483        private static final int _tIHSIU_CONN_TIMEOUT_MS = 9001 + Rnd.fastRnd.nextInt(3001);
484    
485        /**Number of connection attempts before deciding that remote host is dead; strictly positive. */
486        private static final int _tIHSIU_CONN_ATTEMPTS = 13 + Rnd.fastRnd.nextInt(3);
487    
488    
489        /**Private cache for testIfHTTPServerIsUp() for which mirrors are up.
490         * Map from mirror full host name component of URL (not just the mirror tag)
491         * to last time server at that URL was tested and found to respond properly
492         * (eg fast enough and with no error) or not.
493         * <p>
494         * Uses a thread-safe Map implementation.
495         * <p>
496         * TODO: must cap size and/or ensure that dead entries can be trimmed especially under memory stress.
497         */
498        private static final Map<String,MirrorInfo> _cache_tIHSIU_mirrorStatus = new Hashtable<String,MirrorInfo>();
499    
500        /**Tests if an HTTP Gallery mirror server is up and running and responding quickly.
501         * The hostname or the hostname:port must be supplied;
502         * the "null" page will be polled
503         * (which should always work unless the host is down or busy).
504         * <p>
505         * Tests that the web server responds quickly and without error
506         * (we must get response code 200),
507         * to a request for its top-level page "/null.jsp"
508         * (redirections are allowed, eg to an index.html or index.jsp).
509         * <p>
510         * If an invalid (non-null) host name is supplied this will return false.
511         * <p>
512         * We should only do one of these at a time for a given host
513         * in order to avoid overloading any one remote mirror.
514         * <p>
515         * Note that even if we check synchronously,
516         * we may still record any result in the cache
517         * so that another caller may benefit.
518         * This also means that this routine can call itself recursively
519         * (with the checkAsynchronously argument false)
520         * via asynchronous threads.
521         * <p>
522         * Note that this may run as many threads as there are different hosts
523         * whose status is being requested.
524         * We could probably do better with non-blocking I/O
525         * and a single worker thread if there are many hosts to check,
526         * but the assumption is that there will not be.
527         *
528         * @param hostAndPort  hostname or hostname:port for mirror; never null
529         * @param checkAsynchronously  if false we check synchronously
530         *     and block until we can return TRUE or FALSE,
531         *     else we return immediately and check in the background
532         * @return TRUE if the specified server seems to be up and running,
533         *         FALSE if the server seems to be down,
534         *         and null if we do not know at the moment
535         *         (never returns null for a synchronous check)
536         */
537        public static Boolean testIfHTTPServerIsUp(final String hostAndPort,
538                                                   final boolean checkAsynchronously)
539            {
540            // If we are not using the cache then block here
541            // and update the cache if there is no thread updating
542            // or if the updating thread is us.
543            if(!checkAsynchronously)
544                {
545                boolean r; // = false;
546                try { r = testIfHTTPServerIsUp(new URL("http://" + hostAndPort + CoreConsts.DO_NOTHING_PAGE_RRURL)); }
547                catch(final MalformedURLException e)
548                    {
549                    e.printStackTrace(); // Unexpected error...
550                    r = false;
551                    }
552    
553                final Boolean result = r ? Boolean.TRUE : Boolean.FALSE;
554                return(result);
555                }
556    
557            // Removing expired entries...
558            // See if this thread is going to be picked to expire stale entries.
559            // We pick at random in inverse proportion to the size of the cache,
560            // in order to make the average cost per call constant.
561            // We do this under a lock on the cache to eliminate races,
562            // but we can release this lock again as soon as we are done.
563            synchronized(_cache_tIHSIU_mirrorStatus)
564                {
565                final int size = _cache_tIHSIU_mirrorStatus.size();
566                if((size > 0) && (Rnd.fastRnd.nextInt(2 + size) == 0))
567                    {
568                    // Expire old entries.
569                    for(final Iterator<String> it = _cache_tIHSIU_mirrorStatus.keySet().iterator(); it.hasNext(); )
570                        {
571                        final String tag = it.next();
572                        // Remove a stale entry as long as
573                        // it does not still have a live thread updating it.
574                        if(_cache_tIHSIU_mirrorStatus.get(tag).canExpire())
575                            { it.remove(); }
576                        }
577                    }
578                }
579    
580            // See what we currently have in the cache for this host.
581            // Make this, and the possible launch of a check thread, atomic.
582            synchronized(_cache_tIHSIU_mirrorStatus)
583                {
584                final MirrorInfo extant = _cache_tIHSIU_mirrorStatus.get(hostAndPort);
585    
586                // If there is no entry or it has nearly expired
587                // then launch a background thread to get it updated.
588                // Create an entry that preserves the current value while we check;
589                // this limits us to at most one checker thread per host.
590                final Boolean result;
591                if((extant == null) || extant.expiresSoon())
592                    {
593                    // Compute result to return now.
594                    result = (extant == null) ? null : extant.isUp;
595    
596                    // Create a checker Thread.
597                    final Thread t = new Thread("checking liveness of server " + hostAndPort){
598                        @Override
599                        public final void run()
600                            {
601                            // Test synchronously...
602                            final Boolean r = testIfHTTPServerIsUp(hostAndPort, false);
603                            // This should always give a definite answer.
604                            assert(r != null);
605    
606                            // If we can update the cache, then do it.
607                            // We hold a lock to keep any test-and-update atomic.
608                            synchronized(_cache_tIHSIU_mirrorStatus)
609                                {
610                                // If we can update the cache then do so.
611                                // It can be updated if:
612                                //   * There is no entry.
613                                //   * There is no (live) checker thread.
614                                //   * There is a current entry but we are the checker thread.
615                                final MirrorInfo extant = _cache_tIHSIU_mirrorStatus.get(hostAndPort);
616                                if((extant == null) ||
617                                   (extant.checker == null) || !extant.checker.isAlive() ||
618                                   (extant.checker == Thread.currentThread()))
619                                    {
620                                    final MirrorInfo newInfo = new MirrorInfo(r, null);
621                                    final MirrorInfo oldInfo = _cache_tIHSIU_mirrorStatus.put(hostAndPort, newInfo);
622                                    // Report any change in status.
623                                    // Assumes that the Boolean can only be
624                                    // one of the fixed TRUE/FALSE/null
625                                    // so !=/== can be used rather than equals().
626                                    if((oldInfo == null) || (r != oldInfo.isUp))
627                                        {
628                                        System.out.println("[testIfHTTPServerIsUp("+hostAndPort+"): status change: now "+r+"; recheck after: "+(new Date(newInfo.validUntil))+"]");
629                                        }
630                                    }
631                                }
632                            }
633                        };
634    
635                    _cache_tIHSIU_mirrorStatus.put(hostAndPort,
636                                                   new MirrorInfo(result, t));
637                    // Mark the the thread as a daemon and start it off.
638                    t.setDaemon(true);
639                    t.start();
640                    }
641                else
642                    {
643                    result = extant.isUp;
644                    }
645    
646                return(result);
647                }
648            }
649    
650        /**Tests if a Web server is up and running and responding quickly.
651         * A full URL must be supplied.
652         * <p>
653         * Tests that the web server responds quickly and without error
654         * (we must get response code 200),
655         * to a HEAD request for a special light-weight page "/null.jsp"
656         * (redirections are allowed, eg to an index.html or index.jsp).
657         * <p>
658         * If an invalid (non-null) host name is supplied this will return false.
659         * <p>
660         * We should only do one of these at a time for a given host
661         * in order to avoid overloading any one remote mirror.
662         *
663         * @param serverURL  full URL for remote server (page) to test; never null
664         * @return true if the specified server seems to be up and running, false otherwise
665         */
666        public static boolean testIfHTTPServerIsUp(final URL serverURL)
667            {
668            if(serverURL == null)
669                { throw new IllegalArgumentException(); }
670    
671            for(int attemptsLeft = _tIHSIU_CONN_ATTEMPTS; --attemptsLeft >= 0; )
672                {
673                HttpURLConnection uc = null;
674                try
675                    {
676                    uc = (HttpURLConnection) serverURL.openConnection();
677                    if(!CoreConsts.AVOID_UNSAFE_TCP_TIMEOUTS)
678                        {
679                        uc.setConnectTimeout(_tIHSIU_CONN_TIMEOUT_MS); // Allow DNS lookup, TCP handshake, etc...
680                        uc.setReadTimeout(_tIHSIU_CONN_TIMEOUT_MS); // Page may be expensive/slow to generate.
681                        }
682                    uc.setUseCaches(false); // Force a fresh fetch/connection.
683                    uc.setAllowUserInteraction(false);
684                    uc.setInstanceFollowRedirects(true);
685                    uc.setRequestMethod("HEAD"); // Only need to get the status, not the data.
686                    uc.connect(); // Try to force a connection...
687                    final int responseCode = uc.getResponseCode();
688                    if(200 == responseCode)
689                        {
690                        // All seems OK, so return immediately.
691                        return(true);
692                        }
693    
694    //System.err.println("[testIfHTTPServerIsUp("+serverURL+") failed (attemptsLeft="+attemptsLeft+") with status code " + responseCode + ".]");
695                    }
696                // Any IOException probably indicates that the server is NOT well.
697                catch(final IOException e)
698                    {
699    //System.err.println("[testIfHTTPServerIsUp("+serverURL+") failed (attemptsLeft="+attemptsLeft+") with error " + e.getMessage() + ".]");
700                    }
701    
702                // Drop possibly-broken connection.
703                if(uc != null) { uc.disconnect(); }
704    
705                // Pause a while before trying again...
706                // This may give DNS and other temporary glitches time to pass.
707                try { Thread.sleep(15101 + 2*_tIHSIU_CONN_TIMEOUT_MS); }
708                catch(final InterruptedException e)
709                    {
710                    Thread.currentThread().interrupt();
711                    return(false); // Return immediately after interrupt.
712                    }
713                }
714    
715            // Failed to connect...
716            return(false);
717            }
718        }