org.sonar.application.SchedulerImplTest.java Source code

Java tutorial

Introduction

Here is the source code for org.sonar.application.SchedulerImplTest.java

Source

/*
 * SonarQube
 * Copyright (C) 2009-2018 SonarSource SA
 * mailto:info AT sonarsource DOT com
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */
package org.sonar.application;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.DisableOnDebug;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.TestRule;
import org.junit.rules.Timeout;
import org.mockito.Mockito;
import org.sonar.application.command.AbstractCommand;
import org.sonar.application.command.CommandFactory;
import org.sonar.application.command.EsScriptCommand;
import org.sonar.application.command.JavaCommand;
import org.sonar.application.config.TestAppSettings;
import org.sonar.application.process.ProcessLauncher;
import org.sonar.application.process.ProcessMonitor;
import org.sonar.process.ProcessId;
import org.sonar.process.cluster.hz.HazelcastMember;

import static java.util.Collections.synchronizedList;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
import static org.sonar.process.ProcessId.ELASTICSEARCH;
import static org.sonar.process.ProcessId.WEB_SERVER;
import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_HOST;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_NAME;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_PORT;
import static org.sonar.process.ProcessProperties.CLUSTER_NODE_TYPE;

public class SchedulerImplTest {

    @Rule
    public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
    @Rule
    public ExpectedException expectedException = ExpectedException.none();
    @Rule
    public TemporaryFolder temporaryFolder = new TemporaryFolder();

    private EsScriptCommand esScriptCommand;
    private JavaCommand esJavaCommand;
    private JavaCommand webLeaderCommand;
    private JavaCommand webFollowerCommand;
    private JavaCommand ceCommand;

    private AppReloader appReloader = mock(AppReloader.class);
    private TestAppSettings settings = new TestAppSettings();
    private TestCommandFactory javaCommandFactory = new TestCommandFactory();
    private TestProcessLauncher processLauncher = new TestProcessLauncher();
    private TestAppState appState = new TestAppState();
    private HazelcastMember hazelcastMember = mock(HazelcastMember.class);
    private TestClusterAppState clusterAppState = new TestClusterAppState(hazelcastMember);
    private List<ProcessId> orderedStops = synchronizedList(new ArrayList<>());

    @Before
    public void setUp() throws Exception {
        File tempDir = temporaryFolder.newFolder();
        esScriptCommand = new EsScriptCommand(ELASTICSEARCH, tempDir);
        esJavaCommand = new JavaCommand(ELASTICSEARCH, tempDir);
        webLeaderCommand = new JavaCommand(WEB_SERVER, tempDir);
        webFollowerCommand = new JavaCommand(WEB_SERVER, tempDir);
        ceCommand = new JavaCommand(COMPUTE_ENGINE, tempDir);
    }

    @After
    public void tearDown() {
        processLauncher.close();
    }

    @Test
    public void start_and_stop_sequence_of_ES_WEB_CE_in_order() throws Exception {
        SchedulerImpl underTest = newScheduler(false);
        underTest.schedule();

        // elasticsearch does not have preconditions to start
        TestProcess es = processLauncher.waitForProcess(ELASTICSEARCH);
        assertThat(es.isAlive()).isTrue();
        assertThat(processLauncher.processes).hasSize(1);

        // elasticsearch becomes operational -> web leader is starting
        es.operational = true;
        waitForAppStateOperational(appState, ELASTICSEARCH);
        TestProcess web = processLauncher.waitForProcess(WEB_SERVER);
        assertThat(web.isAlive()).isTrue();
        assertThat(processLauncher.processes).hasSize(2);
        assertThat(processLauncher.commands).containsExactly(esScriptCommand, webLeaderCommand);

        // web becomes operational -> CE is starting
        web.operational = true;
        waitForAppStateOperational(appState, WEB_SERVER);
        TestProcess ce = processLauncher.waitForProcess(COMPUTE_ENGINE);
        assertThat(ce.isAlive()).isTrue();
        assertThat(processLauncher.processes).hasSize(3);
        assertThat(processLauncher.commands).containsExactly(esScriptCommand, webLeaderCommand, ceCommand);

        // all processes are up
        processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isTrue());

        // processes are stopped in reverse order of startup
        underTest.terminate();
        assertThat(orderedStops).containsExactly(COMPUTE_ENGINE, WEB_SERVER, ELASTICSEARCH);
        processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());

        // does nothing because scheduler is already terminated
        underTest.awaitTermination();
    }

    @Test
    public void all_processes_are_stopped_if_one_process_goes_down() throws Exception {
        Scheduler underTest = startAll();

        processLauncher.waitForProcess(WEB_SERVER).destroyForcibly();

        underTest.awaitTermination();
        assertThat(orderedStops).containsExactly(WEB_SERVER, COMPUTE_ENGINE, ELASTICSEARCH);
        processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());

        // following does nothing
        underTest.terminate();
        underTest.awaitTermination();
    }

    @Test
    public void all_processes_are_stopped_if_one_process_fails_to_start() throws Exception {
        SchedulerImpl underTest = newScheduler(false);
        processLauncher.makeStartupFail = COMPUTE_ENGINE;

        underTest.schedule();

        processLauncher.waitForProcess(ELASTICSEARCH).operational = true;
        processLauncher.waitForProcess(WEB_SERVER).operational = true;

        underTest.awaitTermination();
        assertThat(orderedStops).containsExactly(WEB_SERVER, ELASTICSEARCH);
        processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
    }

    @Test
    public void terminate_can_be_called_multiple_times() throws Exception {
        Scheduler underTest = startAll();

        underTest.terminate();
        processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());

        // does nothing
        underTest.terminate();
    }

    @Test
    public void awaitTermination_blocks_until_all_processes_are_stopped() throws Exception {
        Scheduler underTest = startAll();

        Thread awaitingTermination = new Thread(() -> underTest.awaitTermination());
        awaitingTermination.start();
        assertThat(awaitingTermination.isAlive()).isTrue();

        underTest.terminate();
        // the thread is being stopped
        awaitingTermination.join();
        assertThat(awaitingTermination.isAlive()).isFalse();
    }

    @Test
    public void restart_stops_all_if_new_settings_are_not_allowed() throws Exception {
        Scheduler underTest = startAll();
        doThrow(new IllegalStateException("reload error")).when(appReloader).reload(settings);

        processLauncher.waitForProcess(WEB_SERVER).askedForRestart = true;

        // waiting for all processes to be stopped
        processLauncher.waitForProcessDown(ELASTICSEARCH);
        processLauncher.waitForProcessDown(COMPUTE_ENGINE);
        processLauncher.waitForProcessDown(WEB_SERVER);

        // verify that awaitTermination() does not block
        underTest.awaitTermination();
    }

    @Test
    public void search_node_starts_only_elasticsearch() throws Exception {
        settings.set(CLUSTER_ENABLED, "true");
        settings.set(CLUSTER_NODE_TYPE, "search");
        addRequiredNodeProperties();
        SchedulerImpl underTest = newScheduler(true);
        underTest.schedule();

        processLauncher.waitForProcessAlive(ProcessId.ELASTICSEARCH);
        assertThat(processLauncher.processes).hasSize(1);

        underTest.terminate();
    }

    @Test
    public void application_node_starts_only_web_and_ce() throws Exception {
        clusterAppState.setOperational(ProcessId.ELASTICSEARCH);
        settings.set(CLUSTER_ENABLED, "true");
        settings.set(CLUSTER_NODE_TYPE, "application");
        SchedulerImpl underTest = newScheduler(true);
        underTest.schedule();

        TestProcess web = processLauncher.waitForProcessAlive(WEB_SERVER);
        web.operational = true;
        processLauncher.waitForProcessAlive(COMPUTE_ENGINE);
        assertThat(processLauncher.processes).hasSize(2);

        underTest.terminate();
    }

    @Test
    public void search_node_starts_even_if_web_leader_is_not_yet_operational() throws Exception {
        // leader takes the lock, so underTest won't get it
        assertThat(clusterAppState.tryToLockWebLeader()).isTrue();

        clusterAppState.setOperational(ProcessId.ELASTICSEARCH);
        settings.set(CLUSTER_ENABLED, "true");
        settings.set(CLUSTER_NODE_TYPE, "search");
        addRequiredNodeProperties();
        SchedulerImpl underTest = newScheduler(true);
        underTest.schedule();

        processLauncher.waitForProcessAlive(ProcessId.ELASTICSEARCH);
        assertThat(processLauncher.processes).hasSize(1);

        underTest.terminate();
    }

    @Test
    public void web_follower_starts_only_when_web_leader_is_operational() throws Exception {
        // leader takes the lock, so underTest won't get it
        assertThat(clusterAppState.tryToLockWebLeader()).isTrue();
        clusterAppState.setOperational(ProcessId.ELASTICSEARCH);

        settings.set(CLUSTER_ENABLED, "true");
        settings.set(CLUSTER_NODE_TYPE, "application");
        SchedulerImpl underTest = newScheduler(true);
        underTest.schedule();

        assertThat(processLauncher.processes).hasSize(0);

        // leader becomes operational -> follower can start
        clusterAppState.setOperational(WEB_SERVER);
        processLauncher.waitForProcessAlive(WEB_SERVER);
        processLauncher.waitForProcessAlive(COMPUTE_ENGINE);
        assertThat(processLauncher.processes).hasSize(2);

        underTest.terminate();
    }

    @Test
    public void web_server_waits_for_remote_elasticsearch_to_be_started_if_local_es_is_disabled() throws Exception {
        settings.set(CLUSTER_ENABLED, "true");
        settings.set(CLUSTER_NODE_TYPE, "application");
        SchedulerImpl underTest = newScheduler(true);
        underTest.schedule();

        // WEB and CE wait for ES to be up
        assertThat(processLauncher.processes).isEmpty();

        // ES becomes operational on another node -> web leader can start
        clusterAppState.setRemoteOperational(ProcessId.ELASTICSEARCH);
        processLauncher.waitForProcessAlive(WEB_SERVER);
        assertThat(processLauncher.processes).hasSize(1);

        underTest.terminate();
    }

    private SchedulerImpl newScheduler(boolean clustered) {
        return new SchedulerImpl(settings, appReloader, javaCommandFactory, processLauncher,
                clustered ? clusterAppState : appState).setProcessWatcherDelayMs(1L);
    }

    private Scheduler startAll() throws InterruptedException {
        SchedulerImpl scheduler = newScheduler(false);
        scheduler.schedule();
        processLauncher.waitForProcess(ELASTICSEARCH).operational = true;
        processLauncher.waitForProcess(WEB_SERVER).operational = true;
        processLauncher.waitForProcess(COMPUTE_ENGINE).operational = true;
        return scheduler;
    }

    private static void waitForAppStateOperational(AppState appState, ProcessId id) throws InterruptedException {
        while (true) {
            if (appState.isOperational(id, true)) {
                return;
            }
            Thread.sleep(1L);
        }
    }

    private void addRequiredNodeProperties() {
        settings.set(CLUSTER_NODE_NAME, randomAlphanumeric(4));
        settings.set(CLUSTER_NODE_HOST, randomAlphanumeric(4));
        settings.set(CLUSTER_NODE_PORT, String.valueOf(1 + new Random().nextInt(999)));
    }

    private class TestCommandFactory implements CommandFactory {
        @Override
        public EsScriptCommand createEsCommand() {
            return esScriptCommand;
        }

        @Override
        public JavaCommand createWebCommand(boolean leader) {
            return leader ? webLeaderCommand : webFollowerCommand;
        }

        @Override
        public JavaCommand createCeCommand() {
            return ceCommand;
        }
    }

    private class TestProcessLauncher implements ProcessLauncher {
        private final EnumMap<ProcessId, TestProcess> processes = new EnumMap<>(ProcessId.class);
        private final List<AbstractCommand<?>> commands = synchronizedList(new ArrayList<>());
        private ProcessId makeStartupFail = null;

        @Override
        public ProcessMonitor launch(AbstractCommand command) {
            return launchImpl(command);
        }

        private ProcessMonitor launchImpl(AbstractCommand<?> javaCommand) {
            commands.add(javaCommand);
            if (makeStartupFail == javaCommand.getProcessId()) {
                throw new IllegalStateException("cannot start " + javaCommand.getProcessId());
            }
            TestProcess process = new TestProcess(javaCommand.getProcessId());
            processes.put(javaCommand.getProcessId(), process);
            return process;
        }

        private TestProcess waitForProcess(ProcessId id) throws InterruptedException {
            while (true) {
                TestProcess p = processes.get(id);
                if (p != null) {
                    return p;
                }
                Thread.sleep(1L);
            }
        }

        private TestProcess waitForProcessAlive(ProcessId id) throws InterruptedException {
            while (true) {
                TestProcess p = processes.get(id);
                if (p != null && p.isAlive()) {
                    return p;
                }
                Thread.sleep(1L);
            }
        }

        private TestProcess waitForProcessDown(ProcessId id) throws InterruptedException {
            while (true) {
                TestProcess p = processes.get(id);
                if (p != null && !p.isAlive()) {
                    return p;
                }
                Thread.sleep(1L);
            }
        }

        @Override
        public void close() {
            for (TestProcess process : processes.values()) {
                process.destroyForcibly();
            }
        }
    }

    private class TestProcess implements ProcessMonitor, AutoCloseable {
        private final ProcessId processId;
        private final CountDownLatch alive = new CountDownLatch(1);
        private boolean operational = false;
        private boolean askedForRestart = false;

        private TestProcess(ProcessId processId) {
            this.processId = processId;
        }

        @Override
        public InputStream getInputStream() {
            return mock(InputStream.class, Mockito.RETURNS_MOCKS);
        }

        @Override
        public InputStream getErrorStream() {
            return mock(InputStream.class, Mockito.RETURNS_MOCKS);
        }

        @Override
        public void closeStreams() {
        }

        @Override
        public boolean isAlive() {
            return alive.getCount() == 1;
        }

        @Override
        public void askForStop() {
            destroyForcibly();
        }

        @Override
        public void destroyForcibly() {
            if (isAlive()) {
                orderedStops.add(processId);
            }
            alive.countDown();
        }

        @Override
        public void waitFor() throws InterruptedException {
            alive.await();
        }

        @Override
        public void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
            alive.await(timeout, timeoutUnit);
        }

        @Override
        public boolean isOperational() {
            return operational;
        }

        @Override
        public boolean askedForRestart() {
            return askedForRestart;
        }

        @Override
        public void acknowledgeAskForRestart() {
            this.askedForRestart = false;
        }

        @Override
        public void close() {
            alive.countDown();
        }
    }
}