gobblin.service.modules.core.GobblinServiceHATest.java Source code

Java tutorial

Introduction

Here is the source code for gobblin.service.modules.core.GobblinServiceHATest.java

Source

/*
 * 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 gobblin.service.modules.core;

import gobblin.service.FlowId;
import gobblin.service.Schedule;
import java.io.File;
import java.util.Map;
import java.util.Properties;

import java.util.UUID;
import org.apache.commons.io.FileUtils;
import org.apache.curator.test.TestingServer;
import org.apache.hadoop.fs.Path;
import org.eclipse.jetty.http.HttpStatus;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.Assert;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import com.google.common.base.Optional;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.linkedin.data.template.StringMap;
import com.linkedin.restli.client.RestLiResponseException;

import gobblin.configuration.ConfigurationKeys;
import gobblin.runtime.api.FlowSpec;
import gobblin.runtime.api.TopologySpec;
import gobblin.runtime.app.ServiceBasedAppLauncher;
import gobblin.runtime.spec_catalog.FlowCatalog;
import gobblin.runtime.spec_catalog.TopologyCatalog;
import gobblin.service.FlowConfig;
import gobblin.service.FlowConfigClient;
import gobblin.service.FlowId;
import gobblin.service.HelixUtils;
import gobblin.service.ServiceConfigKeys;
import gobblin.service.modules.orchestration.Orchestrator;
import gobblin.util.ConfigUtils;

public class GobblinServiceHATest {

    private static final Logger logger = LoggerFactory.getLogger(GobblinServiceHATest.class);
    private static Gson gson = new GsonBuilder().setPrettyPrinting().create();

    private static final String COMMON_SPEC_STORE_PARENT_DIR = "/tmp/serviceCoreCommon/";

    private static final String NODE_1_SERVICE_WORK_DIR = "/tmp/serviceWorkDirNode1/";
    private static final String NODE_1_SPEC_STORE_PARENT_DIR = "/tmp/serviceCoreNode1/";
    private static final String NODE_1_TOPOLOGY_SPEC_STORE_DIR = "/tmp/serviceCoreNode1/topologyTestSpecStoreNode1";
    private static final String NODE_1_FLOW_SPEC_STORE_DIR = "/tmp/serviceCoreCommon/flowTestSpecStore";

    private static final String NODE_2_SERVICE_WORK_DIR = "/tmp/serviceWorkDirNode2/";
    private static final String NODE_2_SPEC_STORE_PARENT_DIR = "/tmp/serviceCoreNode2/";
    private static final String NODE_2_TOPOLOGY_SPEC_STORE_DIR = "/tmp/serviceCoreNode2/topologyTestSpecStoreNode2";
    private static final String NODE_2_FLOW_SPEC_STORE_DIR = "/tmp/serviceCoreCommon/flowTestSpecStore";

    private static final String TEST_HELIX_CLUSTER_NAME = "testGobblinServiceCluster";

    private static final String TEST_GROUP_NAME_1 = "testGroup1";
    private static final String TEST_FLOW_NAME_1 = "testFlow1";
    private static final String TEST_SCHEDULE_1 = "0 1/0 * ? * *";
    private static final String TEST_TEMPLATE_URI_1 = "FS:///templates/test.template";
    private static final String TEST_DUMMY_GROUP_NAME_1 = "dummyGroup";
    private static final String TEST_DUMMY_FLOW_NAME_1 = "dummyFlow";

    private static final String TEST_GROUP_NAME_2 = "testGroup2";
    private static final String TEST_FLOW_NAME_2 = "testFlow2";
    private static final String TEST_SCHEDULE_2 = "0 1/0 * ? * *";
    private static final String TEST_TEMPLATE_URI_2 = "FS:///templates/test.template";
    private static final String TEST_DUMMY_GROUP_NAME_2 = "dummyGroup";
    private static final String TEST_DUMMY_FLOW_NAME_2 = "dummyFlow";

    private static final String TEST_GOBBLIN_EXECUTOR_NAME = "testGobblinExecutor";
    private static final String TEST_SOURCE_NAME = "testSource";
    private static final String TEST_SINK_NAME = "testSink";

    private ServiceBasedAppLauncher serviceLauncher;
    private TopologyCatalog topologyCatalog;
    private TopologySpec topologySpec;

    private FlowCatalog flowCatalog;
    private FlowSpec flowSpec;

    private Orchestrator orchestrator;

    private GobblinServiceManager node1GobblinServiceManager;
    private FlowConfigClient node1FlowConfigClient;

    private GobblinServiceManager node2GobblinServiceManager;
    private FlowConfigClient node2FlowConfigClient;

    private TestingServer testingZKServer;

    @BeforeClass
    public void setup() throws Exception {
        // Clean up common Flow Spec Dir
        cleanUpDir(COMMON_SPEC_STORE_PARENT_DIR);

        // Clean up work dir for Node 1
        cleanUpDir(NODE_1_SERVICE_WORK_DIR);
        cleanUpDir(NODE_1_SPEC_STORE_PARENT_DIR);

        // Clean up work dir for Node 2
        cleanUpDir(NODE_2_SERVICE_WORK_DIR);
        cleanUpDir(NODE_2_SPEC_STORE_PARENT_DIR);

        // Use a random ZK port
        this.testingZKServer = new TestingServer(-1);
        logger.info("Testing ZK Server listening on: " + testingZKServer.getConnectString());
        HelixUtils.createGobblinHelixCluster(testingZKServer.getConnectString(), TEST_HELIX_CLUSTER_NAME);

        Properties commonServiceCoreProperties = new Properties();
        commonServiceCoreProperties.put(ServiceConfigKeys.ZK_CONNECTION_STRING_KEY,
                testingZKServer.getConnectString());
        commonServiceCoreProperties.put(ServiceConfigKeys.HELIX_CLUSTER_NAME_KEY, TEST_HELIX_CLUSTER_NAME);
        commonServiceCoreProperties.put(ServiceConfigKeys.HELIX_INSTANCE_NAME_KEY,
                "GaaS_" + UUID.randomUUID().toString());
        commonServiceCoreProperties.put(ServiceConfigKeys.TOPOLOGY_FACTORY_TOPOLOGY_NAMES_KEY,
                TEST_GOBBLIN_EXECUTOR_NAME);
        commonServiceCoreProperties.put(
                ServiceConfigKeys.TOPOLOGY_FACTORY_PREFIX + TEST_GOBBLIN_EXECUTOR_NAME + ".description",
                "StandaloneTestExecutor");
        commonServiceCoreProperties
                .put(ServiceConfigKeys.TOPOLOGY_FACTORY_PREFIX + TEST_GOBBLIN_EXECUTOR_NAME + ".version", "1");
        commonServiceCoreProperties.put(
                ServiceConfigKeys.TOPOLOGY_FACTORY_PREFIX + TEST_GOBBLIN_EXECUTOR_NAME + ".uri", "gobblinExecutor");
        commonServiceCoreProperties.put(ServiceConfigKeys.TOPOLOGY_FACTORY_PREFIX + TEST_GOBBLIN_EXECUTOR_NAME
                + ".specExecutorInstanceProducer", "gobblin.service.InMemorySpecExecutorInstanceProducer");
        commonServiceCoreProperties.put(ServiceConfigKeys.TOPOLOGY_FACTORY_PREFIX + TEST_GOBBLIN_EXECUTOR_NAME
                + ".specExecInstance.capabilities", TEST_SOURCE_NAME + ":" + TEST_SINK_NAME);

        Properties node1ServiceCoreProperties = new Properties();
        node1ServiceCoreProperties.putAll(commonServiceCoreProperties);
        node1ServiceCoreProperties.put(ConfigurationKeys.TOPOLOGYSPEC_STORE_DIR_KEY,
                NODE_1_TOPOLOGY_SPEC_STORE_DIR);
        node1ServiceCoreProperties.put(ConfigurationKeys.FLOWSPEC_STORE_DIR_KEY, NODE_1_FLOW_SPEC_STORE_DIR);

        Properties node2ServiceCoreProperties = new Properties();
        node2ServiceCoreProperties.putAll(commonServiceCoreProperties);
        node2ServiceCoreProperties.put(ConfigurationKeys.TOPOLOGYSPEC_STORE_DIR_KEY,
                NODE_2_TOPOLOGY_SPEC_STORE_DIR);
        node2ServiceCoreProperties.put(ConfigurationKeys.FLOWSPEC_STORE_DIR_KEY, NODE_2_FLOW_SPEC_STORE_DIR);

        // Start Node 1
        this.node1GobblinServiceManager = new GobblinServiceManager("CoreService", "1",
                ConfigUtils.propertiesToConfig(node1ServiceCoreProperties),
                Optional.of(new Path(NODE_1_SERVICE_WORK_DIR)));
        this.node1GobblinServiceManager.start();

        // Start Node 2
        this.node2GobblinServiceManager = new GobblinServiceManager("CoreService", "1",
                ConfigUtils.propertiesToConfig(node2ServiceCoreProperties),
                Optional.of(new Path(NODE_2_SERVICE_WORK_DIR)));
        this.node2GobblinServiceManager.start();

        // Initialize Node 1 Client
        this.node1FlowConfigClient = new FlowConfigClient(
                String.format("http://localhost:%s/", this.node1GobblinServiceManager.restliServer.getPort()));

        // Initialize Node 2 Client
        this.node2FlowConfigClient = new FlowConfigClient(
                String.format("http://localhost:%s/", this.node2GobblinServiceManager.restliServer.getPort()));
    }

    private void cleanUpDir(String dir) throws Exception {
        File specStoreDir = new File(dir);
        if (specStoreDir.exists()) {
            FileUtils.deleteDirectory(specStoreDir);
        }
    }

    @AfterClass
    public void cleanUp() throws Exception {
        // Shutdown Node 1
        try {
            this.node1GobblinServiceManager.stop();
        } catch (Exception e) {
            logger.warn("Could not cleanly stop Node 1 of Gobblin Service", e);
        }

        // Shutdown Node 2
        try {
            this.node2GobblinServiceManager.stop();
        } catch (Exception e) {
            logger.warn("Could not cleanly stop Node 2 of Gobblin Service", e);
        }

        // Stop Zookeeper
        try {
            this.testingZKServer.close();
        } catch (Exception e) {
            logger.warn("Could not cleanly stop Testing Zookeeper", e);
        }

        // Cleanup Node 1
        try {
            cleanUpDir(NODE_1_SERVICE_WORK_DIR);
        } catch (Exception e) {
            logger.warn("Could not completely cleanup Node 1 Work Dir");
        }
        try {
            cleanUpDir(NODE_1_SPEC_STORE_PARENT_DIR);
        } catch (Exception e) {
            logger.warn("Could not completely cleanup Node 1 Spec Store Parent Dir");
        }

        // Cleanup Node 2
        try {
            cleanUpDir(NODE_2_SERVICE_WORK_DIR);
        } catch (Exception e) {
            logger.warn("Could not completely cleanup Node 2 Work Dir");
        }
        try {
            cleanUpDir(NODE_2_SPEC_STORE_PARENT_DIR);
        } catch (Exception e) {
            logger.warn("Could not completely cleanup Node 2 Spec Store Parent Dir");
        }

        cleanUpDir(COMMON_SPEC_STORE_PARENT_DIR);
    }

    @Test
    public void testCreate() throws Exception {
        Map<String, String> flowProperties = Maps.newHashMap();
        flowProperties.put("param1", "value1");
        flowProperties.put(ServiceConfigKeys.FLOW_SOURCE_IDENTIFIER_KEY, TEST_SOURCE_NAME);
        flowProperties.put(ServiceConfigKeys.FLOW_DESTINATION_IDENTIFIER_KEY, TEST_SINK_NAME);

        FlowConfig flowConfig1 = new FlowConfig()
                .setId(new FlowId().setFlowGroup(TEST_GROUP_NAME_1).setFlowName(TEST_FLOW_NAME_1))
                .setTemplateUris(TEST_TEMPLATE_URI_1)
                .setSchedule(new Schedule().setCronSchedule(TEST_SCHEDULE_1).setRunImmediately(true))
                .setProperties(new StringMap(flowProperties));
        FlowConfig flowConfig2 = new FlowConfig()
                .setId(new FlowId().setFlowGroup(TEST_GROUP_NAME_2).setFlowName(TEST_FLOW_NAME_2))
                .setTemplateUris(TEST_TEMPLATE_URI_2)
                .setSchedule(new Schedule().setCronSchedule(TEST_SCHEDULE_2).setRunImmediately(true))
                .setProperties(new StringMap(flowProperties));

        // Try create on both nodes
        long schedulingStartTime = System.currentTimeMillis();
        this.node1FlowConfigClient.createFlowConfig(flowConfig1);
        this.node2FlowConfigClient.createFlowConfig(flowConfig2);

        // Check if created on master
        GobblinServiceManager master;
        if (this.node1GobblinServiceManager.isLeader()) {
            master = this.node1GobblinServiceManager;
        } else if (this.node2GobblinServiceManager.isLeader()) {
            master = this.node2GobblinServiceManager;
        } else {
            Assert.fail("No leader found in service cluster");
            return;
        }

        int attempt = 0;
        boolean assertSuccess = false;
        while (attempt < 800) {
            int masterJobs = master.flowCatalog.getSpecs().size();
            if (masterJobs == 2) {
                assertSuccess = true;
                break;
            }
            Thread.sleep(5);
            attempt++;
        }
        long schedulingEndTime = System.currentTimeMillis();
        logger.info("Total scheduling time in ms: " + (schedulingEndTime - schedulingStartTime));

        Assert.assertTrue(assertSuccess, "Flow that was created is not reflecting in FlowCatalog");
    }

    @Test(dependsOnMethods = "testCreate")
    public void testCreateAgain() throws Exception {
        Map<String, String> flowProperties = Maps.newHashMap();
        flowProperties.put("param1", "value1");
        flowProperties.put(ServiceConfigKeys.FLOW_SOURCE_IDENTIFIER_KEY, TEST_SOURCE_NAME);
        flowProperties.put(ServiceConfigKeys.FLOW_DESTINATION_IDENTIFIER_KEY, TEST_SINK_NAME);

        FlowConfig flowConfig1 = new FlowConfig()
                .setId(new FlowId().setFlowGroup(TEST_GROUP_NAME_1).setFlowName(TEST_FLOW_NAME_1))
                .setTemplateUris(TEST_TEMPLATE_URI_1)
                .setSchedule(new Schedule().setCronSchedule(TEST_SCHEDULE_1).setRunImmediately(true))
                .setProperties(new StringMap(flowProperties));
        FlowConfig flowConfig2 = new FlowConfig()
                .setId(new FlowId().setFlowGroup(TEST_GROUP_NAME_2).setFlowName(TEST_FLOW_NAME_2))
                .setTemplateUris(TEST_TEMPLATE_URI_2)
                .setSchedule(new Schedule().setCronSchedule(TEST_SCHEDULE_2).setRunImmediately(true))
                .setProperties(new StringMap(flowProperties));

        // Try create on both nodes
        try {
            this.node1FlowConfigClient.createFlowConfig(flowConfig1);
            Assert.fail("Get should have gotten a 409 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.CONFLICT_409);
        }

        try {
            this.node2FlowConfigClient.createFlowConfig(flowConfig2);
            Assert.fail("Get should have gotten a 409 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.CONFLICT_409);
        }
    }

    @Test(dependsOnMethods = "testCreateAgain")
    public void testGet() throws Exception {
        FlowId flowId1 = new FlowId().setFlowGroup(TEST_GROUP_NAME_1).setFlowName(TEST_FLOW_NAME_1);

        FlowConfig flowConfig1 = this.node1FlowConfigClient.getFlowConfig(flowId1);
        Assert.assertEquals(flowConfig1.getId().getFlowGroup(), TEST_GROUP_NAME_1);
        Assert.assertEquals(flowConfig1.getId().getFlowName(), TEST_FLOW_NAME_1);
        Assert.assertEquals(flowConfig1.getSchedule().getCronSchedule(), TEST_SCHEDULE_1);
        Assert.assertEquals(flowConfig1.getTemplateUris(), TEST_TEMPLATE_URI_1);
        Assert.assertTrue(flowConfig1.getSchedule().isRunImmediately());
        Assert.assertEquals(flowConfig1.getProperties().get("param1"), "value1");

        flowConfig1 = this.node2FlowConfigClient.getFlowConfig(flowId1);
        Assert.assertEquals(flowConfig1.getId().getFlowGroup(), TEST_GROUP_NAME_1);
        Assert.assertEquals(flowConfig1.getId().getFlowName(), TEST_FLOW_NAME_1);
        Assert.assertEquals(flowConfig1.getSchedule().getCronSchedule(), TEST_SCHEDULE_1);
        Assert.assertEquals(flowConfig1.getTemplateUris(), TEST_TEMPLATE_URI_1);
        Assert.assertTrue(flowConfig1.getSchedule().isRunImmediately());
        Assert.assertEquals(flowConfig1.getProperties().get("param1"), "value1");
    }

    @Test(dependsOnMethods = "testGet")
    public void testUpdate() throws Exception {
        // Update on one node and retrieve from another
        FlowId flowId = new FlowId().setFlowGroup(TEST_GROUP_NAME_1).setFlowName(TEST_FLOW_NAME_1);

        Map<String, String> flowProperties = Maps.newHashMap();
        flowProperties.put("param1", "value1b");
        flowProperties.put("param2", "value2b");
        flowProperties.put(ServiceConfigKeys.FLOW_SOURCE_IDENTIFIER_KEY, TEST_SOURCE_NAME);
        flowProperties.put(ServiceConfigKeys.FLOW_DESTINATION_IDENTIFIER_KEY, TEST_SINK_NAME);

        FlowConfig flowConfig = new FlowConfig()
                .setId(new FlowId().setFlowGroup(TEST_GROUP_NAME_1).setFlowName(TEST_FLOW_NAME_1))
                .setTemplateUris(TEST_TEMPLATE_URI_1).setSchedule(new Schedule().setCronSchedule(TEST_SCHEDULE_1))
                .setProperties(new StringMap(flowProperties));

        this.node1FlowConfigClient.updateFlowConfig(flowConfig);

        FlowConfig retrievedFlowConfig = this.node2FlowConfigClient.getFlowConfig(flowId);

        Assert.assertEquals(retrievedFlowConfig.getId().getFlowGroup(), TEST_GROUP_NAME_1);
        Assert.assertEquals(retrievedFlowConfig.getId().getFlowName(), TEST_FLOW_NAME_1);
        Assert.assertEquals(retrievedFlowConfig.getSchedule().getCronSchedule(), TEST_SCHEDULE_1);
        Assert.assertEquals(retrievedFlowConfig.getTemplateUris(), TEST_TEMPLATE_URI_1);
        Assert.assertFalse(retrievedFlowConfig.getSchedule().isRunImmediately());
        Assert.assertEquals(retrievedFlowConfig.getProperties().get("param1"), "value1b");
        Assert.assertEquals(retrievedFlowConfig.getProperties().get("param2"), "value2b");
    }

    @Test(dependsOnMethods = "testUpdate")
    public void testDelete() throws Exception {
        FlowId flowId = new FlowId().setFlowGroup(TEST_GROUP_NAME_1).setFlowName(TEST_FLOW_NAME_1);

        // make sure flow config exists
        FlowConfig flowConfig = this.node1FlowConfigClient.getFlowConfig(flowId);
        Assert.assertEquals(flowConfig.getId().getFlowGroup(), TEST_GROUP_NAME_1);
        Assert.assertEquals(flowConfig.getId().getFlowName(), TEST_FLOW_NAME_1);

        this.node1FlowConfigClient.deleteFlowConfig(flowId);

        // Check if deletion is reflected on both nodes
        try {
            this.node1FlowConfigClient.getFlowConfig(flowId);
            Assert.fail("Get should have gotten a 404 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.NOT_FOUND_404);
        }

        try {
            this.node2FlowConfigClient.getFlowConfig(flowId);
            Assert.fail("Get should have gotten a 404 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.NOT_FOUND_404);
        }
    }

    @Test(dependsOnMethods = "testDelete")
    public void testBadGet() throws Exception {
        FlowId flowId = new FlowId().setFlowGroup(TEST_DUMMY_GROUP_NAME_1).setFlowName(TEST_DUMMY_FLOW_NAME_1);

        try {
            this.node1FlowConfigClient.getFlowConfig(flowId);
            Assert.fail("Get should have raised a 404 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.NOT_FOUND_404);
        }

        try {
            this.node2FlowConfigClient.getFlowConfig(flowId);
            Assert.fail("Get should have raised a 404 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.NOT_FOUND_404);
        }
    }

    @Test(dependsOnMethods = "testBadGet")
    public void testBadDelete() throws Exception {
        FlowId flowId = new FlowId().setFlowGroup(TEST_DUMMY_GROUP_NAME_1).setFlowName(TEST_DUMMY_FLOW_NAME_1);

        try {
            this.node1FlowConfigClient.getFlowConfig(flowId);
            Assert.fail("Get should have raised a 404 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.NOT_FOUND_404);
        }

        try {
            this.node2FlowConfigClient.getFlowConfig(flowId);
            Assert.fail("Get should have raised a 404 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.NOT_FOUND_404);
        }
    }

    @Test(dependsOnMethods = "testBadDelete")
    public void testBadUpdate() throws Exception {
        Map<String, String> flowProperties = Maps.newHashMap();
        flowProperties.put("param1", "value1b");
        flowProperties.put("param2", "value2b");

        FlowConfig flowConfig = new FlowConfig()
                .setId(new FlowId().setFlowGroup(TEST_DUMMY_GROUP_NAME_1).setFlowName(TEST_DUMMY_FLOW_NAME_1))
                .setTemplateUris(TEST_TEMPLATE_URI_1).setSchedule(new Schedule().setCronSchedule(TEST_SCHEDULE_1))
                .setProperties(new StringMap(flowProperties));

        try {
            this.node1FlowConfigClient.updateFlowConfig(flowConfig);
            Assert.fail("Get should have raised a 404 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.NOT_FOUND_404);
        }

        try {
            this.node2FlowConfigClient.updateFlowConfig(flowConfig);
            Assert.fail("Get should have raised a 404 error");
        } catch (RestLiResponseException e) {
            Assert.assertEquals(e.getStatus(), HttpStatus.NOT_FOUND_404);
        }
    }

    @Test(dependsOnMethods = "testBadUpdate")
    public void testKillNode() throws Exception {
        GobblinServiceManager master, secondary;
        if (this.node1GobblinServiceManager.isLeader()) {
            master = this.node1GobblinServiceManager;
            secondary = this.node2GobblinServiceManager;
        } else {
            master = this.node2GobblinServiceManager;
            secondary = this.node1GobblinServiceManager;
        }

        int initialMasterJobs = master.getScheduler().getScheduledFlowSpecs().size();
        int initialSecondaryJobs = secondary.getScheduler().getScheduledFlowSpecs().size();

        Assert.assertTrue(initialMasterJobs > 0, "Master initially should have a few jobs by now in test suite.");
        Assert.assertTrue(initialSecondaryJobs == 0, "Secondary node should not schedule any jobs initially.");

        // Stop current master
        long failOverStartTime = System.currentTimeMillis();
        master.stop();

        // Wait until secondary becomes master, max 4 seconds
        int attempt = 0;
        while (!secondary.isLeader()) {
            if (attempt > 800) {
                Assert.fail("Timeout waiting for Secondary to become master.");
            }
            Thread.sleep(5);
            attempt++;
        }
        long failOverOwnerShipTransferTime = System.currentTimeMillis();

        attempt = 0;
        boolean assertSuccess = false;
        while (attempt < 800) {
            int newMasterJobs = secondary.getScheduler().getScheduledFlowSpecs().size();
            if (newMasterJobs == initialMasterJobs) {
                assertSuccess = true;
                break;
            }
            Thread.sleep(5);
            attempt++;
        }
        long failOverEndTime = System.currentTimeMillis();
        logger.info("Total ownership transfer time in ms: " + (failOverOwnerShipTransferTime - failOverStartTime));
        logger.info("Total rescheduling time in ms: " + (failOverEndTime - failOverOwnerShipTransferTime));
        logger.info("Total failover time in ms: " + (failOverEndTime - failOverStartTime));

        Assert.assertTrue(assertSuccess, "New master should take over all old master jobs.");
    }
}