2 * ============LICENSE_START=======================================================
3 * Copyright (C) 2020 Nordix Foundation.
4 * ================================================================================
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
17 * SPDX-License-Identifier: Apache-2.0
18 * ============LICENSE_END=========================================================
21 package org.onap.policy.apex.plugins.executor.javascript;
23 import java.util.concurrent.BlockingQueue;
24 import java.util.concurrent.CountDownLatch;
25 import java.util.concurrent.LinkedBlockingQueue;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.atomic.AtomicReference;
29 import lombok.AccessLevel;
31 import lombok.NonNull;
34 import org.apache.commons.lang3.StringUtils;
35 import org.mozilla.javascript.Context;
36 import org.mozilla.javascript.Script;
37 import org.mozilla.javascript.Scriptable;
38 import org.onap.policy.apex.core.engine.executor.exception.StateMachineException;
39 import org.onap.policy.apex.model.basicmodel.concepts.AxKey;
40 import org.slf4j.ext.XLogger;
41 import org.slf4j.ext.XLoggerFactory;
44 * The Class JavascriptExecutor is the executor for task logic written in Javascript.
46 * @author Liam Fallon (liam.fallon@ericsson.com)
48 public class JavascriptExecutor implements Runnable {
49 private static final XLogger LOGGER = XLoggerFactory.getXLogger(JavascriptExecutor.class);
51 public static final int DEFAULT_OPTIMIZATION_LEVEL = 9;
53 // Recurring string constants
54 private static final String WITH_MESSAGE = " with message: ";
56 @Setter(AccessLevel.PROTECTED)
57 private static TimeUnit timeunit4Latches = TimeUnit.SECONDS;
58 @Setter(AccessLevel.PROTECTED)
59 private static int intializationLatchTimeout = 60;
60 @Setter(AccessLevel.PROTECTED)
61 private static int cleanupLatchTimeout = 60;
63 // The key of the subject that wants to execute Javascript code
64 final AxKey subjectKey;
66 private String javascriptCode;
67 private Context javascriptContext;
68 private Script script;
70 private final BlockingQueue<Object> executionQueue = new LinkedBlockingQueue<>();
71 private final BlockingQueue<Boolean> resultQueue = new LinkedBlockingQueue<>();
73 @Getter(AccessLevel.PROTECTED)
74 private Thread executorThread;
75 private CountDownLatch intializationLatch;
76 private CountDownLatch cleanupLatch;
77 private AtomicReference<StateMachineException> executorException = new AtomicReference<>(null);
80 * Initializes the Javascript executor.
82 * @param subjectKey the key of the subject that is requesting Javascript execution
84 public JavascriptExecutor(final AxKey subjectKey) {
85 this.subjectKey = subjectKey;
89 * Prepares the executor for processing and compiles the Javascript code.
91 * @param javascriptCode the Javascript code to execute
92 * @throws StateMachineException thrown when instantiation of the executor fails
94 public synchronized void init(@NonNull final String javascriptCode) throws StateMachineException {
95 LOGGER.debug("JavascriptExecutor {} starting ... ", subjectKey.getId());
97 if (executorThread != null) {
98 throw new StateMachineException("initiation failed, executor " + subjectKey.getId()
99 + " already initialized, run cleanUp to clear executor");
102 if (StringUtils.isBlank(javascriptCode)) {
103 throw new StateMachineException("initiation failed, no logic specified for executor " + subjectKey.getId());
106 this.javascriptCode = javascriptCode;
108 executorThread = new Thread(this);
109 executorThread.setName(this.getClass().getSimpleName() + ":" + subjectKey.getId());
110 intializationLatch = new CountDownLatch(1);
111 cleanupLatch = new CountDownLatch(1);
114 executorThread.start();
115 } catch (IllegalThreadStateException e) {
116 throw new StateMachineException("initiation failed, executor " + subjectKey.getId() + " failed to start",
121 if (!intializationLatch.await(intializationLatchTimeout, timeunit4Latches)) {
122 executorThread.interrupt();
123 throw new StateMachineException("JavascriptExecutor " + subjectKey.getId()
124 + " initiation timed out after " + intializationLatchTimeout + " " + timeunit4Latches);
126 } catch (InterruptedException e) {
127 LOGGER.debug("JavascriptExecutor {} interrupted on execution thread startup", subjectKey.getId(), e);
128 Thread.currentThread().interrupt();
131 checkAndThrowExecutorException();
133 LOGGER.debug("JavascriptExecutor {} started ... ", subjectKey.getId());
137 * Execute a Javascript script.
139 * @param executionContext the execution context to use for script execution
140 * @return true if execution was successful, false otherwise
141 * @throws StateMachineException on execution errors
143 public synchronized boolean execute(final Object executionContext) throws StateMachineException {
144 if (executorThread == null) {
145 throw new StateMachineException("execution failed, executor " + subjectKey.getId() + " is not initialized");
148 if (!executorThread.isAlive()) {
149 throw new StateMachineException("execution failed, executor " + subjectKey.getId()
150 + " is not running, run cleanUp to clear executor and init to restart executor");
153 executionQueue.add(executionContext);
155 boolean result = false;
158 result = resultQueue.take();
159 } catch (final InterruptedException e) {
160 executorThread.interrupt();
161 Thread.currentThread().interrupt();
162 throw new StateMachineException(
163 "JavascriptExecutor " + subjectKey.getId() + "interrupted on execution result wait", e);
166 checkAndThrowExecutorException();
172 * Cleans up the executor after processing.
174 * @throws StateMachineException thrown when cleanup of the executor fails
176 public synchronized void cleanUp() throws StateMachineException {
177 if (executorThread == null) {
178 throw new StateMachineException("cleanup failed, executor " + subjectKey.getId() + " is not initialized");
181 if (executorThread.isAlive()) {
182 executorThread.interrupt();
185 if (!cleanupLatch.await(cleanupLatchTimeout, timeunit4Latches)) {
186 executorException.set(new StateMachineException("JavascriptExecutor " + subjectKey.getId()
187 + " cleanup timed out after " + cleanupLatchTimeout + " " + timeunit4Latches));
189 } catch (InterruptedException e) {
190 LOGGER.debug("JavascriptExecutor {} interrupted on execution cleanup wait", subjectKey.getId(), e);
191 Thread.currentThread().interrupt();
195 executorThread = null;
196 executionQueue.clear();
199 checkAndThrowExecutorException();
204 LOGGER.debug("JavascriptExecutor {} initializing ... ", subjectKey.getId());
208 } catch (StateMachineException sme) {
209 LOGGER.warn("JavascriptExecutor {} initialization failed", subjectKey.getId(), sme);
210 executorException.set(sme);
211 intializationLatch.countDown();
212 cleanupLatch.countDown();
216 intializationLatch.countDown();
218 LOGGER.debug("JavascriptExecutor {} executing ... ", subjectKey.getId());
220 // Take jobs from the execution queue of the worker and execute them
221 while (!Thread.currentThread().isInterrupted()) {
223 Object contextObject = executionQueue.take();
225 boolean result = executeScript(contextObject);
226 resultQueue.add(result);
227 } catch (final InterruptedException e) {
228 LOGGER.debug("execution was interruped for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
229 resultQueue.add(false);
230 Thread.currentThread().interrupt();
231 } catch (StateMachineException sme) {
232 executorException.set(sme);
233 resultQueue.add(false);
239 } catch (final Exception e) {
240 executorException.set(new StateMachineException(
241 "executor close failed to close for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e));
244 cleanupLatch.countDown();
246 LOGGER.debug("JavascriptExecutor {} completed processing", subjectKey.getId());
249 private void initExecutor() throws StateMachineException {
251 // Create a Javascript context for this thread
252 javascriptContext = Context.enter();
254 // Set up the default values of the context
255 javascriptContext.setOptimizationLevel(DEFAULT_OPTIMIZATION_LEVEL);
256 javascriptContext.setLanguageVersion(Context.VERSION_1_8);
258 script = javascriptContext.compileString(javascriptCode, subjectKey.getId(), 1, null);
259 } catch (Exception e) {
261 throw new StateMachineException(
262 "logic failed to compile for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
266 private boolean executeScript(final Object executionContext) throws StateMachineException {
267 Object returnObject = null;
270 // Pass the subject context to the Javascript engine
271 Scriptable javascriptScope = javascriptContext.initStandardObjects();
272 javascriptScope.put("executor", javascriptScope, executionContext);
275 returnObject = script.exec(javascriptContext, javascriptScope);
276 } catch (final Exception e) {
277 throw new StateMachineException(
278 "logic failed to run for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
281 if (!(returnObject instanceof Boolean)) {
282 throw new StateMachineException(
283 "execute: logic for " + subjectKey.getId() + " returned a non-boolean value " + returnObject);
286 return (boolean) returnObject;
289 private void checkAndThrowExecutorException() throws StateMachineException {
290 StateMachineException exceptionToThrow = executorException.getAndSet(null);
291 if (exceptionToThrow != null) {
292 throw exceptionToThrow;