-
Notifications
You must be signed in to change notification settings - Fork 152
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
Spec proposal: support for IProgress<T> #139
Comments
@dbaeumer is this something you already have a spec for? Any feedback? |
I've left a message on the json-rpc spec forum. |
Hi. :) |
@AArnott yes, but the discussion is currently LSP specific. Major reason is that progress needs to be reported for things that are not necessarily bound to a request. Consider that a server builds an index on startup and wants to report corresponding progress. The discussion happens here: microsoft/language-server-protocol#70 |
@dbaeumer This isn't directly related to the LSP conversation you referenced. I'm proposing a generalized progress reporting system. I am puzzled though that for LSP you say it may not be bound to a request, since such a condition is directly stated in the title of your issue. |
In your example, what would |
@felixfbecker: That's an application protocol level detail. It's whatever the server wants to return to the client to communicate progress. It might be a |
Found my way here from the JSON-RPC forum, figured I'd offer up the anecdotal experience that I've seen basically this implemented before and like @mpcm said: it can be done within the existing spec without extension. |
I agree: it doesn't require an extension. My proposal is to spec out the protocol so that multiple libraries can interoperate using a standard pattern. |
Seems to me an application level pattern doesn't need to be formalized in the underlying protocol, or even be implemented in the direct JSON-RPC libraries. That's not to say that I don't think that a more formal pattern would be a bad thing. In fact I think it would be good to see a protocol built on top of the JSON-RPC protocol, and likewise libraries for that protocol built on top of existing JSON-RPC libraries (which should be not too difficult, as that's what many people do today). |
Thanks for the pointer. |
The current version of the next libraries contain a first cut of progress reporting support. The corresponding implementation and spec part can be found here: This currently allows to initiate progress from the server to the client. To bind progress to a request the idea is to add a Let me know if this addresses your problems. |
It looks like more discussion is happening over at https://github.com/microsoft/ms-lsp/issues/2 |
Thanks for the link, @dbaeumer. It looks like your work there is pretty unrelated to this proposal after all, if I'm reading it correctly. Your proposal is based on the server driving the client's global UI in terms of some "we're busy" progress bar, without any context to a particular RPC request that preceded it. |
I've refined my spec proposal in the issue description and plan to start implementation soon. |
@AArnott actually providing progress for a specific call will be available as well (not implemented yet). The difference is that the proposal adds that to LSP itself and not to JSON-RPC. How it will work is that if the client sends a request and the client will provide UI to report progress for that request the request params will contain an additional property
I also looked into whether reporting progress and reporting partial results should be combined and I decided against it. Major reason being is that I think that there is not always a direct correlation between the progress of an operation and its final result set. |
@dbaeumer Can you share a link to the code or spec or discussion you're talking about for the I agree what you just described sounds much closer to what I'm attempting here. And I'd love to come up with compatible solutions.
The spec here doesn't define one way or the other. I'm leaving it up to the server to define what results and progress are actually returned to the client. If I understand you correctly, an example of the no direct correlation between the two could be that progress is merely an integer between 0 and 100 while the final result set is 100,000 lines of text. Or perhaps the progress is each fragment found while the final result set is sorted. The latter would be highly redundant and perhaps not the best use of traffic, but that's why I'm leaving it up to the server to decide and document how the two relate to each other. Would that mitigate your concern?
It sounds like you're saying where my spec is that the client specifies
I can see how you came to that, given your design is to be completely application level and thus for the server method to be able to send notifications, it needs to know the progress token, and it has no visibility into the request ID of the original message. In .NET, it's very natural to reuse But now that I'm talking to you, I realize this may not be compatible with a TypeScript app/library, since at runtime there are no types to parameters in the method signature. And even if we could somehow divine that from a method signature, we surely couldn't if the RPC server method just took an arbitrary JSON object as one parameter as LSP servers do. So your spec works well for TypeScript&LSP but is incompatible with ordered args at the JSON-RPC layer, and isn't the greatest experience in other languages. Mine doesn't work for TypeScript at all. How to reconcile these and get one spec that works everywhere? I guess I'll finish here rather than brainstorming at the moment to give you a chance to confirm/refute any of my points above. |
How about this modification to my spec above as a solution to add support for TypeScript: -The progress argument is {} or null to indicate that the client supports and wants progress notifications or does not, respectively.
+The progress argument is {progressToken: <token>} or null to indicate that the client supports and wants progress notifications or does not, respectively. -id must match the value of the request that progress is being reported for.
+id must match the value of the progressToken supplied in the request that progress is being reported for. This allows TypeScript based apps and libraries to keep working since they'll just pass this Thoughts? |
OK, after talking with @jasongin, I've modified the spec in the issue description. I think this should allow even more functionality and work for all languages/libraries. It would even work with a JSON-RPC library that doesn't have progress reporting support built-in, but where it is built-in, the library can make the RPC server and calling client very simple by exposing native Progress functionality. |
There has not been a lot of discussions around this. It happened in this PR microsoft/vscode-languageserver-node#261 and in a comment here: microsoft/vscode-languageserver-node#261 (comment) I was looking at progress from two viewpoints:
For the first one I would definitely use a For the second one I am not so sure due to the following reasons:
So far my goal was to keep server initiate UI progress (work done) the same as reporting UI progress for a request. This is why I was thinking about a progressToken in the request params. But may be things are easier if we keep progress (independent of kind) for request together and have server initiated UI progress different. Then the Need to think a little :-) |
Looking at folding UI progress and partial result progress raises the problem that one tagging interface
makes it hard to convey which of those the client accepts. For example in LSP we might have messages that accept UI progress but no partial results. We could easily drop UI progress on the client side, but accepting partial results where the editor has no corresponding API might complicate things. We could encapsulate this into a library but this needs then to be done for every implementation language. Alternatively we could have
which makes it more clear for the server what is supported. |
I don't think we should combine these two forms of progress (reporting on a request, and pushing progress to client UI based on some server-initiated operation). The latter seems to clearly be app-specific and trivially implemented by sending a notification from server to client for a method and params of the client's choosing (e.g. setGlobalProgress). As each client may present progress differently and have its own servers that want to represent their progress in different ways, leaving this strictly at the app level seems like a good fit. LSP as a protocol that builds on top of JSON-RPC could certainly define what this should look like. So I'm primarily focused on the request-progress-response use case. More on that in my next message. |
As for your beefed up 3-member interface, can you explain what each member would mean? We can't collect from the client or provider this on to the server side if the API contract is If the server would cater to switches from the client to customize progress reporting, I think that can and should be done as separate arguments, allowing an API in .NET or TypeScript, both on the client and server, to look like this: interface ISomeService
{
SomeServerOperationAsync(
arg1: string,
arg2: int,
progress: Progress<StepProgress>?,
progressSwitch1: bool?,
progressSwitch2: bool?);
} This way whether .NET's What I also like about this protocol is that you don't actually need native library support. For example if the server-side uses a JSON-RPC library that doesn't support this progress protocol extension, the server method can simply accept the |
The idea of these where as follows:
I proposed this since implementation wise I would like to encapsulate progress in the library and not expose these directly to the user of the lib. I do the same with cancellation in the This would lead in TS to a handler signature like this: interface RequestHandler {
(params: P, cancellationToken: CancellationToken, workDoneToken: WorkDoneToken, partialResultToken?: PartialResultToken)
} If If I understand your proposal correctly I can only have a signature: interface RequestHandler {
(params: P, cancellationToken: CancellationToken, progressToken: ProgressToken);
} with some values in the I personally would like to at least make partial result reporting a first class citizen in the vscode-jsonrpc library as cancellation is. Work done progress might be different since how work done is reported differs from application model to application model. This is why I had work done progress as a separate token in the params. This is independent of server initiated progress which I agree can't be map onto this and is best implemented using special messages. |
We share that goal. StreamJsonRpc hides the
You lose two things with this statement: the ability to support multiple progress arguments in a single method call, and (perhaps more importantly), the freedom for an application to interop properly via this protocol even if the library doesn't support it.
What is this token? How does the server method use it? Do they have to call back into the JSON-RPC library and pass this in as an argument along with whatever progress they have to report?
I don't see my proposal as limited to that one. In fact I don't think that is even one of the valid ones. JSON-RPC patterns asideI'm starting from the JSON-RPC spec which suggests that methods are invoked with either named or positional arguments. See section 4.2 of the spec. So a server method would always have one parameter for each argument that the client is passing in: myServerMethod(arg1: string, arg2: int); Then per the JSON-RPC spec, the request can provide positional arguments or named arguments, and the responsibility of the JSON-RPC library is to line those up with the method on this signature. Now, Javascript makes positional arguments easy, but AFAIK named arguments are impossible to implement this way in javascript because there is no reflection that will tell you the parameter names. In .NET we can easily support both. It seems vscode-jsonrpc went a (creatively) different direction by instead of passing in the request messages parameters as regular parameters to the method, it always passes in a single parameters object. This gives you named arguments support in javascript, but (I think?) at the expense of being unable to support positional arguments (unless Back to the discussion at handAnyway in StreamJsonRpc I expose Task MyServerMethodAsync(string arg1, int arg2, IProgress<MyDataProgress> progress = null, CancellationToken cancellationToken = null) I expect that a typical json-rpc library for TypeScript/Javascript would express it in nearly an identical way. But whereas in .NET we would recognize the need to specially interpret the progress argument by virtue of the type of the parameter on the server method, in Javascript of course that's not an option, and you could instead recognize the special schema conformed to by the progress argument as it came in, and instantiate a For vscode-jsonrpc which uses different signatures, I nevertheless expected progress to still be just one of the parameters like the others, leaving the interface of the server method like this: interface RequestHandler {
(params: P);
}
interface P {
arg1: string,
arg2: int,
progress: Progress<MyData>
} See, I don't think progress, or cancellation for that matter, should be kept separate from the rest of the parameters on the server method. Both of these concepts aren't unique to JSON-RPC. Any method may want to take such arguments. So why not just let it define its parameters, including progress (and cancellation, but I'm not trying to change the past) and let JSON-RPC be one of the means for calling that method, without that method having to structure its signature around the specific library that it expects to be called with? In your proposal, @dbaeumer, what does the client code that calls this method look like, including setting up and receiving progress? My goal is to make it look as absolutely natural as can be. And I'm hoping that TypeScript can achieve it with the spec we end up with here. In .NET, it's totally natural: // doesn't care about cancellation or progress
await server.MyServerMethodAsync("arg1", arg2: 51);
// cares
var progress = new Progress<MyDataProgress>(data => { /* we got progress! */ });
await server.MyServerMethodAsync("arg1", arg2: 51, progress, cancellationToken); Note that there is absolutely no RPC awareness at all. Or if you don't have a client proxy, then it would look like this:
If the server supports reporting "work done" progress as well as "partial result" progress, it can just take another Task MyServerMethodAsync(string arg1, int arg2, IProgress<MyDataProgress> progress = null, IProgress<WorkProgress> workProgress = null, CancellationToken cancellationToken = null) Or if the server method were willing to report one kind of progress, but at varying verbosity levels, the server would of course take an extra parameter for that: Task MyServerMethodAsync(string arg1, int arg2, IProgress<MyDataProgress> progress = null, ProgressVerbosityLevel verbosity = 1, CancellationToken cancellationToken = null) So I'm curious whether you share the goal and feel you can meet it of having syntax that is as natural on its platform with either of our proposals. |
Yes, The reason why I went with one I do agree that a generic Progress gives you all the flexibility. The problem I have is that at least in a JS/TS implementation the dispatching has to happen by inspecting properties in JSON literals or by remembering tokens and not by method name. Currently different types of things are dispatch by different method names and for me reporting partial progress was always different than reporting work done. I also believe that having one generic interface / mechanism is harder to understand and needs more documentation (at least in LSP since all I can spec is data that flows over the wire). To support both progress styles in LSP using one generic $/progress method I need to spec the additional params using the proposed {
workDone?: {
__jsonrpc__progressToken: string | number;
},
partialResult?: {
__jsonrpc__progressToken: string | number;
}
} in contrast to two separate methods The same needs to be done even if I would allow to use positional parameters for result progress and work done progress. The spec would still need to say that the second parameter is for I hope that clarifies my thinking around it. I am not trying to say this will not work for LSP and can't be implemented. It is IMO harder to explain, to document and needs more work in the json-rpc library. |
It makes sense since Javascript doesn't have method overloads that you want to give different names to methods that take different parameter types. In your preferred spec, I believe you last expressed it as: interface JsonRpcProgress {
__jsonrpc__workCompleted?: boolean;
__jsonrpc__partialResults?: boolean;
__jsonrpc__generic?: boolean;
} Is that correct? Further (and please correct me if I'm wrong):
IMO the above constraints seem both burdensome and limiting. And it seems obviously tailored for the LSP case where you have a design that strongly favors two distinct types of progress that might be reported. But I think you can do it in the more general way, and naturally even in TypeScript. Let me sketch it out in TS/JS and you can tell me if it helps or if I went off the rails somewhere. In particular as your concern seems to focus on one method that could take two different types of progress data, I'll demonstrate how that needn't be the case. I'm going to make-up the syntax for using the let workDoneProgress = new Progress<WorkDone>(workDone => { displayUI(workDone.Percent); });
let partialResultProgress = new Progress<PartialSymbolsResult>(partial => { addToResultsList(partial); });
let finalResult = await rpc.invokeAsync('lookupSymbols', {
entry: 'workspa', // this is one of the regular parameters for the method
workDone: workDoneProgress,
partialResult: partialResultProgress
},
cancellationToken); The And yes, in the LSP spec you'll probably want to describe it as you've done, but perhaps link to a reference spec that describes progress so that you don't have to. But for the server it seems pretty simple: to send progress updates just send a notification to Compared to passing in bool's and leaving the server to find the request message's "id" which may not be visible to them as it's hidden in the JSON-RPC library they're using, I think this is actually easier to implement and even to describe. And if the server's JSON-RPC library happened to be progress aware, it becomes even easier, as they can simply expect the |
(note, I edited-in the last paragraph to my last comment after posting the comment, so if you're reading email, please take care to notice it). As you also expressed a concern regarding the implementation of the jsonrpc library, I'd like to address that. Is your concern mostly on the client or the server side? |
Actually I was not very clear in my last comment. Even worse I mixed arguments of my initial approach with the one for the generic approach, which made things worse. So let me try to clarify this:
Let me detail this a little:
I think specifying this with a generic approach that covers both cases is absolutely doable but IMO harder to understand since it is more complex to spec. I either need to inspect properties or use positional arguments. Inspecting properties and converting them to a progress type inside a literal seems wired to me. How deep would the library inspect properties. Would this only happen on the first param?. So implementing this in a json-rpc library looks strange to me. So what is left are positional parameters. So what I can do for LSP is specing that if there is a second parameter it is partial progress and if there is a third parameter there is a work done progress using the tagging interface you propose. We nowhere in the LSP spec have functionality / semantic bound to param positions. This is why I think this is harder to understand on a LSP spec level. I also think that positional parameters are harder to evolve. This is in general not an issue if you have a typed language with method signature overloads. But on the protocol level all there is are JSON literals and positions. So you either need to specify a new method name or add any additional parameters to the end (this is by the way why LSP uses one param literal; it is extremely easy to evolve. And we might want to evolve the work done progress in the future). So my bottom line is: can this be nicely implemented in a JSON RPC library using the proposed tagging interface for positional parameters. Definitely YES. Will this help with specifying a protocol and evolving it. There I have my doubts. |
I spend a little time on thinking how I would spec and implement that in a more general way in jsonrpc and LSP and here is an idea (which would not be like this without the valuable discussion in this issue):
The reason for this approach is that a tagging interface alone doesn't help to convey the type of progress. The jsonrpc layer can only convert a tagging interface into Here are a couple of concrete examples: Using a special token for both partial progress and work done. In LSP the param literal could look like this: {
workDone: 15, // Use token 15
partialProgress: 27 // User token 27
} If the request id is used for partial progress this could look like this: {
workDone: 15, // Use token 15
partialProgress: true
} The app layer would convert |
In .NET this isn't a problem because the JSON-RPC library can see that the target method to be invoked has a Your most recent proposal looks similar to mine, except that instead of the progress arguments being JSON objects with a specially recognized property name whose value is the token, your argument value is simply the token. That, as you say, requires the application-level to recognize and apply the special treatment of "this is a progress token". Either the application must then translate this to the more useful, native and JSON-RPC agnostic Our .NET JSON-RPC library could do that too (and without registration) by recognizing that the target method takes a void MyMethod(int arg1);
void MyMethod(Progress<WorkDone> arg1); Now when "MyMethod" is called with an integer, which overload should be selected? In your proposal it would be ambiguous because all we have for the argument is an integer which could be interpreted two ways. We might simply "define" that when there is ambiguity, we prefer the closest match (in this case, take the So I guess I could live with your proposal here. But it seems like it's making your own job harder (at the application level at least, if not also in the library) by requiring registration or manual handling of the token. And given I'm still confused by your |
It is about typing and creating the right instance (the Progress comment). The typing we could may be fix using some kind of type inference as we do with the
Agree, but from an abstraction point of view there is not difference between the two. In both cases (at least in TS where as not reflection) I need to tell the system what progress type to instantiate. We could look into decorations but this still means write code.
I think you could even instantiate a concrete type (e.g. I also find it a little wired if the library does this for progress and not for other types. Or do you have some smart conversion code as well that converts a JSON object into a specific object if there is a special constructor on that class. I do that in a layer we maintain in the application code to shield users from interpreting these JSON strcutures. I do see now that a tagging interface helps with languages that have reflection. But TS doesn't so this in one way or the other has to be handled in the application layer either be registering converters in the JSON-RPC library or by handling it in the app layer. So far in LSP all did all this in the app layer. I am out for a public holiday tomorrow. I will think about it a little more. |
I didn't think "instantiating a more specific type" mattered. In .NET every single value has a type, but in Javascript both of these types would just be object from Javascript's perspective, would it not? I thought the type instantiated was just a TypeScript concept, and one that a simple declarative cast to type
That all sounds highly app-specific. I think it's a worthy goal, and one that the protocol shouldn't prohibit, but I don't think the protocol should be in the business of actively helping achieve that either.
Well, we'd have to know how to recognize the parameter type as a special Progress related one so that we could apply the custom deserialization handling. The pattern in .NET is that a method takes
I'm only imagining special progress treatment for whole arguments -- not deep inspection of argument values to see if we can find patterns inside them.
In .NET, virtually all serializers include built-in support for .NET primitives and then require that other complex objects define their own serialization schema in one way or another. With newtonsoft.json and .NET's DataContractSerializer, members can be decorated as whether they should be serialized or not. When deserializing, the outer type So yes, StreamJsonRpc makes a habit of deserializing each JSON-RPC argument to the type that the method parameter expects to receive.
Maybe this goes back to the |
OK, regarding the https://github.com/AArnott/ProgressTypeScriptSample/blob/master/index.ts Feel free to clone and run locally. Does that help? |
I am not at all saying that a generic What I am trying to get to is the following:
So I guess my question is: will this tagging interface cause more complexity (in terms of explaining it and documenting it) then it helps on the implementation side? Regarding your example: what I would like to do is folding interface WorkDone {
precent: number;
}
class WorkDoneProgress extends Progress<WorkDone> {
private lastSent: number;
report(value: WorkDone) {
let current = (new Date()).getTime();
if (this.lastSent === undefined || current - this.lastSent > 100 || value.percentage >== 100) {
super.report(value);
this.lastSent = current;
}
}
} |
@dbaeumer, I've updated the issue description to simplify the progress token from a JSON object that conforms to an interface to any legal JSON token. Does this look good? |
@AArnott I does look good to me. However I would like to add the following two things:
|
Thanks for the feedback. I've clarified that the progress token is expected to be unique to the session. Correlating the progress notification with the request for logging purposes sounds useful. I wouldn't want the client library to decide to drop "stale" progress for a completed request however in case that progress includes partial results that the caller is still expecting. The client that minted the progress token and also has access to the request id would already have the ability to correlate the two (if it chose), putting the onus on the server to specify both is an extra burden that I wonder if it's worth adding. Especially considering the spec thus far allows for the server to implement this protocol without any special json-rpc library support, but if the notification must be sent with the request id, no server could implement it without library support or the library exposing the request id in some way. This spec isn't incompatible with progress notification responses to requests without an We could document that a server MAY include a |
Very valid point. Then I opt to stick with not having the property at all. I agree that the client can do the correlation as well. |
@AArnott I started to spec / implement this for LSP and have a question regarding the
Should we name |
Sure. I've updated the spec. |
The following is a proposal for an extension to the JSON-RPC protocol that serves as a means for a server to provide progress reports on an invoked method to the client, leading to a final result.
In C#, this might be expressed as
IProgress<T>
. A server method signature might take this form:A request might look like this:
The
progress
argument may have any name or position, and may have any valid JSON token as its value. A value ofnull
is specially recognized as an indication that the client does not want progress updates from the server. This argument may not be recognizable as anIProgress<T>
token from the request, but the JSON-RPC library can offer a means for an application to specify that this argument is used as a progress token during a callback to the client.For instance, in the above C# method signature, the
IProgress<WorkUpdate>
parameter type would signify to the JSON-RPC library that it should reinterpret the second/"progress" argument as a progress token and instantiate anIProgress<WorkUpdate>
instance whoseReport
method will send the response message as specified below, carrying that token.A JSON-RPC client may find it convenient to simply reuse the value from the
id
in the request itself, but may choose to avoid this so that multiple progress arguments can be passed in via the same request. This would enable different progress updates or frequencies to be supplied at the client's discretion. The progress token SHOULD be unique to the session so that any progress callbacks from the server can be distinctly routed to the right handler on the client side.In processing such a message, the server provides updates to the client via a notification sent back to the client. These notifications use the special method name
$/progress
. For example:Parameters are
(progressToken, value)
, which may be named in a params object or as a params array in that order.progressToken
must match the value of the argument from the request that progress is being reported for. Thevalue
property in this$/progress
message may be set to any json token representing the update from the server. It may be a number, string, bool, object, or even thenull
literal.Implementation notes
It is conceivable that client applications of a JSON-RPC library that supports this feature would expect that progress notifications are raised to the application in order, and possibly all before the outbound RPC call is marked completed. In multi-threaded environments this can introduce special considerations. In .NET for example, the
IProgress<T>
interface makes such guarantees hard, and we'll likely need to document that folks use a particular implementation of that interface that guarantees ordering, and also suggest ways to mitigate the chance of their async call completing before the last progress message has been recognized by the client.The text was updated successfully, but these errors were encountered: