Skip to content

Commit c6520c2

Browse files
Copilotnixel2007
andcommitted
Add per-document executors to prevent race conditions in didChange
- Added Map<String, ExecutorService> to track single-threaded executors per document - Initialize executor in didOpen using normalized URI from documentContext - Submit didChange operations to document's executor for serialization - Properly shutdown executors in didClose with 30-second timeout for pending changes - Use normalized URI from documentContext instead of params for consistency - Prevents race conditions when multiple didChange requests arrive rapidly Co-authored-by: nixel2007 <[email protected]>
1 parent 4791b1d commit c6520c2

File tree

1 file changed

+59
-12
lines changed

1 file changed

+59
-12
lines changed

src/main/java/com/github/_1c_syntax/bsl/languageserver/BSLTextDocumentService.java

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,12 @@
108108
import java.net.URI;
109109
import java.util.Collections;
110110
import java.util.List;
111+
import java.util.Map;
111112
import java.util.concurrent.CompletableFuture;
113+
import java.util.concurrent.ConcurrentHashMap;
112114
import java.util.concurrent.ExecutorService;
113115
import java.util.concurrent.Executors;
116+
import java.util.concurrent.TimeUnit;
114117

115118
/**
116119
* Сервис обработки запросов, связанных с текстовым документом.
@@ -144,10 +147,17 @@ public class BSLTextDocumentService implements TextDocumentService, ProtocolExte
144147

145148
private final ExecutorService executorService = Executors.newCachedThreadPool(new CustomizableThreadFactory("text-document-service-"));
146149

150+
// Executors per document URI to serialize didChange operations and avoid race conditions
151+
private final Map<String, ExecutorService> documentExecutors = new ConcurrentHashMap<>();
152+
147153
private boolean clientSupportsPullDiagnostics;
148154

149155
@PreDestroy
150156
private void onDestroy() {
157+
// Shutdown all document executors
158+
documentExecutors.values().forEach(ExecutorService::shutdown);
159+
documentExecutors.clear();
160+
151161
executorService.shutdown();
152162
}
153163

@@ -392,6 +402,12 @@ public CompletableFuture<List<InlayHint>> inlayHint(InlayHintParams params) {
392402
public void didOpen(DidOpenTextDocumentParams params) {
393403
var textDocumentItem = params.getTextDocument();
394404
var documentContext = context.addDocument(URI.create(textDocumentItem.getUri()));
405+
406+
// Create single-threaded executor for this document to serialize didChange operations
407+
// Use normalized URI from documentContext
408+
var normalizedUri = documentContext.getUri().toString();
409+
documentExecutors.computeIfAbsent(normalizedUri, key ->
410+
Executors.newSingleThreadExecutor(new CustomizableThreadFactory("doc-" + documentContext.getUri().getPath() + "-")));
395411

396412
context.openDocument(documentContext, textDocumentItem.getText(), textDocumentItem.getVersion());
397413

@@ -402,23 +418,36 @@ public void didOpen(DidOpenTextDocumentParams params) {
402418

403419
@Override
404420
public void didChange(DidChangeTextDocumentParams params) {
405-
406421
var documentContext = context.getDocument(params.getTextDocument().getUri());
407422
if (documentContext == null) {
408423
return;
409424
}
410-
411-
var newContent = applyTextDocumentChanges(documentContext.getContent(), params.getContentChanges());
412-
413-
context.rebuildDocument(
414-
documentContext,
415-
newContent,
416-
params.getTextDocument().getVersion()
417-
);
418-
419-
if (configuration.getDiagnosticsOptions().getComputeTrigger() == ComputeTrigger.ONTYPE) {
420-
validate(documentContext);
425+
426+
// Use normalized URI from documentContext
427+
var normalizedUri = documentContext.getUri().toString();
428+
var version = params.getTextDocument().getVersion();
429+
430+
// Get executor for this document
431+
var executor = documentExecutors.get(normalizedUri);
432+
if (executor == null) {
433+
// Document not opened or already closed
434+
return;
421435
}
436+
437+
// Submit change operation to document's executor to serialize operations
438+
executor.submit(() -> {
439+
var newContent = applyTextDocumentChanges(documentContext.getContent(), params.getContentChanges());
440+
441+
context.rebuildDocument(
442+
documentContext,
443+
newContent,
444+
version
445+
);
446+
447+
if (configuration.getDiagnosticsOptions().getComputeTrigger() == ComputeTrigger.ONTYPE) {
448+
validate(documentContext);
449+
}
450+
});
422451
}
423452

424453
@Override
@@ -427,6 +456,24 @@ public void didClose(DidCloseTextDocumentParams params) {
427456
if (documentContext == null) {
428457
return;
429458
}
459+
460+
// Use normalized URI from documentContext
461+
var normalizedUri = documentContext.getUri().toString();
462+
463+
// Remove and shutdown the executor for this document, waiting for all pending changes
464+
var executor = documentExecutors.remove(normalizedUri);
465+
if (executor != null) {
466+
executor.shutdown();
467+
try {
468+
// Wait for all queued changes to complete (with timeout to avoid hanging)
469+
if (!executor.awaitTermination(30, TimeUnit.SECONDS)) {
470+
executor.shutdownNow();
471+
}
472+
} catch (InterruptedException e) {
473+
executor.shutdownNow();
474+
Thread.currentThread().interrupt();
475+
}
476+
}
430477

431478
context.closeDocument(documentContext);
432479

0 commit comments

Comments
 (0)