Skip to content

Commit 20d116c

Browse files
committed
Implement basic lock-free capability registration.
1 parent 00ebc2b commit 20d116c

File tree

4 files changed

+127
-138
lines changed

4 files changed

+127
-138
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -993,7 +993,7 @@ public synchronized void registerLanguage(LanguageParameter lang) {
993993
multiplexer.addContributor(buildContributionKey(lang),
994994
new InterpretedLanguageContributions(lang, this, availableWorkspaceService(), (IBaseLanguageClient)clientCopy, exec));
995995

996-
availableCapabilities().updateCapabilities(Collections.unmodifiableCollection(contributions.values()));
996+
availableCapabilities().update(Collections.unmodifiableCollection(contributions.values()));
997997

998998
fact.reloadContributions();
999999
fact.setClient(clientCopy);
@@ -1058,7 +1058,7 @@ public synchronized void unregisterLanguage(LanguageParameter lang) {
10581058
contributions.remove(lang.getName());
10591059
}
10601060

1061-
availableCapabilities().updateCapabilities(Collections.unmodifiableCollection(contributions.values()));
1061+
availableCapabilities().update(Collections.unmodifiableCollection(contributions.values()));
10621062
}
10631063

10641064
@Override

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ protected final boolean preferStaticRegistration() {
136136
*
137137
* If this capability prefers static registration or if the client does not support dynamic registration, set it statically.
138138
* @param client Client capabilities to determine dynamic registration support.
139-
* @param result Server capabilities to modify when registerting statically.
139+
* @param result Server capabilities to modify when registering statically.
140140
* @return `true` if this capability should be registered statically, `false` otherwise.
141141
*/
142142
protected final boolean shouldRegisterStatically(ClientCapabilities client) {

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

Lines changed: 80 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
import java.util.concurrent.CompletableFuture;
3737
import java.util.concurrent.ConcurrentHashMap;
3838
import java.util.concurrent.Executor;
39+
import java.util.concurrent.atomic.AtomicReference;
40+
import java.util.function.Function;
3941
import java.util.stream.Collectors;
4042
import org.apache.commons.lang3.StringUtils;
4143
import org.apache.logging.log4j.LogManager;
@@ -53,58 +55,59 @@
5355
import org.eclipse.lsp4j.services.LanguageClient;
5456
import org.rascalmpl.vscode.lsp.parametric.ILanguageContributions;
5557
import org.rascalmpl.vscode.lsp.util.Lists;
56-
import org.rascalmpl.vscode.lsp.util.NamedThreadPool;
5758
import org.rascalmpl.vscode.lsp.util.concurrent.CompletableFutureUtils;
5859

5960
/**
6061
* Takes care of (un)registering capabilities dynamically.
61-
* Receives a list of supported capabiltities ({@link AbstractDynamicCapability}) and notifies the client when capabilities change.
62+
* Receives a list of supported capabilities ({@link AbstractDynamicCapability}) and notifies the client when capabilities change.
6263
*/
6364
public class DynamicCapabilities {
6465

6566
private static final Logger logger = LogManager.getLogger(DynamicCapabilities.class);
6667

6768
private final LanguageClient client;
6869
private final Executor exec;
69-
private final Executor singleExec = NamedThreadPool.singleDaemon("parametric-capabilities");
70-
private final CompletableFuture<Boolean> truthy;
71-
private final CompletableFuture<Boolean> falsy;
72-
private final Collection<AbstractDynamicCapability<?>> supportedCapabilities;
70+
private final CompletableFuture<Void> noop;
7371

74-
// Set of capabilities that should bre registered statically instead of dynamically
72+
private final Set<AbstractDynamicCapability<?>> dynamicCapabilities;
7573
private final Set<AbstractDynamicCapability<?>> staticCapabilities;
7674

7775
// Map of method names with current registration values
76+
private final AtomicReference<Collection<ILanguageContributions>> lastContributions = new AtomicReference<>(Collections.emptyList());
7877
private final Map<String, Registration> currentRegistrations = new ConcurrentHashMap<>();
7978

8079
/**
8180
* @param client The language client to send register/unregister requests to.
8281
* @param supportedCapabilities The capabilities to register with the client.
8382
* @param clientCapabilities The capabilities of the client. Determine whether dynamic registration is supported at all.
8483
*/
85-
public DynamicCapabilities(LanguageClient client, Executor exec, List<AbstractDynamicCapability<?>> supportedCapabilities, ClientCapabilities clientCapabilities) {
84+
public DynamicCapabilities(LanguageClient client, Executor exec, /* TODO -> Set */ List<AbstractDynamicCapability<?>> supportedCapabilities, ClientCapabilities clientCapabilities) {
8685
this.client = client;
8786
this.exec = exec;
88-
this.truthy = CompletableFutureUtils.completedFuture(true, singleExec);
89-
this.falsy = CompletableFutureUtils.completedFuture(false, singleExec);
90-
this.supportedCapabilities = List.copyOf(supportedCapabilities);
87+
this.noop = CompletableFutureUtils.completedFuture(null, exec);
9188

92-
// Check which capabilities to register statically
93-
var caps = new HashSet<AbstractDynamicCapability<?>>();
89+
// Check whether to register capabilities dynamically or statically
90+
var dynamicCaps = new HashSet<AbstractDynamicCapability<?>>();
91+
var staticCaps = new HashSet<AbstractDynamicCapability<?>>();
9492
for (var cap : supportedCapabilities) {
9593
if (cap.shouldRegisterStatically(clientCapabilities)) {
96-
caps.add(cap);
94+
logger.trace("Skipping capability {} for dynamic registration", cap.methodName());
95+
staticCaps.add(cap);
96+
} else {
97+
dynamicCaps.add(cap);
9798
}
9899
}
99100
// Set once and only read from now on
100-
this.staticCapabilities = Collections.unmodifiableSet(caps);
101+
this.dynamicCapabilities = Collections.unmodifiableSet(dynamicCaps);
102+
this.staticCapabilities = Collections.unmodifiableSet(staticCaps);
101103
}
102104

103105
/**
104106
* Register static capabilities with the server.
105107
*/
106108
public void registerStaticCapabilities(ServerCapabilities result) {
107109
for (var cap : staticCapabilities) {
110+
logger.trace("Registering capability {} statically", cap.methodName());
108111
cap.registerStatically(result);
109112
}
110113
}
@@ -114,15 +117,14 @@ public void registerStaticCapabilities(ServerCapabilities result) {
114117
* @param contribs The contributions to represent.
115118
* @return A future that completes with a boolean that is false when any registration failed, and true otherwise.
116119
*/
117-
public CompletableFuture<Boolean> updateCapabilities(Collection<ILanguageContributions> contribs) {
118-
// Copy the contributions so we know we are looking at a stable set of elements.
119-
// If the contributions change, we expect our caller to call again.
120-
var stableContribs = List.copyOf(contribs);
121-
var registrations = supportedCapabilities.stream()
122-
.filter(cap -> !staticCapabilities.contains(cap))
123-
.map(c -> tryBuildRegistration(c, stableContribs)
124-
.thenComposeAsync(r -> updateRegistration(c, r), singleExec));
125-
return CompletableFutureUtils.reduce(registrations, CompletableFutureUtils.completedFuture(true, singleExec), Boolean::booleanValue, Boolean::logicalAnd);
120+
public CompletableFuture<Void> update(Collection<ILanguageContributions> contribs) {
121+
return CompletableFuture.supplyAsync(() -> {
122+
logger.debug("Updating {} dynamic capabilities from {} contributions", dynamicCapabilities.size(), contribs.size());
123+
// Copy the contributions so we know we are looking at a stable set of elements.
124+
return lastContributions.updateAndGet(_old -> Set.copyOf(contribs));
125+
}, exec)
126+
.thenCompose(stableContribs -> CompletableFutureUtils.reduce(dynamicCapabilities.stream().map(cap -> updateRegistration(cap, stableContribs)), exec))
127+
.thenAccept(_v -> logger.debug("Done updating dynamic capabilities"));
126128
}
127129

128130
/**
@@ -134,31 +136,57 @@ public CompletableFuture<Boolean> updateCapabilities(Collection<ILanguageContrib
134136
* @param registration The computed registration to do, or `null` when this capability is absent.
135137
* @return A future completing with `true` when successful, or `false` otherwise.
136138
*/
137-
private <T> CompletableFuture<Boolean> updateRegistration(AbstractDynamicCapability<T> cap, @Nullable Registration registration) {
139+
private <T> CompletableFuture<Void> updateRegistration(AbstractDynamicCapability<T> cap, Collection<ILanguageContributions> contribs) {
140+
var reg = tryBuildRegistration(cap, contribs);
138141
var method = cap.methodName();
139142
var existingRegistration = currentRegistrations.get(method);
140143

141-
if (registration == null) {
142-
if (existingRegistration != null) {
143-
// this capability was removed
144-
logger.trace("{} is no longer supported by contributions", method);
145-
return unregister(existingRegistration);
144+
return reg.<Void>thenCompose(registration -> {
145+
Function<Boolean, CompletableFuture<Void>> checkDone = successful -> successful
146+
? eventuallyConsistent(cap, registration, contribs)
147+
: noop;
148+
149+
if (registration == null) {
150+
if (existingRegistration != null) {
151+
// this capability was removed
152+
logger.trace("{} is no longer supported by contributions", method);
153+
return unregister(existingRegistration)
154+
.thenCompose(checkDone);
155+
}
156+
logger.trace("{} is still not supported by contributions", method);
157+
// nothing more to do
158+
return checkDone.apply(true);
146159
}
147-
// nothing more to do
148-
return truthy;
149-
}
150160

151-
if (existingRegistration != null) {
152-
if (Objects.deepEquals(registration.getRegisterOptions(), existingRegistration.getRegisterOptions())) {
153-
logger.trace("Options for {} did not change since last registration: {}", method, registration.getRegisterOptions());
154-
return truthy;
161+
if (existingRegistration != null) {
162+
if (Objects.deepEquals(registration.getRegisterOptions(), existingRegistration.getRegisterOptions())) {
163+
logger.trace("Options for {} did not change since last registration", method);
164+
return checkDone.apply(true);
165+
}
166+
logger.trace("Options for {} changed since the previous registration ({} vs. {})", method, registration.getRegisterOptions(), existingRegistration.getRegisterOptions());
167+
return register(registration, existingRegistration)
168+
.thenCompose(checkDone);
155169
}
156-
logger.trace("Options for {} changed since the previous registration; remove before adding again", method);
157-
return changeOptions(registration, existingRegistration);
170+
171+
logger.trace("Registering dynamic capability {}", registration);
172+
return register(registration, existingRegistration)
173+
.thenCompose(checkDone);
174+
});
175+
}
176+
177+
private <T> CompletableFuture<Void> eventuallyConsistent(AbstractDynamicCapability<T> cap, @Nullable Registration r, Collection<ILanguageContributions> contribs) {
178+
var currentContribs = lastContributions.get();
179+
var currentReg = currentRegistrations.get(cap.methodName());
180+
if (contribs == currentContribs // instance comparison is safe since we took a read-only copy
181+
&& Objects.equals(currentReg, r)) {
182+
// Nothing changed; we're done
183+
return noop;
158184
}
159185

160-
logger.trace("Registering dynamic capability {}", method);
161-
return register(registration);
186+
// Something changed since we started...
187+
// To be sure we arrive at a fixpoint, we need to go again
188+
logger.trace("Something changed while registering {}; iterate until nothing changes", cap.methodName());
189+
return updateRegistration(cap, currentContribs);
162190
}
163191

164192
/**
@@ -168,20 +196,14 @@ private <T> CompletableFuture<Boolean> updateRegistration(AbstractDynamicCapabil
168196
* @return A future completing with `true` if successful, and `false` otherwise.
169197
*/
170198
private CompletableFuture<Boolean> unregister(Registration reg) {
171-
// If our administration contains exactly this registration, remove it and inform the client
172-
if (!currentRegistrations.remove(reg.getMethod(), reg)) {
173-
return falsy;
174-
}
175-
176199
return client.unregisterCapability(new UnregistrationParams(List.of(new Unregistration(reg.getId(), reg.getMethod()))))
177200
.handle((_v, t) -> {
178-
if (t == null) {
179-
return true;
201+
if (t != null) {
202+
handleError(t, "unregistering", reg.getMethod());
203+
return false;
180204
}
181-
handleError(t, "unregistering", reg.getMethod());
182-
// Unregistration failed; put this back in our administration
183-
currentRegistrations.putIfAbsent(reg.getMethod(), reg);
184-
return false;
205+
currentRegistrations.remove(reg.getMethod(), reg);
206+
return true;
185207
});
186208
}
187209

@@ -191,21 +213,15 @@ private CompletableFuture<Boolean> unregister(Registration reg) {
191213
* @param reg The registration to do.
192214
* @return A future completing with `true` if successful, and `false` otherwise.
193215
*/
194-
private CompletableFuture<Boolean> register(Registration reg) {
195-
// If our administration contains no registration, inform the client
196-
if (currentRegistrations.putIfAbsent(reg.getMethod(), reg) != null) {
197-
return falsy;
198-
}
199-
216+
private CompletableFuture<Boolean> register(Registration reg, @Nullable Registration existingRegistration) {
200217
return client.registerCapability(new RegistrationParams(List.of(reg)))
201218
.handle((_v, t) -> {
202-
if (t == null) {
203-
return true;
219+
if (t != null) {
220+
handleError(t, "registering", reg.getMethod());
221+
return false;
204222
}
205-
handleError(t, "registering", reg.getMethod());
206-
// Registration failed; remove this from our administration
207-
currentRegistrations.remove(reg.getMethod(), reg);
208-
return false;
223+
currentRegistrations.compute(reg.getMethod(), (k, v) -> Objects.equals(v, existingRegistration) ? reg : v);
224+
return true;
209225
});
210226
}
211227

@@ -216,27 +232,6 @@ private void handleError(Throwable t, String task, String method) {
216232
logger.error("Exception while {} capability {}", task, method, t);
217233
}
218234

219-
/**
220-
* Update a registration.
221-
* Aims to be atomic, i.e. keeps local administration of registered capabilities in sync with the client.
222-
* @param newRegistration The registration with the updated options.
223-
* @param existingRegistration The registration that we expect to currently be in place.
224-
* @return A future completing with `true` if successful, or `false` otherwise.
225-
*/
226-
private CompletableFuture<Boolean> changeOptions(Registration newRegistration, Registration existingRegistration) {
227-
return unregister(existingRegistration)
228-
.thenCompose(b -> {
229-
if (!b.booleanValue()) {
230-
/* If unregistration fails, this has one of multiple causes:
231-
1. Someone raced us, won, and updated `currentRegistrations`. Our view of the current state is outdated, so we don't do anything and leave it to the winner.
232-
2. The unregistration request failed. This happens when a capability is not supported by the client. Since we successfully registered, this should not happen.
233-
*/
234-
return falsy;
235-
}
236-
return register(newRegistration);
237-
});
238-
}
239-
240235
private <T> CompletableFuture<@Nullable Registration> tryBuildRegistration(AbstractDynamicCapability<T> cap, Collection<ILanguageContributions> contribs) {
241236
if (contribs.isEmpty()) {
242237
return CompletableFutureUtils.completedFuture(null, exec);

0 commit comments

Comments
 (0)