/*
* SonarQube
* Copyright (C) 2009-2017 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.util.EnumMap;
import java.util.Optional;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.application.config.AppSettings;
import org.sonar.application.config.ClusterSettings;
import org.sonar.application.process.JavaCommand;
import org.sonar.application.process.JavaCommandFactory;
import org.sonar.application.process.JavaProcessLauncher;
import org.sonar.application.process.Lifecycle;
import org.sonar.application.process.ProcessEventListener;
import org.sonar.application.process.ProcessLifecycleListener;
import org.sonar.application.process.SQProcess;
import org.sonar.process.ProcessId;
public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLifecycleListener, AppStateListener {
private static final Logger LOG = LoggerFactory.getLogger(SchedulerImpl.class);
private final AppSettings settings;
private final AppReloader appReloader;
private final JavaCommandFactory javaCommandFactory;
private final JavaProcessLauncher javaProcessLauncher;
private final AppState appState;
private final NodeLifecycle nodeLifecycle = new NodeLifecycle();
private final CountDownLatch keepAlive = new CountDownLatch(1);
private final AtomicBoolean restartRequested = new AtomicBoolean(false);
private final AtomicBoolean restartDisabled = new AtomicBoolean(false);
private final EnumMap<ProcessId, SQProcess> processesById = new EnumMap<>(ProcessId.class);
private final AtomicInteger operationalCountDown = new AtomicInteger();
private final AtomicInteger stopCountDown = new AtomicInteger(0);
private StopperThread stopperThread;
private RestarterThread restarterThread;
private long processWatcherDelayMs = SQProcess.DEFAULT_WATCHER_DELAY_MS;
public SchedulerImpl(AppSettings settings, AppReloader appReloader, JavaCommandFactory javaCommandFactory,
JavaProcessLauncher javaProcessLauncher,
AppState appState) {
this.settings = settings;
this.appReloader = appReloader;
this.javaCommandFactory = javaCommandFactory;
this.javaProcessLauncher = javaProcessLauncher;
this.appState = appState;
this.appState.addListener(this);
}
SchedulerImpl setProcessWatcherDelayMs(long l) {
this.processWatcherDelayMs = l;
return this;
}
@Override
public void schedule() {
if (!nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STARTING)) {
return;
}
processesById.clear();
for (ProcessId processId : ClusterSettings.getEnabledProcesses(settings)) {
SQProcess process = SQProcess.builder(processId)
.addProcessLifecycleListener(this)
.addEventListener(this)
.setWatcherDelayMs(processWatcherDelayMs)
.build();
processesById.put(process.getProcessId(), process);
}
operationalCountDown.set(processesById.size());
tryToStartAll();
}
private void tryToStartAll() {
tryToStartEs();
tryToStartWeb();
tryToStartCe();
}
private void tryToStartEs() {
SQProcess process = processesById.get(ProcessId.ELASTICSEARCH);
if (process != null) {
tryToStartProcess(process, javaCommandFactory::createEsCommand);
}
}
private void tryToStartWeb() {
SQProcess process = processesById.get(ProcessId.WEB_SERVER);
if (process == null || !isEsClientStartable()) {
return;
}
if (appState.isOperational(ProcessId.WEB_SERVER, false)) {
tryToStartProcess(process, () -> javaCommandFactory.createWebCommand(false));
} else if (appState.tryToLockWebLeader()) {
tryToStartProcess(process, () -> javaCommandFactory.createWebCommand(true));
} else {
Optional<String> leader = appState.getLeaderHostName();
if (leader.isPresent()) {
LOG.info("Waiting for initialization from " + leader.get());
} else {
LOG.error("Initialization failed. All nodes must be restarted");
}
}
}
private void tryToStartCe() {
SQProcess process = processesById.get(ProcessId.COMPUTE_ENGINE);
if (process != null && appState.isOperational(ProcessId.WEB_SERVER, false) && isEsClientStartable()) {
tryToStartProcess(process, javaCommandFactory::createCeCommand);
}
}
private boolean isEsClientStartable() {
boolean requireLocalEs = ClusterSettings.isLocalElasticsearchEnabled(settings);
return appState.isOperational(ProcessId.ELASTICSEARCH, requireLocalEs);
}
private void tryToStartProcess(SQProcess process, Supplier<JavaCommand> commandSupplier) {
try {
process.start(() -> {
JavaCommand command = commandSupplier.get();
return javaProcessLauncher.launch(command);
});
} catch (RuntimeException e) {
// failed to start command -> stop everything
terminate();
throw e;
}
}
private void stopAll() {
// order is important for non-cluster mode
stopProcess(ProcessId.COMPUTE_ENGINE);
stopProcess(ProcessId.WEB_SERVER);
stopProcess(ProcessId.ELASTICSEARCH);
}
/**
* Request for graceful stop then blocks until process is stopped.
* Returns immediately if the process is disabled in configuration.
*/
private void stopProcess(ProcessId processId) {
SQProcess process = processesById.get(processId);
if (process != null) {
process.stop(1, TimeUnit.MINUTES);
}
}
/**
* Blocks until all processes are stopped. Pending restart, if
* any, is disabled.
*/
@Override
public void terminate() {
// disable ability to request for restart
restartRequested.set(false);
restartDisabled.set(true);
if (nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPING)) {
LOG.info("Stopping SonarQube");
}
stopAll();
if (stopperThread != null) {
stopperThread.interrupt();
}
if (restarterThread != null) {
restarterThread.interrupt();
}
keepAlive.countDown();
}
@Override
public void awaitTermination() {
try {
keepAlive.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
@Override
public void onProcessEvent(ProcessId processId, Type type) {
if (type == Type.OPERATIONAL) {
onProcessOperational(processId);
} else if (type == Type.ASK_FOR_RESTART && restartRequested.compareAndSet(false, true)) {
stopAsync();
}
}
private void onProcessOperational(ProcessId processId) {
LOG.info("Process[{}] is up", processId.getKey());
appState.setOperational(processId);
if (operationalCountDown.decrementAndGet() == 0 && nodeLifecycle.tryToMoveTo(NodeLifecycle.State.OPERATIONAL)) {
LOG.info("SonarQube is up");
}
}
@Override
public void onAppStateOperational(ProcessId processId) {
if (nodeLifecycle.getState() == NodeLifecycle.State.STARTING) {
tryToStartAll();
}
}
@Override
public void onProcessState(ProcessId processId, Lifecycle.State to) {
switch (to) {
case STOPPED:
onProcessStop(processId);
break;
case STARTING:
stopCountDown.incrementAndGet();
break;
default:
// Nothing to do
break;
}
}
private void onProcessStop(ProcessId processId) {
LOG.info("Process [{}] is stopped", processId.getKey());
if (stopCountDown.decrementAndGet() == 0 && nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPED)) {
if (!restartDisabled.get() &&
restartRequested.compareAndSet(true, false)) {
LOG.info("SonarQube is restarting");
restartAsync();
} else {
LOG.info("SonarQube is stopped");
// all processes are stopped, no restart requested
// Let's clean-up resources
terminate();
}
} else if (nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPING)) {
// this is the first process stopping
stopAsync();
}
}
private void stopAsync() {
stopperThread = new StopperThread();
stopperThread.start();
}
private void restartAsync() {
restarterThread = new RestarterThread();
restarterThread.start();
}
private class RestarterThread extends Thread {
public RestarterThread() {
super("Restarter");
}
@Override
public void run() {
try {
appReloader.reload(settings);
schedule();
} catch (Exception e) {
LOG.error("Fail to restart", e);
terminate();
}
}
}
private class StopperThread extends Thread {
public StopperThread() {
super("Stopper");
}
@Override
public void run() {
stopAll();
}
}
}