[anaysis_server] Cancel in-progress LSP completion requests if another arrives
Change-Id: Idf49b70750a3427529f71e952aa036fae4c7c6cb
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/252422
Commit-Queue: Brian Wilkerson <brianwilkerson@google.com>
Reviewed-by: Brian Wilkerson <brianwilkerson@google.com>
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
index a27fa77..57dae65 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/perform_refactor.dart
@@ -33,13 +33,10 @@
final refactoring = await getRefactoring(
RefactoringKind(kind), result, offset, length, options);
return refactoring.mapResult((refactoring) async {
- // If the token we were given is not cancellable, replace it with one that
- // is for the rest of this request, as a future refactor may need to cancel
- // this request.
- // The original token should be kept and also checked for cancellation.
- final cancelableToken = cancellationToken is CancelableToken
- ? cancellationToken
- : CancelableToken();
+ // If the token we were given is not cancelable, wrap it with one that
+ // is for the rest of this request as a future refactor may need to
+ // cancel this request.
+ final cancelableToken = cancellationToken.asCancelable();
manager.begin(cancelableToken);
try {
@@ -54,15 +51,13 @@
return success(null);
}
- if (cancellationToken.isCancellationRequested ||
- cancelableToken.isCancellationRequested) {
+ if (cancelableToken.isCancellationRequested) {
return error(ErrorCodes.RequestCancelled, 'Request was cancelled');
}
final change = await refactoring.createChange();
- if (cancellationToken.isCancellationRequested ||
- cancelableToken.isCancellationRequested) {
+ if (cancelableToken.isCancellationRequested) {
return error(ErrorCodes.RequestCancelled, 'Request was cancelled');
}
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/commands/validate_refactor.dart b/pkg/analysis_server/lib/src/lsp/handlers/commands/validate_refactor.dart
index 72107e8..ba9d18f 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/commands/validate_refactor.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/commands/validate_refactor.dart
@@ -42,13 +42,10 @@
final refactoring = await getRefactoring(
RefactoringKind(kind), result, offset, length, options);
return refactoring.mapResult((refactoring) async {
- // If the token we were given is not cancellable, replace it with one that
- // is for the rest of this request, as a future refactor may need to cancel
- // this request.
- // The original token should be kept and also checked for cancellation.
- final cancelableToken = cancellationToken is CancelableToken
- ? cancellationToken
- : CancelableToken();
+ // If the token we were given is not cancelable, wrap it with one that
+ // is for the rest of this request as a future refactor may need to
+ // cancel this request.
+ final cancelableToken = cancellationToken.asCancelable();
manager.begin(cancelableToken);
try {
diff --git a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
index 40fdf76..f9cbe3b 100644
--- a/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
+++ b/pkg/analysis_server/lib/src/lsp/handlers/handler_completion.dart
@@ -48,6 +48,15 @@
/// debugging).
late final Duration completionBudgetDuration;
+ /// A cancellation token for the previous completion request.
+ ///
+ /// A new completion request will cancel the previous request. We do not allow
+ /// concurrent completion requests.
+ ///
+ /// `null` if there is no previous request. It the previous request has
+ /// already completed, cancelling this token will not do anything.
+ CancelableToken? previousRequestCancellationToken;
+
CompletionHandler(super.server, LspInitializationOptions options)
: suggestFromUnimportedLibraries = options.suggestFromUnimportedLibraries,
previewNotImportedCompletions = options.previewNotImportedCompletions {
@@ -73,6 +82,12 @@
return serverNotInitializedError;
}
+ // Cancel any existing in-progress completion request in case the client did
+ // not do it explicitly, because the results will not be useful and it may
+ // delay processing this one.
+ previousRequestCancellationToken?.cancel();
+ previousRequestCancellationToken = token.asCancelable();
+
final requestLatency = message.timeSinceRequest;
final triggerCharacter = params.context?.triggerCharacter;
final pos = params.position;
diff --git a/pkg/analysis_server/test/lsp/completion_dart_test.dart b/pkg/analysis_server/test/lsp/completion_dart_test.dart
index 8388626..30298a0 100644
--- a/pkg/analysis_server/test/lsp/completion_dart_test.dart
+++ b/pkg/analysis_server/test/lsp/completion_dart_test.dart
@@ -735,6 +735,27 @@
request, throwsA(isResponseError(ErrorCodes.InvalidParams)));
}
+ Future<void> test_concurrentRequestsCancellation() async {
+ // We expect a new completion request to cancel any in-flight request so
+ // send multiple without awaiting, then check only the last one completes.
+ final content = '^';
+
+ await initialize();
+ await openFile(mainFileUri, withoutMarkers(content));
+ final position = positionFromMarker(content);
+ final responseFutures = [
+ getCompletion(mainFileUri, position),
+ getCompletion(mainFileUri, position),
+ getCompletion(mainFileUri, position),
+ ];
+ expect(responseFutures[0],
+ throwsA(isResponseError(ErrorCodes.RequestCancelled)));
+ expect(responseFutures[1],
+ throwsA(isResponseError(ErrorCodes.RequestCancelled)));
+ final results = await responseFutures[2];
+ expect(results, isNotEmpty);
+ }
+
Future<void> test_filterTextNotIncludeAdditionalText() async {
// Some completions (eg. overrides) have additional text that is not part
// of the label. That text should _not_ appear in filterText as it will
diff --git a/pkg/analyzer/lib/src/utilities/cancellation.dart b/pkg/analyzer/lib/src/utilities/cancellation.dart
index 4dff1bd..44448b1 100644
--- a/pkg/analyzer/lib/src/utilities/cancellation.dart
+++ b/pkg/analyzer/lib/src/utilities/cancellation.dart
@@ -23,3 +23,37 @@
@override
bool get isCancellationRequested => false;
}
+
+/// A cancellable wrapper over another cancellation token.
+///
+/// This token will be considered cancelled if either it is itself cancelled,
+/// or if [child] is cancelled.
+///
+/// Cancelling this token will also cancel [child] if it is a cancelable
+/// token.
+class _WrappedCancelableToken extends CancelableToken {
+ final CancellationToken _child;
+
+ _WrappedCancelableToken(this._child);
+
+ @override
+ bool get isCancellationRequested =>
+ super.isCancellationRequested || _child.isCancellationRequested;
+
+ @override
+ void cancel() {
+ super.cancel();
+ final child = _child;
+ if (child is CancelableToken) {
+ child.cancel();
+ }
+ }
+}
+
+extension CancellationTokenExtension on CancellationToken {
+ /// Wraps this token to make it cancelable if it is not already.
+ CancelableToken asCancelable() {
+ final token = this;
+ return token is CancelableToken ? token : _WrappedCancelableToken(token);
+ }
+}