Use configured gson instance for toString#772
Conversation
There was a problem hiding this comment.
Overall, the patch looks reasonable to me.
There are a few stylistic nitpicks.
However, my main concern is the removal of toString() methods from Message and related classes.
This can create not only inconveniences while debugging, but also problems with logging.
Even in TracingMessageConsumer itself, there are a number of places where messages are logged, which need to be changed in the same way as https://github.com/henryju/lsp4j/blob/bd47976a6a45957e5be9973e74196e1ce5e3f212/org.eclipse.lsp4j.jsonrpc/src/main/java/org/eclipse/lsp4j/jsonrpc/TracingMessageConsumer.java#L176.
Basically, each of the log calls in LSP4J needs to be analyzed, and amended if necessary. What's more, we'll need to ensure every time, both now and in the future, that there is an accessible instance of MessageJsonHandler in every such case. What's even more, the existing clients that log messages using Message.toString() would also be affected and would need to be changed in the same way as LSP4J itself.
I don't see a good solution here, but it seems that we need to restore those 'basic' toString() methods, make sure they no longer throw exceptions, and try to use the new MessageJsonHandler.toString(Object) method for message logging where it is possible (as, for example, in TracingMessageConsumer itself).
WDYT?
| RequestMetadata requestMetadata = sentRequests.remove(id); | ||
| if (requestMetadata == null) { | ||
| LOG.log(WARNING, String.format("Unmatched response message: %s", message)); | ||
| LOG.log(WARNING, String.format("Unmatched response message: %s", jsonHandler.toString(message))); |
There was a problem hiding this comment.
The same change needs to be done in all other places where messages are logged, potentially not only in this file. This is my main concern.
There was a problem hiding this comment.
I fixed the one I could find, but I agree there is a risk of someone still relying on the default toString of a message. I will try to think about a better solution.
|
To illustrate, here is how the console log looks like after running the |
bd47976 to
016ccdc
Compare
|
I fixed most of the feedback, except the point about having no more |
| Map<String, RequestMetadata> sentRequests, | ||
| Map<String, RequestMetadata> receivedRequests, | ||
| PrintWriter printWriter, | ||
| MessageJsonHandler jsonHandler, |
There was a problem hiding this comment.
This is a breaking API change: a new parameter is added to the existing constructors. Also, when the constructor is called from the MessageTracer, the jsonHandler argument can now be null. I'd suggest either to add a new constructor with the additional jsonHandler parameter, or to add a setter like in MessageTracer. In any case, if jsonHandler is null, the class needs to fallback to the old behaviour.
There was a problem hiding this comment.
By
class needs to fallback to the old behaviour.
do you mean it should create a new Gson instance and try to serialize it without custom adapters? I find this dangerous as it may still throw an exception as before.
There was a problem hiding this comment.
I think that in general we need to keep the old behavior as a fallback in cases where a MessageJsonHandler is unavailable for whatever reason, but make sure that possible exceptions are properly handled and no longer thrown from those toString methods. The goal is to avoid any breaking changes in API or behavior.
There was a problem hiding this comment.
To illustrate, WebSocketLauncherBuilders are currently broken, because they completely override the Builder.create() method and we have not applied the messageTracer.setJsonHandler(jsonHandler) change there. But such cases may also happen in existing clients. If there were a fallback strategy in TracingMessageConsumer, they would not have been broken.
Similarly, even if it would be possible to inject a MessageJsonHandler into each of the Message objects when they are created inside the framework as you have suggested, we still need to have a 'baseline' behaviour in the Message.toString method to avoid breaking existing clients that may create them outside the framework.
There was a problem hiding this comment.
I made another attempt. I didn't like the fact that the jsonHandler could be null on the MessageTracer, so I went for a deprecation approach.
There was a problem hiding this comment.
If you ask me, I liked the previous approach more. To me, the problem looks like a 'progressive enhancement' of the old behaviour in cases where a MessageJsonHandler is available. As I have mentioned, there might still be cases where it will not be available, so I can see no reason why we should require it in a constructor. This is my 'informed opinion' so to speak. Nevertheless, I really appreciate your effort.
There was a problem hiding this comment.
Since MessageTracer constructor is package private, I take for granted that it is not instantiated/extended outside lsp4j. So changing it is fine.
Regarding TracingMessageConsumer, I deprecated the old constructors, but keep them working as before using a fresh MessageJsonHandler instance.
If you think the two use cases are valid on the long run, I can remove deprecation tags.
The last "issue" is the removal of toString on the various messages. I don't like reverting to the old behavior, since this could be really creating bugs on user side. What about providing toString with the best information we could, but not relying on gson serialization?
8d89633 to
df463f8
Compare
| private final Map<String, RequestMetadata> receivedRequests = new HashMap<>(); | ||
|
|
||
| MessageTracer(PrintWriter printWriter) { | ||
| MessageTracer(PrintWriter printWriter, MessageJsonHandler jsonHandler) { |
There was a problem hiding this comment.
This is also a breaking API change.
There was a problem hiding this comment.
The constructor is package private, so I would say this is not a breaking API change
There was a problem hiding this comment.
My bad. You are right, of course. But I'd still prefer a setter in this case.
05b6708 to
6cd0673
Compare
|
To sum up and hopefully clarify, here is what I'd like to see in this PR. Thanks to the prompt by @szarnekow and the following changes by @henryju, I have been persuaded that there is a clean non-breaking solution to this problem. So, one of the main goals should be to preserve all of the existing API elements and augment the existing behaviour rather than trying to replace it. To put it in a perspective, there were no issues reported about this problem since 2019 when the Therefore, let's keep the current API and implementation to avoid breaking any of the existing clients, and just augment it with the enhanced implementation where it is possible, using the old one as a fallback. To that end, let's first reinstate all of the removed methods and their behaviour, including the static Let's rename the (new) instance Let's use a setter for passing a When no That would be a clean non-breaking solution I'd like to see. As a bonus, let's make sure that possible exceptions are properly handled in the existing static However, I'd consider this bonus part as optional for this PR. |
|
I won't say I am super convinced by keeping dangerous |
On one hand, at least it will not make things any worse for existing clients. With your current proposal, clients might start to see something like instead of I don't think it would be fine for most purposes. On the other hand, we can try to make the 'dangerous'
|
|
|
||
| public Builder<T> traceMessages(PrintWriter tracer) { | ||
| if (tracer != null) { | ||
| this.messageTracer = new MessageTracer(tracer); |
There was a problem hiding this comment.
With your current approach, we'd still need to keep this line with the one-arg constructor as a fallback. Otherwise, an existing subclass that completely overrides Builder.create() would have no messageTracer set, which is a breaking change. Similarly, we'd need to make the new two-arg constructor public so that such subclasses could reset the default messageTracer in their create() implementations. That's why I much prefer your previous approach with MessageTracer.setJsonHandler. It is so much less of a hassle.
|
For the sake of convenience, I have pushed what I'd consider a clean patch against If you don't mind, let's use it as a baseline for further discussion and development. |
|
Let's see if I can come up with something along the lines sketched out in #768 (comment) to make the 'dangerous' |
There was a problem hiding this comment.
Note that I had to copy ToStringBuilder from the org.eclipse.lsp4j.util package as it could not be accessed from here. I also had to add a couple of new methods: addAllFields(Predicate<Field>), which is called from Message.toStringFallback(), and, for the sake of symmetry, addDeclaredFields(Predicate<Field>).
I don't think it is good for us to have three copies of ToStringBuilder in LSP4J now.
It seems that the best solution would be to extract a separate bundle such as org.eclipse.lsp4j.common and move ToStringBuilder there, perhaps with some other common stuff, so that it could be accessed from everywhere.
However, I'd consider that to be out of the scope of this PR.
In the meantime, I think we'll have to live with yet another copy of ToStringBuilder here as a package-private class. At least, it is not API and can be safely removed at any time.
There was a problem hiding this comment.
As lsp4j.jsonrpc is at the root of all dependency chains I think having it just in this new place is fine. And it is certainly fine to defer to a future PR
There was a problem hiding this comment.
I'll leave it to a future PR to keep this one free of the associated noise.
| */ | ||
| public abstract class Message { | ||
|
|
||
| private transient MessageJsonHandler jsonHandler; |
There was a problem hiding this comment.
I'm not entirely comfortable with this idea of providing a way to inject a MessageJsonHandler into a Message and storing it in a transient field, as it can potentially break existing reflective code that enumerates/introspects message properties using fields rather than JavaBeans-properties.
Note that I had to rename Message.getJsonHandler() to jsonHandler() previously so that ReflectiveMessageValidator did not actually fail by treating jsonHandler as a general property of the message, which seems to indicate that it might be more than a hypothetical issue.
There was a problem hiding this comment.
I am not really concerned by this - but I have added an entry to the Changelog that mentions what to do in this case.
| Message message = gson.fromJson(jsonReader, Message.class); | ||
|
|
||
| if (message != null) { | ||
| message.setJsonHandler(this); |
There was a problem hiding this comment.
This call might seem unnecessary, since we already set the MessageJsonHandler in MessageTypeAdapter and DebugMessageTypeAdapter as soon as a message is created, but it serves those clients that register their own message type adapter (similarly to how DebugMessageTypeAdapter is registered), but don't set the MessageJsonHandler for created messages.
| private final MessageConsumer out; | ||
| private final Endpoint localEndpoint; | ||
| private final Function<Throwable, ResponseError> exceptionHandler; | ||
| private MessageJsonHandler jsonHandler; |
There was a problem hiding this comment.
Note that RemoteEndpoint now has the reference to a MessageJsonHandler to be able to inject the handler into the messages it creates.
|
@jonahgraham Could you please review this PR with a fresh pair of eyes? We tried to enhance the existing implementation without any breaking changes by passing a specific If all else fails, As far as I can see, the main potential issue with the current patch is that existing reflective code might be broken by the addition of the |
|
Regarding the potential issue with existing reflective code, I now think that it would probably be sufficient to just mention it in the CHANGELOG as a potential breaking change, but I would still appreciate an extra pair of eyes to review this PR. It would be great if we could merge it before the 0.22.0 release. |
|
I added it to the 0.22.0 milestone. It fell off the bottom of my todo list, I will schedule to look at it sometime before the 0.22.0 release. See conversation about timing in #732 |
OK. Thank you! |
This is important when users have registered custom type adapters Fixes eclipse-lsp4j#768
jonahgraham
left a comment
There was a problem hiding this comment.
This looks fine to me. I am about to push a rebased squashed version with the above mentioned changelog entry too to ensure that it builds cleanly.
e8ab10c to
03a4a14
Compare
next *week :-) |
pisv
left a comment
There was a problem hiding this comment.
@jonahgraham Thank you very much for your review and approval.
May I suggest a couple of minor enhancements to the changelog entry?
Co-authored-by: Vladimir Piskarev <pisv@1c.ru>
|
Thanks everyone. This is now committed and the next release of LSP4J (expected next week) will contain this improvement. |
This is important when users have registered custom type adapters
Fixes #768