3636import java .util .concurrent .CompletableFuture ;
3737import java .util .concurrent .ConcurrentHashMap ;
3838import java .util .concurrent .Executor ;
39+ import java .util .concurrent .atomic .AtomicReference ;
40+ import java .util .function .Function ;
3941import java .util .stream .Collectors ;
4042import org .apache .commons .lang3 .StringUtils ;
4143import org .apache .logging .log4j .LogManager ;
5355import org .eclipse .lsp4j .services .LanguageClient ;
5456import org .rascalmpl .vscode .lsp .parametric .ILanguageContributions ;
5557import org .rascalmpl .vscode .lsp .util .Lists ;
56- import org .rascalmpl .vscode .lsp .util .NamedThreadPool ;
5758import 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 */
6364public 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