001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.activemq.transport.failover;
018
019import java.io.BufferedReader;
020import java.io.FileReader;
021import java.io.IOException;
022import java.io.InputStreamReader;
023import java.io.InterruptedIOException;
024import java.net.InetAddress;
025import java.net.MalformedURLException;
026import java.net.URI;
027import java.net.URISyntaxException;
028import java.net.URL;
029import java.security.cert.X509Certificate;
030import java.util.ArrayList;
031import java.util.Collections;
032import java.util.HashSet;
033import java.util.Iterator;
034import java.util.LinkedHashMap;
035import java.util.LinkedHashSet;
036import java.util.List;
037import java.util.Map;
038import java.util.StringTokenizer;
039import java.util.concurrent.CopyOnWriteArrayList;
040import java.util.concurrent.atomic.AtomicReference;
041
042import org.apache.activemq.broker.SslContext;
043import org.apache.activemq.command.Command;
044import org.apache.activemq.command.ConnectionControl;
045import org.apache.activemq.command.ConnectionId;
046import org.apache.activemq.command.ConsumerControl;
047import org.apache.activemq.command.MessageDispatch;
048import org.apache.activemq.command.MessagePull;
049import org.apache.activemq.command.RemoveInfo;
050import org.apache.activemq.command.Response;
051import org.apache.activemq.state.ConnectionStateTracker;
052import org.apache.activemq.state.Tracked;
053import org.apache.activemq.thread.Task;
054import org.apache.activemq.thread.TaskRunner;
055import org.apache.activemq.thread.TaskRunnerFactory;
056import org.apache.activemq.transport.CompositeTransport;
057import org.apache.activemq.transport.DefaultTransportListener;
058import org.apache.activemq.transport.FutureResponse;
059import org.apache.activemq.transport.ResponseCallback;
060import org.apache.activemq.transport.Transport;
061import org.apache.activemq.transport.TransportFactory;
062import org.apache.activemq.transport.TransportListener;
063import org.apache.activemq.util.IOExceptionSupport;
064import org.apache.activemq.util.ServiceSupport;
065import org.apache.activemq.util.URISupport;
066import org.apache.activemq.wireformat.WireFormat;
067import org.slf4j.Logger;
068import org.slf4j.LoggerFactory;
069
070/**
071 * A Transport that is made reliable by being able to fail over to another
072 * transport when a transport failure is detected.
073 */
074public class FailoverTransport implements CompositeTransport {
075
076    private static final Logger LOG = LoggerFactory.getLogger(FailoverTransport.class);
077    private static final int DEFAULT_INITIAL_RECONNECT_DELAY = 10;
078    private static final int INFINITE = -1;
079    private TransportListener transportListener;
080    private volatile boolean disposed;
081    private final CopyOnWriteArrayList<URI> uris = new CopyOnWriteArrayList<URI>();
082    private final CopyOnWriteArrayList<URI> updated = new CopyOnWriteArrayList<URI>();
083
084    private final Object reconnectMutex = new Object();
085    private final Object backupMutex = new Object();
086    private final Object sleepMutex = new Object();
087    private final Object listenerMutex = new Object();
088    private final ConnectionStateTracker stateTracker = new ConnectionStateTracker();
089    private final Map<Integer, Command> requestMap = new LinkedHashMap<Integer, Command>();
090
091    private URI connectedTransportURI;
092    private URI failedConnectTransportURI;
093    private final AtomicReference<Transport> connectedTransport = new AtomicReference<Transport>();
094    private final TaskRunnerFactory reconnectTaskFactory;
095    private final TaskRunner reconnectTask;
096    private volatile boolean started;
097    private long initialReconnectDelay = DEFAULT_INITIAL_RECONNECT_DELAY;
098    private long maxReconnectDelay = 1000 * 30;
099    private double backOffMultiplier = 2d;
100    private long timeout = INFINITE;
101    private boolean useExponentialBackOff = true;
102    private boolean randomize = true;
103    private int maxReconnectAttempts = INFINITE;
104    private int startupMaxReconnectAttempts = INFINITE;
105    private int connectFailures;
106    private int warnAfterReconnectAttempts = 10;
107    private long reconnectDelay = DEFAULT_INITIAL_RECONNECT_DELAY;
108    private Exception connectionFailure;
109    private boolean firstConnection = true;
110    // optionally always have a backup created
111    private boolean backup = false;
112    private final List<BackupTransport> backups = new CopyOnWriteArrayList<BackupTransport>();
113    private int backupPoolSize = 1;
114    private boolean trackMessages = false;
115    private boolean trackTransactionProducers = true;
116    private int maxCacheSize = 128 * 1024;
117    private final TransportListener disposedListener = new DefaultTransportListener() {};
118    private boolean updateURIsSupported = true;
119    private boolean reconnectSupported = true;
120    // remember for reconnect thread
121    private SslContext brokerSslContext;
122    private String updateURIsURL = null;
123    private boolean rebalanceUpdateURIs = true;
124    private boolean doRebalance = false;
125    private boolean connectedToPriority = false;
126
127    private boolean priorityBackup = false;
128    private final ArrayList<URI> priorityList = new ArrayList<URI>();
129    private boolean priorityBackupAvailable = false;
130    private String nestedExtraQueryOptions;
131    private volatile boolean shuttingDown = false;
132
133    public FailoverTransport() {
134        brokerSslContext = SslContext.getCurrentSslContext();
135        stateTracker.setTrackTransactions(true);
136        // Setup a task that is used to reconnect the a connection async.
137        reconnectTaskFactory = new TaskRunnerFactory();
138        reconnectTaskFactory.init();
139        reconnectTask = reconnectTaskFactory.createTaskRunner(new Task() {
140            @Override
141            public boolean iterate() {
142                boolean result = false;
143                if (!started) {
144                    return result;
145                }
146                boolean buildBackup = true;
147                synchronized (backupMutex) {
148                    if ((connectedTransport.get() == null || doRebalance || priorityBackupAvailable) && !disposed) {
149                        result = doReconnect();
150                        buildBackup = false;
151                    }
152                }
153                if (buildBackup) {
154                    buildBackups();
155                    if (priorityBackup && !connectedToPriority) {
156                        try {
157                            doDelay();
158                            if (reconnectTask == null) {
159                                return true;
160                            }
161                            reconnectTask.wakeup();
162                        } catch (InterruptedException e) {
163                            LOG.debug("Reconnect task has been interrupted.", e);
164                        }
165                    }
166                } else {
167                    // build backups on the next iteration
168                    buildBackup = true;
169                    try {
170                        if (reconnectTask == null) {
171                            return true;
172                        }
173                        reconnectTask.wakeup();
174                    } catch (InterruptedException e) {
175                        LOG.debug("Reconnect task has been interrupted.", e);
176                    }
177                }
178                return result;
179            }
180
181        }, "ActiveMQ Failover Worker: " + System.identityHashCode(this));
182    }
183
184    private void processCommand(Object incoming) {
185        Command command = (Command) incoming;
186        if (command == null) {
187            return;
188        }
189        if (command.isResponse()) {
190            Object object = null;
191            synchronized (requestMap) {
192                object = requestMap.remove(Integer.valueOf(((Response) command).getCorrelationId()));
193            }
194            if (object != null && object.getClass() == Tracked.class) {
195                ((Tracked) object).onResponses(command);
196            }
197        }
198
199        if (command.isConnectionControl()) {
200            handleConnectionControl((ConnectionControl) command);
201        } else if (command.isConsumerControl()) {
202            ConsumerControl consumerControl = (ConsumerControl)command;
203            if (consumerControl.isClose()) {
204                stateTracker.processRemoveConsumer(consumerControl.getConsumerId(), RemoveInfo.LAST_DELIVERED_UNKNOWN);
205            }
206        }
207
208        if (transportListener != null) {
209            transportListener.onCommand(command);
210        }
211    }
212
213    private TransportListener createTransportListener(final Transport owner) {
214        return new TransportListener() {
215
216            @Override
217            public void onCommand(Object o) {
218                processCommand(o);
219            }
220
221            @Override
222            public void onException(IOException error) {
223                try {
224                    handleTransportFailure(owner, error);
225                } catch (InterruptedException e) {
226                    Thread.currentThread().interrupt();
227                    if (transportListener != null) {
228                        transportListener.onException(new InterruptedIOException());
229                    }
230                }
231            }
232
233            @Override
234            public void transportInterupted() {
235            }
236
237            @Override
238            public void transportResumed() {
239            }
240        };
241    }
242
243    public final void disposeTransport(Transport transport) {
244        transport.setTransportListener(disposedListener);
245        ServiceSupport.dispose(transport);
246    }
247
248    public final void handleTransportFailure(IOException e) throws InterruptedException {
249        handleTransportFailure(getConnectedTransport(), e);
250    }
251
252    public final void handleTransportFailure(Transport failed, IOException e) throws InterruptedException {
253        if (shuttingDown) {
254            // shutdown info sent and remote socket closed and we see that before a local close
255            // let the close do the work
256            return;
257        }
258
259        if (LOG.isTraceEnabled()) {
260            LOG.trace(this + " handleTransportFailure: " + e, e);
261        }
262
263        // could be blocked in write with the reconnectMutex held, but still needs to be whacked
264        Transport transport = null;
265
266        if (connectedTransport.compareAndSet(failed, null)) {
267            transport = failed;
268            if (transport != null) {
269                disposeTransport(transport);
270            }
271        }
272
273        synchronized (reconnectMutex) {
274            if (transport != null && connectedTransport.get() == null) {
275                boolean reconnectOk = false;
276
277                if (canReconnect()) {
278                    reconnectOk = true;
279                }
280
281                LOG.warn("Transport ({}) failed{} attempting to automatically reconnect",
282                         connectedTransportURI, (reconnectOk ? "," : ", not"), e);
283
284                failedConnectTransportURI = connectedTransportURI;
285                connectedTransportURI = null;
286                connectedToPriority = false;
287
288                if (reconnectOk) {
289                    // notify before any reconnect attempt so ack state can be whacked
290                    if (transportListener != null) {
291                        transportListener.transportInterupted();
292                    }
293
294                    reconnectTask.wakeup();
295                } else if (!isDisposed()) {
296                    propagateFailureToExceptionListener(e);
297                }
298            }
299        }
300    }
301
302    private boolean canReconnect() {
303        return started && 0 != calculateReconnectAttemptLimit();
304    }
305
306    public final void handleConnectionControl(ConnectionControl control) {
307        String reconnectStr = control.getReconnectTo();
308        if (LOG.isTraceEnabled()) {
309            LOG.trace("Received ConnectionControl: {}", control);
310        }
311
312        if (reconnectStr != null) {
313            reconnectStr = reconnectStr.trim();
314            if (reconnectStr.length() > 0) {
315                try {
316                    URI uri = new URI(reconnectStr);
317                    if (isReconnectSupported()) {
318                        reconnect(uri);
319                        LOG.info("Reconnected to: " + uri);
320                    }
321                } catch (Exception e) {
322                    LOG.error("Failed to handle ConnectionControl reconnect to " + reconnectStr, e);
323                }
324            }
325        }
326        processNewTransports(control.isRebalanceConnection(), control.getConnectedBrokers());
327    }
328
329    private final void processNewTransports(boolean rebalance, String newTransports) {
330        if (newTransports != null) {
331            newTransports = newTransports.trim();
332            if (newTransports.length() > 0 && isUpdateURIsSupported()) {
333                List<URI> list = new ArrayList<URI>();
334                StringTokenizer tokenizer = new StringTokenizer(newTransports, ",");
335                while (tokenizer.hasMoreTokens()) {
336                    String str = tokenizer.nextToken();
337                    try {
338                        URI uri = new URI(str);
339                        list.add(uri);
340                    } catch (Exception e) {
341                        LOG.error("Failed to parse broker address: " + str, e);
342                    }
343                }
344                if (list.isEmpty() == false) {
345                    try {
346                        updateURIs(rebalance, list.toArray(new URI[list.size()]));
347                    } catch (IOException e) {
348                        LOG.error("Failed to update transport URI's from: " + newTransports, e);
349                    }
350                }
351            }
352        }
353    }
354
355    @Override
356    public void start() throws Exception {
357        synchronized (reconnectMutex) {
358            LOG.debug("Started {}", this);
359            if (started) {
360                return;
361            }
362            started = true;
363            stateTracker.setMaxCacheSize(getMaxCacheSize());
364            stateTracker.setTrackMessages(isTrackMessages());
365            stateTracker.setTrackTransactionProducers(isTrackTransactionProducers());
366            if (connectedTransport.get() != null) {
367                stateTracker.restore(connectedTransport.get());
368            } else {
369                reconnect(false);
370            }
371        }
372    }
373
374    @Override
375    public void stop() throws Exception {
376        Transport transportToStop = null;
377        List<Transport> backupsToStop = new ArrayList<Transport>(backups.size());
378
379        try {
380            synchronized (reconnectMutex) {
381                if (LOG.isDebugEnabled()) {
382                    LOG.debug("Stopped {}", this);
383                }
384                if (!started) {
385                    return;
386                }
387                started = false;
388                disposed = true;
389
390                if (connectedTransport.get() != null) {
391                    transportToStop = connectedTransport.getAndSet(null);
392                }
393                reconnectMutex.notifyAll();
394            }
395            synchronized (sleepMutex) {
396                sleepMutex.notifyAll();
397            }
398        } finally {
399            reconnectTask.shutdown();
400            reconnectTaskFactory.shutdownNow();
401        }
402
403        synchronized(backupMutex) {
404            for (BackupTransport backup : backups) {
405                backup.setDisposed(true);
406                Transport transport = backup.getTransport();
407                if (transport != null) {
408                    transport.setTransportListener(disposedListener);
409                    backupsToStop.add(transport);
410                }
411            }
412            backups.clear();
413        }
414        for (Transport transport : backupsToStop) {
415            try {
416                LOG.trace("Stopped backup: {}", transport);
417                disposeTransport(transport);
418            } catch (Exception e) {
419            }
420        }
421        if (transportToStop != null) {
422            transportToStop.stop();
423        }
424    }
425
426    public long getInitialReconnectDelay() {
427        return initialReconnectDelay;
428    }
429
430    public void setInitialReconnectDelay(long initialReconnectDelay) {
431        this.initialReconnectDelay = initialReconnectDelay;
432    }
433
434    public long getMaxReconnectDelay() {
435        return maxReconnectDelay;
436    }
437
438    public void setMaxReconnectDelay(long maxReconnectDelay) {
439        this.maxReconnectDelay = maxReconnectDelay;
440    }
441
442    public long getReconnectDelay() {
443        return reconnectDelay;
444    }
445
446    public void setReconnectDelay(long reconnectDelay) {
447        this.reconnectDelay = reconnectDelay;
448    }
449
450    public double getReconnectDelayExponent() {
451        return backOffMultiplier;
452    }
453
454    public void setReconnectDelayExponent(double reconnectDelayExponent) {
455        this.backOffMultiplier = reconnectDelayExponent;
456    }
457
458    public Transport getConnectedTransport() {
459        return connectedTransport.get();
460    }
461
462    public URI getConnectedTransportURI() {
463        return connectedTransportURI;
464    }
465
466    public int getMaxReconnectAttempts() {
467        return maxReconnectAttempts;
468    }
469
470    public void setMaxReconnectAttempts(int maxReconnectAttempts) {
471        this.maxReconnectAttempts = maxReconnectAttempts;
472    }
473
474    public int getStartupMaxReconnectAttempts() {
475        return this.startupMaxReconnectAttempts;
476    }
477
478    public void setStartupMaxReconnectAttempts(int startupMaxReconnectAttempts) {
479        this.startupMaxReconnectAttempts = startupMaxReconnectAttempts;
480    }
481
482    public long getTimeout() {
483        return timeout;
484    }
485
486    public void setTimeout(long timeout) {
487        this.timeout = timeout;
488    }
489
490    /**
491     * @return Returns the randomize.
492     */
493    public boolean isRandomize() {
494        return randomize;
495    }
496
497    /**
498     * @param randomize The randomize to set.
499     */
500    public void setRandomize(boolean randomize) {
501        this.randomize = randomize;
502    }
503
504    public boolean isBackup() {
505        return backup;
506    }
507
508    public void setBackup(boolean backup) {
509        this.backup = backup;
510    }
511
512    public int getBackupPoolSize() {
513        return backupPoolSize;
514    }
515
516    public void setBackupPoolSize(int backupPoolSize) {
517        this.backupPoolSize = backupPoolSize;
518    }
519
520    public int getCurrentBackups() {
521        return this.backups.size();
522    }
523
524    public boolean isTrackMessages() {
525        return trackMessages;
526    }
527
528    public void setTrackMessages(boolean trackMessages) {
529        this.trackMessages = trackMessages;
530    }
531
532    public boolean isTrackTransactionProducers() {
533        return this.trackTransactionProducers;
534    }
535
536    public void setTrackTransactionProducers(boolean trackTransactionProducers) {
537        this.trackTransactionProducers = trackTransactionProducers;
538    }
539
540    public int getMaxCacheSize() {
541        return maxCacheSize;
542    }
543
544    public void setMaxCacheSize(int maxCacheSize) {
545        this.maxCacheSize = maxCacheSize;
546    }
547
548    public boolean isPriorityBackup() {
549        return priorityBackup;
550    }
551
552    public void setPriorityBackup(boolean priorityBackup) {
553        this.priorityBackup = priorityBackup;
554    }
555
556    public void setPriorityURIs(String priorityURIs) {
557        StringTokenizer tokenizer = new StringTokenizer(priorityURIs, ",");
558        while (tokenizer.hasMoreTokens()) {
559            String str = tokenizer.nextToken();
560            try {
561                URI uri = new URI(str);
562                priorityList.add(uri);
563            } catch (Exception e) {
564                LOG.error("Failed to parse broker address: " + str, e);
565            }
566        }
567    }
568
569    @Override
570    public void oneway(Object o) throws IOException {
571
572        Command command = (Command) o;
573        Exception error = null;
574        try {
575
576            synchronized (reconnectMutex) {
577
578                if (command != null && connectedTransport.get() == null) {
579                    if (command.isShutdownInfo()) {
580                        // Skipping send of ShutdownInfo command when not connected.
581                        return;
582                    } else if (command instanceof RemoveInfo || command.isMessageAck()) {
583                        // Simulate response to RemoveInfo command or MessageAck (as it will be stale)
584                        stateTracker.track(command);
585                        if (command.isResponseRequired()) {
586                            Response response = new Response();
587                            response.setCorrelationId(command.getCommandId());
588                            processCommand(response);
589                        }
590                        return;
591                    } else if (command instanceof MessagePull) {
592                        // Simulate response to MessagePull if timed as we can't honor that now.
593                        MessagePull pullRequest = (MessagePull) command;
594                        if (pullRequest.getTimeout() != 0) {
595                            MessageDispatch dispatch = new MessageDispatch();
596                            dispatch.setConsumerId(pullRequest.getConsumerId());
597                            dispatch.setDestination(pullRequest.getDestination());
598                            processCommand(dispatch);
599                        }
600                        return;
601                    }
602                }
603
604                // Keep trying until the message is sent.
605                for (int i = 0; !disposed; i++) {
606                    try {
607
608                        // Wait for transport to be connected.
609                        Transport transport = connectedTransport.get();
610                        long start = System.currentTimeMillis();
611                        boolean timedout = false;
612                        while (transport == null && !disposed && connectionFailure == null
613                                && !Thread.currentThread().isInterrupted() && willReconnect()) {
614
615                            LOG.trace("Waiting for transport to reconnect..: {}", command);
616                            long end = System.currentTimeMillis();
617                            if (command.isMessage() && timeout > 0 && (end - start > timeout)) {
618                                timedout = true;
619                                LOG.info("Failover timed out after {} ms", (end - start));
620                                break;
621                            }
622                            try {
623                                reconnectMutex.wait(100);
624                            } catch (InterruptedException e) {
625                                Thread.currentThread().interrupt();
626                                LOG.debug("Interupted:", e);
627                            }
628                            transport = connectedTransport.get();
629                        }
630
631                        if (transport == null) {
632                            // Previous loop may have exited due to use being
633                            // disposed.
634                            if (disposed) {
635                                error = new IOException("Transport disposed.");
636                            } else if (connectionFailure != null) {
637                                error = connectionFailure;
638                            } else if (timedout == true) {
639                                error = new IOException("Failover timeout of " + timeout + " ms reached.");
640                            } else if (!willReconnect()) {
641                                error = new IOException("Reconnect attempts of " + maxReconnectAttempts + " exceeded");
642                            } else {
643                                error = new IOException("Unexpected failure.");
644                            }
645                            break;
646                        }
647
648                        Tracked tracked = null;
649                        try {
650                            tracked = stateTracker.track(command);
651                        } catch (IOException ioe) {
652                            LOG.debug("Cannot track the command {} {}", command, ioe);
653                        }
654                        // If it was a request and it was not being tracked by
655                        // the state tracker,
656                        // then hold it in the requestMap so that we can replay
657                        // it later.
658                        synchronized (requestMap) {
659                            if (tracked != null && tracked.isWaitingForResponse()) {
660                                requestMap.put(Integer.valueOf(command.getCommandId()), tracked);
661                            } else if (tracked == null && command.isResponseRequired()) {
662                                requestMap.put(Integer.valueOf(command.getCommandId()), command);
663                            }
664                        }
665
666                        // Send the message.
667                        try {
668                            transport.oneway(command);
669                            stateTracker.trackBack(command);
670                            if (command.isShutdownInfo()) {
671                                shuttingDown = true;
672                            }
673                        } catch (IOException e) {
674
675                            // If the command was not tracked.. we will retry in
676                            // this method
677                            if (tracked == null && canReconnect()) {
678
679                                // since we will retry in this method.. take it
680                                // out of the request
681                                // map so that it is not sent 2 times on
682                                // recovery
683                                if (command.isResponseRequired()) {
684                                    requestMap.remove(Integer.valueOf(command.getCommandId()));
685                                }
686
687                                // Rethrow the exception so it will handled by
688                                // the outer catch
689                                throw e;
690                            } else {
691                                // Handle the error but allow the method to return since the
692                                // tracked commands are replayed on reconnect.
693                                LOG.debug("Send oneway attempt: {} failed for command: {}", i, command);
694                                handleTransportFailure(e);
695                            }
696                        }
697
698                        return;
699                    } catch (IOException e) {
700                        LOG.debug("Send oneway attempt: {} failed for command: {}", i, command);
701                        handleTransportFailure(e);
702                    }
703                }
704            }
705        } catch (InterruptedException e) {
706            // Some one may be trying to stop our thread.
707            Thread.currentThread().interrupt();
708            throw new InterruptedIOException();
709        }
710
711        if (!disposed) {
712            if (error != null) {
713                if (error instanceof IOException) {
714                    throw (IOException) error;
715                }
716                throw IOExceptionSupport.create(error);
717            }
718        }
719    }
720
721    private boolean willReconnect() {
722        return firstConnection || 0 != calculateReconnectAttemptLimit();
723    }
724
725    @Override
726    public FutureResponse asyncRequest(Object command, ResponseCallback responseCallback) throws IOException {
727        throw new AssertionError("Unsupported Method");
728    }
729
730    @Override
731    public Object request(Object command) throws IOException {
732        throw new AssertionError("Unsupported Method");
733    }
734
735    @Override
736    public Object request(Object command, int timeout) throws IOException {
737        throw new AssertionError("Unsupported Method");
738    }
739
740    @Override
741    public void add(boolean rebalance, URI u[]) {
742        boolean newURI = false;
743        for (URI uri : u) {
744            if (!contains(uri)) {
745                uris.add(uri);
746                newURI = true;
747            }
748        }
749        if (newURI) {
750            reconnect(rebalance);
751        }
752    }
753
754    @Override
755    public void remove(boolean rebalance, URI u[]) {
756        for (URI uri : u) {
757            uris.remove(uri);
758        }
759        // rebalance is automatic if any connected to removed/stopped broker
760    }
761
762    public void add(boolean rebalance, String u) {
763        try {
764            URI newURI = new URI(u);
765            if (contains(newURI) == false) {
766                uris.add(newURI);
767                reconnect(rebalance);
768            }
769
770        } catch (Exception e) {
771            LOG.error("Failed to parse URI: {}", u);
772        }
773    }
774
775    public void reconnect(boolean rebalance) {
776        synchronized (reconnectMutex) {
777            if (started) {
778                if (rebalance) {
779                    doRebalance = true;
780                }
781                LOG.debug("Waking up reconnect task");
782                try {
783                    reconnectTask.wakeup();
784                } catch (InterruptedException e) {
785                    Thread.currentThread().interrupt();
786                }
787            } else {
788                LOG.debug("Reconnect was triggered but transport is not started yet. Wait for start to connect the transport.");
789            }
790        }
791    }
792
793    private List<URI> getConnectList() {
794        // updated have precedence
795        LinkedHashSet<URI> uniqueUris = new LinkedHashSet<URI>(updated);
796        uniqueUris.addAll(uris);
797
798        boolean removed = false;
799        if (failedConnectTransportURI != null) {
800            removed = uniqueUris.remove(failedConnectTransportURI);
801        }
802
803        ArrayList<URI> l = new ArrayList<URI>(uniqueUris);
804        if (randomize) {
805            // Randomly, reorder the list by random swapping
806            for (int i = 0; i < l.size(); i++) {
807                // meed parenthesis due other JDKs (see AMQ-4826)
808                int p = ((int) (Math.random() * 100)) % l.size();
809                URI t = l.get(p);
810                l.set(p, l.get(i));
811                l.set(i, t);
812            }
813        }
814        if (removed) {
815            l.add(failedConnectTransportURI);
816        }
817
818        LOG.debug("urlList connectionList:{}, from: {}", l, uniqueUris);
819
820        return l;
821    }
822
823    @Override
824    public TransportListener getTransportListener() {
825        return transportListener;
826    }
827
828    @Override
829    public void setTransportListener(TransportListener commandListener) {
830        synchronized (listenerMutex) {
831            this.transportListener = commandListener;
832            listenerMutex.notifyAll();
833        }
834    }
835
836    @Override
837    public <T> T narrow(Class<T> target) {
838        if (target.isAssignableFrom(getClass())) {
839            return target.cast(this);
840        }
841        Transport transport = connectedTransport.get();
842        if (transport != null) {
843            return transport.narrow(target);
844        }
845        return null;
846    }
847
848    protected void restoreTransport(Transport t) throws Exception, IOException {
849        t.start();
850        // send information to the broker - informing it we are an ft client
851        ConnectionControl cc = new ConnectionControl();
852        cc.setFaultTolerant(true);
853        t.oneway(cc);
854        stateTracker.restore(t);
855        Map<Integer, Command> tmpMap = null;
856        synchronized (requestMap) {
857            tmpMap = new LinkedHashMap<Integer, Command>(requestMap);
858        }
859        for (Command command : tmpMap.values()) {
860            LOG.trace("restore requestMap, replay: {}", command);
861            t.oneway(command);
862        }
863    }
864
865    public boolean isUseExponentialBackOff() {
866        return useExponentialBackOff;
867    }
868
869    public void setUseExponentialBackOff(boolean useExponentialBackOff) {
870        this.useExponentialBackOff = useExponentialBackOff;
871    }
872
873    @Override
874    public String toString() {
875        return connectedTransportURI == null ? "unconnected" : connectedTransportURI.toString();
876    }
877
878    @Override
879    public String getRemoteAddress() {
880        Transport transport = connectedTransport.get();
881        if (transport != null) {
882            return transport.getRemoteAddress();
883        }
884        return null;
885    }
886
887    @Override
888    public boolean isFaultTolerant() {
889        return true;
890    }
891
892    private void doUpdateURIsFromDisk() {
893        // If updateURIsURL is specified, read the file and add any new
894        // transport URI's to this FailOverTransport.
895        // Note: Could track file timestamp to avoid unnecessary reading.
896        String fileURL = getUpdateURIsURL();
897        if (fileURL != null) {
898            BufferedReader in = null;
899            String newUris = null;
900            StringBuffer buffer = new StringBuffer();
901
902            try {
903                in = new BufferedReader(getURLStream(fileURL));
904                while (true) {
905                    String line = in.readLine();
906                    if (line == null) {
907                        break;
908                    }
909                    buffer.append(line);
910                }
911                newUris = buffer.toString();
912            } catch (IOException ioe) {
913                LOG.error("Failed to read updateURIsURL: {} {}",fileURL, ioe);
914            } finally {
915                if (in != null) {
916                    try {
917                        in.close();
918                    } catch (IOException ioe) {
919                        // ignore
920                    }
921                }
922            }
923
924            processNewTransports(isRebalanceUpdateURIs(), newUris);
925        }
926    }
927
928    final boolean doReconnect() {
929        Exception failure = null;
930        synchronized (reconnectMutex) {
931            List<URI> connectList = null;
932            // First ensure we are up to date.
933            doUpdateURIsFromDisk();
934
935            if (disposed || connectionFailure != null) {
936                reconnectMutex.notifyAll();
937            }
938            if ((connectedTransport.get() != null && !doRebalance && !priorityBackupAvailable) || disposed || connectionFailure != null) {
939                return false;
940            } else {
941                connectList = getConnectList();
942                if (connectList.isEmpty()) {
943                    failure = new IOException("No uris available to connect to.");
944                } else {
945                    if (doRebalance) {
946                        if (connectedToPriority || compareURIs(connectList.get(0), connectedTransportURI)) {
947                            // already connected to first in the list, no need to rebalance
948                            doRebalance = false;
949                            return false;
950                        } else {
951                            LOG.debug("Doing rebalance from: {} to {}", connectedTransportURI, connectList);
952
953                            try {
954                                Transport transport = this.connectedTransport.getAndSet(null);
955                                if (transport != null) {
956                                    disposeTransport(transport);
957                                }
958                            } catch (Exception e) {
959                                LOG.debug("Caught an exception stopping existing transport for rebalance", e);
960                            }
961                        }
962                        doRebalance = false;
963                    }
964
965                    resetReconnectDelay();
966
967                    Transport transport = null;
968                    URI uri = null;
969
970                    // If we have a backup already waiting lets try it.
971                    synchronized (backupMutex) {
972                        if ((priorityBackup || backup) && !backups.isEmpty()) {
973                            ArrayList<BackupTransport> l = new ArrayList<BackupTransport>(backups);
974                            if (randomize) {
975                                Collections.shuffle(l);
976                            }
977                            BackupTransport bt = l.remove(0);
978                            backups.remove(bt);
979                            transport = bt.getTransport();
980                            uri = bt.getUri();
981                            processCommand(bt.getBrokerInfo());
982                            if (priorityBackup && priorityBackupAvailable) {
983                                Transport old = this.connectedTransport.getAndSet(null);
984                                if (old != null) {
985                                    disposeTransport(old);
986                                }
987                                priorityBackupAvailable = false;
988                            }
989                        }
990                    }
991
992                    // When there was no backup and we are reconnecting for the first time
993                    // we honor the initialReconnectDelay before trying a new connection, after
994                    // this normal reconnect delay happens following a failed attempt.
995                    if (transport == null && !firstConnection && connectFailures == 0 && initialReconnectDelay > 0 && !disposed) {
996                        // reconnectDelay will be equal to initialReconnectDelay since we are on
997                        // the first connect attempt after we had a working connection, doDelay
998                        // will apply updates to move to the next reconnectDelay value based on
999                        // configuration.
1000                        doDelay();
1001                    }
1002
1003                    Iterator<URI> iter = connectList.iterator();
1004                    while ((transport != null || iter.hasNext()) && (connectedTransport.get() == null && !disposed)) {
1005
1006                        try {
1007                            SslContext.setCurrentSslContext(brokerSslContext);
1008
1009                            // We could be starting with a backup and if so we wait to grab a
1010                            // URI from the pool until next time around.
1011                            if (transport == null) {
1012                                uri = addExtraQueryOptions(iter.next());
1013                                transport = TransportFactory.compositeConnect(uri);
1014                            }
1015
1016                            LOG.debug("Attempting {}th connect to: {}", connectFailures, uri);
1017
1018                            transport.setTransportListener(createTransportListener(transport));
1019                            transport.start();
1020
1021                            if (started && !firstConnection) {
1022                                restoreTransport(transport);
1023                            }
1024
1025                            LOG.debug("Connection established");
1026
1027                            reconnectDelay = initialReconnectDelay;
1028                            connectedTransportURI = uri;
1029                            connectedTransport.set(transport);
1030                            connectedToPriority = isPriority(connectedTransportURI);
1031                            reconnectMutex.notifyAll();
1032                            connectFailures = 0;
1033
1034                            // Make sure on initial startup, that the transportListener
1035                            // has been initialized for this instance.
1036                            synchronized (listenerMutex) {
1037                                if (transportListener == null) {
1038                                    try {
1039                                        // if it isn't set after 2secs - it probably never will be
1040                                        listenerMutex.wait(2000);
1041                                    } catch (InterruptedException ex) {
1042                                    }
1043                                }
1044                            }
1045
1046                            if (transportListener != null) {
1047                                transportListener.transportResumed();
1048                            } else {
1049                                LOG.debug("transport resumed by transport listener not set");
1050                            }
1051
1052                            if (firstConnection) {
1053                                firstConnection = false;
1054                                LOG.info("Successfully connected to {}", uri);
1055                            } else {
1056                                LOG.info("Successfully reconnected to {}", uri);
1057                            }
1058
1059                            return false;
1060                        } catch (Exception e) {
1061                            failure = e;
1062                            LOG.debug("Connect fail to: {}, reason: {}", uri, e);
1063                            if (transport != null) {
1064                                try {
1065                                    transport.stop();
1066                                    transport = null;
1067                                } catch (Exception ee) {
1068                                    LOG.debug("Stop of failed transport: {} failed with reason: {}", transport, ee);
1069                                }
1070                            }
1071                        } finally {
1072                            SslContext.setCurrentSslContext(null);
1073                        }
1074                    }
1075                }
1076            }
1077
1078            int reconnectLimit = calculateReconnectAttemptLimit();
1079
1080            connectFailures++;
1081            if (reconnectLimit != INFINITE && connectFailures >= reconnectLimit) {
1082                LOG.error("Failed to connect to {} after: {} attempt(s)", connectList, connectFailures);
1083                connectionFailure = failure;
1084
1085                // Make sure on initial startup, that the transportListener has been
1086                // initialized for this instance.
1087                synchronized (listenerMutex) {
1088                    if (transportListener == null) {
1089                        try {
1090                            listenerMutex.wait(2000);
1091                        } catch (InterruptedException ex) {
1092                        }
1093                    }
1094                }
1095
1096                propagateFailureToExceptionListener(connectionFailure);
1097                return false;
1098            }
1099
1100            int warnInterval = getWarnAfterReconnectAttempts();
1101            if (warnInterval > 0 && (connectFailures == 1 || (connectFailures % warnInterval) == 0)) {
1102                LOG.warn("Failed to connect to {} after: {} attempt(s) with {}, continuing to retry.",
1103                         connectList, connectFailures, (failure == null ? "?" : failure.getLocalizedMessage()));
1104            }
1105        }
1106
1107        if (!disposed) {
1108            doDelay();
1109        }
1110
1111        return !disposed;
1112    }
1113
1114    private void doDelay() {
1115        if (reconnectDelay > 0) {
1116            synchronized (sleepMutex) {
1117                LOG.debug("Waiting {} ms before attempting connection", reconnectDelay);
1118                try {
1119                    sleepMutex.wait(reconnectDelay);
1120                } catch (InterruptedException e) {
1121                    Thread.currentThread().interrupt();
1122                }
1123            }
1124        }
1125
1126        if (useExponentialBackOff) {
1127            // Exponential increment of reconnect delay.
1128            reconnectDelay *= backOffMultiplier;
1129            if (reconnectDelay > maxReconnectDelay) {
1130                reconnectDelay = maxReconnectDelay;
1131            }
1132        }
1133    }
1134
1135    private void resetReconnectDelay() {
1136        if (!useExponentialBackOff || reconnectDelay == DEFAULT_INITIAL_RECONNECT_DELAY) {
1137            reconnectDelay = initialReconnectDelay;
1138        }
1139    }
1140
1141    /*
1142     * called with reconnectMutex held
1143     */
1144    private void propagateFailureToExceptionListener(Exception exception) {
1145        if (transportListener != null) {
1146            if (exception instanceof IOException) {
1147                transportListener.onException((IOException)exception);
1148            } else {
1149                transportListener.onException(IOExceptionSupport.create(exception));
1150            }
1151        }
1152        reconnectMutex.notifyAll();
1153    }
1154
1155    private int calculateReconnectAttemptLimit() {
1156        int maxReconnectValue = this.maxReconnectAttempts;
1157        if (firstConnection && this.startupMaxReconnectAttempts != INFINITE) {
1158            maxReconnectValue = this.startupMaxReconnectAttempts;
1159        }
1160        return maxReconnectValue;
1161    }
1162
1163    private boolean shouldBuildBackups() {
1164       return (backup && backups.size() < backupPoolSize) || (priorityBackup && !(priorityBackupAvailable || connectedToPriority));
1165    }
1166
1167    final boolean buildBackups() {
1168        synchronized (backupMutex) {
1169            if (!disposed && shouldBuildBackups()) {
1170                ArrayList<URI> backupList = new ArrayList<URI>(priorityList);
1171                List<URI> connectList = getConnectList();
1172                for (URI uri: connectList) {
1173                    if (!backupList.contains(uri)) {
1174                        backupList.add(uri);
1175                    }
1176                }
1177                // removed disposed backups
1178                List<BackupTransport> disposedList = new ArrayList<BackupTransport>();
1179                for (BackupTransport bt : backups) {
1180                    if (bt.isDisposed()) {
1181                        disposedList.add(bt);
1182                    }
1183                }
1184                backups.removeAll(disposedList);
1185                disposedList.clear();
1186                for (Iterator<URI> iter = backupList.iterator(); !disposed && iter.hasNext() && shouldBuildBackups(); ) {
1187                    URI uri = addExtraQueryOptions(iter.next());
1188                    if (connectedTransportURI != null && !connectedTransportURI.equals(uri)) {
1189                        try {
1190                            SslContext.setCurrentSslContext(brokerSslContext);
1191                            BackupTransport bt = new BackupTransport(this);
1192                            bt.setUri(uri);
1193                            if (!backups.contains(bt)) {
1194                                Transport t = TransportFactory.compositeConnect(uri);
1195                                t.setTransportListener(bt);
1196                                t.start();
1197                                bt.setTransport(t);
1198                                if (priorityBackup && isPriority(uri)) {
1199                                   priorityBackupAvailable = true;
1200                                   backups.add(0, bt);
1201                                   // if this priority backup overflows the pool
1202                                   // remove the backup with the lowest priority
1203                                   if (backups.size() > backupPoolSize) {
1204                                       BackupTransport disposeTransport = backups.remove(backups.size() - 1);
1205                                       disposeTransport.setDisposed(true);
1206                                       Transport transport = disposeTransport.getTransport();
1207                                       if (transport != null) {
1208                                           transport.setTransportListener(disposedListener);
1209                                           disposeTransport(transport);
1210                                       }
1211                                   }
1212                                } else {
1213                                    backups.add(bt);
1214                                }
1215                            }
1216                        } catch (Exception e) {
1217                            LOG.debug("Failed to build backup ", e);
1218                        } finally {
1219                            SslContext.setCurrentSslContext(null);
1220                        }
1221                    }
1222                }
1223            }
1224        }
1225        return false;
1226    }
1227
1228    protected boolean isPriority(URI uri) {
1229        if (!priorityBackup) {
1230            return false;
1231        }
1232
1233        if (!priorityList.isEmpty()) {
1234            for (URI priorityURI : priorityList) {
1235                if (compareURIs(priorityURI, uri)) {
1236                    return true;
1237                }
1238            }
1239
1240        } else if (!uris.isEmpty()) {
1241            return compareURIs(uris.get(0), uri);
1242        }
1243
1244        return false;
1245    }
1246
1247    @Override
1248    public boolean isDisposed() {
1249        return disposed;
1250    }
1251
1252    @Override
1253    public boolean isConnected() {
1254        return connectedTransport.get() != null;
1255    }
1256
1257    @Override
1258    public void reconnect(URI uri) throws IOException {
1259        add(true, new URI[]{uri});
1260    }
1261
1262    @Override
1263    public boolean isReconnectSupported() {
1264        return this.reconnectSupported;
1265    }
1266
1267    public void setReconnectSupported(boolean value) {
1268        this.reconnectSupported = value;
1269    }
1270
1271    @Override
1272    public boolean isUpdateURIsSupported() {
1273        return this.updateURIsSupported;
1274    }
1275
1276    public void setUpdateURIsSupported(boolean value) {
1277        this.updateURIsSupported = value;
1278    }
1279
1280    @Override
1281    public void updateURIs(boolean rebalance, URI[] updatedURIs) throws IOException {
1282        if (isUpdateURIsSupported()) {
1283            HashSet<URI> copy = new HashSet<URI>();
1284            synchronized (reconnectMutex) {
1285                copy.addAll(this.updated);
1286                updated.clear();
1287                if (updatedURIs != null && updatedURIs.length > 0) {
1288                    for (URI uri : updatedURIs) {
1289                        if (uri != null && !updated.contains(uri)) {
1290                            updated.add(uri);
1291                            if (failedConnectTransportURI != null && failedConnectTransportURI.equals(uri)) {
1292                                failedConnectTransportURI = null;
1293                            }
1294                        }
1295                    }
1296                }
1297            }
1298            if (!(copy.isEmpty() && updated.isEmpty()) && !copy.equals(new HashSet<URI>(updated))) {
1299                buildBackups();
1300                reconnect(rebalance);
1301            }
1302        }
1303    }
1304
1305    /**
1306     * @return the updateURIsURL
1307     */
1308    public String getUpdateURIsURL() {
1309        return this.updateURIsURL;
1310    }
1311
1312    /**
1313     * @param updateURIsURL the updateURIsURL to set
1314     */
1315    public void setUpdateURIsURL(String updateURIsURL) {
1316        this.updateURIsURL = updateURIsURL;
1317    }
1318
1319    /**
1320     * @return the rebalanceUpdateURIs
1321     */
1322    public boolean isRebalanceUpdateURIs() {
1323        return this.rebalanceUpdateURIs;
1324    }
1325
1326    /**
1327     * @param rebalanceUpdateURIs the rebalanceUpdateURIs to set
1328     */
1329    public void setRebalanceUpdateURIs(boolean rebalanceUpdateURIs) {
1330        this.rebalanceUpdateURIs = rebalanceUpdateURIs;
1331    }
1332
1333    @Override
1334    public int getReceiveCounter() {
1335        Transport transport = connectedTransport.get();
1336        if (transport == null) {
1337            return 0;
1338        }
1339        return transport.getReceiveCounter();
1340    }
1341
1342    public int getConnectFailures() {
1343        return connectFailures;
1344    }
1345
1346    public void connectionInterruptProcessingComplete(ConnectionId connectionId) {
1347        synchronized (reconnectMutex) {
1348            stateTracker.connectionInterruptProcessingComplete(this, connectionId);
1349        }
1350    }
1351
1352    public ConnectionStateTracker getStateTracker() {
1353        return stateTracker;
1354    }
1355
1356    public boolean isConnectedToPriority() {
1357        return connectedToPriority;
1358    }
1359
1360    private boolean contains(URI newURI) {
1361        boolean result = false;
1362        for (URI uri : uris) {
1363            if (compareURIs(newURI, uri)) {
1364                result = true;
1365                break;
1366            }
1367        }
1368
1369        return result;
1370    }
1371
1372    private boolean compareURIs(final URI first, final URI second) {
1373
1374        boolean result = false;
1375        if (first == null || second == null) {
1376            return result;
1377        }
1378
1379        if (first.getPort() == second.getPort()) {
1380            InetAddress firstAddr = null;
1381            InetAddress secondAddr = null;
1382            try {
1383                firstAddr = InetAddress.getByName(first.getHost());
1384                secondAddr = InetAddress.getByName(second.getHost());
1385
1386                if (firstAddr.equals(secondAddr)) {
1387                    result = true;
1388                }
1389
1390            } catch(IOException e) {
1391
1392                if (firstAddr == null) {
1393                    LOG.error("Failed to Lookup INetAddress for URI[{}] : {}", first, e);
1394                } else {
1395                    LOG.error("Failed to Lookup INetAddress for URI[{}] : {}", second, e);
1396                }
1397
1398                if (first.getHost().equalsIgnoreCase(second.getHost())) {
1399                    result = true;
1400                }
1401            }
1402        }
1403
1404        return result;
1405    }
1406
1407    private InputStreamReader getURLStream(String path) throws IOException {
1408        InputStreamReader result = null;
1409        URL url = null;
1410        try {
1411            url = new URL(path);
1412            result = new InputStreamReader(url.openStream());
1413        } catch (MalformedURLException e) {
1414            // ignore - it could be a path to a a local file
1415        }
1416        if (result == null) {
1417            result = new FileReader(path);
1418        }
1419        return result;
1420    }
1421
1422    private URI addExtraQueryOptions(URI uri) {
1423        try {
1424            if( nestedExtraQueryOptions!=null && !nestedExtraQueryOptions.isEmpty() ) {
1425                if( uri.getQuery() == null ) {
1426                    uri = URISupport.createURIWithQuery(uri, nestedExtraQueryOptions);
1427                } else {
1428                    uri = URISupport.createURIWithQuery(uri, uri.getQuery()+"&"+nestedExtraQueryOptions);
1429                }
1430            }
1431        } catch (URISyntaxException e) {
1432            throw new RuntimeException(e);
1433        }
1434        return uri;
1435    }
1436
1437    public void setNestedExtraQueryOptions(String nestedExtraQueryOptions) {
1438        this.nestedExtraQueryOptions = nestedExtraQueryOptions;
1439    }
1440
1441    public int getWarnAfterReconnectAttempts() {
1442        return warnAfterReconnectAttempts;
1443    }
1444
1445    /**
1446     * Sets the number of Connect / Reconnect attempts that must occur before a warn message
1447     * is logged indicating that the transport is not connected.  This can be useful when the
1448     * client is running inside some container or service as it give an indication of some
1449     * problem with the client connection that might not otherwise be visible.  To disable the
1450     * log messages this value should be set to a value @{code attempts <= 0}
1451     *
1452     * @param warnAfterReconnectAttempts
1453     *      The number of failed connection attempts that must happen before a warning is logged.
1454     */
1455    public void setWarnAfterReconnectAttempts(int warnAfterReconnectAttempts) {
1456        this.warnAfterReconnectAttempts = warnAfterReconnectAttempts;
1457    }
1458
1459    @Override
1460    public X509Certificate[] getPeerCertificates() {
1461        Transport transport = connectedTransport.get();
1462        if (transport != null) {
1463            return transport.getPeerCertificates();
1464        } else {
1465            return null;
1466        }
1467    }
1468
1469    @Override
1470    public void setPeerCertificates(X509Certificate[] certificates) {
1471    }
1472
1473    @Override
1474    public WireFormat getWireFormat() {
1475        Transport transport = connectedTransport.get();
1476        if (transport != null) {
1477            return transport.getWireFormat();
1478        } else {
1479            return null;
1480        }
1481    }
1482}