/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package org.apache.camel.component.zookeeper.policy;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.apache.camel.CamelContext;
import org.apache.camel.ExchangePattern;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.mock.MockEndpoint;
import org.apache.camel.component.zookeeper.ZooKeeperTestSupport;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.commons.logging.LogFactory;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
public class MultiMasterCuratorLeaderRoutePolicyTest extends ZooKeeperTestSupport {
public static final String ZNODE = "/multimaster";
public static final String BASE_ZNODE = "/someapp";
private static final Logger LOG = LoggerFactory.getLogger(MultiMasterCuratorLeaderRoutePolicyTest.class);
protected CamelContext createCamelContext() throws Exception {
disableJMX();
return super.createCamelContext();
}
@Test
public void ensureRoutesDoNotStartAutomatically() throws Exception {
DefaultCamelContext context = new DefaultCamelContext();
context.addRoutes(new RouteBuilder() {
@Override
public void configure() throws Exception {
CuratorMultiMasterLeaderRoutePolicy policy = new CuratorMultiMasterLeaderRoutePolicy("zookeeper:localhost:" + getServerPort() + BASE_ZNODE + ZNODE + 2);
from("timer://foo?fixedRate=true&period=5").routePolicy(policy).id("single_route").autoStartup(true).to("mock:controlled");
}
});
context.start();
// this check verifies that a route marked as autostartable is not started automatically. It will be the policy responsibility to eventually start it.
assertThat(context.getRouteStatus("single_route").isStarted(), is(false));
assertThat(context.getRouteStatus("single_route").isStarting(), is(false));
try {
context.shutdown();
} catch (Exception e) {
//concurrency can raise some InterruptedException but we don't really care in this scenario.
}
}
@Test
public void oneMasterOneSlaveScenarioContolledByPolicy() throws Exception {
final String path = "oneMasterOneSlaveScenarioContolledByPolicy";
final String firstDestination = "first" + System.currentTimeMillis();
final String secondDestination = "second" + System.currentTimeMillis();
final CountDownLatch waitForSecondRouteCompletedLatch = new CountDownLatch(1);
final int activeNodesDesired = 1;
MultiMasterZookeeperPolicyEnforcedContext first = createEnforcedContext(firstDestination, activeNodesDesired, path);
DefaultCamelContext controlledContext = (DefaultCamelContext) first.controlledContext;
// get reference to the Policy object to check if it's already a master
CuratorMultiMasterLeaderRoutePolicy routePolicy = (CuratorMultiMasterLeaderRoutePolicy) controlledContext.getRouteDefinition(firstDestination).getRoutePolicies().get(0);
assertWeHaveMasters(routePolicy);
LOG.info("Starting first CamelContext");
final MultiMasterZookeeperPolicyEnforcedContext[] arr = new MultiMasterZookeeperPolicyEnforcedContext[1];
new Thread() {
@Override
public void run() {
MultiMasterZookeeperPolicyEnforcedContext second = null;
try {
LOG.info("Starting second CamelContext in a separate thread");
second = createEnforcedContext(secondDestination, activeNodesDesired, path);
arr[0] = second;
second.sendMessageToEnforcedRoute("message for second", 0);
waitForSecondRouteCompletedLatch.countDown();
} catch (Exception e) {
LOG.error("Error in the thread controlling the second context", e);
fail("Error in the thread controlling the second context: " + e.getMessage());
}
}
}.start();
first.sendMessageToEnforcedRoute("message for first", 1);
waitForSecondRouteCompletedLatch.await(2, TimeUnit.MINUTES);
LOG.info("Explicitly shutting down the first camel context.");
LOG.info("Shutting down first con");
first.shutdown();
MultiMasterZookeeperPolicyEnforcedContext second = arr[0];
DefaultCamelContext secondCamelContext = (DefaultCamelContext) second.controlledContext;
assertWeHaveMasters((CuratorMultiMasterLeaderRoutePolicy)secondCamelContext.getRouteDefinition(secondDestination).getRoutePolicies().get(0));
//second.mock = secondCamelContext.getEndpoint("mock:controlled", MockEndpoint.class);
second.sendMessageToEnforcedRoute("message for slave", 1);
second.shutdown();
}
@Test
public void oneMasterOneSlaveAndFlippedAgainScenarioContolledByPolicy() throws Exception {
final String path = "oneMasterOneSlaveScenarioContolledByPolicy";
final String firstDestination = "first" + System.currentTimeMillis();
final String secondDestination = "second" + System.currentTimeMillis();
final CountDownLatch waitForSecondRouteCompletedLatch = new CountDownLatch(1);
final int activeNodeDesired = 1;
MultiMasterZookeeperPolicyEnforcedContext first = createEnforcedContext(firstDestination, activeNodeDesired, path);
DefaultCamelContext controlledContext = (DefaultCamelContext) first.controlledContext;
// get reference to the Policy object to check if it's already a master
CuratorMultiMasterLeaderRoutePolicy routePolicy = (CuratorMultiMasterLeaderRoutePolicy) controlledContext.getRouteDefinition(firstDestination).getRoutePolicies().get(0);
assertWeHaveMasters(routePolicy);
LOG.info("Starting first CamelContext");
final MultiMasterZookeeperPolicyEnforcedContext[] arr = new MultiMasterZookeeperPolicyEnforcedContext[1];
new Thread() {
@Override
public void run() {
MultiMasterZookeeperPolicyEnforcedContext slave = null;
try {
LOG.info("Starting second CamelContext in a separate thread");
slave = createEnforcedContext(secondDestination, activeNodeDesired, path);
arr[0] = slave;
slave.sendMessageToEnforcedRoute("message for second", 0);
waitForSecondRouteCompletedLatch.countDown();
} catch (Exception e) {
LOG.error("Error in the thread controlling the second context", e);
fail("Error in the thread controlling the second context: " + e.getMessage());
}
}
}.start();
first.sendMessageToEnforcedRoute("message for first", 1);
waitForSecondRouteCompletedLatch.await(2, TimeUnit.MINUTES);
MultiMasterZookeeperPolicyEnforcedContext second = arr[0];
LOG.info("Explicitly shutting down the first camel context.");
first.shutdown();
DefaultCamelContext secondCamelContext = (DefaultCamelContext) second.controlledContext;
assertWeHaveMasters((CuratorMultiMasterLeaderRoutePolicy)secondCamelContext.getRouteDefinition(secondDestination).getRoutePolicies().get(0));
CountDownLatch restartFirstLatch = new CountDownLatch(1);
LOG.info("Start back first context");
new Thread() {
@Override
public void run() {
try {
first.startup();
restartFirstLatch.countDown();
} catch (Exception e) {
e.printStackTrace();
}
}
}.start();
restartFirstLatch.await();
second.sendMessageToEnforcedRoute("message for second", 1);
first.mock.reset();
first.sendMessageToEnforcedRoute("message for first", 0);
second.shutdown();
controlledContext = (DefaultCamelContext) first.controlledContext;
// get reference to the Policy object to check if it's already a master
routePolicy = (CuratorMultiMasterLeaderRoutePolicy) controlledContext.getRouteDefinition(firstDestination).getRoutePolicies().get(0);
log.info("Asserting route is up. context: [{}]", controlledContext.getName());
assertWeHaveMasters(routePolicy);
first.controlledContext.setTracing(true);
first.mock = controlledContext.getEndpoint("mock:controlled", MockEndpoint.class);
first.sendMessageToEnforcedRoute("message for first", 1);
first.shutdown();
}
@Test
public void oneMasterTwoSlavesScenarioContolledByPolicy() throws Exception {
final String path = "oneMasterTwoSlavesScenarioContolledByPolicy";
final String master = "master" + System.currentTimeMillis();
final String secondDestination = "second" + System.currentTimeMillis();
final String thirdDestination = "third" + System.currentTimeMillis();
final CountDownLatch waitForNonActiveRoutesLatch = new CountDownLatch(2);
final int activeNodesDesired = 1;
LOG.info("Starting first CamelContext");
MultiMasterZookeeperPolicyEnforcedContext first = createEnforcedContext(master, activeNodesDesired, path);
DefaultCamelContext controlledContext = (DefaultCamelContext) first.controlledContext;
// get reference to the Policy object to check if it's already a master
CuratorMultiMasterLeaderRoutePolicy routePolicy = (CuratorMultiMasterLeaderRoutePolicy) controlledContext.getRouteDefinition(master).getRoutePolicies().get(0);
assertWeHaveMasters(routePolicy);
final MultiMasterZookeeperPolicyEnforcedContext[] arr = new MultiMasterZookeeperPolicyEnforcedContext[2];
new Thread() {
@Override
public void run() {
MultiMasterZookeeperPolicyEnforcedContext second = null;
try {
LOG.info("Starting second CamelContext");
second = createEnforcedContext(secondDestination, activeNodesDesired, path);
arr[0] = second;
second.sendMessageToEnforcedRoute("message for second", 0);
waitForNonActiveRoutesLatch.countDown();
} catch (Exception e) {
LOG.error("Error in the thread controlling the second context", e);
fail("Error in the thread controlling the second context: " + e.getMessage());
}
}
}.start();
new Thread() {
@Override
public void run() {
MultiMasterZookeeperPolicyEnforcedContext third = null;
try {
LOG.info("Starting third CamelContext");
third = createEnforcedContext(thirdDestination, activeNodesDesired, path);
arr[1] = third;
third.sendMessageToEnforcedRoute("message for third", 0);
waitForNonActiveRoutesLatch.countDown();
} catch (Exception e) {
LOG.error("Error in the thread controlling the third context", e);
fail("Error in the thread controlling the third context: " + e.getMessage());
}
}
}.start();
// Send messages to the master and the slave.
// The route is enabled in the master and gets through, but that sent to
// the slave context is rejected.
first.sendMessageToEnforcedRoute("message for master", 1);
waitForNonActiveRoutesLatch.await();
LOG.info("Explicitly shutting down the first camel context.");
// trigger failover by killing the master..
first.shutdown();
// let's find out who's active now:
CuratorMultiMasterLeaderRoutePolicy routePolicySecond = (CuratorMultiMasterLeaderRoutePolicy) arr[0].controlledContext.getRouteDefinition(secondDestination).getRoutePolicies().get(0);
CuratorMultiMasterLeaderRoutePolicy routePolicyThird = (CuratorMultiMasterLeaderRoutePolicy) arr[1].controlledContext.getRouteDefinition(thirdDestination).getRoutePolicies().get(0);
MultiMasterZookeeperPolicyEnforcedContext newMaster = null;
MultiMasterZookeeperPolicyEnforcedContext slave = null;
final int maxWait = 20;
for (int i = 0; i < maxWait; i++) {
if (routePolicySecond.getElection().isMaster()) {
newMaster = arr[0];
slave = arr[1];
LOG.info("[second] is the new master");
break;
} else if (routePolicyThird.getElection().isMaster()) {
newMaster = arr[1];
slave = arr[0];
LOG.info("[third] is the new master");
break;
} else {
Thread.sleep(2000);
LOG.info("waiting for a new master to be elected");
}
}
assertThat(newMaster, is(notNullValue()));
newMaster.sendMessageToEnforcedRoute("message for second", 1);
slave.sendMessageToEnforcedRoute("message for third", 0);
slave.shutdown();
newMaster.shutdown();
}
@Test
public void twoMasterOneSlavesScenarioContolledByPolicy() throws Exception {
final String path = "twoMasterOneSlavesScenarioContolledByPolicy";
final String firstDestination = "first" + System.currentTimeMillis();
final String secondDestination = "second" + System.currentTimeMillis();
final String thirdDestination = "third" + System.currentTimeMillis();
final CountDownLatch waitForThirdRouteCompletedLatch = new CountDownLatch(1);
final int activeNodeDesired = 2;
MultiMasterZookeeperPolicyEnforcedContext first = createEnforcedContext(firstDestination, activeNodeDesired, path);
DefaultCamelContext firstControlledContext = (DefaultCamelContext) first.controlledContext;
CuratorMultiMasterLeaderRoutePolicy firstRoutePolicy = (CuratorMultiMasterLeaderRoutePolicy) firstControlledContext.getRouteDefinition(firstDestination).getRoutePolicies().get(0);
MultiMasterZookeeperPolicyEnforcedContext second = createEnforcedContext(secondDestination, activeNodeDesired, path);
DefaultCamelContext secondControlledContext = (DefaultCamelContext) second.controlledContext;
CuratorMultiMasterLeaderRoutePolicy secondRoutePolicy = (CuratorMultiMasterLeaderRoutePolicy) secondControlledContext.getRouteDefinition(secondDestination).getRoutePolicies().get(0);
assertWeHaveMasters(firstRoutePolicy, secondRoutePolicy);
final MultiMasterZookeeperPolicyEnforcedContext[] arr = new MultiMasterZookeeperPolicyEnforcedContext[1];
new Thread() {
@Override
public void run() {
MultiMasterZookeeperPolicyEnforcedContext third = null;
try {
LOG.info("Starting third CamelContext");
third = createEnforcedContext(thirdDestination, activeNodeDesired, path);
arr[0] = third;
third.sendMessageToEnforcedRoute("message for third", 0);
waitForThirdRouteCompletedLatch.countDown();
} catch (Exception e) {
LOG.error("Error in the thread controlling the third context", e);
fail("Error in the thread controlling the third context: " + e.getMessage());
}
}
}.start();
first.sendMessageToEnforcedRoute("message for first", 1);
second.sendMessageToEnforcedRoute("message for second", 1);
waitForThirdRouteCompletedLatch.await();
LOG.info("Explicitly shutting down the first camel context.");
first.shutdown();
arr[0].sendMessageToEnforcedRoute("message for third", 1);
second.shutdown();
arr[0].shutdown();
}
void assertWeHaveMasters(CuratorMultiMasterLeaderRoutePolicy... routePolicies) throws InterruptedException {
final int maxWait = 20;
boolean global = false;
for (int i = 0; i < maxWait; i++) {
boolean iteration = true;
for (CuratorMultiMasterLeaderRoutePolicy policy : routePolicies) {
log.info("Policy: {}, master: {}", policy, policy.getElection().isMaster());
iteration = iteration & policy.getElection().isMaster();
}
if (iteration) {
LOG.info("the number of required active routes is available");
global = true;
break;
} else {
Thread.sleep(2000);
LOG.info("waiting routes to become leader and be activated.");
}
}
if (!global) {
fail("The expected number of route never became master");
}
}
private class MultiMasterZookeeperPolicyEnforcedContext {
CamelContext controlledContext;
ProducerTemplate template;
MockEndpoint mock;
String routename;
String path;
MultiMasterZookeeperPolicyEnforcedContext(String name, int activeNodesDesired, String path) throws Exception {
controlledContext = new DefaultCamelContext();
routename = name;
this.path = path;
template = controlledContext.createProducerTemplate();
mock = controlledContext.getEndpoint("mock:controlled", MockEndpoint.class);
controlledContext.addRoutes(new FailoverRoute(name, activeNodesDesired, path));
controlledContext.start();
}
public void sendMessageToEnforcedRoute(String message, int expected) throws InterruptedException {
mock.expectedMessageCount(expected);
try {
LOG.info("Sending message to: {}", "vm:" + routename);
template.sendBody("vm:" + routename, ExchangePattern.InOut, message);
} catch (Exception e) {
if (expected > 0) {
LOG.error(e.getMessage(), e);
fail("Expected messages...");
}
}
mock.await(2, TimeUnit.SECONDS);
mock.assertIsSatisfied(2000);
}
public void shutdown() throws Exception {
LogFactory.getLog(getClass()).debug("stopping");
controlledContext.stop();
LogFactory.getLog(getClass()).debug("stopped");
}
public void startup() throws Exception {
LogFactory.getLog(getClass()).debug("starting");
controlledContext.start();
LogFactory.getLog(getClass()).debug("started");
}
}
private MultiMasterZookeeperPolicyEnforcedContext createEnforcedContext(String name, int activeNodesDesired, String path) throws Exception, InterruptedException {
MultiMasterZookeeperPolicyEnforcedContext context = new MultiMasterZookeeperPolicyEnforcedContext(name, activeNodesDesired, path);
delay(1000);
return context;
}
public class FailoverRoute extends RouteBuilder {
private String path;
private String routename;
private int activeNodesDesired;
public FailoverRoute(String routename, int activeNodesDesired, String path) {
// need names as if we use the same direct ep name in two contexts
// in the same vm shutting down one context shuts the endpoint for
// both.
this.routename = routename;
this.activeNodesDesired = activeNodesDesired;
this.path = path;
}
public void configure() throws Exception {
CuratorMultiMasterLeaderRoutePolicy policy = new CuratorMultiMasterLeaderRoutePolicy("zookeeper:localhost:" + getServerPort() + BASE_ZNODE + ZNODE + "/" + path, this.activeNodesDesired);
from("vm:" + routename).routePolicy(policy).id(routename).to("mock:controlled");
}
}
}