Skip to content

Implement general middleware for requests and notifications#1158

Merged
dbaeumer merged 2 commits intomicrosoft:mainfrom
vinistock:vs/implement_general_middleware
Mar 8, 2023
Merged

Implement general middleware for requests and notifications#1158
dbaeumer merged 2 commits intomicrosoft:mainfrom
vinistock:vs/implement_general_middleware

Conversation

@vinistock
Copy link
Copy Markdown
Contributor

Closes #918

Allow adding a general middleware provideRequestOrNotification to client options that is used for all requests and notifications. The attached issue has a bit more context, but this is useful for measuring performance and even appending additional parameters to requests if desired.

Please let me know if this is the right direction for the implementation and if I'm following the right style and naming conventions. I wasn't sure if this was the right level for the implementation, but the general middleware can alternatively be implemented as a connection option instead.

Comment thread client/src/common/client.ts Outdated
Comment thread client/src/common/client.ts Outdated
Comment thread client/src/common/client.ts Outdated
@vinistock vinistock force-pushed the vs/implement_general_middleware branch from 136535d to 04672a2 Compare January 12, 2023 20:44
@dbaeumer
Copy link
Copy Markdown
Member

@vinistock this looks all good to me. Could you please rebase the PR on latest from main. I needed to change the two methods a little bit due to a serious bug with document synchronization. Thanks Dirk

@vinistock vinistock force-pushed the vs/implement_general_middleware branch from 04672a2 to dcf3a2f Compare January 17, 2023 12:27
@vinistock
Copy link
Copy Markdown
Contributor Author

@microsoft-github-policy-service agree

Comment thread client/src/common/client.ts Outdated
Comment thread client/src/common/client.ts
@vinistock vinistock force-pushed the vs/implement_general_middleware branch from dcf3a2f to 9d99222 Compare February 16, 2023 14:38
@vinistock vinistock requested a review from dbaeumer February 16, 2023 15:16
@vinistock
Copy link
Copy Markdown
Contributor Author

Not sure why the windows build timed out, but I assume it's not related to these changes. Let me know if that's not the case.

Comment thread client/src/common/client.ts Outdated
this: void,
type: string | MessageSignature,
next: (type: string | MessageSignature, ...params: any[]) => Promise<R>,
...params: any[]
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually thinking about this signature again it is not very client friendly. The reason is that the params array is hard to interpret in that form for a middleware since it can have the following combinatation:

  • length 0: no param not token
  • length 1: (no param and a token) or (a param no token)
  • lenght 2: param and token

I have no good idea yet on how to improve this.

If all we want is to support some logging when sendRequest / sendNotification is invoked we might want to add a different API than a middleware which should allow interprestation / modification.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@vinistock any idea regarding the above comment.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One idea could be to have a signature like

(type: string | MessageSignature, param: any | undefined, token: CancellationToken | undefined) => Promise<R>

and then unfold the params and fold them again in the next handler.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The main use case for us is for doing performance and error metrics. Something like this

const clientOptions = {
  middleware: {
    sendRequest: () => {
      try {
        // Benchmark running the request (invoke next with params)
      } catch (error) {
        // Report error returned by the server
      }
    }
  }
}

For this scenario, I do think a middleware would make the most sense. It provides a lot of flexibility on what clients can do around requests and notifications.

In terms of a different API, one possibility would be to "break" the middleware into parts. This is not as flexible, and frankly feels less convenient than a middleware, but it might be easier to define an interface for. For example,

const clientOptions = {
  beforeRequest: (type, params) => { /* start measuring performance */ },
  afterRequest: (type, params) => { /* stop measuring performance and log */ },
  onRequestError: (error, type, params) => { /* log error */ } // Could potentially be a part of `afterRequest` to avoid an extra handler
}

and then unfold the params and fold them again in the next handler.

Can you explain with an example? I'm not sure I understand the approach.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not syntax or semantic checked but here is what I mean:

interface GeneralMiddleware {
	sendRequest?<P, R>(
		this: void,
		type: string | MessageSignature,
		param: P | undefined,
		token: CancellationToken | undefined,
		next: (type: string | MessageSignature, param: P | undefined, token?: CancellationToken) => Promise<R>,
	): Promise<R>;

	sendNotification?<P>(
		this: void,
		type: string | MessageSignature,
		params: P,
		next: (type: string | MessageSignature, params?: P) => Promise<void>
	): Promise<void>;
}

and in sendRequest

		const _sendRequest = this._clientOptions.middleware?.sendRequest;
		if (_sendRequest !== undefined) {
			const param: any | undefined = undefined;
			const token: CancellationToken | undefined = undefined;
			if (params.length === 1) {
				if (CancellationToken.is(params[0])) {
					token = params[0];
				} else {
					param = params[0];
				}
			} else if (params.length === 2) {
				param = params[0];
				token = params[1];
			}
			return _sendRequest(type, param, token, (type, param, token) => {
				const params: any[] = [];
				if (param !== undefined) {
					params.push(param);
				}
				if (token !== undefined) {
					params.push(token);
				}
				return connection.sendRequest<R>(type, ...params);
			});
		} else {
			return connection.sendRequest<R>(type, ...params);
		}

Copy link
Copy Markdown
Contributor Author

@vinistock vinistock Mar 1, 2023

Choose a reason for hiding this comment

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

I added your suggestion to the latest commit, so that it's easier to analyze and drop if necessary.

I wonder if there's a type signature we can come up for params that would improve the client interface without requiring this folding and unfolding of params. Something like

// Params can be a CancellationToken, an `any` set of params, both of those or undefined
type ParamType = [CancellationToken] | [any] | [any, CancellationToken] |  undefined;

interface GeneralMiddleware {
	sendRequest?<R>(
		this: void,
		type: string | MessageSignature,
		next: (type: string | MessageSignature, params: ParamType) => Promise<R>,
		...params: ParamType,
	): Promise<R>;
}

Not sure if TypeScript would be smart enough to differentiate those or if any would just mess it up.

@vinistock vinistock force-pushed the vs/implement_general_middleware branch from 9d99222 to 3e5df54 Compare March 1, 2023 15:34
Comment thread client/src/common/client.ts Outdated
// Separate cancellation tokens from other parameters for a better client interface
if (params.length === 1) {
// CancellationToken is an interface, so we need to check if the first param complies to it
if ('onCancellationRequested' in params[0] && 'isCancellationRequested' in params[0]) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I imported and used it instead. Let me know if you believe this is indeed the way to go and if we should do the same for notifications.

@vinistock vinistock force-pushed the vs/implement_general_middleware branch from 3e5df54 to 6e3fd57 Compare March 6, 2023 20:57
@vinistock vinistock force-pushed the vs/implement_general_middleware branch from 6e3fd57 to 9fbd749 Compare March 6, 2023 20:58
Comment thread client/src/common/client.ts Outdated
MessageStrategy, DidOpenTextDocumentParams, CodeLensResolveRequest, CompletionResolveRequest, CodeActionResolveRequest, InlayHintResolveRequest, DocumentLinkResolveRequest, WorkspaceSymbolResolveRequest
} from 'vscode-languageserver-protocol';

import { CancellationToken as JSONRPCCancellationToken } from 'vscode-jsonrpc';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Protocol reexport vscode-jsonrpc. So you should get that type from vscode-languageserver-protocol

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

@vinistock vinistock force-pushed the vs/implement_general_middleware branch from 9fbd749 to 7023895 Compare March 7, 2023 14:22
@dbaeumer dbaeumer requested a review from aeschli March 8, 2023 11:09
@dbaeumer dbaeumer enabled auto-merge (squash) March 8, 2023 11:09
@dbaeumer dbaeumer merged commit 18fad46 into microsoft:main Mar 8, 2023
@vinistock vinistock deleted the vs/implement_general_middleware branch March 8, 2023 15:28
@vinistock
Copy link
Copy Markdown
Contributor Author

@dbaeumer do we need to do the same separation for notifications as well? Let me know and I can follow up with another PR.

@dbaeumer
Copy link
Copy Markdown
Member

dbaeumer commented Mar 9, 2023

@vinistock what exactly do you mean?

@vinistock
Copy link
Copy Markdown
Contributor Author

Sorry, I just realized I got confused. I meant if we needed to do the folding and unfolding of parameters for notifications too, but I don't think notifications have a cancellation token.

@dbaeumer
Copy link
Copy Markdown
Member

Correct, notifications have no token.

@vinistock
Copy link
Copy Markdown
Contributor Author

Wanted to report back that I tested this on 8.2.0-next.0 and it's working well for our purposes. Here are the changes in our client if you'd like to see an example usage.

@ejgallego
Copy link
Copy Markdown

That's exactly what we needed for coq-lsp to actually instrument the calls to our server worker, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow middleware that can handles all messages

4 participants