Skip to content

Commit 5765df6

Browse files
SLCORE-1237 Cancel analysis when the corresponding config scope is closed
1 parent 4457d37 commit 5765df6

File tree

31 files changed

+623
-1036
lines changed

31 files changed

+623
-1036
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* SonarLint Core - Analysis Engine
3+
* Copyright (C) 2016-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*/
20+
package org.sonarsource.sonarlint.core.analysis;
21+
22+
import java.util.ArrayList;
23+
import java.util.Comparator;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Objects;
27+
import java.util.Optional;
28+
import java.util.PriorityQueue;
29+
import java.util.function.Predicate;
30+
import org.sonarsource.sonarlint.core.analysis.command.AnalyzeCommand;
31+
import org.sonarsource.sonarlint.core.analysis.command.Command;
32+
import org.sonarsource.sonarlint.core.analysis.command.NotifyModuleEventCommand;
33+
import org.sonarsource.sonarlint.core.analysis.command.RegisterModuleCommand;
34+
import org.sonarsource.sonarlint.core.analysis.command.UnregisterModuleCommand;
35+
36+
import static java.util.Map.entry;
37+
38+
public class AnalysisQueue {
39+
private final PriorityQueue<Command> queue = new PriorityQueue<>(new CommandComparator());
40+
41+
public synchronized void post(Command command) {
42+
queue.add(command);
43+
notifyAll();
44+
}
45+
46+
public synchronized void wakeUp() {
47+
notifyAll();
48+
}
49+
50+
public synchronized List<Command> removeAll() {
51+
var pendingTasks = new ArrayList<>(queue);
52+
queue.clear();
53+
return pendingTasks;
54+
}
55+
56+
public synchronized Command takeNextCommand() throws InterruptedException {
57+
while (true) {
58+
var firstReadyCommand = pollNextReadyCommand();
59+
if (firstReadyCommand.isPresent()) {
60+
var command = firstReadyCommand.get();
61+
tidyUp(command);
62+
return command;
63+
}
64+
// wait for a new command to come in
65+
wait();
66+
}
67+
}
68+
69+
public synchronized void clearAllButAnalyses() {
70+
removeAll(command -> !(command instanceof AnalyzeCommand));
71+
}
72+
73+
private Optional<Command> pollNextReadyCommand() {
74+
var commandsToKeep = new ArrayList<Command>();
75+
// cannot use iterator as priority order is not guaranteed
76+
while (!queue.isEmpty()) {
77+
var candidateCommand = queue.poll();
78+
if (candidateCommand.isReady()) {
79+
queue.addAll(commandsToKeep);
80+
return Optional.of(candidateCommand);
81+
}
82+
commandsToKeep.add(candidateCommand);
83+
}
84+
queue.addAll(commandsToKeep);
85+
return Optional.empty();
86+
}
87+
88+
private void tidyUp(Command nextCommand) {
89+
cleanUpOutdatedCommands(nextCommand);
90+
}
91+
92+
private void cleanUpOutdatedCommands(Command nextCommand) {
93+
if (nextCommand instanceof UnregisterModuleCommand unregisterCommand) {
94+
removeAll(command -> command instanceof AnalyzeCommand analyzeCommand && analyzeCommand.getModuleKey().equals(unregisterCommand.getModuleKey())
95+
|| command instanceof NotifyModuleEventCommand);
96+
}
97+
}
98+
99+
private void removeAll(Predicate<Command> predicate) {
100+
var iterator = queue.iterator();
101+
while (queue.iterator().hasNext()) {
102+
var command = iterator.next();
103+
if (predicate.test(command)) {
104+
iterator.remove();
105+
}
106+
}
107+
}
108+
109+
private static class CommandComparator implements Comparator<Command> {
110+
private static final Map<Class<?>, Integer> COMMAND_TYPES_ORDERED = Map.ofEntries(
111+
// registering and unregistering modules have the highest priority
112+
// even if inserted later, they should be pulled first from the queue, before file events and analyzes: they might make them irrelevant
113+
// they both have the same priority so insertion order is respected
114+
entry(RegisterModuleCommand.class, 0), entry(UnregisterModuleCommand.class, 0),
115+
// forwarding file events takes priority over analyses, to make sure they give more accurate results
116+
entry(NotifyModuleEventCommand.class, 1),
117+
// analyses have the lowest priority
118+
entry(AnalyzeCommand.class, 2));
119+
120+
@Override
121+
public int compare(Command command, Command otherCommand) {
122+
var commandRank = COMMAND_TYPES_ORDERED.get(command.getClass());
123+
var otherCommandRank = COMMAND_TYPES_ORDERED.get(otherCommand.getClass());
124+
return !Objects.equals(commandRank, otherCommandRank) ? commandRank - otherCommandRank :
125+
// for same command types, respect insertion order
126+
(int) (command.getSequenceNumber() - otherCommand.getSequenceNumber());
127+
}
128+
}
129+
}

backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisEngine.java backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/AnalysisScheduler.java

+34-65
Original file line numberDiff line numberDiff line change
@@ -19,56 +19,60 @@
1919
*/
2020
package org.sonarsource.sonarlint.core.analysis;
2121

22-
import java.util.ArrayList;
23-
import java.util.List;
24-
import java.util.concurrent.BlockingQueue;
25-
import java.util.concurrent.CompletableFuture;
26-
import java.util.concurrent.LinkedBlockingQueue;
2722
import java.util.concurrent.atomic.AtomicReference;
2823
import javax.annotation.Nullable;
29-
import org.sonarsource.sonarlint.core.analysis.api.AnalysisEngineConfiguration;
24+
import org.sonarsource.sonarlint.core.analysis.api.AnalysisSchedulerConfiguration;
3025
import org.sonarsource.sonarlint.core.analysis.command.Command;
3126
import org.sonarsource.sonarlint.core.analysis.container.global.GlobalAnalysisContainer;
32-
import org.sonarsource.sonarlint.core.analysis.container.global.ModuleRegistry;
3327
import org.sonarsource.sonarlint.core.commons.log.LogOutput;
3428
import org.sonarsource.sonarlint.core.commons.log.SonarLintLogger;
35-
import org.sonarsource.sonarlint.core.commons.progress.ProgressMonitor;
3629
import org.sonarsource.sonarlint.core.plugin.commons.LoadedPlugins;
3730

38-
public class AnalysisEngine {
31+
public class AnalysisScheduler {
3932
private static final SonarLintLogger LOG = SonarLintLogger.get();
4033
private static final Runnable CANCELING_TERMINATION = () -> {
4134
};
4235

43-
private final GlobalAnalysisContainer globalAnalysisContainer;
44-
private final BlockingQueue<AsyncCommand<?>> commandQueue = new LinkedBlockingQueue<>();
45-
private final Thread analysisThread = new Thread(this::executeQueuedCommands, "sonarlint-analysis-engine");
36+
private final AtomicReference<GlobalAnalysisContainer> globalAnalysisContainer = new AtomicReference<>();
37+
private final AnalysisQueue analysisQueue = new AnalysisQueue();
38+
private final Thread analysisThread = new Thread(this::executeQueuedCommands, "sonarlint-analysis-scheduler");
4639
private final LogOutput logOutput;
4740
private final AtomicReference<Runnable> termination = new AtomicReference<>();
48-
private final AtomicReference<AsyncCommand<?>> executingCommand = new AtomicReference<>();
41+
private final AtomicReference<Command> executingCommand = new AtomicReference<>();
4942

50-
public AnalysisEngine(AnalysisEngineConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins, @Nullable LogOutput logOutput) {
51-
globalAnalysisContainer = new GlobalAnalysisContainer(analysisGlobalConfig, loadedPlugins);
43+
public AnalysisScheduler(AnalysisSchedulerConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins, @Nullable LogOutput logOutput) {
5244
this.logOutput = logOutput;
53-
start();
54-
}
55-
56-
private void start() {
5745
// if the container cannot be started, the thread won't be started
58-
globalAnalysisContainer.startComponents();
46+
startContainer(analysisGlobalConfig, loadedPlugins);
5947
analysisThread.start();
6048
}
6149

50+
public void reset(AnalysisSchedulerConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins) {
51+
// recreate the context
52+
globalAnalysisContainer.get().stopComponents();
53+
startContainer(analysisGlobalConfig, loadedPlugins);
54+
analysisQueue.clearAllButAnalyses();
55+
}
56+
57+
private void startContainer(AnalysisSchedulerConfiguration analysisGlobalConfig, LoadedPlugins loadedPlugins) {
58+
globalAnalysisContainer.set(new GlobalAnalysisContainer(analysisGlobalConfig, loadedPlugins));
59+
globalAnalysisContainer.get().startComponents();
60+
}
61+
62+
public void wakeUp() {
63+
analysisQueue.wakeUp();
64+
}
65+
6266
private void executeQueuedCommands() {
6367
while (termination.get() == null) {
6468
SonarLintLogger.get().setTarget(logOutput);
6569
try {
66-
executingCommand.set(commandQueue.take());
70+
executingCommand.set(analysisQueue.takeNextCommand());
6771
if (termination.get() == CANCELING_TERMINATION) {
6872
executingCommand.get().cancel();
6973
break;
7074
}
71-
executingCommand.get().execute(getModuleRegistry());
75+
executingCommand.get().execute(globalAnalysisContainer.get().getModuleRegistry());
7276
executingCommand.set(null);
7377
} catch (InterruptedException e) {
7478
if (termination.get() != CANCELING_TERMINATION) {
@@ -79,23 +83,18 @@ private void executeQueuedCommands() {
7983
termination.get().run();
8084
}
8185

82-
public <T> CompletableFuture<T> post(Command<T> command, ProgressMonitor progressMonitor) {
86+
public void post(Command command) {
8387
if (termination.get() != null) {
8488
LOG.error("Analysis engine stopping, ignoring command");
85-
return CompletableFuture.completedFuture(null);
89+
command.cancel();
90+
return;
8691
}
8792
if (!analysisThread.isAlive()) {
8893
LOG.error("Analysis engine not started, ignoring command");
89-
return CompletableFuture.completedFuture(null);
90-
}
91-
92-
var asyncCommand = new AsyncCommand<>(command, progressMonitor);
93-
try {
94-
commandQueue.put(asyncCommand);
95-
} catch (InterruptedException e) {
96-
asyncCommand.future.completeExceptionally(e);
94+
command.cancel();
95+
return;
9796
}
98-
return asyncCommand.future;
97+
analysisQueue.post(command);
9998
}
10099

101100
public void stop() {
@@ -111,37 +110,7 @@ public void stop() {
111110
command.cancel();
112111
}
113112
analysisThread.interrupt();
114-
List<AsyncCommand<?>> pendingCommands = new ArrayList<>();
115-
commandQueue.drainTo(pendingCommands);
116-
pendingCommands.forEach(c -> c.future.cancel(false));
117-
globalAnalysisContainer.stopComponents();
118-
}
119-
120-
private ModuleRegistry getModuleRegistry() {
121-
return globalAnalysisContainer.getModuleRegistry();
122-
}
123-
124-
public static class AsyncCommand<T> {
125-
private final CompletableFuture<T> future = new CompletableFuture<>();
126-
private final Command<T> command;
127-
private final ProgressMonitor progressMonitor;
128-
129-
public AsyncCommand(Command<T> command, ProgressMonitor progressMonitor) {
130-
this.command = command;
131-
this.progressMonitor = progressMonitor;
132-
}
133-
134-
public void execute(ModuleRegistry moduleRegistry) {
135-
try {
136-
var result = command.execute(moduleRegistry, progressMonitor);
137-
future.complete(result);
138-
} catch (Throwable e) {
139-
future.completeExceptionally(e);
140-
}
141-
}
142-
143-
public void cancel() {
144-
progressMonitor.cancel();
145-
}
113+
analysisQueue.removeAll().forEach(Command::cancel);
114+
globalAnalysisContainer.get().stopComponents();
146115
}
147116
}

backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/AnalysisEngineConfiguration.java backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/AnalysisSchedulerConfiguration.java

+4-4
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
import javax.annotation.concurrent.Immutable;
3131

3232
@Immutable
33-
public class AnalysisEngineConfiguration {
33+
public class AnalysisSchedulerConfiguration {
3434

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

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

43-
private AnalysisEngineConfiguration(Builder builder) {
43+
private AnalysisSchedulerConfiguration(Builder builder) {
4444
this.workDir = builder.workDir;
4545
this.extraProperties = new LinkedHashMap<>(builder.extraProperties);
4646
this.nodeJsPath = builder.nodeJsPath;
@@ -117,8 +117,8 @@ public Builder setModulesProvider(Supplier<List<ClientModuleInfo>> modulesProvid
117117
return this;
118118
}
119119

120-
public AnalysisEngineConfiguration build() {
121-
return new AnalysisEngineConfiguration(this);
120+
public AnalysisSchedulerConfiguration build() {
121+
return new AnalysisSchedulerConfiguration(this);
122122
}
123123
}
124124

backend/core/src/main/java/org/sonarsource/sonarlint/core/analysis/TriggerType.java backend/analysis-engine/src/main/java/org/sonarsource/sonarlint/core/analysis/api/TriggerType.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* SonarLint Core - Implementation
2+
* SonarLint Core - Analysis Engine
33
* Copyright (C) 2016-2025 SonarSource SA
44
* mailto:info AT sonarsource DOT com
55
*
@@ -17,7 +17,7 @@
1717
* along with this program; if not, write to the Free Software Foundation,
1818
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919
*/
20-
package org.sonarsource.sonarlint.core.analysis;
20+
package org.sonarsource.sonarlint.core.analysis.api;
2121

2222
public enum TriggerType {
2323
AUTO, FORCED

0 commit comments

Comments
 (0)