Skip to content
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

Automatic reconnect with Language Server. #9691

Merged
merged 29 commits into from
Apr 19, 2024
Merged

Conversation

farmaazon
Copy link
Contributor

@farmaazon farmaazon commented Apr 12, 2024

Pull Request Description

Fixes #8520

If the websocket is closed not by us, we automatically try to reconnect with it, and initialize the protocol again. Restoring state (execution contexts, attached visualizations) is not part of this PR.

It's a part of making IDE work after hibernation (or LS crash).

Important Notes

It required somewhat heavy refactoring:

  1. I decided to use an existing implementation of reconnecting websocket. Replaced (later discovered by me) our implementation.
  2. The LanguageServer class now handles both reconnecting and re-initializing - that make usage of it simpler (no more Promise<LanguageServer> - each method will just wait for (re)connection and initialization.
  3. The stuff in net src's module was partially moved to shared's counterpart (with tests). Merged exponentialBackoff implementations, which also brought me to
  4. Rewriting LS client, so it returns Result instead of throwing, what is closer our desired state, and allows us using exponentialBackoff method without any wrappers.

Checklist

Please ensure that the following checklist has been satisfied before submitting the PR:

  • The documentation has been updated, if necessary.
  • [ ] Screenshots/screencasts have been attached, if there are any visual changes. For interactive or animated visual changes, a screencast is preferred.
  • All code follows the
    Scala,
    Java,
    and
    Rust
    style guides. In case you are using a language not listed above, follow the Rust style guide.
  • All code has been tested:
    • [ ] Unit tests have been written where possible.
    • If GUI codebase was changed, the GUI was tested when built using ./run ide build.

@farmaazon farmaazon added CI: No changelog needed Do not require a changelog entry for this PR. x-new-feature Type: new feature request -gui labels Apr 12, 2024
Comment on lines 1 to 2
/// <reference lib="DOM" />
/* eslint-env browser */
Copy link
Contributor

Choose a reason for hiding this comment

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

How is this correct? This ordinarily will in node or JVM, so using DOM types seems invalid.

})
}

private scheduleInitializationAfterConnect() {
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be scheduleConnectAndInitialize? The current name doesn't actually communicate that this does the "connect" part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And this is valid: this function does not connect, because it does not make the reconnect (the websocket underneath does), only listens for "open" event to start initialization after.

throw new LsRpcError(error, method, params)
return Err(new LsRpcError(error, method, params))
} else {
return Err(new LsRpcError(`Unspecified error: ${error}`, method, params))
Copy link
Contributor

Choose a reason for hiding this comment

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

For errors that we actually don't expect to ever happen, I'd rather keep this as a throw error, treating it basically as a panic.

Copy link
Contributor Author

@farmaazon farmaazon Apr 17, 2024

Choose a reason for hiding this comment

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

I think we expect some problems not being rpc responses (network errors, timeouts), and there is no documentation about those in open-rpc (at least I cannot find any), so I think it's save to catch all Errors. non-Error values I will rethrow.

@@ -254,6 +254,9 @@ test('Visualization preview: user visualization selection', async ({ page }) =>
await input.fill('4')
await expect(input).toHaveValue('4')
await expect(locate.jsonVisualization(page)).toExist()
// Before picking table vis, we must make sure we wan't be hit by debounced visualization update
// during the process.
await page.waitForTimeout(250)
Copy link
Contributor

Choose a reason for hiding this comment

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

I would strongly prefer any other way to check for expected final state than by doing a timeout. The timeout times quickly add up to a significant total testing runtime, and also it has a high probability of false negatives.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Found another way; the preview presents now the expression, so we may wait until it shows the actually written code. I also fixed the bug where the previews always interpreted received object as a string.

Comment on lines 146 to 147
groups.error.log('Cannot read component groups')
console.error('Continuing without gruops')
Copy link
Contributor

Choose a reason for hiding this comment

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

Those two logs could be merged, or one can be removed.

console.error(`Unexpected LS connection error:`, error)
})
transport.on('error', (error) => console.error('Language Server transport error:', error))
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if this is significant, but this event handler is not being cleared in the onAbort.

I would actually recommend using handleObserve or similar method to register such handlers.

Copy link
Contributor Author

@farmaazon farmaazon Apr 17, 2024

Choose a reason for hiding this comment

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

In this case the transport is "owned" by LanguageServer object and it's closed on dispose; it should not be used anywhere else (and if it is, or there are errors after closing, we ought to see them).

@@ -344,6 +344,8 @@ export type Notifications = {
'file/rootAdded': (param: {}) => void
'file/rootRemoved': (param: {}) => void
'refactoring/projectRenamed': (param: {}) => void
'transport/closed': () => void
Copy link
Contributor

Choose a reason for hiding this comment

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

Are those actual notifications related to language server protocol? If not, I would prefer those to be in a separate type definition. It can still be merged at the usage point as the ObservableV2 parameter.

@farmaazon farmaazon requested a review from Frizi April 18, 2024 07:56
@farmaazon
Copy link
Contributor Author

Applied review and fix some tests. Fix the display of visualization preview, by the way.

Now I have problems with AI completion test, but I think the code is ready to re-review.

Copy link
Contributor

@Frizi Frizi left a comment

Choose a reason for hiding this comment

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

Few last small nits.

@@ -39,7 +39,7 @@ onMounted(() => setTimeout(() => rootNode.value?.querySelector('button')?.focus(
<div
ref="rootNode"
class="VisualizationSelector"
@focusout="$event.relatedTarget == null && emit('hide')"
@focusout="$event.relatedTarget == null && (console.log('FOCUSOUT', new Error()), emit('hide'))"
Copy link
Contributor

Choose a reason for hiding this comment

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

stray log

| { ok: true; value: T }
| { ok: false; error: ResultError<E> }

export function Ok<T>(data: T): Result<T, never> {
export function Ok(): Result<void, never>
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be undefined, not void. We actually guarantee that return type is undefined. void is not exactly the same as undefined when used in function return position, it should be treated more like "the return type can be anything, and you shouldn't look at it". We do use Promise<void> as a return from many functions already, but there that semantic mostly makes sense (e.g. in LS requests, which we kinda assume return nothing interesting, but no guarantees what it exactly is).

See https://www.typescriptlang.org/docs/handbook/2/functions.html#assignability-of-functions

Today's typescript is a bit more strict about this type than it used to, but there are still some subtle ways where it can bite you.

projectStore.executionContext.on('executionComplete', () => toastExecutionFailed.dismiss())
projectStore.lsRpcConnection.client.onError((e) =>
toastLspError.show(`Language server error: ${e}`),
),
Copy link
Contributor

Choose a reason for hiding this comment

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

remove comma and reformat

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is the reformatter who puts this comma.

Comment on lines 407 to 408
this.state satisfies never
return Ok()
Copy link
Contributor

Choose a reason for hiding this comment

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

I know it was originally like that, but it would be better to use assertNever here. Now we have it in shared, so it shouldn't be a problem to change.

})
}
default: {
this.state satisfies never
Copy link
Contributor

Choose a reason for hiding this comment

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

Another place for assertNever.

@farmaazon farmaazon added the CI: Ready to merge This PR is eligible for automatic merge label Apr 19, 2024
@mergify mergify bot merged commit de406c6 into develop Apr 19, 2024
36 of 37 checks passed
@mergify mergify bot deleted the wip/farmaazon/reconnecting branch April 19, 2024 13:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
-gui CI: No changelog needed Do not require a changelog entry for this PR. CI: Ready to merge This PR is eligible for automatic merge x-new-feature Type: new feature request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Reconnect with engine with exponential backoff
2 participants