Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SLCORE-1237 Cancel analysis when the corresponding config scope is closed #1292

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* SonarLint Core - Analysis Engine
* Copyright (C) 2016-2025 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.sonarsource.sonarlint.core.analysis;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.PriorityQueue;
import java.util.function.Predicate;
import org.sonarsource.sonarlint.core.analysis.command.AnalyzeCommand;
import org.sonarsource.sonarlint.core.analysis.command.Command;
import org.sonarsource.sonarlint.core.analysis.command.NotifyModuleEventCommand;
import org.sonarsource.sonarlint.core.analysis.command.RegisterModuleCommand;
import org.sonarsource.sonarlint.core.analysis.command.UnregisterModuleCommand;

import static java.util.Map.entry;

public class AnalysisQueue {
private final PriorityQueue<Command> queue = new PriorityQueue<>(new CommandComparator());

public synchronized void post(Command command) {
queue.add(command);
notifyAll();
}

public synchronized void wakeUp() {
notifyAll();
}

public synchronized List<Command> removeAll() {
var pendingTasks = new ArrayList<>(queue);
queue.clear();
return pendingTasks;
}

public synchronized Command takeNextCommand() throws InterruptedException {
while (true) {
var firstReadyCommand = pollNextReadyCommand();
if (firstReadyCommand.isPresent()) {
var command = firstReadyCommand.get();
tidyUp(command);
return command;
}
// wait for a new command to come in
wait();
}
}

public synchronized void clearAllButAnalyses() {
removeAll(command -> !(command instanceof AnalyzeCommand));
}

private Optional<Command> pollNextReadyCommand() {
var commandsToKeep = new ArrayList<Command>();
// cannot use iterator as priority order is not guaranteed
while (!queue.isEmpty()) {
var candidateCommand = queue.poll();
if (candidateCommand.isReady()) {
queue.addAll(commandsToKeep);
return Optional.of(candidateCommand);
}
commandsToKeep.add(candidateCommand);
}
queue.addAll(commandsToKeep);
return Optional.empty();
}

private void tidyUp(Command nextCommand) {
cleanUpOutdatedCommands(nextCommand);
}

private void cleanUpOutdatedCommands(Command nextCommand) {
if (nextCommand instanceof UnregisterModuleCommand unregisterCommand) {
removeAll(command -> command instanceof AnalyzeCommand analyzeCommand && analyzeCommand.getModuleKey().equals(unregisterCommand.getModuleKey())
|| command instanceof NotifyModuleEventCommand);
}
}

private void removeAll(Predicate<Command> predicate) {
var iterator = queue.iterator();
while (queue.iterator().hasNext()) {
var command = iterator.next();
if (predicate.test(command)) {
iterator.remove();
}
}
}

private static class CommandComparator implements Comparator<Command> {
private static final Map<Class<?>, Integer> COMMAND_TYPES_ORDERED = Map.ofEntries(
// registering and unregistering modules have the highest priority
// even if inserted later, they should be pulled first from the queue, before file events and analyzes: they might make them irrelevant
// they both have the same priority so insertion order is respected
entry(RegisterModuleCommand.class, 0), entry(UnregisterModuleCommand.class, 0),
// forwarding file events takes priority over analyses, to make sure they give more accurate results
entry(NotifyModuleEventCommand.class, 1),
// analyses have the lowest priority
entry(AnalyzeCommand.class, 2));

@Override
public int compare(Command command, Command otherCommand) {
var commandRank = COMMAND_TYPES_ORDERED.get(command.getClass());
var otherCommandRank = COMMAND_TYPES_ORDERED.get(otherCommand.getClass());
return !Objects.equals(commandRank, otherCommandRank) ? commandRank - otherCommandRank :
// for same command types, respect insertion order
(int) (command.getSequenceNumber() - otherCommand.getSequenceNumber());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,56 +19,60 @@
*/
package org.sonarsource.sonarlint.core.analysis;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
import javax.annotation.Nullable;
import org.sonarsource.sonarlint.core.analysis.api.AnalysisEngineConfiguration;
import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration;
import org.sonarsource.sonarlint.core.analysis.command.Command;
import org.sonarsource.sonarlint.core.analysis.container.global.GlobalAnalysisContainer;
import org.sonarsource.sonarlint.core.analysis.container.global.ModuleRegistry;
import org.sonarsource.sonarlint.core.commons.log.LogOutput;
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
import org.sonarsource.sonarlint.core.commons.progress.ProgressMonitor;
import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins;

public class AnalysisEngine {
public class AnalysisScheduler {
private static final SonarLintLogger LOG = SonarLintLogger.get();
private static final Runnable CANCELING_TERMINATION = () -> {
};

private final GlobalAnalysisContainer globalAnalysisContainer;
private final BlockingQueue<AsyncCommand<?>> commandQueue = new LinkedBlockingQueue<>();
private final Thread analysisThread = new Thread(this::executeQueuedCommands, "sonarlint-analysis-engine");
private final AtomicReference<GlobalAnalysisContainer> globalAnalysisContainer = new AtomicReference<>();
private final AnalysisQueue analysisQueue = new AnalysisQueue();
private final Thread analysisThread = new Thread(this::executeQueuedCommands, "sonarlint-analysis-scheduler");
private final LogOutput logOutput;
private final AtomicReference<Runnable> termination = new AtomicReference<>();
private final AtomicReference<AsyncCommand<?>> executingCommand = new AtomicReference<>();
private final AtomicReference<Command> executingCommand = new AtomicReference<>();

public AnalysisEngine(AnalysisEngineConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins, @Nullable LogOutput logOutput) {
globalAnalysisContainer = new GlobalAnalysisContainer(analysisGlobalConfig, loadedPlugins);
public AnalysisScheduler(AnalysisSchedulerConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins, @Nullable LogOutput logOutput) {
this.logOutput = logOutput;
start();
}

private void start() {
// if the container cannot be started, the thread won't be started
globalAnalysisContainer.startComponents();
startContainer(analysisGlobalConfig, loadedPlugins);
analysisThread.start();
}

public void reset(AnalysisSchedulerConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins) {
// recreate the context
globalAnalysisContainer.get().stopComponents();
startContainer(analysisGlobalConfig, loadedPlugins);
analysisQueue.clearAllButAnalyses();
}

private void startContainer(AnalysisSchedulerConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins) {
globalAnalysisContainer.set(new GlobalAnalysisContainer(analysisGlobalConfig, loadedPlugins));
globalAnalysisContainer.get().startComponents();
}

public void wakeUp() {
analysisQueue.wakeUp();
}

private void executeQueuedCommands() {
while (termination.get() == null) {
SonarLintLogger.get().setTarget(logOutput);
try {
executingCommand.set(commandQueue.take());
executingCommand.set(analysisQueue.takeNextCommand());
if (termination.get() == CANCELING_TERMINATION) {
executingCommand.get().cancel();
break;
}
executingCommand.get().execute(getModuleRegistry());
executingCommand.get().execute(globalAnalysisContainer.get().getModuleRegistry());
executingCommand.set(null);
} catch (InterruptedException e) {
if (termination.get() != CANCELING_TERMINATION) {
Expand All @@ -79,23 +83,18 @@ private void executeQueuedCommands() {
termination.get().run();
}

public <T> CompletableFuture<T> post(Command<T> command, ProgressMonitor progressMonitor) {
public void post(Command command) {
if (termination.get() != null) {
LOG.error("Analysis engine stopping, ignoring command");
return CompletableFuture.completedFuture(null);
command.cancel();
return;
}
if (!analysisThread.isAlive()) {
LOG.error("Analysis engine not started, ignoring command");
return CompletableFuture.completedFuture(null);
}

var asyncCommand = new AsyncCommand<>(command, progressMonitor);
try {
commandQueue.put(asyncCommand);
} catch (InterruptedException e) {
asyncCommand.future.completeExceptionally(e);
command.cancel();
return;
}
return asyncCommand.future;
analysisQueue.post(command);
}

public void stop() {
Expand All @@ -111,37 +110,7 @@ public void stop() {
command.cancel();
}
analysisThread.interrupt();
List<AsyncCommand<?>> pendingCommands = new ArrayList<>();
commandQueue.drainTo(pendingCommands);
pendingCommands.forEach(c -> c.future.cancel(false));
globalAnalysisContainer.stopComponents();
}

private ModuleRegistry getModuleRegistry() {
return globalAnalysisContainer.getModuleRegistry();
}

public static class AsyncCommand<T> {
private final CompletableFuture<T> future = new CompletableFuture<>();
private final Command<T> command;
private final ProgressMonitor progressMonitor;

public AsyncCommand(Command<T> command, ProgressMonitor progressMonitor) {
this.command = command;
this.progressMonitor = progressMonitor;
}

public void execute(ModuleRegistry moduleRegistry) {
try {
var result = command.execute(moduleRegistry, progressMonitor);
future.complete(result);
} catch (Throwable e) {
future.completeExceptionally(e);
}
}

public void cancel() {
progressMonitor.cancel();
}
analysisQueue.removeAll().forEach(Command::cancel);
globalAnalysisContainer.get().stopComponents();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import javax.annotation.concurrent.Immutable;

@Immutable
public class AnalysisEngineConfiguration {
public class AnalysisSchedulerConfiguration {

private static final String NODE_EXECUTABLE_PROPERTY = "sonar.nodejs.executable";

Expand All @@ -40,7 +40,7 @@ public class AnalysisEngineConfiguration {
private final long clientPid;
private final Supplier<List<ClientModuleInfo>> modulesProvider;

private AnalysisEngineConfiguration(Builder builder) {
private AnalysisSchedulerConfiguration(Builder builder) {
this.workDir = builder.workDir;
this.extraProperties = new LinkedHashMap<>(builder.extraProperties);
this.nodeJsPath = builder.nodeJsPath;
Expand Down Expand Up @@ -117,8 +117,8 @@ public Builder setModulesProvider(Supplier<List<ClientModuleInfo>> modulesProvid
return this;
}

public AnalysisEngineConfiguration build() {
return new AnalysisEngineConfiguration(this);
public AnalysisSchedulerConfiguration build() {
return new AnalysisSchedulerConfiguration(this);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* SonarLint Core - Implementation
* SonarLint Core - Analysis Engine
* Copyright (C) 2016-2025 SonarSource SA
* mailto:info AT sonarsource DOT com
*
Expand All @@ -17,7 +17,7 @@
* 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.sonarsource.sonarlint.core.analysis;
package org.sonarsource.sonarlint.core.analysis.api;

public enum TriggerType {
AUTO, FORCED
Expand Down
Loading