Skip to content

Refactored configuration management for LSP module #8514

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

Achal1607
Copy link
Collaborator

@Achal1607 Achal1607 commented May 21, 2025

Currently, configuration requests require a round trip every time the language server needs a value from the client. For example, in TextDocumentServiceImpl, the completion method, which provides auto-complete suggestions, performs a round trip to fetch configuration values like netbeans.javadoc.load.timeout and netbeans.completion.warning.time. Additionally, listening to changes in specific configurations is difficult, as it requires registration in WorkspaceServiceImpl and it is not extensible.

This PR aims to address these challenges by introducing a new class, ClientConfigurationManager, which manages clients along with their configurations. It serves configuration values from a local cache if available, and only performs a request to the client when necessary. It also listens for configuration changes to keep the cache in sync.
There is also a new class ConfigValueCache which takes care of all the business logic of traversing the tree and maintaining configs according to the scopes as well.
Moreover, it allows listeners in the form of BiConsumer to be attached to specific configuration keys, enabling appropriate actions to be taken automatically when those configurations change.

PS: Thanks to @sid-srini for helping in the design.

@Achal1607 Achal1607 requested review from lahodaj, sdedic and dbalek and removed request for lahodaj May 21, 2025 10:06
@Achal1607 Achal1607 added Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) LSP [ci] enable Language Server Protocol tests VSCode Extension [ci] enable VSCode Extension tests labels May 21, 2025
@Achal1607 Achal1607 added this to the NB27 milestone May 23, 2025
@apache apache locked and limited conversation to collaborators Jun 2, 2025
@apache apache unlocked this conversation Jun 2, 2025
Copy link
Contributor

@lahodaj lahodaj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, seems like a sensible direction to me, with some comments for consideration inline.

@sdedic - any insights?

Thanks.

}
textDocumentService.updateProjectJDKHome(newProjectJDKHomePath);
});
AtomicReference<FileObject> projectDirectory = new AtomicReference<>(null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of AtomicReference here is probably unnecessary, correct? Can it be simply FileObject projectDirectory? It seems to be assign on one place only.

}
}

public void registerConfigCache(NbCodeLanguageClient client, String section) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what is the purpose of this registering. Can the ConfigValueCache be simply registered on the first query?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought was that if a config doesn't want to be cached by the implementor then it can be skipped. Like lets say if we have editor preferences in the future as a configuration and it becomes a large object, then we may not want to cache the entire object, so it is left upto the implementor what they want to cache and what not.
This was my point of view but if you see value in caching the object on first query then we can change to that approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my concern is this: given the registration and the actual config read are on completely different places, it is very easy to imagine that when adding a configuration, only the config read will be added, and not the registration. And given it will all work(?), no one will notice, except it will be slower. And after a little while, no one will be sure anymore if the non-caching was intentional or an oversight.

I wonder if we have a particular setting now or in a foreseeable future that we don't want to cache. And if yes, is there a reason why the read cannot go directly through the API without the cache? (With a clear comment saying something along the lines of "not using the cache, as this setting is too big, and should be fetched anew every time, instead of being cache".)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, so thinking on this lines, would this approach be fine:
When the caller requests for the config we will cache it by default and also expose a method which accepts boolean if the caller doesn't want to cache the value (whatever be the reason be it large object size or something else).
So, then it would be the responsibility of the caller to state explicitly if they don't want to cache the value. Would this approach work?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that sounds reasonable. Thanks!

*/
public class ClientConfigurationManager {

final WeakHashMap<LanguageClient, ConfigValueCache> clientCaches = new WeakHashMap<>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not necessarily wrong, but I wonder if NbCodeLanguageClient should hold an instance of ClientConfigurationManagement. On the call sites it would then be like:

client.getConfigurationManagement().registerConfigChangeListener("<section>", consumer);

and it seems it could be more clearly non-leaking, and also even easier for to use.

if (consumer != null) {
consumer.accept(section, tree);
}
} catch (RuntimeException e) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching and throwing away an exception like this is usually wrong. If there's something wrong, and the exception happens, we will never find out, things will just appear broken.

In cases there's a valid reason to continue after a callback fails with an exception, the exception should be logged, so that at least from the log we can find out what happened.

In cases we don't see a valid reason why the callback would fail, I would simply not catch the error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any update on this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I missed this change, will push this as well.

@Achal1607
Copy link
Collaborator Author

@lahodaj addressed your review comments, please have a look again whenever it is possible. Thanks.

@Achal1607 Achal1607 requested a review from lahodaj June 7, 2025 09:15
Copy link
Contributor

@dbalek dbalek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall, looks reasonable to me.

@@ -346,7 +346,7 @@ void cancelDiagnostics(AtomicBoolean cancel) {
}
}

class LspClient implements LanguageClient {
class LspClient implements LanguageClient {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this change seems unnecessary?

}
}

public void registerConfigCache(NbCodeLanguageClient client, String section) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my concern is this: given the registration and the actual config read are on completely different places, it is very easy to imagine that when adding a configuration, only the config read will be added, and not the registration. And given it will all work(?), no one will notice, except it will be slower. And after a little while, no one will be sure anymore if the non-caching was intentional or an oversight.

I wonder if we have a particular setting now or in a foreseeable future that we don't want to cache. And if yes, is there a reason why the read cannot go directly through the API without the cache? (With a clear comment saying something along the lines of "not using the cache, as this setting is too big, and should be fetched anew every time, instead of being cache".)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Java [ci] enable extra Java tests (java.completion, java.source.base, java.hints, refactoring.java, form) LSP [ci] enable Language Server Protocol tests VSCode Extension [ci] enable VSCode Extension tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants