├── .gitignore ├── README.md ├── config └── eclipse │ ├── cleanup.xml │ └── formatter.xml ├── pom.xml └── src ├── main └── java │ └── net │ └── joshdevins │ └── rabbitmq │ └── client │ └── ha │ ├── AbstractHaConnectionListener.java │ ├── BooleanReentrantLatch.java │ ├── HaChannelProxy.java │ ├── HaConnectionFactory.java │ ├── HaConnectionListener.java │ ├── HaConnectionProxy.java │ ├── HaConsumerProxy.java │ ├── HaUtils.java │ ├── InvocationHandlerUtils.java │ └── retry │ ├── AlwaysRetryStrategy.java │ ├── BlockingRetryStrategy.java │ ├── NeverRetryStrategy.java │ ├── RetryStrategy.java │ └── SimpleRetryStrategy.java └── test ├── java └── net │ └── joshdevins │ └── rabbitmq │ └── client │ └── ha │ ├── BooleanReentrantLatchTest.java │ ├── it │ ├── RabbitBlockingConsumerIntegrationTest.java │ ├── RabbitConsistencyIntegrationTest.java │ ├── RabbitTemplateConsumerIntegrationTest.java │ ├── RabbitTemplatePublisherIntegrationTest.java │ ├── TestChannelCallback.java │ └── TestHaConnectionListener.java │ └── retry │ ├── AlwaysRetryStrategyTest.java │ ├── BlockingRetryStrategyTest.java │ └── NeverRetryStrategyTest.java └── resources ├── META-INF └── spring │ └── applicationContext.xml └── log4j.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | 3 | # Eclipse 4 | .classpath 5 | .project 6 | .settings 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RabbitMQ HA Client 2 | 3 | Some AMQP brokers and specifically RabbitMQ do not support HA out of the box. Rationale for this varies as much as peoples' requirements do, so it's not super surprising that this is the case. However there are basic HA possibilities with RabbitMQ, specifically active-passive brokers using [Pacemaker](http://www.rabbitmq.com/pacemaker.html) or behind a plain old TCP load balancer. For a better description of the latter scenario, please read the [blog post](http://www.joshdevins.net/2010/04/16/rabbitmq-ha-testing-with-haproxy) that started this project. Suffice it to say that in order to make this and many HA topologies work, a client that can do automatic, graceful connection recovery and message redelivery is required. Bonus points of course if you can auto-magically de-duplicate messages in the consumer as is done in [Beetle](http://github.com/xing/beetle). 4 | 5 | ## Functionality 6 | 7 | ### Completed 8 | 9 | * callbacks to listeners on: connection, connection failure, reconnection, reconnection failure, disconnection (facilitates auto-delete queue recreation) 10 | * creating a new connection while a broker is down (publisher will block until connection is created, so you should probably lazily create your connections) 11 | * publishing messages while a broker is down (publisher will block until connection returns) 12 | * publishing messages after broker has restarted 13 | * consuming messages (non-blocking) using basicGet while a broker is down (consumer will block on basicGet until connection returns) 14 | * consuming messages (non-blocking) using basicGet after a broker has restarted 15 | * consuming messages (blocking) using basicConsume after a broker has restarted (consumer will not notice connection drop at all) 16 | * consistency testing (non-transactional, durable queue): 17 | * 1000 publishes, 20ms between publishes, ~50 messages/sec, 1 node restart, 0 messages lost 18 | * 1000 publishes, 10ms between publishes, ~100 messages/sec, 1 node restart, 1 message lost 19 | 20 | ### To Be Done 21 | 22 | * handling of ACKs after a reconnect for messages sent before reconnect 23 | * adding more tests of course 24 | * documentation and examples, specifically what to do on connection and reconnection events (auto-delete queue recreation, etc.) 25 | * handling of transactions after a reconnect for messages sent before reconnect (transaction will fail) 26 | * consistency testing (transactional, durable queue) 27 | * more customizability and tuning for reconnection values 28 | * hook in message receipt path to do message de-duplication 29 | * ability to specify non-blocking option while broker is down/reconnecting (i.e. queue up messages in-memory; this wouldn't make much sense with transactional channels though) 30 | 31 | ## Usage 32 | 33 | Basically this is a drop-in replacement for the standard RabbitMQ ConnectionFactory. Anything that uses that should be able to use the HaConnectionFactory instead. Be certain to review the retry strategies (there are some built-in) if you want custom behaviour on channel failures. 34 | 35 | ## License 36 | 37 | Copyright 2010-2012 [Josh Devins](http://www.joshdevins.net) 38 | 39 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 40 | 41 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 42 | 43 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 44 | 45 | ## Resources 46 | 47 | This RabbitMQ HA client internally makes use of the standard [RabbitMQ AMQP client](http://www.rabbitmq.com/java-client.html) and has borrowed ideas and inspiration from the following sources. Please respect their licenses. 48 | 49 | * [RabbitMQ Java messagepaterns library, v0.1.3](http://hg.rabbitmq.com/rabbitmq-java-messagepatterns) 50 | * [Spring Framework v3.0.x](http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/jms.html) 51 | -------------------------------------------------------------------------------- /config/eclipse/cleanup.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /config/eclipse/formatter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | net.joshdevins.rabbitmq 6 | rabbitmq-ha-client 7 | 0.2.0-SNAPSHOT 8 | jar 9 | 10 | RabbitMQ HA Client 11 | http://github.com/joshdevins/rabbitmq-ha-client 12 | 13 | 14 | scm:git:git://github.com/joshdevins/rabbitmq-ha-client.git 15 | scm:git:git@github.com:joshdevins/rabbitmq-ha-client.git 16 | http://github.com/joshdevins/rabbitmq-ha-client/ 17 | 18 | 19 | 20 | github 21 | http://github.com/joshdevins/rabbitmq-ha-client/issues#issue/ 22 | 23 | 24 | 25 | 26 | soundcloud.thirdparty.snapshots 27 | SoundCloud 3rd Party - Snapshots 28 | http://maven.int.s-cloud.net/content/repositories/thirdparty_snapshots 29 | 30 | 31 | soundcloud.thirdparty.releases 32 | SoundCloud 3rd Party - Releases 33 | http://maven.int.s-cloud.net/content/repositories/thirdparty_releases 34 | 35 | 36 | 37 | 38 | 39 | com.rabbitmq 40 | amqp-client 41 | 2.8.4 42 | 43 | 44 | commons-cli 45 | commons-cli 46 | 47 | 48 | commons-io 49 | commons-io 50 | 51 | 52 | 53 | 54 | 55 | 56 | log4j 57 | log4j 58 | 1.2.15 59 | 60 | 61 | javax.mail 62 | mail 63 | 64 | 65 | javax.jms 66 | jms 67 | 68 | 69 | com.sun.jdmk 70 | jmxtools 71 | 72 | 73 | com.sun.jmx 74 | jmxri 75 | 76 | 77 | 78 | 79 | commons-lang 80 | commons-lang 81 | 2.5 82 | 83 | 84 | 85 | 86 | junit 87 | junit 88 | 4.10 89 | test 90 | 91 | 92 | org.mockito 93 | mockito-core 94 | 1.9.0 95 | test 96 | 97 | 98 | 99 | 100 | 101 | org.springframework.amqp 102 | spring-rabbit 103 | 1.0.0.RELEASE 104 | test 105 | 106 | 107 | org.springframework 108 | spring-test 109 | 3.1.1.RELEASE 110 | test 111 | 112 | 113 | 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-compiler-plugin 119 | 2.3.2 120 | 121 | 1.5 122 | 1.5 123 | 124 | 125 | 126 | org.apache.maven.plugins 127 | maven-eclipse-plugin 128 | 2.9 129 | 130 | true 131 | true 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-surefire-plugin 137 | 2.8.1 138 | 139 | true 140 | 141 | 142 | 143 | surefire-test 144 | test 145 | 146 | test 147 | 148 | 149 | false 150 | 151 | **/*IntegrationTest.java 152 | 153 | 154 | 155 | 156 | surefire-itest 157 | integration-test 158 | 159 | test 160 | 161 | 162 | true 163 | 164 | **/*IntegrationTest.java 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | UTF-8 175 | 176 | 177 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/AbstractHaConnectionListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import com.rabbitmq.client.ShutdownSignalException; 20 | 21 | /** 22 | * Abstract implementation of {@link HaConnectionListener} with empty method implementations. 23 | * 24 | * @author Josh Devins 25 | */ 26 | public abstract class AbstractHaConnectionListener implements HaConnectionListener { 27 | 28 | public void onConnectFailure(final HaConnectionProxy connectionProxy, final Exception exception) { 29 | } 30 | 31 | public void onConnection(final HaConnectionProxy connectionProxy) { 32 | } 33 | 34 | public void onDisconnect(final HaConnectionProxy connectionProxy, 35 | final ShutdownSignalException shutdownSignalException) { 36 | } 37 | 38 | public void onReconnectFailure(final HaConnectionProxy connectionProxy, final Exception exception) { 39 | } 40 | 41 | public void onReconnection(final HaConnectionProxy connectionProxy) { 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/BooleanReentrantLatch.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.concurrent.locks.AbstractQueuedSynchronizer; 21 | 22 | /** 23 | * A reentrant latch with simple open/closed semantics. 24 | * 25 | * @author Josh Devins 26 | */ 27 | public class BooleanReentrantLatch { 28 | 29 | /** 30 | * Synchronization control for {@link BooleanReentrantLatch}. Uses {@link AbstractQueuedSynchronizer} state to 31 | * represent gate state. 32 | * 33 | *

34 | * states: open == 0, closed == 1 35 | *

36 | */ 37 | private static final class Sync extends AbstractQueuedSynchronizer { 38 | 39 | private static final long serialVersionUID = -7271227048279204885L; 40 | 41 | protected Sync(final boolean open) { 42 | setState(open ? 0 : 1); 43 | } 44 | 45 | @Override 46 | public boolean tryReleaseShared(final int releases) { 47 | 48 | // only open gate if it's currently closed (atomically) 49 | return compareAndSetState(1, 0); 50 | } 51 | 52 | protected boolean isOpen() { 53 | return getState() == 0; 54 | } 55 | 56 | @Override 57 | protected int tryAcquireShared(final int acquires) { 58 | 59 | // if acquires is 0, this is a test only not an acquisition attempt 60 | if (acquires == 0) { 61 | 62 | // if open, thread can proceed right away 63 | // if closed, thread needs to wait 64 | // this is a fake out since lock is not actually obtained 65 | return isOpen() ? 1 : -1; 66 | } 67 | 68 | // if acquires is 1, this is an acquisition attempt 69 | // close gate even if it's already closed 70 | setState(1); 71 | return 1; 72 | } 73 | } 74 | 75 | private final Sync sync; 76 | 77 | public BooleanReentrantLatch() { 78 | this(true); 79 | } 80 | 81 | public BooleanReentrantLatch(final boolean open) { 82 | sync = new Sync(open); 83 | } 84 | 85 | public void close() { 86 | sync.acquireShared(1); 87 | } 88 | 89 | /** 90 | * Equality is based on the current state of both latches. That is, they 91 | * must both be open or both closed. 92 | */ 93 | @Override 94 | public boolean equals(final Object obj) { 95 | 96 | if (!(obj instanceof BooleanReentrantLatch)) { 97 | return false; 98 | } 99 | 100 | BooleanReentrantLatch rhs = (BooleanReentrantLatch) obj; 101 | return isOpen() == rhs.isOpen(); 102 | } 103 | 104 | /** 105 | * For information purposes only. It is safest to call {@link #waitUntilOpen(long)}. 106 | */ 107 | public boolean isClosed() { 108 | return !isOpen(); 109 | } 110 | 111 | /** 112 | * For information purposes only. It is safest to call {@link #waitUntilOpen(long)}. 113 | */ 114 | public boolean isOpen() { 115 | return sync.isOpen(); 116 | } 117 | 118 | public void open() { 119 | sync.releaseShared(1); 120 | } 121 | 122 | /** 123 | * Returns a string identifying this latch, as well as its state: open or 124 | * closed. 125 | * 126 | * @return a string identifying this latch, as well as its state 127 | */ 128 | @Override 129 | public String toString() { 130 | return super.toString() + "[" + (isOpen() ? "open" : "closed") + "]"; 131 | } 132 | 133 | /** 134 | * Waits for the gate to open. If this returns without an exception, the 135 | * gate is open. 136 | */ 137 | public void waitUntilOpen() throws InterruptedException { 138 | sync.acquireSharedInterruptibly(0); 139 | } 140 | 141 | /** 142 | * Waits for the gate to open. 143 | * 144 | * @return true if the gate is now open, false if the timeout was reached 145 | */ 146 | public boolean waitUntilOpen(final long timeout, final TimeUnit unit) throws InterruptedException { 147 | return sync.tryAcquireSharedNanos(0, unit.toNanos(timeout)); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/HaChannelProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import java.io.IOException; 20 | import java.lang.reflect.InvocationHandler; 21 | import java.lang.reflect.Method; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | 24 | import net.joshdevins.rabbitmq.client.ha.retry.RetryStrategy; 25 | 26 | import org.apache.log4j.Logger; 27 | 28 | import com.rabbitmq.client.AlreadyClosedException; 29 | import com.rabbitmq.client.Channel; 30 | import com.rabbitmq.client.Consumer; 31 | 32 | /** 33 | * A proxy around the standard {@link Channel}. 34 | * 35 | * @author Josh Devins 36 | */ 37 | public class HaChannelProxy implements InvocationHandler { 38 | 39 | private static final Logger LOG = Logger.getLogger(HaChannelProxy.class); 40 | 41 | private static final String BASIC_CONSUME_METHOD_NAME = "basicConsume"; 42 | 43 | private static final String CLOSE_METHOD_NAME = "close"; 44 | 45 | private final HaConnectionProxy connectionProxy; 46 | 47 | private Channel target; 48 | 49 | private final RetryStrategy retryStrategy; 50 | 51 | private final BooleanReentrantLatch connectionLatch; 52 | 53 | private final ConcurrentHashMap consumerProxies; 54 | 55 | public HaChannelProxy(final HaConnectionProxy connectionProxy, final Channel target, 56 | final RetryStrategy retryStrategy) { 57 | 58 | assert connectionProxy != null; 59 | assert target != null; 60 | assert retryStrategy != null; 61 | 62 | this.connectionProxy = connectionProxy; 63 | this.target = target; 64 | this.retryStrategy = retryStrategy; 65 | 66 | connectionLatch = new BooleanReentrantLatch(); 67 | consumerProxies = new ConcurrentHashMap(); 68 | } 69 | 70 | public void closeConnectionLatch() { 71 | connectionLatch.close(); 72 | } 73 | 74 | public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { 75 | 76 | if (LOG.isDebugEnabled()) { 77 | LOG.debug("Invoke: " + method.getName()); 78 | } 79 | 80 | // TODO: Rethink this assumption! 81 | // close is special since we can ignore failures safely 82 | if (method.getName().equals(CLOSE_METHOD_NAME)) { 83 | try { 84 | target.close(); 85 | } catch (Exception e) { 86 | 87 | if (LOG.isDebugEnabled()) { 88 | LOG.debug("Failed to close underlying channel, not a problem: " + e.getMessage()); 89 | } 90 | } 91 | 92 | connectionProxy.removeClosedChannel(this); 93 | 94 | // FIXME: Is this the right return value for a void method? 95 | return null; 96 | } 97 | 98 | // invoke a method max times 99 | Exception lastException = null; 100 | boolean shutdownRecoverable = true; 101 | boolean keepOnInvoking = true; 102 | 103 | // don't check for open state, just let it fail 104 | // this will ensure that after a connection has been made, setup can 105 | // proceed before letting operations retry 106 | for (int numOperationInvocations = 1; keepOnInvoking && shutdownRecoverable; numOperationInvocations++) { 107 | 108 | // sych on target Channel to make sure it's not being replaced 109 | synchronized (target) { 110 | 111 | try { 112 | 113 | // wrap the incoming consumer with a proxy, then invoke 114 | if (method.getName().equals(BASIC_CONSUME_METHOD_NAME)) { 115 | 116 | // Consumer is always the last argument, let it fail if not 117 | Consumer targetConsumer = (Consumer) args[args.length - 1]; 118 | 119 | // already wrapped? 120 | if (!(targetConsumer instanceof HaConsumerProxy)) { 121 | 122 | // check to see if we already have a proxied Consumer 123 | HaConsumerProxy consumerProxy = consumerProxies.get(targetConsumer); 124 | if (consumerProxy == null) { 125 | consumerProxy = new HaConsumerProxy(targetConsumer, this, method, args); 126 | } 127 | 128 | // currently we think there is not a proxy 129 | // try to do this atomically and worse case someone else already created one 130 | HaConsumerProxy existingConsumerProxy = consumerProxies.putIfAbsent(targetConsumer, 131 | consumerProxy); 132 | 133 | // replace with the proxy for the real invocation 134 | args[args.length - 1] = existingConsumerProxy == null ? consumerProxy 135 | : existingConsumerProxy; 136 | } 137 | } 138 | 139 | // delegate all other method invocations 140 | return InvocationHandlerUtils.delegateMethodInvocation(method, args, target); 141 | 142 | // deal with exceptions outside the synchronized block so 143 | // that if a reconnection does occur, it can replace the 144 | // target 145 | } catch (IOException ioe) { 146 | lastException = ioe; 147 | shutdownRecoverable = HaUtils.isShutdownRecoverable(ioe); 148 | 149 | } catch (AlreadyClosedException ace) { 150 | lastException = ace; 151 | shutdownRecoverable = HaUtils.isShutdownRecoverable(ace); 152 | } catch (Throwable t) { 153 | // catch all 154 | if (LOG.isDebugEnabled()) { 155 | LOG.debug("Catch all", t); 156 | } 157 | 158 | throw t; 159 | } 160 | } 161 | 162 | // only keep on invoking if error is recoverable 163 | if (shutdownRecoverable) { 164 | 165 | if (LOG.isDebugEnabled()) { 166 | LOG.debug("Invocation failed, calling retry strategy: " + lastException.getMessage()); 167 | } 168 | 169 | keepOnInvoking = retryStrategy.shouldRetry(lastException, numOperationInvocations, connectionLatch); 170 | } 171 | } 172 | 173 | if (shutdownRecoverable) { 174 | LOG.warn("Operation invocation failed after retry strategy gave up", lastException); 175 | } else { 176 | LOG.warn("Operation invocation failed with unrecoverable shutdown signal", lastException); 177 | } 178 | 179 | throw lastException; 180 | } 181 | 182 | protected Channel getTargetChannel() { 183 | return target; 184 | } 185 | 186 | protected void markAsClosed() { 187 | connectionLatch.close(); 188 | } 189 | 190 | protected void markAsOpen() { 191 | connectionLatch.open(); 192 | } 193 | 194 | protected void setTargetChannel(final Channel target) { 195 | 196 | assert target != null; 197 | 198 | if (LOG.isDebugEnabled() && this.target != null) { 199 | LOG.debug("Replacing channel: channel=" + this.target.toString()); 200 | } 201 | 202 | synchronized (this.target) { 203 | 204 | this.target = target; 205 | 206 | if (LOG.isDebugEnabled() && this.target != null) { 207 | LOG.debug("Replaced channel: channel=" + this.target.toString()); 208 | } 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/HaConnectionFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import java.io.IOException; 20 | import java.lang.reflect.Proxy; 21 | import java.net.ConnectException; 22 | import java.util.HashSet; 23 | import java.util.Set; 24 | import java.util.concurrent.ConcurrentSkipListSet; 25 | import java.util.concurrent.ExecutorService; 26 | import java.util.concurrent.Executors; 27 | 28 | import net.joshdevins.rabbitmq.client.ha.retry.BlockingRetryStrategy; 29 | import net.joshdevins.rabbitmq.client.ha.retry.RetryStrategy; 30 | 31 | import org.apache.commons.lang.Validate; 32 | import org.apache.log4j.Logger; 33 | 34 | import com.rabbitmq.client.Address; 35 | import com.rabbitmq.client.Channel; 36 | import com.rabbitmq.client.Connection; 37 | import com.rabbitmq.client.ConnectionFactory; 38 | import com.rabbitmq.client.ShutdownListener; 39 | import com.rabbitmq.client.ShutdownSignalException; 40 | 41 | /** 42 | * A simple {@link ConnectionFactory} proxy that further proxies any created {@link Connection} and subsequent 43 | * {@link Channel}s. Sadly a dynamic proxy 44 | * cannot be used since the RabbitMQ {@link ConnectionFactory} does not have an 45 | * interface. As such, this class extends {@link ConnectionFactory} and 46 | * overrides necessary methods. 47 | * 48 | *

49 | * TODO: Create utility to populate some connections in the CachingConnectionFactory on startup. Should fail fast but 50 | * will reconnect using this underlying. 51 | *

52 | * 53 | * @author Josh Devins 54 | */ 55 | public class HaConnectionFactory extends ConnectionFactory { 56 | 57 | private class ConnectionSet { 58 | 59 | private final Connection wrapped; 60 | 61 | private final HaConnectionProxy proxy; 62 | 63 | private final HaShutdownListener listener; 64 | 65 | private ConnectionSet(final Connection wrapped, final HaConnectionProxy proxy, final HaShutdownListener listener) { 66 | 67 | this.wrapped = wrapped; 68 | this.proxy = proxy; 69 | this.listener = listener; 70 | } 71 | } 72 | 73 | /** 74 | * Listener to {@link Connection} shutdowns. Hooks together the {@link HaConnectionProxy} to the shutdown event. 75 | */ 76 | private class HaShutdownListener implements ShutdownListener { 77 | 78 | private final HaConnectionProxy connectionProxy; 79 | 80 | // needs also to be able to call asyncReconnect or to create own 81 | // ReconnectionTask 82 | public HaShutdownListener(final HaConnectionProxy connectionProxy) { 83 | 84 | assert connectionProxy != null; 85 | this.connectionProxy = connectionProxy; 86 | } 87 | 88 | public void shutdownCompleted(final ShutdownSignalException shutdownSignalException) { 89 | 90 | if (LOG.isDebugEnabled()) { 91 | LOG.debug("Shutdown signal caught: " + shutdownSignalException.getMessage()); 92 | } 93 | 94 | for (HaConnectionListener listener : listeners) { 95 | listener.onDisconnect(connectionProxy, shutdownSignalException); 96 | } 97 | 98 | // only try to reconnect if it was a problem with the broker 99 | if (!shutdownSignalException.isInitiatedByApplication()) { 100 | 101 | // start an async reconnection 102 | executorService.submit(new ReconnectionTask(true, this, connectionProxy)); 103 | 104 | } else { 105 | if (LOG.isDebugEnabled()) { 106 | LOG.debug("Ignoring shutdown signal, application initiated"); 107 | } 108 | } 109 | } 110 | } 111 | 112 | private class ReconnectionTask implements Runnable { 113 | 114 | private final boolean reconnection; 115 | 116 | private final ShutdownListener shutdownListener; 117 | 118 | private final HaConnectionProxy connectionProxy; 119 | 120 | public ReconnectionTask(final boolean reconnection, final ShutdownListener shutdownListener, 121 | final HaConnectionProxy connectionProxy) { 122 | 123 | Validate.notNull(shutdownListener, "shutdownListener is required"); 124 | Validate.notNull(connectionProxy, "connectionProxy is required"); 125 | 126 | this.reconnection = reconnection; 127 | this.shutdownListener = shutdownListener; 128 | this.connectionProxy = connectionProxy; 129 | } 130 | 131 | public void run() { 132 | 133 | // need to close the connection gate on the channels 134 | connectionProxy.closeConnectionLatch(); 135 | 136 | String addressesAsString = getAddressesAsString(); 137 | 138 | if (LOG.isDebugEnabled()) { 139 | LOG.info("Reconnection starting, sleeping: addresses=" + addressesAsString + ", wait=" 140 | + reconnectionWaitMillis); 141 | } 142 | 143 | // TODO: Add max reconnection attempts 144 | boolean connected = false; 145 | while (!connected) { 146 | 147 | try { 148 | Thread.sleep(reconnectionWaitMillis); 149 | } catch (InterruptedException ie) { 150 | 151 | if (LOG.isDebugEnabled()) { 152 | LOG.debug("Reconnection timer thread was interrupted, ignoring and reconnecting now"); 153 | } 154 | } 155 | 156 | Exception exception = null; 157 | try { 158 | Connection connection = newTargetConnection(connectionProxy.getAddresses()); 159 | 160 | if (LOG.isDebugEnabled()) { 161 | LOG.info("Reconnection complete: addresses=" + addressesAsString); 162 | } 163 | 164 | connection.addShutdownListener(shutdownListener); 165 | 166 | // refresh any channels created by previous connection 167 | connectionProxy.setTargetConnection(connection); 168 | connectionProxy.replaceChannelsInProxies(); 169 | 170 | connected = true; 171 | 172 | if (reconnection) { 173 | for (HaConnectionListener listener : listeners) { 174 | listener.onReconnection(connectionProxy); 175 | } 176 | 177 | } else { 178 | for (HaConnectionListener listener : listeners) { 179 | listener.onConnection(connectionProxy); 180 | } 181 | } 182 | 183 | connectionProxy.markAsOpen(); 184 | 185 | } catch (ConnectException ce) { 186 | // connection refused 187 | exception = ce; 188 | 189 | } catch (IOException ioe) { 190 | // some other connection problem 191 | exception = ioe; 192 | } 193 | 194 | if (exception != null) { 195 | LOG.warn("Failed to reconnect, retrying: addresses=" + addressesAsString + ", message=" 196 | + exception.getMessage()); 197 | 198 | if (reconnection) { 199 | for (HaConnectionListener listener : listeners) { 200 | listener.onReconnectFailure(connectionProxy, exception); 201 | } 202 | 203 | } else { 204 | for (HaConnectionListener listener : listeners) { 205 | listener.onConnectFailure(connectionProxy, exception); 206 | } 207 | } 208 | } 209 | } 210 | } 211 | 212 | private String getAddressesAsString() { 213 | 214 | StringBuilder sb = new StringBuilder(); 215 | sb.append('['); 216 | 217 | for (int i = 0; i < connectionProxy.getAddresses().length; i++) { 218 | 219 | if (i > 0) { 220 | sb.append(','); 221 | } 222 | 223 | sb.append(connectionProxy.getAddresses()[i].toString()); 224 | } 225 | 226 | sb.append(']'); 227 | return sb.toString(); 228 | } 229 | } 230 | 231 | private static final Logger LOG = Logger.getLogger(HaConnectionFactory.class); 232 | 233 | /** 234 | * Default value = 1000 = 1 second 235 | */ 236 | private static final long DEFAULT_RECONNECTION_WAIT_MILLIS = 1000; 237 | 238 | private long reconnectionWaitMillis = DEFAULT_RECONNECTION_WAIT_MILLIS; 239 | 240 | private final ExecutorService executorService; 241 | 242 | private RetryStrategy retryStrategy; 243 | 244 | private Set listeners; 245 | 246 | public HaConnectionFactory() { 247 | super(); 248 | 249 | executorService = Executors.newCachedThreadPool(); 250 | setDefaultRetryStrategy(); 251 | 252 | // TODO: Should we use a concurrent instance or sync access to this Set? 253 | listeners = new HashSet(); 254 | } 255 | 256 | public void addHaConnectionListener(final HaConnectionListener listener) { 257 | listeners.add(listener); 258 | } 259 | 260 | /** 261 | * Wraps a raw {@link Connection} with an HA-aware proxy. 262 | * 263 | * @see ConnectionFactory#newConnection(Address[], int) 264 | */ 265 | @Override 266 | public Connection newConnection(final Address[] addrs) throws IOException { 267 | 268 | Connection target = null; 269 | try { 270 | target = super.newConnection(addrs); 271 | 272 | } catch (IOException ioe) { 273 | LOG.warn("Initial connection failed, wrapping anyways and letting reconnector go to work: " 274 | + ioe.getMessage()); 275 | } 276 | 277 | ConnectionSet connectionPair = createConnectionProxy(addrs, target); 278 | 279 | // connection success 280 | if (target != null) { 281 | return connectionPair.wrapped; 282 | 283 | } 284 | 285 | // connection failed, reconnect in the same thread 286 | ReconnectionTask task = new ReconnectionTask(false, connectionPair.listener, connectionPair.proxy); 287 | task.run(); 288 | 289 | return connectionPair.wrapped; 290 | } 291 | 292 | /** 293 | * Allows setting a {@link Set} of {@link HaConnectionListener}s. This is 294 | * ammenable for Spring style property setting. Note that this will override 295 | * any existing listeners! 296 | */ 297 | public void setHaConnectionListener(final Set listeners) { 298 | 299 | Validate.notEmpty(listeners, "listeners are required and none can be null"); 300 | this.listeners = new ConcurrentSkipListSet(listeners); 301 | } 302 | 303 | /** 304 | * Set the reconnection wait time in milliseconds. The value must be greater 305 | * than 0. This is the number of milliseconds between getting a dropped 306 | * connection and a reconnection attempt. 307 | */ 308 | public void setReconnectionWaitMillis(final long reconnectionIntervalMillis) { 309 | 310 | Validate.isTrue(reconnectionIntervalMillis > 0, "reconnectionIntervalMillis must be greater than 0"); 311 | reconnectionWaitMillis = reconnectionIntervalMillis; 312 | } 313 | 314 | public void setRetryStrategy(final RetryStrategy retryStrategy) { 315 | this.retryStrategy = retryStrategy; 316 | } 317 | 318 | /** 319 | * Creates an {@link HaConnectionProxy} around a raw {@link Connection}. 320 | */ 321 | protected ConnectionSet createConnectionProxy(final Address[] addrs, 322 | final Connection targetConnection) { 323 | 324 | ClassLoader classLoader = Connection.class.getClassLoader(); 325 | Class[] interfaces = { Connection.class }; 326 | 327 | HaConnectionProxy proxy = new HaConnectionProxy(addrs, targetConnection, retryStrategy); 328 | 329 | if (LOG.isDebugEnabled()) { 330 | LOG 331 | .debug("Creating connection proxy: " 332 | + (targetConnection == null ? "none" : targetConnection.toString())); 333 | } 334 | 335 | Connection target = (Connection) Proxy.newProxyInstance(classLoader, interfaces, proxy); 336 | HaShutdownListener listener = new HaShutdownListener(proxy); 337 | 338 | // failed initial connections will have this set later upon successful connection 339 | if (targetConnection != null) { 340 | target.addShutdownListener(listener); 341 | } 342 | 343 | return new ConnectionSet(target, proxy, listener); 344 | } 345 | 346 | private Connection newTargetConnection(final Address[] addrs) throws IOException { 347 | return super.newConnection(addrs); 348 | } 349 | 350 | private void setDefaultRetryStrategy() { 351 | retryStrategy = new BlockingRetryStrategy(); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/HaConnectionListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import com.rabbitmq.client.Channel; 20 | import com.rabbitmq.client.Connection; 21 | import com.rabbitmq.client.ShutdownSignalException; 22 | 23 | /** 24 | * A listener interface for events on {@link Channel}s and {@link Connection}s. 25 | * Notifications and calls to implementations are done so in a synchronous 26 | * manner. This guarntees that listeners will be called before any operations 27 | * are allowed to take place. This is essential to allow for channel 28 | * initialization like queue creation before messages are sent. As such, please 29 | * don't be stupid in your implementations and keep this fact in mind! 30 | * 31 | * @author Josh Devins 32 | */ 33 | public interface HaConnectionListener { 34 | 35 | void onConnectFailure(final HaConnectionProxy connectionProxy, final Exception exception); 36 | 37 | void onConnection(final HaConnectionProxy connectionProxy); 38 | 39 | void onDisconnect(final HaConnectionProxy connectionProxy, final ShutdownSignalException shutdownSignalException); 40 | 41 | void onReconnectFailure(final HaConnectionProxy connectionProxy, final Exception exception); 42 | 43 | void onReconnection(final HaConnectionProxy connectionProxy); 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/HaConnectionProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import java.io.IOException; 20 | import java.lang.reflect.InvocationHandler; 21 | import java.lang.reflect.InvocationTargetException; 22 | import java.lang.reflect.Method; 23 | import java.lang.reflect.Proxy; 24 | import java.util.HashSet; 25 | import java.util.Set; 26 | 27 | import net.joshdevins.rabbitmq.client.ha.retry.RetryStrategy; 28 | 29 | import org.apache.log4j.Logger; 30 | 31 | import com.rabbitmq.client.Address; 32 | import com.rabbitmq.client.Channel; 33 | import com.rabbitmq.client.Connection; 34 | 35 | /** 36 | * A proxy around the standard {@link Connection}. 37 | * 38 | * TODO: Catch close method on Connection and Channel to do cleanup. 39 | * 40 | * @author Josh Devins 41 | */ 42 | public class HaConnectionProxy implements InvocationHandler { 43 | 44 | private static final Logger LOG = Logger.getLogger(HaConnectionProxy.class); 45 | 46 | private static Method CREATE_CHANNEL_METHOD; 47 | 48 | private static Method CREATE_CHANNEL_INT_METHOD; 49 | 50 | private final Address[] addrs; 51 | 52 | private Connection target; 53 | 54 | private final Set channelProxies; 55 | 56 | private final RetryStrategy retryStrategy; 57 | 58 | public HaConnectionProxy(final Address[] addrs, final Connection target, 59 | final RetryStrategy retryStrategy) { 60 | 61 | assert addrs != null; 62 | assert addrs.length > 0; 63 | assert retryStrategy != null; 64 | 65 | this.target = target; 66 | this.addrs = addrs; 67 | this.retryStrategy = retryStrategy; 68 | 69 | channelProxies = new HashSet(); 70 | } 71 | 72 | public void closeConnectionLatch() { 73 | for (HaChannelProxy proxy : channelProxies) { 74 | proxy.closeConnectionLatch(); 75 | } 76 | } 77 | 78 | public Address[] getAddresses() { 79 | return addrs; 80 | } 81 | 82 | public Connection getTargetConnection() { 83 | return target; 84 | } 85 | 86 | public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable { 87 | 88 | // intercept calls to create a channel 89 | if (method.equals(CREATE_CHANNEL_METHOD) || method.equals(CREATE_CHANNEL_INT_METHOD)) { 90 | 91 | return createChannelAndWrapWithProxy(method, args); 92 | } 93 | 94 | // delegate all other method invocations 95 | return InvocationHandlerUtils.delegateMethodInvocation(method, args, target); 96 | } 97 | 98 | public void markAsOpen() { 99 | 100 | synchronized (channelProxies) { 101 | 102 | for (HaChannelProxy proxy : channelProxies) { 103 | proxy.markAsOpen(); 104 | } 105 | } 106 | } 107 | 108 | protected Channel createChannelAndWrapWithProxy(final Method method, final Object[] args) 109 | throws IllegalArgumentException, IllegalAccessException, InvocationTargetException { 110 | 111 | Channel targetChannel = (Channel) method.invoke(target, args); 112 | 113 | ClassLoader classLoader = Connection.class.getClassLoader(); 114 | Class[] interfaces = { Channel.class }; 115 | 116 | // create the proxy and add to the set of channels we have created 117 | HaChannelProxy proxy = new HaChannelProxy(this, targetChannel, retryStrategy); 118 | 119 | if (LOG.isDebugEnabled()) { 120 | LOG.debug("Creating channel proxy: " + targetChannel.toString()); 121 | } 122 | 123 | // save the channel number-to-proxy relationship to be replaced later 124 | synchronized (channelProxies) { 125 | channelProxies.add(proxy); 126 | } 127 | 128 | return (Channel) Proxy.newProxyInstance(classLoader, interfaces, proxy); 129 | } 130 | 131 | protected void removeClosedChannel(final HaChannelProxy channelProxy) { 132 | synchronized (channelProxies) { 133 | channelProxies.remove(channelProxy); 134 | } 135 | } 136 | 137 | protected void replaceChannelsInProxies() throws IOException { 138 | 139 | synchronized (channelProxies) { 140 | 141 | for (HaChannelProxy proxy : channelProxies) { 142 | 143 | // replace dead channel with a new one using the same ID 144 | int channelNumber = proxy.getTargetChannel().getChannelNumber(); 145 | proxy.setTargetChannel(target.createChannel(channelNumber)); 146 | } 147 | } 148 | } 149 | 150 | protected void setTargetConnection(final Connection target) { 151 | 152 | assert target != null; 153 | this.target = target; 154 | } 155 | 156 | static { 157 | 158 | // initialize static fields or fail fast 159 | try { 160 | CREATE_CHANNEL_METHOD = Connection.class.getMethod("createChannel"); 161 | CREATE_CHANNEL_INT_METHOD = Connection.class.getMethod("createChannel", int.class); 162 | 163 | } catch (Exception e) { 164 | throw new RuntimeException(e); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/HaConsumerProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import java.io.IOException; 20 | import java.lang.reflect.Method; 21 | import java.util.concurrent.Callable; 22 | import java.util.concurrent.ExecutorService; 23 | import java.util.concurrent.Executors; 24 | 25 | import org.apache.log4j.Logger; 26 | 27 | import com.rabbitmq.client.Consumer; 28 | import com.rabbitmq.client.Envelope; 29 | import com.rabbitmq.client.ShutdownSignalException; 30 | import com.rabbitmq.client.AMQP.BasicProperties; 31 | 32 | /** 33 | * A proxy around the standard {@link Consumer}. 34 | * 35 | * @author Josh Devins 36 | */ 37 | public class HaConsumerProxy implements Consumer { 38 | 39 | private class ConsumeRunner implements Callable { 40 | 41 | public Object call() throws Exception { 42 | 43 | try { 44 | return channelProxy.invoke(channelProxy, basicConsumeMethod, basicConsumeArgs); 45 | 46 | } catch (Throwable e) { 47 | 48 | // bad news? 49 | if (LOG.isDebugEnabled()) { 50 | LOG.debug("Error reinvoking basicConsume", e); 51 | } 52 | 53 | return e; 54 | } 55 | } 56 | } 57 | 58 | private static final Logger LOG = Logger.getLogger(HaConsumerProxy.class); 59 | 60 | private final Consumer target; 61 | 62 | private final HaChannelProxy channelProxy; 63 | 64 | private final Method basicConsumeMethod; 65 | 66 | private final Object[] basicConsumeArgs; 67 | 68 | private final ExecutorService executor; 69 | 70 | public HaConsumerProxy(final Consumer target, final HaChannelProxy channelProxy, final Method basicConsumeMethod, 71 | final Object[] basicConsumeArgs) { 72 | 73 | assert target != null; 74 | assert channelProxy != null; 75 | assert basicConsumeMethod != null; 76 | assert basicConsumeArgs != null; 77 | 78 | this.target = target; 79 | this.channelProxy = channelProxy; 80 | this.basicConsumeMethod = basicConsumeMethod; 81 | this.basicConsumeArgs = basicConsumeArgs; 82 | 83 | executor = Executors.newCachedThreadPool(); 84 | } 85 | 86 | public void handleCancel(final String consumerTag) throws IOException { 87 | target.handleCancel(consumerTag); 88 | } 89 | 90 | public void handleCancelOk(final String consumerTag) { 91 | target.handleCancelOk(consumerTag); 92 | } 93 | 94 | public void handleConsumeOk(final String consumerTag) { 95 | target.handleConsumeOk(consumerTag); 96 | } 97 | 98 | public void handleDelivery(final String consumerTag, final Envelope envelope, final BasicProperties properties, 99 | final byte[] body) throws IOException { 100 | target.handleDelivery(consumerTag, envelope, properties, body); 101 | } 102 | 103 | public void handleRecoverOk(final String consumerTag) { 104 | target.handleRecoverOk(consumerTag); 105 | } 106 | 107 | public void handleShutdownSignal(final String consumerTag, final ShutdownSignalException sig) { 108 | 109 | // this is why we wrapped this 110 | if (LOG.isDebugEnabled()) { 111 | LOG.debug("Consumer asked to handle shutdown signal, reregistering consume. " + sig.getMessage()); 112 | } 113 | 114 | // just poke back in and call basicConsume all over again with target 115 | // make sure to close the connected gate 116 | channelProxy.closeConnectionLatch(); 117 | 118 | // this is it? 119 | executor.submit(new ConsumeRunner()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/HaUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import java.io.EOFException; 20 | import java.io.IOException; 21 | 22 | import com.rabbitmq.client.AMQP; 23 | import com.rabbitmq.client.AlreadyClosedException; 24 | import com.rabbitmq.client.ShutdownSignalException; 25 | import com.rabbitmq.client.impl.AMQImpl; 26 | 27 | /** 28 | * Utility class for HA operations. 29 | * 30 | * @author Josh Devins 31 | */ 32 | public final class HaUtils { 33 | 34 | private HaUtils() { 35 | // do not instantiate 36 | } 37 | 38 | /** 39 | * Pulls out the cause of the {@link IOException} and if it is of type {@link ShutdownSignalException}, passes on to 40 | * {@link #isShutdownRecoverable(ShutdownSignalException)}. 41 | */ 42 | public static boolean isShutdownRecoverable(final IOException ioe) { 43 | 44 | if (ioe.getCause() instanceof ShutdownSignalException) { 45 | return isShutdownRecoverable((ShutdownSignalException) ioe.getCause()); 46 | } 47 | 48 | return true; 49 | } 50 | 51 | /** 52 | * Determines if the {@link ShutdownSignalException} can be recovered from. 53 | * 54 | * Straight code copy from RabbitMQ messagepatterns library v0.1.3 {@code 55 | * ConnectorImpl}. 56 | * 57 | *

58 | * Changes: 59 | *

    60 | *
  • added AlreadyClosedException as recoverable when isInitiatedByApplication == true
  • 61 | *
62 | *

63 | */ 64 | public static boolean isShutdownRecoverable(final ShutdownSignalException s) { 65 | 66 | if (s != null) { 67 | int replyCode = 0; 68 | 69 | if (s.getReason() instanceof AMQImpl.Connection.Close) { 70 | replyCode = ((AMQImpl.Connection.Close) s.getReason()).getReplyCode(); 71 | } 72 | 73 | if (s.isInitiatedByApplication()) { 74 | 75 | return replyCode == AMQP.CONNECTION_FORCED || replyCode == AMQP.INTERNAL_ERROR 76 | || s.getCause() instanceof EOFException || s instanceof AlreadyClosedException; 77 | } 78 | } 79 | 80 | return false; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/InvocationHandlerUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import java.lang.reflect.InvocationHandler; 20 | import java.lang.reflect.InvocationTargetException; 21 | import java.lang.reflect.Method; 22 | 23 | /** 24 | * Simple helper methods for {@link InvocationHandler}s. 25 | * 26 | * @author Josh Devins 27 | */ 28 | public final class InvocationHandlerUtils { 29 | 30 | private InvocationHandlerUtils() { 31 | // do not instantiate 32 | } 33 | 34 | /** 35 | * Simple wrapper around {@link Method#invoke(Object, Object...)} which 36 | * rethrows any target exception. 37 | */ 38 | public static Object delegateMethodInvocation(final Method method, final Object[] args, final Object target) 39 | throws Throwable { 40 | 41 | try { 42 | return method.invoke(target, args); 43 | } catch (InvocationTargetException ite) { 44 | throw ite.getTargetException(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/retry/AlwaysRetryStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.retry; 18 | 19 | import net.joshdevins.rabbitmq.client.ha.BooleanReentrantLatch; 20 | 21 | /** 22 | * A {@link RetryStrategy} that will always retry a failed operation. 23 | * 24 | * @author Josh Devins 25 | */ 26 | public class AlwaysRetryStrategy implements RetryStrategy { 27 | 28 | public boolean shouldRetry(final Exception e, final int numOperationInvocations, 29 | final BooleanReentrantLatch connectionGate) { 30 | return true; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/retry/BlockingRetryStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.retry; 18 | 19 | import net.joshdevins.rabbitmq.client.ha.BooleanReentrantLatch; 20 | 21 | import org.apache.log4j.Logger; 22 | 23 | /** 24 | * A simple retry strategy that waits on the connection gate to be opened before 25 | * retrying. There is no retry limit and no timeout on the connection gate. 26 | * 27 | * @author Josh Devins 28 | */ 29 | public class BlockingRetryStrategy implements RetryStrategy { 30 | 31 | private static final Logger LOG = Logger.getLogger(BlockingRetryStrategy.class); 32 | 33 | public boolean shouldRetry(final Exception e, final int numOperationInvocations, 34 | final BooleanReentrantLatch connectionGate) { 35 | 36 | try { 37 | 38 | if (LOG.isDebugEnabled()) { 39 | LOG.debug("Waiting for connection gate to open: no timeout - " + e.getMessage()); 40 | } 41 | 42 | connectionGate.waitUntilOpen(); 43 | 44 | if (LOG.isDebugEnabled()) { 45 | LOG.debug("Waited for connection gate to open: connected=" + connectionGate.isOpen()); 46 | } 47 | } catch (InterruptedException e1) { 48 | 49 | LOG 50 | .warn("Interrupted during timeout waiting for next operation invocation to occurr. Retrying invocation now."); 51 | } 52 | 53 | // always retry 54 | // if connected finally, then of course, retry 55 | // if not connected, just retry again since there is no limit here 56 | return true; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/retry/NeverRetryStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.retry; 18 | 19 | import net.joshdevins.rabbitmq.client.ha.BooleanReentrantLatch; 20 | 21 | /** 22 | * A {@link RetryStrategy} that will never retry a failed operation. 23 | * 24 | * @author Josh Devins 25 | */ 26 | public class NeverRetryStrategy implements RetryStrategy { 27 | 28 | public boolean shouldRetry(final Exception e, final int numOperationInvocations, 29 | final BooleanReentrantLatch connectionGate) { 30 | return false; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/retry/RetryStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.retry; 18 | 19 | import java.io.IOException; 20 | 21 | import net.joshdevins.rabbitmq.client.ha.BooleanReentrantLatch; 22 | import net.joshdevins.rabbitmq.client.ha.HaConnectionFactory; 23 | 24 | import com.rabbitmq.client.AlreadyClosedException; 25 | import com.rabbitmq.client.Channel; 26 | 27 | /** 28 | * Retry strategy for failed {@link Channel} operations. 29 | * 30 | * @author Josh Devins 31 | */ 32 | public interface RetryStrategy { 33 | 34 | /** 35 | * A simple retry handler/strategy callback method. This method can do 36 | * whatever it likes including sleeping for some time, throw some {@link RuntimeException} or doing something else. 37 | * 38 | *

39 | * A standard implementation will ship that retries N times and sleeps for T milliseconds between retries. 40 | *

41 | * 42 | *

43 | * TODO: Add a hook to let the {@link HaConnectionFactory} notify locks when connections have been reestablished. 44 | *

45 | * 46 | * @param e 47 | * The {@link Exception} thrown by the underlying {@link Channel}. This will be either an {@link IOException} 48 | * or an {@link AlreadyClosedException}. 49 | * @param numOperationInvocations 50 | * The number of operation invocations that have been made. This will always be 1 or more. 51 | * @param connectionGate 52 | * A gate that can be waited on to be opened. The gate is opened when a connection is reestablished and the 53 | * channel is ready for use again. 54 | * 55 | * @return true if the method should be invoked again on the same Channel, 56 | * false if we should fail and rethrow the {@link IOException} 57 | */ 58 | public boolean shouldRetry(Exception e, int numOperationInvocations, BooleanReentrantLatch connectionGate); 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/net/joshdevins/rabbitmq/client/ha/retry/SimpleRetryStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.retry; 18 | 19 | import net.joshdevins.rabbitmq.client.ha.BooleanReentrantLatch; 20 | 21 | import org.apache.commons.lang.Validate; 22 | import org.apache.log4j.Logger; 23 | 24 | /** 25 | * A {@link RetryStrategy} that uses a timeout between retry attempts. This 26 | * allows the reconnection that is happening elsewhere to kick in. The strategy 27 | * will retry operations up to {@link #setMaxOperationInvocations(int)} times. 28 | * If the timeout is set to 0, then the strategy will not actually sleep at all, 29 | * and the next operation invocation will happen immediately. 30 | * 31 | * @author Josh Devins 32 | */ 33 | public class SimpleRetryStrategy implements RetryStrategy { 34 | 35 | private static final Logger LOG = Logger.getLogger(SimpleRetryStrategy.class); 36 | 37 | /** 38 | * Default value = 10000 = 10 seconds 39 | */ 40 | public static final long DEFAULT_OPERATION_RETRY_TIMEOUT_MILLIS = 10000; 41 | 42 | /** 43 | * Default value = 2 (one retry) 44 | */ 45 | public static final int DEFAULT_MAX_OPERATION_INVOCATIONS = 2; 46 | 47 | private long operationRetryTimeoutMillis = DEFAULT_OPERATION_RETRY_TIMEOUT_MILLIS; 48 | 49 | private int maxOperationInvocations = DEFAULT_MAX_OPERATION_INVOCATIONS; 50 | 51 | public void setMaxOperationInvocations(final int maxOperationInvocations) { 52 | 53 | Validate.isTrue(maxOperationInvocations >= 2, 54 | "max operation invocations must be 2 or greater, otherwise use a simpler strategy"); 55 | this.maxOperationInvocations = maxOperationInvocations; 56 | } 57 | 58 | public void setOperationRetryTimeoutMillis(final long timeout) { 59 | 60 | Validate.isTrue(timeout >= 0, "timeout must be a positive number"); 61 | operationRetryTimeoutMillis = timeout; 62 | } 63 | 64 | public boolean shouldRetry(final Exception e, final int numOperationInvocations, 65 | final BooleanReentrantLatch connectionGate) { 66 | 67 | if (LOG.isDebugEnabled()) { 68 | LOG.debug("Operation invocation failed on IOException: numOperationInvocations=" + numOperationInvocations 69 | + ", maxOperationInvocations=" + maxOperationInvocations + ", message=" + e.getMessage()); 70 | } 71 | 72 | if (numOperationInvocations == maxOperationInvocations) { 73 | 74 | if (LOG.isDebugEnabled()) { 75 | LOG.debug("Max number of operation invocations reached, not retrying: " + maxOperationInvocations); 76 | } 77 | 78 | return false; 79 | } 80 | 81 | if (operationRetryTimeoutMillis > 0) { 82 | 83 | if (LOG.isDebugEnabled()) { 84 | LOG.debug("Sleeping before next operation invocation (millis): " + operationRetryTimeoutMillis); 85 | } 86 | 87 | try { 88 | Thread.sleep(operationRetryTimeoutMillis); 89 | } catch (InterruptedException ie) { 90 | LOG.warn("Interrupted during timeout waiting for next operation invocation to occurr. " 91 | + "Retrying invocation now."); 92 | } 93 | } else { 94 | 95 | if (LOG.isDebugEnabled()) { 96 | LOG.debug("No timeout set, retrying immediately"); 97 | } 98 | } 99 | 100 | return true; 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/BooleanReentrantLatchTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha; 18 | 19 | import java.util.Date; 20 | import java.util.LinkedList; 21 | import java.util.List; 22 | import java.util.concurrent.Callable; 23 | import java.util.concurrent.ExecutionException; 24 | import java.util.concurrent.ExecutorService; 25 | import java.util.concurrent.Executors; 26 | import java.util.concurrent.Future; 27 | import java.util.concurrent.TimeUnit; 28 | 29 | import org.junit.Assert; 30 | import org.junit.Before; 31 | import org.junit.Test; 32 | 33 | public class BooleanReentrantLatchTest { 34 | 35 | private class TestCallable implements Callable { 36 | 37 | public Long call() throws Exception { 38 | 39 | try { 40 | 41 | long startTime = new Date().getTime(); 42 | latch.waitUntilOpen(); 43 | return new Date().getTime() - startTime; 44 | 45 | } catch (InterruptedException ie) { 46 | return new Long(-1); 47 | } 48 | } 49 | } 50 | 51 | private static final int NUM_REPETITIONS = 3; 52 | 53 | private BooleanReentrantLatch latch; 54 | 55 | private long startTime; 56 | 57 | @Test 58 | public void basicInitialStateTest_Closed() { 59 | 60 | latch = new BooleanReentrantLatch(false); 61 | BooleanReentrantLatch localLatch = new BooleanReentrantLatch(false); 62 | 63 | Assert.assertEquals(latch, localLatch); 64 | 65 | Assert.assertFalse(localLatch.isOpen()); 66 | Assert.assertTrue(localLatch.isClosed()); 67 | } 68 | 69 | @Test 70 | public void basicInitialStateTest_Open() { 71 | 72 | BooleanReentrantLatch localLatch = new BooleanReentrantLatch(true); 73 | 74 | Assert.assertEquals(latch, localLatch); 75 | 76 | Assert.assertTrue(localLatch.isOpen()); 77 | Assert.assertFalse(localLatch.isClosed()); 78 | } 79 | 80 | @Test 81 | public void basicRepeatedCloseTest() { 82 | 83 | latch = new BooleanReentrantLatch(false); 84 | Assert.assertTrue(latch.isClosed()); 85 | 86 | for (int i = 0; i < NUM_REPETITIONS; i++) { 87 | latch.close(); 88 | Assert.assertTrue(latch.isClosed()); 89 | } 90 | } 91 | 92 | @Test 93 | public void basicRepeatedCloseThenOpenTest() { 94 | 95 | for (int i = 0; i < NUM_REPETITIONS; i++) { 96 | 97 | Assert.assertTrue(latch.isOpen()); 98 | 99 | latch.close(); 100 | Assert.assertTrue(latch.isClosed()); 101 | 102 | latch.open(); 103 | } 104 | } 105 | 106 | @Test 107 | public void basicRepeatedOpenTest() { 108 | 109 | Assert.assertTrue(latch.isOpen()); 110 | 111 | for (int i = 0; i < NUM_REPETITIONS; i++) { 112 | latch.open(); 113 | Assert.assertTrue(latch.isOpen()); 114 | } 115 | } 116 | 117 | @Test 118 | public void basicWaitOnOpenLatchTest() throws InterruptedException { 119 | 120 | setStartTime(); 121 | latch.waitUntilOpen(); 122 | 123 | // should return right away 124 | assertTimeLapsed(); 125 | } 126 | 127 | @Test 128 | public void basicWaitOnOpenLatchTest_WithTimeout() throws InterruptedException { 129 | 130 | setStartTime(); 131 | latch.waitUntilOpen(1000, TimeUnit.MILLISECONDS); 132 | 133 | // should return right away 134 | assertTimeLapsed(); 135 | } 136 | 137 | @Before 138 | public void before() { 139 | latch = new BooleanReentrantLatch(); 140 | } 141 | 142 | @Test 143 | public void concurrentRepeatedWaitOnClosedLatchTest() throws InterruptedException, ExecutionException { 144 | 145 | int numTestCallables = 10; 146 | 147 | TestCallable[] testCallables = new TestCallable[numTestCallables]; 148 | for (int i = 0; i < numTestCallables; i++) { 149 | testCallables[i] = new TestCallable(); 150 | } 151 | 152 | ExecutorService service = Executors.newFixedThreadPool(numTestCallables); 153 | 154 | for (int i = 0; i < NUM_REPETITIONS; i++) { 155 | 156 | latch.close(); 157 | 158 | List> futures = new LinkedList>(); 159 | for (int j = 0; j < numTestCallables; j++) { 160 | futures.add(service.submit(testCallables[j])); 161 | } 162 | 163 | for (Future future : futures) { 164 | 165 | // threads are not finished 166 | Assert.assertFalse(future.isDone()); 167 | Assert.assertFalse(future.isCancelled()); 168 | } 169 | 170 | latch.open(); 171 | 172 | // FIXME: This is fraught with peril, I know 173 | // wait a sec before testing the state 174 | Thread.sleep(100); 175 | 176 | for (Future future : futures) { 177 | 178 | // threads are finished 179 | Assert.assertTrue(future.isDone()); 180 | Assert.assertFalse(future.isCancelled()); 181 | 182 | // not interrupted 183 | Assert.assertNotSame(new Long(-1), future.get()); 184 | 185 | // within time frame 186 | Assert.assertTrue(future.get() < 100); 187 | } 188 | } 189 | } 190 | 191 | private void assertTimeLapsed() { 192 | assertTimeLapsed(100); 193 | } 194 | 195 | private void assertTimeLapsed(final long acceptedInterval) { 196 | 197 | // FIXME: This seems suspect, but not sure how else to judge time 198 | long endTime = new Date().getTime(); 199 | Assert.assertTrue(startTime - endTime <= acceptedInterval); 200 | } 201 | 202 | private void setStartTime() { 203 | startTime = new Date().getTime(); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/it/RabbitBlockingConsumerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.joshdevins.rabbitmq.client.ha.it; 2 | 3 | import net.joshdevins.rabbitmq.client.ha.HaConnectionFactory; 4 | 5 | import org.apache.log4j.Logger; 6 | import org.junit.Assert; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 11 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 12 | import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; 13 | import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.test.context.ContextConfiguration; 16 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 17 | 18 | import com.rabbitmq.client.AMQP.Queue.BindOk; 19 | 20 | @RunWith(SpringJUnit4ClassRunner.class) 21 | @ContextConfiguration("/META-INF/spring/applicationContext.xml") 22 | public class RabbitBlockingConsumerIntegrationTest { 23 | 24 | public static class PojoHandler { 25 | 26 | private int msgCount; 27 | 28 | public void handleMessage(final byte[] bytes) { 29 | 30 | synchronized (this) { 31 | ++msgCount; 32 | } 33 | 34 | LOG.info("Thread [" + Thread.currentThread().getId() 35 | + "] message: n=" + msgCount + ", body=" 36 | + new String(bytes)); 37 | } 38 | } 39 | 40 | private static final Logger LOG = Logger 41 | .getLogger(RabbitBlockingConsumerIntegrationTest.class); 42 | 43 | @Autowired 44 | private ConnectionFactory connectionFactory; 45 | 46 | @Autowired 47 | private HaConnectionFactory haConnectionFactory; 48 | 49 | @Autowired 50 | private RabbitTemplate template; 51 | 52 | @Before 53 | public void before() { 54 | 55 | // add my connection listener to the HaConnectionFactory 56 | haConnectionFactory 57 | .addHaConnectionListener(new TestHaConnectionListener( 58 | haConnectionFactory, "localhost")); 59 | } 60 | 61 | @Test 62 | public void testAsyncConsume() throws InterruptedException { 63 | 64 | BindOk bindOk = template.execute(new TestChannelCallback()); 65 | Assert.assertNotNull(bindOk); 66 | 67 | // setup async container 68 | SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); 69 | container.setConnectionFactory(connectionFactory); 70 | container.setQueueNames("testQueue"); 71 | container.setConcurrentConsumers(5); 72 | // container.setChannelTransacted(true); 73 | 74 | MessageListenerAdapter adapter = new MessageListenerAdapter(); 75 | adapter.setDelegate(new PojoHandler()); 76 | container.setMessageListener(adapter); 77 | container.afterPropertiesSet(); 78 | container.start(); 79 | 80 | while (true) { 81 | Thread.sleep(10000); 82 | } 83 | 84 | // container.stop(); 85 | // container.shutdown(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/it/RabbitConsistencyIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.joshdevins.rabbitmq.client.ha.it; 2 | 3 | import java.util.Collections; 4 | import java.util.Date; 5 | import java.util.HashSet; 6 | import java.util.LinkedList; 7 | import java.util.List; 8 | import java.util.Set; 9 | 10 | import org.apache.log4j.Logger; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | import org.junit.runner.RunWith; 15 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 16 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 17 | import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer; 18 | import org.springframework.amqp.rabbit.listener.adapter.MessageListenerAdapter; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | import org.springframework.test.context.ContextConfiguration; 21 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 22 | 23 | import com.rabbitmq.client.AMQP.Queue.BindOk; 24 | 25 | @RunWith(SpringJUnit4ClassRunner.class) 26 | @ContextConfiguration("/META-INF/spring/applicationContext.xml") 27 | public class RabbitConsistencyIntegrationTest { 28 | 29 | public class PojoHandler { 30 | 31 | public void handleMessage(final String message) { 32 | 33 | if (LOG.isInfoEnabled()) { 34 | LOG.info("Received message: " + message); 35 | } 36 | 37 | // check for message 38 | Integer integer = Integer.valueOf(message); 39 | 40 | if (!messages.contains(integer)) { 41 | messagesReceivedNotDelivered.add(integer); 42 | return; 43 | } 44 | 45 | messages.remove(integer); 46 | } 47 | } 48 | 49 | private static final Logger LOG = Logger.getLogger(RabbitConsistencyIntegrationTest.class); 50 | 51 | private static final int NUM_MESSAGES = 1000; 52 | 53 | private static final int NUM_CONSUMERS = 1; 54 | 55 | private Set messages; 56 | 57 | private List messagesReceivedNotDelivered; 58 | 59 | @Autowired 60 | private ConnectionFactory connectionFactory; 61 | 62 | @Autowired 63 | private RabbitTemplate template; 64 | 65 | @Before 66 | public void before() { 67 | messages = Collections.synchronizedSet(new HashSet()); 68 | messagesReceivedNotDelivered = Collections.synchronizedList(new LinkedList()); 69 | } 70 | 71 | @Test 72 | public void test() throws InterruptedException { 73 | 74 | // ensure queue already created 75 | BindOk bindOk = template.execute(new TestChannelCallback()); 76 | Assert.assertNotNull(bindOk); 77 | 78 | // setup async container 79 | SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(); 80 | container.setConnectionFactory(connectionFactory); 81 | container.setQueueNames("testQueue"); 82 | container.setConcurrentConsumers(NUM_CONSUMERS); 83 | // container.setChannelTransacted(true); 84 | 85 | MessageListenerAdapter adapter = new MessageListenerAdapter(); 86 | adapter.setDelegate(new PojoHandler()); 87 | container.setMessageListener(adapter); 88 | container.afterPropertiesSet(); 89 | 90 | // empty out queue 91 | while (template.receive("testQueue") != null) { 92 | // do 93 | } 94 | 95 | container.start(); 96 | 97 | // template.setChannelTransacted(true); 98 | 99 | // send all the messages 100 | long start = new Date().getTime(); 101 | 102 | for (int i = 0; i < NUM_MESSAGES; i++) { 103 | 104 | messages.add(i); 105 | template.convertAndSend(Integer.toString(i)); 106 | Thread.sleep(10); 107 | } 108 | 109 | long end = new Date().getTime(); 110 | float time = (end - start) / (float) 1000; 111 | 112 | Thread.sleep(1000); 113 | 114 | LOG.debug("Time (secs.): " + time); 115 | LOG.debug("Avg. rate (msgs/sec): " + NUM_MESSAGES / time); 116 | 117 | // everything should be empty 118 | Assert.assertEquals(0, messagesReceivedNotDelivered.size()); 119 | Assert.assertEquals(0, messages.size()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/it/RabbitTemplateConsumerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.it; 18 | 19 | import java.io.UnsupportedEncodingException; 20 | 21 | import net.joshdevins.rabbitmq.client.ha.HaConnectionFactory; 22 | 23 | import org.apache.log4j.Logger; 24 | import org.junit.Assert; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | import org.junit.runner.RunWith; 28 | import org.springframework.amqp.core.Message; 29 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 30 | import org.springframework.beans.factory.annotation.Autowired; 31 | import org.springframework.test.context.ContextConfiguration; 32 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 33 | 34 | import com.rabbitmq.client.AMQP.Queue.BindOk; 35 | 36 | @RunWith(SpringJUnit4ClassRunner.class) 37 | @ContextConfiguration("/META-INF/spring/applicationContext.xml") 38 | public class RabbitTemplateConsumerIntegrationTest { 39 | 40 | private static final Logger LOG = Logger.getLogger(RabbitTemplateConsumerIntegrationTest.class); 41 | 42 | @Autowired 43 | private HaConnectionFactory haConnectionFactory; 44 | 45 | @Autowired 46 | private RabbitTemplate template; 47 | 48 | @Before 49 | public void before() { 50 | 51 | // add my connection listener to the HaConnectionFactory 52 | haConnectionFactory.addHaConnectionListener(new TestHaConnectionListener(haConnectionFactory, 53 | "devins-ubuntu-vm01")); 54 | } 55 | 56 | @Test 57 | public void testSyncConsume() throws UnsupportedEncodingException, InterruptedException { 58 | 59 | BindOk bindOk = template.execute(new TestChannelCallback()); 60 | Assert.assertNotNull(bindOk); 61 | 62 | // empty out queue 63 | while (template.receive("testQueue") != null) { 64 | receiveMessage(1, template.receive("testQueue")); 65 | } 66 | 67 | receiveMessage(1, template.receive("testQueue")); 68 | 69 | // empty out queue 70 | while (true) { 71 | Thread.sleep(1000); 72 | receiveMessage(1, template.receive("testQueue")); 73 | } 74 | } 75 | 76 | private void receiveMessage(final int expected, final Message message) throws UnsupportedEncodingException { 77 | 78 | if (message == null) { 79 | LOG.info("no message"); 80 | return; 81 | } 82 | 83 | String str = new String(message.getBody(), "UTF-8"); 84 | LOG.info(str); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/it/RabbitTemplatePublisherIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.it; 18 | 19 | import java.util.Date; 20 | 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | import org.junit.runner.RunWith; 24 | import org.springframework.amqp.rabbit.core.RabbitTemplate; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.test.context.ContextConfiguration; 27 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 28 | 29 | @RunWith(SpringJUnit4ClassRunner.class) 30 | @ContextConfiguration("/META-INF/spring/applicationContext.xml") 31 | public class RabbitTemplatePublisherIntegrationTest { 32 | 33 | @Autowired 34 | private RabbitTemplate template; 35 | 36 | private String date; 37 | 38 | @Before 39 | public void before() { 40 | date = new Date().toString(); 41 | } 42 | 43 | @Test 44 | public void testPublish() { 45 | 46 | template.convertAndSend(date + " : 1"); 47 | template.convertAndSend(date + " : 2"); 48 | template.convertAndSend(date + " : 3"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/it/TestChannelCallback.java: -------------------------------------------------------------------------------- 1 | package net.joshdevins.rabbitmq.client.ha.it; 2 | 3 | import java.util.HashMap; 4 | 5 | import org.springframework.amqp.rabbit.core.ChannelCallback; 6 | 7 | import com.rabbitmq.client.AMQP.Queue.BindOk; 8 | import com.rabbitmq.client.Channel; 9 | 10 | public class TestChannelCallback implements ChannelCallback { 11 | 12 | /* 13 | * TODO: Document: If you use an auto-delete queue, you need to recreate it too, but this can fail depending 14 | * on the retry strategy being used since a consume method could be invoked in a separate thread from the queue 15 | * creation. Furthermore, using any blocking retry strategy on the same channel that was reconnected will cause 16 | * a race condition -- which will get called first, the consume message or the queueDecalre/Binding? Or just 17 | * don't use an auto-delete queue! 18 | */ 19 | public BindOk doInRabbit(final Channel channel) throws Exception { 20 | 21 | // bind to the default topic and consume all messages 22 | channel.queueDeclare("testQueue", true, false, false, new HashMap()); 23 | return channel.queueBind("testQueue", "amq.topic", "#"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/it/TestHaConnectionListener.java: -------------------------------------------------------------------------------- 1 | package net.joshdevins.rabbitmq.client.ha.it; 2 | 3 | import java.util.HashMap; 4 | 5 | import net.joshdevins.rabbitmq.client.ha.AbstractHaConnectionListener; 6 | import net.joshdevins.rabbitmq.client.ha.HaConnectionProxy; 7 | 8 | import com.rabbitmq.client.Address; 9 | import com.rabbitmq.client.Channel; 10 | import com.rabbitmq.client.Connection; 11 | import com.rabbitmq.client.ConnectionFactory; 12 | 13 | public class TestHaConnectionListener extends AbstractHaConnectionListener { 14 | 15 | private final ConnectionFactory connectionFactory; 16 | 17 | private final String host; 18 | 19 | TestHaConnectionListener(final ConnectionFactory connectionFactory, final String host) { 20 | 21 | this.connectionFactory = connectionFactory; 22 | this.host = host; 23 | } 24 | 25 | @Override 26 | public void onReconnection(final HaConnectionProxy connectionProxy) { 27 | 28 | // use a separate connection and channel to avoid race conditions with any operations that are blocked 29 | // waiting for the reconnection to finish...and don't cache anything otherwise you get the same problem! 30 | try { 31 | Connection connection = connectionFactory.newConnection(new Address[] { new Address(host) }); 32 | Channel channel = connection.createChannel(); 33 | 34 | channel.queueDeclare("testQueue", false, false, false, new HashMap()); 35 | channel.queueBind("testQueue", "amq.topic", "#"); 36 | 37 | channel.close(); 38 | connection.close(); 39 | 40 | } catch (Exception e) { 41 | throw new RuntimeException(e); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/retry/AlwaysRetryStrategyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.retry; 18 | 19 | import org.junit.Assert; 20 | import org.junit.Test; 21 | 22 | public class AlwaysRetryStrategyTest { 23 | 24 | @Test 25 | public void basicTest() { 26 | AlwaysRetryStrategy strategy = new AlwaysRetryStrategy(); 27 | 28 | Assert.assertTrue(strategy.shouldRetry(null, 0, null)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/retry/BlockingRetryStrategyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.retry; 18 | 19 | import net.joshdevins.rabbitmq.client.ha.BooleanReentrantLatch; 20 | 21 | import org.junit.Assert; 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | 25 | public class BlockingRetryStrategyTest { 26 | 27 | private class TestRunnable implements Runnable { 28 | 29 | private boolean shouldRetry = false; 30 | 31 | public void run() { 32 | 33 | // this will block until released by the other thread 34 | shouldRetry = strategy.shouldRetry(new Exception("test"), 0, latch); 35 | } 36 | } 37 | 38 | private BooleanReentrantLatch latch; 39 | 40 | private BlockingRetryStrategy strategy; 41 | 42 | @Test 43 | public void basicTest() throws InterruptedException { 44 | TestRunnable runnable = new TestRunnable(); 45 | Thread thread = new Thread(runnable); 46 | thread.start(); 47 | 48 | latch.open(); 49 | 50 | // FIXME: peril! 51 | Thread.sleep(100); 52 | 53 | Assert.assertFalse(thread.isAlive()); 54 | Assert.assertTrue(runnable.shouldRetry); 55 | } 56 | 57 | @Before 58 | public void before() { 59 | strategy = new BlockingRetryStrategy(); 60 | latch = new BooleanReentrantLatch(false); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/test/java/net/joshdevins/rabbitmq/client/ha/retry/NeverRetryStrategyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010 Josh Devins 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.joshdevins.rabbitmq.client.ha.retry; 18 | 19 | import org.junit.Assert; 20 | import org.junit.Test; 21 | 22 | public class NeverRetryStrategyTest { 23 | 24 | @Test 25 | public void basicTest() { 26 | NeverRetryStrategy strategy = new NeverRetryStrategy(); 27 | 28 | Assert.assertFalse(strategy.shouldRetry(null, 0, null)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/resources/META-INF/spring/applicationContext.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/resources/log4j.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | --------------------------------------------------------------------------------