Skip to content

Commit 801deee

Browse files
committed
Ensure atomicity with parallel requests.
1 parent 28a734b commit 801deee

File tree

2 files changed

+35
-34
lines changed

2 files changed

+35
-34
lines changed

rascal-lsp/src/main/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicCapabilities.java

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
import java.util.concurrent.CompletableFuture;
3737
import java.util.concurrent.ConcurrentHashMap;
3838
import java.util.concurrent.Executor;
39-
import java.util.function.Function;
4039
import java.util.function.Supplier;
4140
import java.util.stream.Collectors;
4241
import org.apache.commons.lang3.tuple.Pair;
@@ -52,6 +51,7 @@
5251
import org.eclipse.lsp4j.services.LanguageClient;
5352
import org.rascalmpl.vscode.lsp.parametric.ILanguageContributions;
5453
import org.rascalmpl.vscode.lsp.util.Lists;
54+
import org.rascalmpl.vscode.lsp.util.NamedThreadPool;
5555
import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils;
5656

5757
/**
@@ -64,6 +64,7 @@ public class DynamicCapabilities {
6464

6565
private final LanguageClient client;
6666
private final Executor exec;
67+
private final Executor singleExec = NamedThreadPool.singleDaemon("parametric-capabilities");
6768
private final Supplier<CompletableFuture<Boolean>> falsy;
6869
private final Collection<AbstractDynamicCapability<?>> supportedCapabilities;
6970

@@ -75,14 +76,13 @@ public class DynamicCapabilities {
7576

7677
/**
7778
* @param client The language client to send register/unregister requests to.
78-
* @param exec The executor to use for asynchronous tasks.
7979
* @param supportedCapabilities The capabilities to register with the client.
8080
* @param clientCapabilities The capabilities of the client. Determine whether dynamic registration is supported at all.
8181
*/
8282
public DynamicCapabilities(LanguageClient client, Executor exec, List<AbstractDynamicCapability<?>> supportedCapabilities, ClientCapabilities clientCapabilities) {
8383
this.client = client;
8484
this.exec = exec;
85-
this.falsy = () -> CompletableFutureUtils.completedFuture(false, exec);
85+
this.falsy = () -> CompletableFutureUtils.completedFuture(false, singleExec);
8686
this.supportedCapabilities = List.copyOf(supportedCapabilities);
8787

8888
// Check which capabilities to register statically
@@ -116,10 +116,8 @@ public CompletableFuture<Void> updateCapabilities(Collection<ILanguageContributi
116116
var stableContribs = List.copyOf(contribs);
117117
return CompletableFutureUtils.reduce(supportedCapabilities.stream()
118118
.filter(cap -> !staticCapabilities.contains(cap))
119-
.map(c -> tryBuildRegistration(c, stableContribs)), exec)
120-
.thenApply(caps -> caps.stream().map(c -> updateRegistration(c)))
121-
.thenApply(jobs -> CompletableFutureUtils.reduce(jobs, exec))
122-
.thenCompose(Function.identity())
119+
.map(c -> tryBuildRegistration(c, stableContribs)
120+
.thenComposeAsync(r -> updateRegistration(r), singleExec)), exec)
123121
.thenAccept(_l -> {}); // List<Void> -> Void
124122
}
125123

rascal-lsp/src/test/java/org/rascalmpl/vscode/lsp/parametric/capabilities/DynamicCapabilitiesTest.java

Lines changed: 30 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
import static org.junit.Assert.assertNotNull;
3131
import static org.junit.Assert.assertNull;
3232
import static org.junit.Assert.assertTrue;
33+
import static org.junit.Assert.fail;
3334
import static org.mockito.ArgumentMatchers.any;
35+
import static org.mockito.Mockito.atLeastOnce;
36+
import static org.mockito.Mockito.atMostOnce;
3437
import static org.mockito.Mockito.inOrder;
3538
import static org.mockito.Mockito.never;
3639
import static org.mockito.Mockito.only;
@@ -39,7 +42,6 @@
3942

4043
import java.util.ArrayList;
4144
import java.util.HashSet;
42-
import java.util.LinkedList;
4345
import java.util.List;
4446
import java.util.Map;
4547
import java.util.Set;
@@ -60,7 +62,6 @@
6062
import org.eclipse.lsp4j.RegistrationParams;
6163
import org.eclipse.lsp4j.ServerCapabilities;
6264
import org.eclipse.lsp4j.TextDocumentClientCapabilities;
63-
import org.eclipse.lsp4j.Unregistration;
6465
import org.eclipse.lsp4j.UnregistrationParams;
6566
import org.eclipse.lsp4j.services.LanguageClient;
6667
import org.junit.Before;
@@ -71,6 +72,7 @@
7172
import org.mockito.InOrder;
7273
import org.mockito.Mockito;
7374
import org.mockito.Spy;
75+
import org.mockito.exceptions.verification.VerificationInOrderFailure;
7476
import org.mockito.junit.MockitoJUnitRunner;
7577
import org.rascalmpl.values.IRascalValueFactory;
7678
import org.rascalmpl.vscode.lsp.parametric.ILanguageContributions;
@@ -173,21 +175,15 @@ public CompletableFuture<SummaryConfig> getOndemandSummaryConfig() {
173175
return params.getRegistrations().stream().collect(Collectors.toMap(Registration::getMethod, Registration::getRegisterOptions));
174176
}
175177

176-
private List<String> unregistrationOptions(UnregistrationParams params) {
177-
return params.getUnregisterations().stream().map(Unregistration::getMethod).collect(Collectors.toList());
178-
}
179-
180178
@SafeVarargs
181-
private CompletableFuture<Void> registerIncrementally(List<String>... options) {
182-
return registerIncrementally(Stream.of(options).map(SomeContribs::new).map(ILanguageContributions.class::cast).collect(Collectors.toList()));
179+
private void registerSequentially(List<String>... options) throws InterruptedException, ExecutionException {
180+
registerSequentially(Stream.of(options).map(SomeContribs::new).map(ILanguageContributions.class::cast).collect(Collectors.toList()));
183181
}
184182

185-
private CompletableFuture<Void> registerIncrementally(List<ILanguageContributions> contribs) {
186-
List<CompletableFuture<Void>> jobs = new LinkedList<>();
183+
private void registerSequentially(List<ILanguageContributions> contribs) throws InterruptedException, ExecutionException {
187184
for (int i = 0; i < contribs.size(); i++) {
188-
jobs.add(dynCap.updateCapabilities(contribs.subList(0, i + 1)));
185+
dynCap.updateCapabilities(contribs.subList(0, i + 1)).get();
189186
}
190-
return CompletableFutureUtils.reduce(jobs).thenAccept(_v -> {});
191187
}
192188

193189
//// TESTS
@@ -219,22 +215,17 @@ public void registerSingleContribution() throws InterruptedException, ExecutionE
219215

220216
@Test
221217
public void registerIncrementalContribution() throws InterruptedException, ExecutionException {
222-
registerIncrementally(List.of(".", "::"), List.of("+", "*", "-", "/", "%")).get();
218+
registerSequentially(List.of(".", "::"), List.of("+", "*", "-", "/", "%"));
223219

224-
InOrder inOrder = inOrder(client);
225-
inOrder.verify(client).registerCapability(registrationCaptor.capture());
226-
inOrder.verify(client).unregisterCapability(unregistrationCaptor.capture());
227-
inOrder.verify(client).registerCapability(registrationCaptor.capture());
228-
inOrder.verifyNoMoreInteractions();
220+
verify(client, atLeastOnce()).registerCapability(registrationCaptor.capture());
221+
verify(client, atMostOnce()).unregisterCapability(unregistrationCaptor.capture());
229222

230-
assertEquals(Map.of("textDocument/completion", new CompletionRegistrationOptions(List.of(".", "::"), false)), registrationOptions(registrationCaptor.getAllValues().get(0)));
231-
assertEquals(List.of("textDocument/completion"), unregistrationOptions(unregistrationCaptor.getValue()));
232-
assertEquals(Map.of("textDocument/completion", new CompletionRegistrationOptions(List.of(".", "::", "+", "*", "-", "/", "%"), false)), registrationOptions(registrationCaptor.getAllValues().get(1)));
223+
assertEquals(Map.of("textDocument/completion", new CompletionRegistrationOptions(List.of(".", "::", "+", "*", "-", "/", "%"), false)), registrationOptions(registrationCaptor.getValue()));
233224
}
234225

235226
@Test
236227
public void registerIdenticalContribution() throws InterruptedException, ExecutionException {
237-
registerIncrementally(List.of(".", "::"), List.of(".", "::")).get();
228+
registerSequentially(List.of(".", "::"), List.of(".", "::"));
238229

239230
InOrder inOrder = inOrder(client);
240231
inOrder.verify(client).registerCapability(registrationCaptor.capture());
@@ -250,7 +241,7 @@ public void registerOverlappingContributions() throws InterruptedException, Exec
250241
.map(ILanguageContributions.class::cast)
251242
.collect(Collectors.toList());
252243

253-
registerIncrementally(contribs).get();
244+
registerSequentially(contribs);
254245

255246
// unregister one of both
256247
dynCap.updateCapabilities(contribs.subList(1, 2)).get();
@@ -340,7 +331,7 @@ public void preferStaticRegistration() throws InterruptedException, ExecutionExc
340331
}
341332

342333
@Test
343-
public void multiThreadingConisistency() throws InterruptedException, ExecutionException {
334+
public void multiThreadingAtomicity() throws InterruptedException, ExecutionException {
344335
int N = 50;
345336
List<CompletableFuture<Void>> jobs = new ArrayList<>(N);
346337
var exec = Executors.newFixedThreadPool(N / 2); // less threads than jobs; causes some overlap
@@ -359,9 +350,21 @@ public void multiThreadingConisistency() throws InterruptedException, ExecutionE
359350
InOrder inOrder = inOrder(client);
360351

361352
inOrder.verify(client).registerCapability(any());
362-
for (int i = 1; i < N; i++) {
363-
inOrder.verify(client).unregisterCapability(any());
364-
inOrder.verify(client).registerCapability(registrationCaptor.capture());
353+
boolean atomic = true;
354+
int i = 1;
355+
while (atomic && i < N) {
356+
try {
357+
inOrder.verify(client).unregisterCapability(any());
358+
atomic = false;
359+
inOrder.verify(client).registerCapability(registrationCaptor.capture());
360+
atomic = true;
361+
} catch (VerificationInOrderFailure e) {
362+
if (atomic) {
363+
break;
364+
}
365+
fail(e.toString());
366+
}
367+
i++;
365368
}
366369
inOrder.verifyNoMoreInteractions();
367370

0 commit comments

Comments
 (0)