aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/language-web
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-19 14:39:39 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-08-19 14:39:39 +0200
commitb7a46b805bd7fbb3b21a48a035698ab11fadcb7c (patch)
treeab8787dd0699cfb5a0fa1084719a0375e5b0c85b /subprojects/language-web
parentrefactor: move ITC algorithms (diff)
downloadrefinery-b7a46b805bd7fbb3b21a48a035698ab11fadcb7c.tar.gz
refinery-b7a46b805bd7fbb3b21a48a035698ab11fadcb7c.tar.zst
refinery-b7a46b805bd7fbb3b21a48a035698ab11fadcb7c.zip
feat: interruptible VIATRA engine
Reduce server load by introducing a timeout for semantics analysis.
Diffstat (limited to 'subprojects/language-web')
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java129
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java133
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java46
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java13
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java9
-rw-r--r--subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java14
6 files changed, 219 insertions, 125 deletions
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
index 2495430e..39191162 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsService.java
@@ -5,8 +5,6 @@
5 */ 5 */
6package tools.refinery.language.web.semantics; 6package tools.refinery.language.web.semantics;
7 7
8import com.google.gson.JsonArray;
9import com.google.gson.JsonObject;
10import com.google.inject.Inject; 8import com.google.inject.Inject;
11import com.google.inject.Provider; 9import com.google.inject.Provider;
12import com.google.inject.Singleton; 10import com.google.inject.Singleton;
@@ -14,43 +12,29 @@ import org.eclipse.xtext.service.OperationCanceledManager;
14import org.eclipse.xtext.util.CancelIndicator; 12import org.eclipse.xtext.util.CancelIndicator;
15import org.eclipse.xtext.web.server.model.AbstractCachedService; 13import org.eclipse.xtext.web.server.model.AbstractCachedService;
16import org.eclipse.xtext.web.server.model.IXtextWebDocument; 14import org.eclipse.xtext.web.server.model.IXtextWebDocument;
17import org.eclipse.xtext.web.server.model.XtextWebDocument;
18import org.eclipse.xtext.web.server.validation.ValidationService; 15import org.eclipse.xtext.web.server.validation.ValidationService;
19import org.jetbrains.annotations.Nullable; 16import org.jetbrains.annotations.Nullable;
20import org.slf4j.Logger; 17import org.slf4j.Logger;
21import org.slf4j.LoggerFactory; 18import org.slf4j.LoggerFactory;
22import tools.refinery.language.model.problem.Problem; 19import tools.refinery.language.model.problem.Problem;
23import tools.refinery.language.semantics.model.ModelInitializer; 20import tools.refinery.language.web.xtext.server.push.PushWebDocument;
24import tools.refinery.language.semantics.model.SemanticsUtils;
25import tools.refinery.store.model.Model;
26import tools.refinery.store.model.ModelStore;
27import tools.refinery.store.query.viatra.ViatraModelQueryAdapter;
28import tools.refinery.store.reasoning.ReasoningAdapter;
29import tools.refinery.store.reasoning.ReasoningStoreAdapter;
30import tools.refinery.store.reasoning.literal.Concreteness;
31import tools.refinery.store.reasoning.representation.PartialRelation;
32import tools.refinery.store.representation.TruthValue;
33import tools.refinery.store.tuple.Tuple;
34 21
35import java.util.Arrays; 22import java.util.concurrent.*;
36import java.util.List;
37import java.util.TreeMap;
38 23
39@Singleton 24@Singleton
40public class SemanticsService extends AbstractCachedService<SemanticsResult> { 25public class SemanticsService extends AbstractCachedService<SemanticsResult> {
41 private static final Logger LOG = LoggerFactory.getLogger(SemanticsService.class); 26 private static final Logger LOG = LoggerFactory.getLogger(SemanticsService.class);
42 27
43 @Inject 28 @Inject
44 private SemanticsUtils semanticsUtils; 29 private Provider<SemanticsWorker> workerProvider;
45 30
46 @Inject 31 @Inject
47 private ValidationService validationService; 32 private OperationCanceledManager operationCanceledManager;
48 33
49 @Inject 34 @Inject
50 private Provider<ModelInitializer> initializerProvider; 35 private ValidationService validationService;
51 36
52 @Inject 37 private final ExecutorService executorService = Executors.newCachedThreadPool();
53 private OperationCanceledManager operationCanceledManager;
54 38
55 @Override 39 @Override
56 public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) { 40 public SemanticsResult compute(IXtextWebDocument doc, CancelIndicator cancelIndicator) {
@@ -58,44 +42,42 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> {
58 if (LOG.isTraceEnabled()) { 42 if (LOG.isTraceEnabled()) {
59 start = System.currentTimeMillis(); 43 start = System.currentTimeMillis();
60 } 44 }
61 Problem problem = getProblem(doc, cancelIndicator); 45 var problem = getProblem(doc, cancelIndicator);
62 if (problem == null) { 46 if (problem == null) {
63 return null; 47 return null;
64 } 48 }
65 var initializer = initializerProvider.get(); 49 var worker = workerProvider.get();
66 var builder = ModelStore.builder() 50 worker.setProblem(problem,cancelIndicator);
67 .with(ViatraModelQueryAdapter.builder()) 51 var future = executorService.submit(worker);
68 .with(ReasoningAdapter.builder() 52 SemanticsResult result = null;
69 .requiredInterpretations(Concreteness.PARTIAL));
70 operationCanceledManager.checkCanceled(cancelIndicator);
71 try { 53 try {
72 var modelSeed = initializer.createModel(problem, builder); 54 result = future.get(2, TimeUnit.SECONDS);
73 operationCanceledManager.checkCanceled(cancelIndicator); 55 } catch (InterruptedException e) {
74 var nodeTrace = getNodeTrace(initializer); 56 future.cancel(true);
75 operationCanceledManager.checkCanceled(cancelIndicator); 57 LOG.error("Semantics service interrupted", e);
76 var store = builder.build(); 58 Thread.currentThread().interrupt();
77 operationCanceledManager.checkCanceled(cancelIndicator); 59 } catch (ExecutionException e) {
78 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); 60 operationCanceledManager.propagateAsErrorIfCancelException(e.getCause());
79 operationCanceledManager.checkCanceled(cancelIndicator); 61 throw new IllegalStateException(e);
80 var partialInterpretation = getPartialInterpretation(initializer, model, cancelIndicator); 62 } catch (TimeoutException e) {
81 if (LOG.isTraceEnabled()) { 63 future.cancel(true);
82 long end = System.currentTimeMillis(); 64 LOG.trace("Semantics service timeout", e);
83 LOG.trace("Computed semantics for {} ({}) in {}ms", doc.getResourceId(), doc.getStateId(), 65 return new SemanticsErrorResult("Partial interpretation timed out");
84 end - start);
85 }
86 return new SemanticsSuccessResult(nodeTrace, partialInterpretation);
87 } catch (RuntimeException e) {
88 LOG.debug("Error while computing semantics", e);
89 return new SemanticsErrorResult(e.getMessage());
90 } 66 }
67 if (LOG.isTraceEnabled()) {
68 long end = System.currentTimeMillis();
69 LOG.trace("Computed semantics for {} ({}) in {}ms", doc.getResourceId(), doc.getStateId(),
70 end - start);
71 }
72 return result;
91 } 73 }
92 74
93 @Nullable 75 @Nullable
94 private Problem getProblem(IXtextWebDocument doc, CancelIndicator cancelIndicator) { 76 private Problem getProblem(IXtextWebDocument doc, CancelIndicator cancelIndicator) {
95 if (!(doc instanceof XtextWebDocument webDoc)) { 77 if (!(doc instanceof PushWebDocument pushDoc)) {
96 throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc); 78 throw new IllegalArgumentException("Unexpected IXtextWebDocument: " + doc);
97 } 79 }
98 var validationResult = webDoc.getCachedServiceResult(validationService, cancelIndicator, true); 80 var validationResult = pushDoc.getCachedServiceResult(validationService, cancelIndicator, true);
99 boolean hasError = validationResult.getIssues().stream() 81 boolean hasError = validationResult.getIssues().stream()
100 .anyMatch(issue -> "error".equals(issue.getSeverity())); 82 .anyMatch(issue -> "error".equals(issue.getSeverity()));
101 if (hasError) { 83 if (hasError) {
@@ -111,53 +93,4 @@ public class SemanticsService extends AbstractCachedService<SemanticsResult> {
111 } 93 }
112 return problem; 94 return problem;
113 } 95 }
114
115 private List<String> getNodeTrace(ModelInitializer initializer) {
116 var nodeTrace = new String[initializer.getNodeCount()];
117 for (var entry : initializer.getNodeTrace().keyValuesView()) {
118 var node = entry.getOne();
119 var index = entry.getTwo();
120 nodeTrace[index] = semanticsUtils.getName(node).orElse(null);
121 }
122 return Arrays.asList(nodeTrace);
123 }
124
125 private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model,
126 CancelIndicator cancelIndicator) {
127 var adapter = model.getAdapter(ReasoningAdapter.class);
128 var json = new JsonObject();
129 for (var entry : initializer.getRelationTrace().entrySet()) {
130 var relation = entry.getKey();
131 var partialSymbol = entry.getValue();
132 var tuples = getTuplesJson(adapter, partialSymbol);
133 var name = semanticsUtils.getName(relation).orElse(partialSymbol.name());
134 json.add(name, tuples);
135 operationCanceledManager.checkCanceled(cancelIndicator);
136 }
137 return json;
138 }
139
140 private static JsonArray getTuplesJson(ReasoningAdapter adapter, PartialRelation partialSymbol) {
141 var interpretation = adapter.getPartialInterpretation(Concreteness.PARTIAL, partialSymbol);
142 var cursor = interpretation.getAll();
143 var map = new TreeMap<Tuple, TruthValue>();
144 while (cursor.move()) {
145 map.put(cursor.getKey(), cursor.getValue());
146 }
147 var tuples = new JsonArray();
148 for (var entry : map.entrySet()) {
149 tuples.add(toArray(entry.getKey(), entry.getValue()));
150 }
151 return tuples;
152 }
153
154 private static JsonArray toArray(Tuple tuple, TruthValue value) {
155 int arity = tuple.getSize();
156 var json = new JsonArray(arity + 1);
157 for (int i = 0; i < arity; i++) {
158 json.add(tuple.get(i));
159 }
160 json.add(value.toString());
161 return json;
162 }
163} 96}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
new file mode 100644
index 00000000..25589260
--- /dev/null
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/semantics/SemanticsWorker.java
@@ -0,0 +1,133 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.web.semantics;
7
8import com.google.gson.JsonArray;
9import com.google.gson.JsonObject;
10import com.google.inject.Inject;
11import org.eclipse.xtext.service.OperationCanceledManager;
12import org.eclipse.xtext.util.CancelIndicator;
13import org.slf4j.Logger;
14import org.slf4j.LoggerFactory;
15import tools.refinery.language.model.problem.Problem;
16import tools.refinery.language.semantics.model.ModelInitializer;
17import tools.refinery.language.semantics.model.SemanticsUtils;
18import tools.refinery.store.model.Model;
19import tools.refinery.store.model.ModelStore;
20import tools.refinery.store.query.viatra.ViatraModelQueryAdapter;
21import tools.refinery.store.reasoning.ReasoningAdapter;
22import tools.refinery.store.reasoning.ReasoningStoreAdapter;
23import tools.refinery.store.reasoning.literal.Concreteness;
24import tools.refinery.store.reasoning.representation.PartialRelation;
25import tools.refinery.store.representation.TruthValue;
26import tools.refinery.store.tuple.Tuple;
27import tools.refinery.viatra.runtime.CancellationToken;
28
29import java.util.Arrays;
30import java.util.List;
31import java.util.TreeMap;
32import java.util.concurrent.Callable;
33
34class SemanticsWorker implements Callable<SemanticsResult> {
35 private static final Logger LOG = LoggerFactory.getLogger(SemanticsWorker.class);
36
37 @Inject
38 private SemanticsUtils semanticsUtils;
39
40 @Inject
41 private OperationCanceledManager operationCanceledManager;
42
43 @Inject
44 private ModelInitializer initializer;
45
46 private Problem problem;
47
48 private CancellationToken cancellationToken;
49
50 public void setProblem(Problem problem, CancelIndicator parentIndicator) {
51 this.problem = problem;
52 cancellationToken = () -> {
53 if (Thread.interrupted() || parentIndicator.isCanceled()) {
54 operationCanceledManager.throwOperationCanceledException();
55 }
56 };
57 }
58
59 @Override
60 public SemanticsResult call() {
61 var builder = ModelStore.builder()
62 .with(ViatraModelQueryAdapter.builder()
63 .cancellationToken(cancellationToken))
64 .with(ReasoningAdapter.builder()
65 .requiredInterpretations(Concreteness.PARTIAL));
66 cancellationToken.checkCancelled();
67 try {
68 var modelSeed = initializer.createModel(problem, builder);
69 cancellationToken.checkCancelled();
70 var nodeTrace = getNodeTrace(initializer);
71 cancellationToken.checkCancelled();
72 var store = builder.build();
73 cancellationToken.checkCancelled();
74 var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed);
75 cancellationToken.checkCancelled();
76 var partialInterpretation = getPartialInterpretation(initializer, model);
77
78 return new SemanticsSuccessResult(nodeTrace, partialInterpretation);
79 } catch (RuntimeException e) {
80 LOG.debug("Error while computing semantics", e);
81 var message = e.getMessage();
82 return new SemanticsErrorResult(message == null ? "Partial interpretation error" : e.getMessage());
83 }
84 }
85
86 private List<String> getNodeTrace(ModelInitializer initializer) {
87 var nodeTrace = new String[initializer.getNodeCount()];
88 for (var entry : initializer.getNodeTrace().keyValuesView()) {
89 var node = entry.getOne();
90 var index = entry.getTwo();
91 nodeTrace[index] = semanticsUtils.getName(node).orElse(null);
92 }
93 return Arrays.asList(nodeTrace);
94 }
95
96 private JsonObject getPartialInterpretation(ModelInitializer initializer, Model model) {
97 var adapter = model.getAdapter(ReasoningAdapter.class);
98 var json = new JsonObject();
99 for (var entry : initializer.getRelationTrace().entrySet()) {
100 var relation = entry.getKey();
101 var partialSymbol = entry.getValue();
102 var tuples = getTuplesJson(adapter, partialSymbol);
103 var name = semanticsUtils.getName(relation).orElse(partialSymbol.name());
104 json.add(name, tuples);
105 cancellationToken.checkCancelled();
106 }
107 return json;
108 }
109
110 private static JsonArray getTuplesJson(ReasoningAdapter adapter, PartialRelation partialSymbol) {
111 var interpretation = adapter.getPartialInterpretation(Concreteness.PARTIAL, partialSymbol);
112 var cursor = interpretation.getAll();
113 var map = new TreeMap<Tuple, TruthValue>();
114 while (cursor.move()) {
115 map.put(cursor.getKey(), cursor.getValue());
116 }
117 var tuples = new JsonArray();
118 for (var entry : map.entrySet()) {
119 tuples.add(toArray(entry.getKey(), entry.getValue()));
120 }
121 return tuples;
122 }
123
124 private static JsonArray toArray(Tuple tuple, TruthValue value) {
125 int arity = tuple.getSize();
126 var json = new JsonArray(arity + 1);
127 for (int i = 0; i < arity; i++) {
128 json.add(tuple.get(i));
129 }
130 json.add(value.toString());
131 return json;
132 }
133}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
index 2c0e9329..74456604 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/TransactionExecutor.java
@@ -42,6 +42,8 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
42 42
43 private final List<XtextWebPushMessage> pendingPushMessages = new ArrayList<>(); 43 private final List<XtextWebPushMessage> pendingPushMessages = new ArrayList<>();
44 44
45 private volatile boolean disposed;
46
45 public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) { 47 public TransactionExecutor(ISession session, IResourceServiceProvider.Registry resourceServiceProviderRegistry) {
46 this.session = session; 48 this.session = session;
47 this.resourceServiceProviderRegistry = resourceServiceProviderRegistry; 49 this.resourceServiceProviderRegistry = resourceServiceProviderRegistry;
@@ -52,10 +54,13 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
52 } 54 }
53 55
54 public void handleRequest(XtextWebRequest request) throws ResponseHandlerException { 56 public void handleRequest(XtextWebRequest request) throws ResponseHandlerException {
57 if (disposed) {
58 return;
59 }
55 var serviceContext = new SimpleServiceContext(session, request.getRequestData()); 60 var serviceContext = new SimpleServiceContext(session, request.getRequestData());
56 var ping = serviceContext.getParameter("ping"); 61 var ping = serviceContext.getParameter("ping");
57 if (ping != null) { 62 if (ping != null) {
58 responseHandler.onResponse(new XtextWebOkResponse(request, new PongResult(ping))); 63 onResponse(new XtextWebOkResponse(request, new PongResult(ping)));
59 return; 64 return;
60 } 65 }
61 synchronized (callPendingLock) { 66 synchronized (callPendingLock) {
@@ -72,23 +77,36 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
72 var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class); 77 var serviceDispatcher = injector.getInstance(XtextServiceDispatcher.class);
73 var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this)); 78 var service = serviceDispatcher.getService(new SubscribingServiceContext(serviceContext, this));
74 var serviceResult = service.getService().apply(); 79 var serviceResult = service.getService().apply();
75 responseHandler.onResponse(new XtextWebOkResponse(request, serviceResult)); 80 onResponse(new XtextWebOkResponse(request, serviceResult));
76 } catch (InvalidRequestException e) { 81 } catch (InvalidRequestException e) {
77 responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e)); 82 onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.REQUEST_ERROR, e));
78 } catch (RuntimeException e) { 83 } catch (RuntimeException e) {
79 responseHandler.onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e)); 84 onResponse(new XtextWebErrorResponse(request, XtextWebErrorKind.SERVER_ERROR, e));
80 } finally { 85 } finally {
81 synchronized (callPendingLock) { 86 flushPendingPushMessages();
82 for (var message : pendingPushMessages) { 87 }
83 try { 88 }
84 responseHandler.onResponse(message); 89
85 } catch (ResponseHandlerException | RuntimeException e) { 90 private void onResponse(XtextWebResponse response) throws ResponseHandlerException {
86 LOG.error("Error while flushing push message", e); 91 if (!disposed) {
87 } 92 responseHandler.onResponse(response);
93 }
94 }
95
96 private void flushPendingPushMessages() {
97 synchronized (callPendingLock) {
98 for (var message : pendingPushMessages) {
99 if (disposed) {
100 return;
101 }
102 try {
103 responseHandler.onResponse(message);
104 } catch (ResponseHandlerException | RuntimeException e) {
105 LOG.error("Error while flushing push message", e);
88 } 106 }
89 pendingPushMessages.clear();
90 callPending = false;
91 } 107 }
108 pendingPushMessages.clear();
109 callPending = false;
92 } 110 }
93 } 111 }
94 112
@@ -164,10 +182,12 @@ public class TransactionExecutor implements IDisposable, PrecomputationListener
164 182
165 @Override 183 @Override
166 public void dispose() { 184 public void dispose() {
185 disposed = true;
167 for (var subscription : subscriptions.values()) { 186 for (var subscription : subscriptions.values()) {
168 var document = subscription.get(); 187 var document = subscription.get();
169 if (document != null) { 188 if (document != null) {
170 document.removePrecomputationListener(this); 189 document.removePrecomputationListener(this);
190 document.cancelBackgroundWork();
171 } 191 }
172 } 192 }
173 } 193 }
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
index dfbd4878..1542c694 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocument.java
@@ -27,11 +27,11 @@ public class PushWebDocument extends XtextWebDocument {
27 27
28 private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>(); 28 private final Map<Class<?>, IServiceResult> precomputedServices = new HashMap<>();
29 29
30 private final DocumentSynchronizer synchronizer;
31
30 public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) { 32 public PushWebDocument(String resourceId, DocumentSynchronizer synchronizer) {
31 super(resourceId, synchronizer); 33 super(resourceId, synchronizer);
32 if (resourceId == null) { 34 this.synchronizer = synchronizer;
33 throw new IllegalArgumentException("resourceId must not be null");
34 }
35 } 35 }
36 36
37 public void addPrecomputationListener(PrecomputationListener listener) { 37 public void addPrecomputationListener(PrecomputationListener listener) {
@@ -63,6 +63,9 @@ public class PushWebDocument extends XtextWebDocument {
63 63
64 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) { 64 private <T extends IServiceResult> void notifyPrecomputationListeners(String serviceName, T result) {
65 var resourceId = getResourceId(); 65 var resourceId = getResourceId();
66 if (resourceId == null) {
67 return;
68 }
66 var stateId = getStateId(); 69 var stateId = getStateId();
67 List<PrecomputationListener> copyOfListeners; 70 List<PrecomputationListener> copyOfListeners;
68 synchronized (precomputationListeners) { 71 synchronized (precomputationListeners) {
@@ -83,4 +86,8 @@ public class PushWebDocument extends XtextWebDocument {
83 } 86 }
84 } 87 }
85 } 88 }
89
90 public void cancelBackgroundWork() {
91 synchronizer.setCanceled(true);
92 }
86} 93}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java
index b6f4fb43..ec6204ef 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/server/push/PushWebDocumentProvider.java
@@ -27,12 +27,7 @@ public class PushWebDocumentProvider implements IWebDocumentProvider {
27 27
28 @Override 28 @Override
29 public XtextWebDocument get(String resourceId, IServiceContext serviceContext) { 29 public XtextWebDocument get(String resourceId, IServiceContext serviceContext) {
30 if (resourceId == null) { 30 return new PushWebDocument(resourceId,
31 return new XtextWebDocument(null, synchronizerProvider.get()); 31 serviceContext.getSession().get(DocumentSynchronizer.class, () -> this.synchronizerProvider.get()));
32 } else {
33 // We only need to send push messages if a resourceId is specified.
34 return new PushWebDocument(resourceId,
35 serviceContext.getSession().get(DocumentSynchronizer.class, () -> this.synchronizerProvider.get()));
36 }
37 } 32 }
38} 33}
diff --git a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
index 043d318c..923fecd6 100644
--- a/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
+++ b/subprojects/language-web/src/main/java/tools/refinery/language/web/xtext/servlet/XtextWebSocket.java
@@ -70,10 +70,11 @@ public class XtextWebSocket implements ResponseHandler {
70 70
71 @OnWebSocketError 71 @OnWebSocketError
72 public void onError(Throwable error) { 72 public void onError(Throwable error) {
73 executor.dispose();
73 if (webSocketSession == null) { 74 if (webSocketSession == null) {
74 return; 75 return;
75 } 76 }
76 LOG.error("Internal websocket error in connection from" + webSocketSession.getRemoteSocketAddress(), error); 77 LOG.error("Internal websocket error in connection from " + webSocketSession.getRemoteSocketAddress(), error);
77 } 78 }
78 79
79 @OnWebSocketMessage 80 @OnWebSocketMessage
@@ -86,14 +87,18 @@ public class XtextWebSocket implements ResponseHandler {
86 try { 87 try {
87 request = gson.fromJson(reader, XtextWebRequest.class); 88 request = gson.fromJson(reader, XtextWebRequest.class);
88 } catch (JsonIOException e) { 89 } catch (JsonIOException e) {
89 LOG.error("Cannot read from websocket from" + webSocketSession.getRemoteSocketAddress(), e); 90 LOG.error("Cannot read from websocket from " + webSocketSession.getRemoteSocketAddress(), e);
90 if (webSocketSession.isOpen()) { 91 if (webSocketSession.isOpen()) {
92 executor.dispose();
91 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload", Callback.NOOP); 93 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot read payload", Callback.NOOP);
92 } 94 }
93 return; 95 return;
94 } catch (JsonParseException e) { 96 } catch (JsonParseException e) {
95 LOG.warn("Malformed websocket request from" + webSocketSession.getRemoteSocketAddress(), e); 97 LOG.warn("Malformed websocket request from " + webSocketSession.getRemoteSocketAddress(), e);
96 webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload", Callback.NOOP); 98 if (webSocketSession.isOpen()) {
99 executor.dispose();
100 webSocketSession.close(XtextStatusCode.INVALID_JSON, "Invalid JSON payload", Callback.NOOP);
101 }
97 return; 102 return;
98 } 103 }
99 try { 104 try {
@@ -101,6 +106,7 @@ public class XtextWebSocket implements ResponseHandler {
101 } catch (ResponseHandlerException e) { 106 } catch (ResponseHandlerException e) {
102 LOG.warn("Cannot write websocket response", e); 107 LOG.warn("Cannot write websocket response", e);
103 if (webSocketSession.isOpen()) { 108 if (webSocketSession.isOpen()) {
109 executor.dispose();
104 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response", Callback.NOOP); 110 webSocketSession.close(StatusCode.SERVER_ERROR, "Cannot write response", Callback.NOOP);
105 } 111 }
106 } 112 }