├── .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