From 22263e8ff974e6ad5cf0a914c6b7c4197678ec66 Mon Sep 17 00:00:00 2001 From: Adam Obuchowicz Date: Tue, 20 Aug 2024 16:42:25 +0200 Subject: [PATCH] Y-docs: Fix write capability recovery (#10851) When receiving no write capability in `text/openFile` response, the method returned error without synchronizing, but session state stayed at 'Opening' - this resulted in automatic success on the next retry without actually trying. Added additional state to handle recovery from missing write capability errors. # Important Notes tested by applying the patch below (which simulates problem reported by James): ```diff diff --git a/app/ydoc-shared/src/languageServer.ts b/app/ydoc-shared/src/languageServer.ts index e1403f50420247575b437df8c603e5f1701b5e1d..753bbf9f9f449f6130c79040e44607ce0c308f7f 100644 --- a/app/ydoc-shared/src/languageServer.ts +++ b/app/ydoc-shared/src/languageServer.ts @@ -3,6 +3,7 @@ import { bytesToHex } from '@noble/hashes/utils' import { Client, RequestManager } from '@open-rpc/client-js' import debug from 'debug' import { ObservableV2 } from 'lib0/observable' +import { wait } from 'lib0/promise.js' import { uuidv4 } from 'lib0/random' import { z } from 'zod' import { walkFs } from './languageServer/files' @@ -268,9 +269,25 @@ export class LanguageServer extends ObservableV2> { - return this.request('text/openFile', { path }) + if (this.openCalls === 0) { + this.openCalls = 1 + return wait(1000).then(() => + Err(new LsRpcError('Simluated timeout', 'text/openFile', { path })), + ) + } else if (this.openCalls === 1) { + this.openCalls = 2 + return this.request('text/openFile', { path }).then(value => { + if (value.ok) { + value.value.writeCapability = null + } + return value + }) + } else { + return this.request('text/openFile', { path }) + } } /** [Documentation](https://github.com/enso-org/enso/blob/develop/docs/language-server/protocol-language-server.md#textclosefile) */ ``` --- app/ydoc-server/src/languageServerSession.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/ydoc-server/src/languageServerSession.ts b/app/ydoc-server/src/languageServerSession.ts index 0455cd892cd6..130ebe9c6f5d 100644 --- a/app/ydoc-server/src/languageServerSession.ts +++ b/app/ydoc-server/src/languageServerSession.ts @@ -277,6 +277,7 @@ enum LsSyncState { Synchronized, WritingFile, WriteError, + CapabilityError, Reloading, Closing, Disposed, @@ -402,6 +403,7 @@ class ModulePersistence extends ObservableV2<{ removed: () => void }> { const result = await promise if (!result.ok) return result if (!result.value.writeCapability) { + this.setState(LsSyncState.CapabilityError) return Err( `Could not acquire write capability for module '${this.path.segments.join('/')}'`, ) @@ -410,6 +412,8 @@ class ModulePersistence extends ObservableV2<{ removed: () => void }> { return Ok() }) } + case LsSyncState.CapabilityError: + return this.reload() default: { assertNever(this.state) } @@ -655,6 +659,7 @@ class ModulePersistence extends ObservableV2<{ removed: () => void }> { } return } + case LsSyncState.CapabilityError: case LsSyncState.WriteError: case LsSyncState.Synchronized: { this.setState(LsSyncState.Closing) @@ -703,6 +708,7 @@ class ModulePersistence extends ObservableV2<{ removed: () => void }> { return Ok() }) } + case LsSyncState.CapabilityError: case LsSyncState.WriteError: { return this.withState(LsSyncState.Reloading, async () => { const path = this.path.segments.join('/')