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