001    /*
002    Copyright (c) 1996-2011, 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 log balancing decisions, etc. */
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            // Assume that all candidates will usually make it to the result.
384            final List<String> result = new ArrayList<String>(availableMirrors.size());
385    
386            // First filter out any unsuitable mirrors.
387            // This may be any with non-positive bandwidth,
388            // or bandwidth too small to sustain a good user experience,
389            // or that a geographically inappropriate for the user.
390            for(final String tag : availableMirrors.keySet())
391                {
392                final Long bwL = availableMirrors.get(tag);
393                // Reject malformed or non-positive bandwidth entries...
394                if(bwL == null) { continue; }
395                if(bwL.longValue() <= 0) { continue; }
396    
397    //            // Reject mirrors with far too little bandwidth to be likely to be of use.
398    //            // We filter for much lower than typical per-user bandwidth,
399    //            // in case our number is somewhat wrong.
400    //            if(_OMTBF_DROP_MIRRORS_BW_TOO_LOW &&
401    //                    (bwL.longValue() < WebConsts.DEFAULT_EXPECTED_USER_BW_BYTESPERSEC/10))
402    //                { continue; }
403    
404                final String fullHostname = makeMirrorNameFromTag(tag);
405    
406                // If we don't know the mirror to be up
407                // then move on to the next possible mirror.
408                if(!Boolean.TRUE.equals(testIfHTTPServerIsUp(fullHostname, true)))
409                    { continue; }
410    
411                result.add(tag);
412                }
413    
414            // If there are no remaining results then return immutable empty List to save space.
415            if(result.size() == 0)
416                {
417                final List<String> emptyList = Collections.emptyList();
418                return(emptyList);
419                }
420    
421            // If more than one remaining candidate
422            // then sort the results into bandwidth order
423            // and trim any VERY slow hosts...
424            if(result.size() > 1)
425                {
426                Collections.sort(result, new BWOrder(availableMirrors));
427    
428                // Get the bandwidth of the best entry...
429                final Long bestBW = availableMirrors.get(result.get(0));
430                if(bestBW == null) { throw new IllegalStateException(); }
431    
432                // Compute minimum bandwidth to remain in the result.
433                // In the first instance a fraction of the fastest mirror's bandwidth
434                // that is about the same as the maximum as the maximum "closeness" benefit.
435                // The reasoning is that even being very close does not compensate for being very slow.
436                final long thresholdBW = bestBW.longValue() / (2*GeoProximity.VCLOSE.getCloseness());
437    
438                // Nibble away entries from the end of the list that don't make the grade.
439                // Working backwards, as soon as we find one good enough then we can stop.
440                for(int i = result.size(); --i >= 1; )
441                    {
442                    final Long bw = availableMirrors.get(result.get(i));
443                    if(bw == null) { throw new IllegalStateException(); }
444                    if(bw.longValue() >= thresholdBW) { break; }
445                    result.remove(i);
446                    }
447                }
448    
449            // Return the ordered results.
450            return(result);
451            }
452    
453        /**Our simple sorting comparator for mirrors sorts by bandwidth (or other Long metric) then name. */
454        public static final class BWOrder implements Comparator<String>
455            {
456            /**Create an instance with the tag info map behind it. */
457            public BWOrder(final Map<String,Long> mirrorInfo)
458                { this.mirrorInfo = mirrorInfo; }
459            /**The map from tag name to bandwidth. */
460            private final Map<String,Long> mirrorInfo;
461    
462            /**Sort in order by bandwidth (highest first), then by name to give a total ordering. */
463            public final int compare(final String s1, final String s2)
464                {
465                final long b1 = mirrorInfo.get(s1).longValue();
466                final long b2 = mirrorInfo.get(s2).longValue();
467    
468                // Sort for largest-bandwidth first...
469                if(b1 > b2) { return(-1); }
470                if(b1 < b2) { return(+1); }
471    
472                // Sort by name to break ties and thus to provide a total ordering.
473                return(s1.compareTo(s2));
474                }
475            }
476    
477        /**Connection establishment timeout allowed by testIfHTTPServerIsUp(), in ms.
478         * Fairly short to make sure that a server is responding reasonably quickly,
479         * but long enough to allow for DNS lookup and the odd lost packet
480         * and some intermittent Net congestion.
481         * <p>
482         * Should be of the order of a few seconds to a few tens of seconds.
483         */
484        private static final int _tIHSIU_CONN_TIMEOUT_MS = 9001 + Rnd.fastRnd.nextInt(3001);
485    
486        /**Number of connection attempts before deciding that remote host is dead; strictly positive. */
487        private static final int _tIHSIU_CONN_ATTEMPTS = 13 + Rnd.fastRnd.nextInt(3);
488    
489    
490        /**Private cache for testIfHTTPServerIsUp() for which mirrors are up.
491         * Map from mirror full host name component of URL (not just the mirror tag)
492         * to last time server at that URL was tested and found to respond properly
493         * (eg fast enough and with no error) or not.
494         * <p>
495         * Uses a thread-safe Map implementation.
496         * <p>
497         * TODO: must cap size and/or ensure that dead entries can be trimmed especially under memory stress.
498         */
499        private static final Map<String,MirrorInfo> _cache_tIHSIU_mirrorStatus = new Hashtable<String,MirrorInfo>();
500    
501        /**Tests if an HTTP Gallery mirror server is up and running and responding quickly.
502         * The hostname or the hostname:port must be supplied;
503         * the "null" page will be polled
504         * (which should always work unless the host is down or busy).
505         * <p>
506         * Tests that the web server responds quickly and without error
507         * (we must get response code 200),
508         * to a request for its top-level page "/null.jsp"
509         * (redirections are allowed, eg to an index.html or index.jsp).
510         * <p>
511         * If an invalid (non-null) host name is supplied this will return false.
512         * <p>
513         * We should only do one of these at a time for a given host
514         * in order to avoid overloading any one remote mirror.
515         * <p>
516         * Note that even if we check synchronously,
517         * we may still record any result in the cache
518         * so that another caller may benefit.
519         * This also means that this routine can call itself recursively
520         * (with the checkAsynchronously argument false)
521         * via asynchronous threads.
522         * <p>
523         * Note that this may run as many threads as there are different hosts
524         * whose status is being requested.
525         * We could probably do better with non-blocking I/O
526         * and a single worker thread if there are many hosts to check,
527         * but the assumption is that there will not be.
528         *
529         * @param hostAndPort  hostname or hostname:port for mirror; never null
530         * @param checkAsynchronously  if false we check synchronously
531         *     and block until we can return TRUE or FALSE,
532         *     else we return immediately and check in the background
533         * @return TRUE if the specified server seems to be up and running,
534         *         FALSE if the server seems to be down,
535         *         and null if we do not know at the moment
536         *         (never returns null for a synchronous check)
537         */
538        public static Boolean testIfHTTPServerIsUp(final String hostAndPort,
539                                                   final boolean checkAsynchronously)
540            {
541            // If we are not using the cache then block here
542            // and update the cache if there is no thread updating
543            // or if the updating thread is us.
544            if(!checkAsynchronously)
545                {
546                boolean r; // = false;
547                try { r = testIfHTTPServerIsUp(new URL("http://" + hostAndPort + CoreConsts.DO_NOTHING_PAGE_RRURL)); }
548                catch(final MalformedURLException e)
549                    {
550                    e.printStackTrace(); // Unexpected error...
551                    r = false;
552                    }
553    
554                final Boolean result = r ? Boolean.TRUE : Boolean.FALSE;
555                return(result);
556                }
557    
558            // Removing expired entries...
559            // See if this thread is going to be picked to expire stale entries.
560            // We pick at random in inverse proportion to the size of the cache,
561            // in order to make the average cost per call constant.
562            // We do this under a lock on the cache to eliminate races,
563            // but we can release this lock again as soon as we are done.
564            synchronized(_cache_tIHSIU_mirrorStatus)
565                {
566                final int size = _cache_tIHSIU_mirrorStatus.size();
567                if((size > 0) && (Rnd.fastRnd.nextInt(2 + size) == 0))
568                    {
569                    // Expire old entries.
570                    for(final Iterator<String> it = _cache_tIHSIU_mirrorStatus.keySet().iterator(); it.hasNext(); )
571                        {
572                        final String tag = it.next();
573                        // Remove a stale entry as long as
574                        // it does not still have a live thread updating it.
575                        if(_cache_tIHSIU_mirrorStatus.get(tag).canExpire())
576                            { it.remove(); }
577                        }
578                    }
579                }
580    
581            // See what we currently have in the cache for this host.
582            // Make this, and the possible launch of a check thread, atomic.
583            synchronized(_cache_tIHSIU_mirrorStatus)
584                {
585                final MirrorInfo extant = _cache_tIHSIU_mirrorStatus.get(hostAndPort);
586    
587                // If there is no entry or it has nearly expired
588                // then launch a background thread to get it updated.
589                // Create an entry that preserves the current value while we check;
590                // this limits us to at most one checker thread per host.
591                final Boolean result;
592                if((extant == null) || extant.expiresSoon())
593                    {
594                    // Compute result to return now.
595                    result = (extant == null) ? null : extant.isUp;
596    
597                    // Create a checker Thread.
598                    final Thread t = new Thread("checking liveness of server " + hostAndPort){
599                        @Override
600                        public final void run()
601                            {
602                            // Test synchronously...
603                            final Boolean r = testIfHTTPServerIsUp(hostAndPort, false);
604                            // This should always give a definite answer.
605                            assert(r != null);
606    
607                            // If we can update the cache, then do it.
608                            // We hold a lock to keep any test-and-update atomic.
609                            synchronized(_cache_tIHSIU_mirrorStatus)
610                                {
611                                // If we can update the cache then do so.
612                                // It can be updated if:
613                                //   * There is no entry.
614                                //   * There is no (live) checker thread.
615                                //   * There is a current entry but we are the checker thread.
616                                final MirrorInfo extant = _cache_tIHSIU_mirrorStatus.get(hostAndPort);
617                                if((extant == null) ||
618                                   (extant.checker == null) || !extant.checker.isAlive() ||
619                                   (extant.checker == Thread.currentThread()))
620                                    {
621                                    final MirrorInfo newInfo = new MirrorInfo(r, null);
622                                    final MirrorInfo oldInfo = _cache_tIHSIU_mirrorStatus.put(hostAndPort, newInfo);
623                                    // Report any change in status.
624                                    // Assumes that the Boolean can only be
625                                    // one of the fixed TRUE/FALSE/null
626                                    // so !=/== can be used rather than equals().
627                                    if((oldInfo == null) || (r != oldInfo.isUp))
628                                        {
629                                        System.out.println("[testIfHTTPServerIsUp("+hostAndPort+"): status change: now "+r+"; recheck after: "+(new Date(newInfo.validUntil))+"]");
630                                        }
631                                    }
632                                }
633                            }
634                        };
635    
636                    _cache_tIHSIU_mirrorStatus.put(hostAndPort,
637                                                   new MirrorInfo(result, t));
638                    // Mark the the thread as a daemon and start it off.
639                    t.setDaemon(true);
640                    t.start();
641                    }
642                else
643                    {
644                    result = extant.isUp;
645                    }
646    
647                return(result);
648                }
649            }
650    
651        /**Tests if a Web server is up and running and responding quickly.
652         * A full URL must be supplied.
653         * <p>
654         * Tests that the web server responds quickly and without error
655         * (we must get response code 200),
656         * to a HEAD request for a special light-weight page "/null.jsp"
657         * (redirections are allowed, eg to an index.html or index.jsp).
658         * <p>
659         * If an invalid (non-null) host name is supplied this will return false.
660         * <p>
661         * We should only do one of these at a time for a given host
662         * in order to avoid overloading any one remote mirror.
663         *
664         * @param serverURL  full URL for remote server (page) to test; never null
665         * @return true if the specified server seems to be up and running, false otherwise
666         */
667        public static boolean testIfHTTPServerIsUp(final URL serverURL)
668            {
669            if(serverURL == null)
670                { throw new IllegalArgumentException(); }
671    
672            for(int attemptsLeft = _tIHSIU_CONN_ATTEMPTS; --attemptsLeft >= 0; )
673                {
674                HttpURLConnection uc = null;
675                try
676                    {
677                    uc = (HttpURLConnection) serverURL.openConnection();
678                    if(!CoreConsts.AVOID_UNSAFE_TCP_TIMEOUTS)
679                        {
680                        uc.setConnectTimeout(_tIHSIU_CONN_TIMEOUT_MS); // Allow DNS lookup, TCP handshake, etc...
681                        uc.setReadTimeout(_tIHSIU_CONN_TIMEOUT_MS); // Page may be expensive/slow to generate.
682                        }
683                    uc.setUseCaches(false); // Force a fresh fetch/connection.
684                    uc.setAllowUserInteraction(false);
685                    uc.setInstanceFollowRedirects(true);
686                    uc.setRequestMethod("HEAD"); // Only need to get the status, not the data.
687                    uc.connect(); // Try to force a connection...
688                    final int responseCode = uc.getResponseCode();
689                    if(200 == responseCode)
690                        {
691                        // All seems OK, so return immediately.
692                        return(true);
693                        }
694    
695    //System.err.println("[testIfHTTPServerIsUp("+serverURL+") failed (attemptsLeft="+attemptsLeft+") with status code " + responseCode + ".]");
696                    }
697                // Any IOException probably indicates that the server is NOT well.
698                catch(final IOException e)
699                    {
700    //System.err.println("[testIfHTTPServerIsUp("+serverURL+") failed (attemptsLeft="+attemptsLeft+") with error " + e.getMessage() + ".]");
701                    }
702    
703                // Drop possibly-broken connection.
704                if(uc != null) { uc.disconnect(); }
705    
706                // Pause a while before trying again...
707                // This may give DNS and other temporary glitches time to pass.
708                try { Thread.sleep(15101 + 2*_tIHSIU_CONN_TIMEOUT_MS); }
709                catch(final InterruptedException e)
710                    {
711                    Thread.currentThread().interrupt();
712                    return(false); // Return immediately after interrupt.
713                    }
714                }
715    
716            // Failed to connect...
717            return(false);
718            }
719        }