Skip to content

Commit 27c3071

Browse files
ARTEMIS-5852 Example with lock coordinator to achieve HA using mirroring
I used AI (anthropic) to help me writing the readme.md for this example. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent c05eec2 commit 27c3071

7 files changed

Lines changed: 614 additions & 0 deletions

File tree

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
<?xml version='1.0'?>
2+
<!--
3+
Licensed to the Apache Software Foundation (ASF) under one
4+
or more contributor license agreements. See the NOTICE file
5+
distributed with this work for additional information
6+
regarding copyright ownership. The ASF licenses this file
7+
to you under the Apache License, Version 2.0 (the
8+
"License"); you may not use this file except in compliance
9+
with the License. You may obtain a copy of the License at
10+
11+
http://www.apache.org/licenses/LICENSE-2.0
12+
13+
Unless required by applicable law or agreed to in writing,
14+
software distributed under the License is distributed on an
15+
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16+
KIND, either express or implied. See the License for the
17+
specific language governing permissions and limitations
18+
under the License.
19+
-->
20+
21+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
22+
<modelVersion>4.0.0</modelVersion>
23+
24+
<parent>
25+
<groupId>org.apache.artemis.examples.broker-connection</groupId>
26+
<artifactId>broker-connections</artifactId>
27+
<version>2.51.0-SNAPSHOT</version>
28+
</parent>
29+
30+
<artifactId>ha-with-mirroring</artifactId>
31+
<packaging>jar</packaging>
32+
<name>Apache Artemis Zookeeper Mirroring</name>
33+
34+
<properties>
35+
<activemq.basedir>${project.basedir}/../../../..</activemq.basedir>
36+
</properties>
37+
38+
<dependencies>
39+
<dependency>
40+
<groupId>org.apache.artemis</groupId>
41+
<artifactId>artemis-cli</artifactId>
42+
<version>${project.version}</version>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.apache.qpid</groupId>
46+
<artifactId>qpid-jms-client</artifactId>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.apache.artemis</groupId>
50+
<artifactId>artemis-commons</artifactId>
51+
</dependency>
52+
</dependencies>
53+
54+
<build>
55+
<plugins>
56+
<plugin>
57+
<groupId>org.apache.artemis</groupId>
58+
<artifactId>artemis-maven-plugin</artifactId>
59+
<executions>
60+
<execution>
61+
<id>create0</id>
62+
<goals>
63+
<goal>create</goal>
64+
</goals>
65+
<configuration>
66+
<!-- this makes it easier in certain envs -->
67+
<javaOptions>-Djava.net.preferIPv4Stack=true</javaOptions>
68+
<instance>${basedir}/target/server0</instance>
69+
<allowAnonymous>true</allowAnonymous>
70+
<configuration>${basedir}/target/classes/artemis/server0</configuration>
71+
</configuration>
72+
</execution>
73+
<execution>
74+
<id>create1</id>
75+
<goals>
76+
<goal>create</goal>
77+
</goals>
78+
<configuration>
79+
<!-- this makes it easier in certain envs -->
80+
<instance>${basedir}/target/server1</instance>
81+
<allowAnonymous>true</allowAnonymous>
82+
<configuration>${basedir}/target/classes/artemis/server1</configuration>
83+
</configuration>
84+
</execution>
85+
<execution>
86+
<id>runClient</id>
87+
<goals>
88+
<goal>runClient</goal>
89+
</goals>
90+
<configuration>
91+
<clientClass>org.apache.artemis.jms.example.HAWithMirroringExample</clientClass>
92+
<args>
93+
<param>${basedir}/target/server0</param>
94+
<param>${basedir}/target/server1</param>
95+
</args>
96+
</configuration>
97+
</execution>
98+
</executions>
99+
<dependencies>
100+
<dependency>
101+
<groupId>org.apache.artemis.examples.broker-connection</groupId>
102+
<artifactId>ha-with-mirroring</artifactId>
103+
<version>${project.version}</version>
104+
</dependency>
105+
</dependencies>
106+
</plugin>
107+
<plugin>
108+
<groupId>org.apache.maven.plugins</groupId>
109+
<artifactId>maven-clean-plugin</artifactId>
110+
</plugin>
111+
</plugins>
112+
</build>
113+
</project>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# High Availability with Mirroring and Distributed Locks
2+
3+
To run the example, simply type **mvn verify** from this directory.
4+
5+
This example demonstrates how to achieve high availability (HA) using mirroring combined with distributed locks from the Lock Coordinator feature. The distributed locks ensure that only one broker accepts client connections at a time, providing automatic failover without split-brain scenarios.
6+
7+
## Overview
8+
9+
This example configures two brokers (server0 and server1) that:
10+
- Mirror all messaging operations to each other using broker connections
11+
- Share a distributed file-based lock to coordinate which broker accepts client connections
12+
- Automatically failover client connections when the active broker fails
13+
14+
## How It Works
15+
16+
### Mirroring Configuration
17+
18+
Both brokers are configured with bidirectional mirroring using AMQP broker connections. Each broker mirrors its data to the other:
19+
20+
**server0/broker.xml:**
21+
```xml
22+
<broker-connections>
23+
<amqp-connection uri="tcp://localhost:61001" name="mirror" retry-interval="2000">
24+
<mirror sync="true"/>
25+
</amqp-connection>
26+
</broker-connections>
27+
```
28+
29+
**server1/broker.xml:**
30+
```xml
31+
<broker-connections>
32+
<amqp-connection uri="tcp://localhost:61000" name="mirror" retry-interval="2000">
33+
<mirror sync="false"/>
34+
</amqp-connection>
35+
</broker-connections>
36+
```
37+
38+
This ensures that messages, queues, and other operations are replicated across both brokers.
39+
40+
### Lock Coordinator for HA
41+
42+
The key feature of this example is the use of **distributed locks** to control which broker accepts client connections. Both brokers are configured with a lock coordinator on their client acceptors:
43+
44+
```xml
45+
<lock-coordinators>
46+
<lock-coordinator name="clients-lock">
47+
<type>file</type>
48+
<lock-id>mirror-cluster-clients</lock-id>
49+
<check-period>1000</check-period>
50+
<properties>
51+
<property key="locks-folder" value="/path/to/shared/locks"/>
52+
</properties>
53+
</lock-coordinator>
54+
</lock-coordinators>
55+
56+
<acceptors>
57+
<acceptor name="forClients" lock-coordinator="clients-lock">tcp://localhost:61616</acceptor>
58+
</acceptors>
59+
```
60+
61+
The lock coordinator ensures that:
62+
- Only the broker holding the lock accepts client connections on that acceptor
63+
- If the active broker fails, the lock is automatically released and acquired by the other broker
64+
- The backup broker immediately starts accepting connections when it acquires the lock
65+
- The shared lock file prevents split-brain scenarios
66+
67+
### Client Failover
68+
69+
Clients connect using a failover URL that includes both broker addresses:
70+
71+
```java
72+
ConnectionFactory factory = new org.apache.qpid.jms.JmsConnectionFactory(
73+
"failover:(amqp://localhost:61616,amqp://localhost:61617)?failover.maxReconnectAttempts=-1");
74+
```
75+
76+
When the active broker (holding the lock) fails:
77+
1. The lock is automatically released
78+
2. The backup broker acquires the lock and starts accepting connections
79+
3. The client automatically reconnects to the now-active broker
80+
4. All messages are available due to mirroring
81+
82+
## Example Flow
83+
84+
1. Both brokers start with mirroring configured
85+
2. One broker (typically server0) acquires the distributed lock and accepts client connections
86+
3. The client connects and sends 30 messages to a queue
87+
4. Server0 is killed (simulating a failure)
88+
5. Server1 automatically acquires the lock and starts accepting connections
89+
6. The client reconnects to server1 via failover
90+
7. All 30 messages are consumed from server1 (due to mirroring)
91+
92+
## Configuration Notes
93+
94+
The lock coordinator supports different lock types (file-based, zookeeper). This example uses file-based locks where both brokers must have access to a shared filesystem location.
95+
96+
The `check-period` parameter (in milliseconds) controls how frequently the lock holder verifies it still owns the lock, affecting how quickly failover occurs when a broker crashes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.artemis.jms.example;
18+
19+
import javax.jms.Connection;
20+
import javax.jms.ConnectionFactory;
21+
import javax.jms.MessageConsumer;
22+
import javax.jms.MessageProducer;
23+
import javax.jms.Queue;
24+
import javax.jms.Session;
25+
import javax.jms.TextMessage;
26+
27+
import java.io.File;
28+
29+
import org.apache.activemq.artemis.util.ServerUtil;
30+
import org.apache.activemq.artemis.utils.FileUtil;
31+
32+
/**
33+
* Example of live and replicating backup pair using mirroring and a distributed lock from the Lock Coordinator
34+
*/
35+
public class HAWithMirroringExample {
36+
37+
private static Process server0;
38+
39+
private static Process server1;
40+
41+
public static void main(final String[] args) throws Exception {
42+
final int numMessages = 30;
43+
44+
// Configure the locks folder. The broker.xml needs to have the proper path in place.
45+
// also the locks folder needs to be created before the server starts
46+
configureLocksFolder(args);
47+
48+
49+
try {
50+
51+
// Start the two servers
52+
server0 = ServerUtil.startServer(args[0], HAWithMirroringExample.class.getSimpleName() + "-peer0", 0, 0);
53+
Thread.sleep(2_000);
54+
server1 = ServerUtil.startServer(args[1], HAWithMirroringExample.class.getSimpleName() + "-peer1", 1, 0);
55+
56+
// We connect to the broker holding the lock on the distributed lock
57+
ConnectionFactory factory = new org.apache.qpid.jms.JmsConnectionFactory(
58+
"failover:(amqp://localhost:61616,amqp://localhost:61617)?failover.maxReconnectAttempts=-1");
59+
60+
61+
try (Connection connection = factory.createConnection()) {
62+
Session session = connection.createSession(true, Session.SESSION_TRANSACTED);
63+
Queue queue = session.createQueue("exampleQueue");
64+
MessageProducer producer = session.createProducer(queue);
65+
66+
// Send messages in one of the brokers
67+
for (int i = 0; i < numMessages; i++) {
68+
producer.send(session.createTextMessage("hello " + i));
69+
}
70+
session.commit();
71+
72+
// kill the server that was probably holding the lock:
73+
ServerUtil.killServer(server0);
74+
75+
// now we consume messages after the broker is killed, the client should reconnect to the correct broker
76+
77+
MessageConsumer consumer = session.createConsumer(queue);
78+
connection.start();
79+
for (int i = 0; i < numMessages; i++) {
80+
TextMessage message = (TextMessage) consumer.receive(5000);
81+
System.out.println("Received message " + message.getText());
82+
}
83+
}
84+
85+
86+
} finally {
87+
ServerUtil.killServer(server0);
88+
ServerUtil.killServer(server1);
89+
}
90+
}
91+
92+
private static void configureLocksFolder(String[] args) throws Exception {
93+
File lockFolder = new File("./target/locks");
94+
lockFolder.mkdirs();
95+
FileUtil.findReplace(new File(args[0] + "/etc/broker.xml"), "CHANGEME", lockFolder.getAbsolutePath());
96+
FileUtil.findReplace(new File(args[1] + "/etc/broker.xml"), "CHANGEME", lockFolder.getAbsolutePath());
97+
}
98+
}

0 commit comments

Comments
 (0)