/* * SPDX-FileCopyrightText: 2023 The Refinery Authors * * SPDX-License-Identifier: EPL-2.0 */ package tools.refinery.language.web.generator; import com.google.inject.Inject; import com.google.inject.Provider; import org.eclipse.emf.common.util.URI; import org.eclipse.xtext.diagnostics.Severity; import org.eclipse.xtext.resource.IResourceFactory; import org.eclipse.xtext.resource.XtextResourceSet; import org.eclipse.xtext.service.OperationCanceledManager; import org.eclipse.xtext.util.LazyStringInputStream; import org.eclipse.xtext.validation.CheckMode; import org.eclipse.xtext.validation.IResourceValidator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import tools.refinery.language.model.problem.Problem; import tools.refinery.language.semantics.metadata.MetadataCreator; import tools.refinery.language.semantics.model.ModelInitializer; import tools.refinery.language.web.semantics.PartialInterpretation2Json; import tools.refinery.language.web.xtext.server.ThreadPoolExecutorServiceProvider; import tools.refinery.language.web.xtext.server.push.PushWebDocument; import tools.refinery.store.dse.propagation.PropagationAdapter; import tools.refinery.store.dse.strategy.BestFirstStoreManager; import tools.refinery.store.dse.transition.DesignSpaceExplorationAdapter; import tools.refinery.store.model.ModelStore; import tools.refinery.store.query.viatra.ViatraModelQueryAdapter; import tools.refinery.store.reasoning.ReasoningAdapter; import tools.refinery.store.reasoning.ReasoningStoreAdapter; import tools.refinery.store.reasoning.literal.Concreteness; import tools.refinery.store.statecoding.StateCoderAdapter; import tools.refinery.store.util.CancellationToken; import java.io.IOException; import java.util.Map; import java.util.UUID; import java.util.concurrent.*; public class ModelGenerationWorker implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(ModelGenerationWorker.class); private final UUID uuid = UUID.randomUUID(); private PushWebDocument state; private String text; private volatile boolean timedOut; private volatile boolean cancelled; @Inject private OperationCanceledManager operationCanceledManager; @Inject private Provider resourceSetProvider; @Inject private IResourceFactory resourceFactory; @Inject private IResourceValidator resourceValidator; @Inject private ModelInitializer initializer; @Inject private MetadataCreator metadataCreator; @Inject private PartialInterpretation2Json partialInterpretation2Json; private final Object lockObject = new Object(); private ExecutorService executorService; private ScheduledExecutorService scheduledExecutorService; private int randomSeed; private long timeoutSec; private Future future; private ScheduledFuture timeoutFuture; private final CancellationToken cancellationToken = () -> { if (cancelled || Thread.interrupted()) { operationCanceledManager.throwOperationCanceledException(); } }; @Inject public void setExecutorServiceProvider(ThreadPoolExecutorServiceProvider provider) { executorService = provider.get(ModelGenerationService.MODEL_GENERATION_EXECUTOR); scheduledExecutorService = provider.getScheduled(ModelGenerationService.MODEL_GENERATION_TIMEOUT_EXECUTOR); } public void setState(PushWebDocument state, int randomSeed, long timeoutSec) { this.state = state; this.randomSeed = randomSeed; this.timeoutSec = timeoutSec; text = state.getText(); } public UUID getUuid() { return uuid; } public void start() { synchronized (lockObject) { LOG.debug("Enqueueing model generation: {}", uuid); future = executorService.submit(this); } } public void startTimeout() { synchronized (lockObject) { LOG.debug("Starting model generation: {}", uuid); cancellationToken.checkCancelled(); timeoutFuture = scheduledExecutorService.schedule(() -> cancel(true), timeoutSec, TimeUnit.SECONDS); } } // We catch {@code Throwable} to handle {@code OperationCancelledError}, but we rethrow fatal JVM errors. @SuppressWarnings("squid:S1181") @Override public void run() { startTimeout(); notifyResult(new ModelGenerationStatusResult(uuid, "Initializing model generator")); ModelGenerationResult result; try { result = doRun(); } catch (Throwable e) { if (operationCanceledManager.isOperationCanceledException(e)) { var message = timedOut ? "Model generation timed out" : "Model generation cancelled"; LOG.debug("{}: {}", message, uuid); notifyResult(new ModelGenerationErrorResult(uuid, message)); } else if (e instanceof Error error) { // Make sure we don't try to recover from any fatal JVM errors. throw error; } else { LOG.debug("Model generation error", e); notifyResult(new ModelGenerationErrorResult(uuid, e.toString())); } return; } notifyResult(result); } private void notifyResult(ModelGenerationResult result) { state.notifyPrecomputationListeners(ModelGenerationService.SERVICE_NAME, result); } public ModelGenerationResult doRun() throws IOException { cancellationToken.checkCancelled(); var resourceSet = resourceSetProvider.get(); var uri = URI.createURI("__synthetic_" + uuid + ".problem"); var resource = resourceFactory.createResource(uri); resourceSet.getResources().add(resource); var inputStream = new LazyStringInputStream(text); resource.load(inputStream, Map.of()); cancellationToken.checkCancelled(); var issues = resourceValidator.validate(resource, CheckMode.ALL, () -> cancelled || Thread.interrupted()); cancellationToken.checkCancelled(); for (var issue : issues) { if (issue.getSeverity() == Severity.ERROR) { return new ModelGenerationErrorResult(uuid, "Validation error: " + issue.getMessage()); } } if (resource.getContents().isEmpty() || !(resource.getContents().get(0) instanceof Problem problem)) { return new ModelGenerationErrorResult(uuid, "Model generation problem not found"); } cancellationToken.checkCancelled(); var storeBuilder = ModelStore.builder() .cancellationToken(cancellationToken) .with(ViatraModelQueryAdapter.builder()) .with(PropagationAdapter.builder()) .with(StateCoderAdapter.builder()) .with(DesignSpaceExplorationAdapter.builder()) .with(ReasoningAdapter.builder() .requiredInterpretations(Concreteness.CANDIDATE)); var modelSeed = initializer.createModel(problem, storeBuilder); var store = storeBuilder.build(); cancellationToken.checkCancelled(); var model = store.getAdapter(ReasoningStoreAdapter.class).createInitialModel(modelSeed); var initialVersion = model.commit(); cancellationToken.checkCancelled(); notifyResult(new ModelGenerationStatusResult(uuid, "Generating model")); var bestFirst = new BestFirstStoreManager(store, 1); bestFirst.startExploration(initialVersion, randomSeed); cancellationToken.checkCancelled(); var solutionStore = bestFirst.getSolutionStore(); if (solutionStore.getSolutions().isEmpty()) { return new ModelGenerationErrorResult(uuid, "Problem is unsatisfiable"); } notifyResult(new ModelGenerationStatusResult(uuid, "Saving generated model")); model.restore(solutionStore.getSolutions().get(0).version()); cancellationToken.checkCancelled(); metadataCreator.setInitializer(initializer); var nodesMetadata = metadataCreator.getNodesMetadata(model.getAdapter(ReasoningAdapter.class).getNodeCount(), false); cancellationToken.checkCancelled(); var relationsMetadata = metadataCreator.getRelationsMetadata(); cancellationToken.checkCancelled(); var partialInterpretation = partialInterpretation2Json.getPartialInterpretation(initializer, model, Concreteness.CANDIDATE, cancellationToken); return new ModelGenerationSuccessResult(uuid, nodesMetadata, relationsMetadata, partialInterpretation); } public void cancel() { cancel(false); } public void cancel(boolean timedOut) { synchronized (lockObject) { LOG.trace("Cancelling model generation: {}", uuid); this.timedOut = timedOut; cancelled = true; if (future != null) { future.cancel(true); future = null; } if (timeoutFuture != null) { timeoutFuture.cancel(true); timeoutFuture = null; } } } }