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