diff options
Diffstat (limited to 'subprojects/language-web')
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 | */ |
6 | package tools.refinery.language.web.semantics; | 6 | package tools.refinery.language.web.semantics; |
7 | 7 | ||
8 | import com.google.gson.JsonArray; | ||
9 | import com.google.gson.JsonObject; | ||
10 | import com.google.inject.Inject; | 8 | import com.google.inject.Inject; |
11 | import com.google.inject.Provider; | 9 | import com.google.inject.Provider; |
12 | import com.google.inject.Singleton; | 10 | import com.google.inject.Singleton; |
@@ -14,43 +12,29 @@ import org.eclipse.xtext.service.OperationCanceledManager; | |||
14 | import org.eclipse.xtext.util.CancelIndicator; | 12 | import org.eclipse.xtext.util.CancelIndicator; |
15 | import org.eclipse.xtext.web.server.model.AbstractCachedService; | 13 | import org.eclipse.xtext.web.server.model.AbstractCachedService; |
16 | import org.eclipse.xtext.web.server.model.IXtextWebDocument; | 14 | import org.eclipse.xtext.web.server.model.IXtextWebDocument; |
17 | import org.eclipse.xtext.web.server.model.XtextWebDocument; | ||
18 | import org.eclipse.xtext.web.server.validation.ValidationService; | 15 | import org.eclipse.xtext.web.server.validation.ValidationService; |
19 | import org.jetbrains.annotations.Nullable; | 16 | import org.jetbrains.annotations.Nullable; |
20 | import org.slf4j.Logger; | 17 | import org.slf4j.Logger; |
21 | import org.slf4j.LoggerFactory; | 18 | import org.slf4j.LoggerFactory; |
22 | import tools.refinery.language.model.problem.Problem; | 19 | import tools.refinery.language.model.problem.Problem; |
23 | import tools.refinery.language.semantics.model.ModelInitializer; | 20 | import tools.refinery.language.web.xtext.server.push.PushWebDocument; |
24 | import tools.refinery.language.semantics.model.SemanticsUtils; | ||
25 | import tools.refinery.store.model.Model; | ||
26 | import tools.refinery.store.model.ModelStore; | ||
27 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; | ||
28 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
29 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | ||
30 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
31 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
32 | import tools.refinery.store.representation.TruthValue; | ||
33 | import tools.refinery.store.tuple.Tuple; | ||
34 | 21 | ||
35 | import java.util.Arrays; | 22 | import java.util.concurrent.*; |
36 | import java.util.List; | ||
37 | import java.util.TreeMap; | ||
38 | 23 | ||
39 | @Singleton | 24 | @Singleton |
40 | public class SemanticsService extends AbstractCachedService<SemanticsResult> { | 25 | public 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 | */ | ||
6 | package tools.refinery.language.web.semantics; | ||
7 | |||
8 | import com.google.gson.JsonArray; | ||
9 | import com.google.gson.JsonObject; | ||
10 | import com.google.inject.Inject; | ||
11 | import org.eclipse.xtext.service.OperationCanceledManager; | ||
12 | import org.eclipse.xtext.util.CancelIndicator; | ||
13 | import org.slf4j.Logger; | ||
14 | import org.slf4j.LoggerFactory; | ||
15 | import tools.refinery.language.model.problem.Problem; | ||
16 | import tools.refinery.language.semantics.model.ModelInitializer; | ||
17 | import tools.refinery.language.semantics.model.SemanticsUtils; | ||
18 | import tools.refinery.store.model.Model; | ||
19 | import tools.refinery.store.model.ModelStore; | ||
20 | import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; | ||
21 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
22 | import tools.refinery.store.reasoning.ReasoningStoreAdapter; | ||
23 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
24 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
25 | import tools.refinery.store.representation.TruthValue; | ||
26 | import tools.refinery.store.tuple.Tuple; | ||
27 | import tools.refinery.viatra.runtime.CancellationToken; | ||
28 | |||
29 | import java.util.Arrays; | ||
30 | import java.util.List; | ||
31 | import java.util.TreeMap; | ||
32 | import java.util.concurrent.Callable; | ||
33 | |||
34 | class 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 | } |