f7bafdd74ec65ff2e5d45828a7be0fce796695a3
[policy/apex-pdp.git] /
1 /*-
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
8  *
9  *      http://www.apache.org/licenses/LICENSE-2.0
10  *
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.
16  *
17  * SPDX-License-Identifier: Apache-2.0
18  * ============LICENSE_END=========================================================
19  */
20
21 package org.onap.policy.apex.plugins.executor.javascript;
22
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;
28
29 import org.apache.commons.lang3.StringUtils;
30 import org.mozilla.javascript.Context;
31 import org.mozilla.javascript.Script;
32 import org.mozilla.javascript.Scriptable;
33 import org.onap.policy.apex.core.engine.executor.exception.StateMachineException;
34 import org.onap.policy.apex.model.basicmodel.concepts.AxKey;
35 import org.slf4j.ext.XLogger;
36 import org.slf4j.ext.XLoggerFactory;
37
38 /**
39  * The Class JavascriptExecutor is the executor for task logic written in Javascript.
40  *
41  * @author Liam Fallon (liam.fallon@ericsson.com)
42  */
43 public class JavascriptExecutor implements Runnable {
44     private static final XLogger LOGGER = XLoggerFactory.getXLogger(JavascriptExecutor.class);
45
46     public static final int DEFAULT_OPTIMIZATION_LEVEL = 9;
47
48     // Recurring string constants
49     private static final String WITH_MESSAGE = " with message: ";
50
51     // The key of the subject that wants to execute Javascript code
52     final AxKey subjectKey;
53
54     private String javascriptCode;
55     private Context javascriptContext;
56     private Script script;
57
58     private final BlockingQueue<Object> executionQueue = new LinkedBlockingQueue<>();
59     private final BlockingQueue<Boolean> resultQueue = new LinkedBlockingQueue<>();
60
61     private final Thread executorThread;
62     private CountDownLatch intializationLatch = new CountDownLatch(1);
63     private CountDownLatch shutdownLatch = new CountDownLatch(1);
64     private AtomicReference<StateMachineException> executorException = new AtomicReference<>(null);
65
66     /**
67      * Initializes the Javascript executor.
68      *
69      * @param subjectKey the key of the subject that is requesting Javascript execution
70      */
71     public JavascriptExecutor(final AxKey subjectKey) {
72         this.subjectKey = subjectKey;
73
74         executorThread = new Thread(this);
75         executorThread.setName(this.getClass().getSimpleName() + ":" + subjectKey.getId());
76     }
77
78     /**
79      * Prepares the executor for processing and compiles the Javascript code.
80      *
81      * @param javascriptCode the Javascript code to execute
82      * @throws StateMachineException thrown when instantiation of the executor fails
83      */
84     public void init(final String javascriptCode) throws StateMachineException {
85         LOGGER.debug("JavascriptExecutor {} starting ... ", subjectKey.getId());
86
87         if (executorThread.isAlive()) {
88             throw new StateMachineException(
89                 "initiation failed, executor " + subjectKey.getId() + " is already running");
90         }
91
92         if (StringUtils.isEmpty(javascriptCode)) {
93             throw new StateMachineException("no logic specified for " + subjectKey.getId());
94         }
95
96         this.javascriptCode = javascriptCode;
97
98         try {
99             executorThread.start();
100         } catch (Exception e) {
101             throw new StateMachineException("initiation failed, executor " + subjectKey.getId() + " failed to start",
102                 e);
103         }
104
105         try {
106             if (!intializationLatch.await(60, TimeUnit.SECONDS)) {
107                 LOGGER.warn("JavascriptExecutor {}, initiation timed out", subjectKey.getId());
108             }
109         } catch (InterruptedException e) {
110             LOGGER.debug("JavascriptExecutor {} interrupted on execution thread startup", subjectKey.getId(), e);
111             Thread.currentThread().interrupt();
112         }
113
114         if (executorException.get() != null) {
115             clearAndThrowExecutorException();
116         }
117
118         LOGGER.debug("JavascriptExecutor {} started ... ", subjectKey.getId());
119     }
120
121     /**
122      * Execute a Javascript script.
123      *
124      * @param executionContext the execution context to use for script execution
125      * @return true if execution was successful, false otherwise
126      * @throws StateMachineException on execution errors
127      */
128     public boolean execute(final Object executionContext) throws StateMachineException {
129         if (!executorThread.isAlive()) {
130             throw new StateMachineException("execution failed, executor " + subjectKey.getId() + " is not running");
131         }
132
133         executionQueue.add(executionContext);
134
135         boolean result = false;
136
137         try {
138             result = resultQueue.take();
139         } catch (final InterruptedException e) {
140             LOGGER.debug("JavascriptExecutor {} interrupted on execution result wait", subjectKey.getId(), e);
141             Thread.currentThread().interrupt();
142         }
143
144         if (executorException.get() != null) {
145             clearAndThrowExecutorException();
146         }
147
148         return result;
149     }
150
151     /**
152      * Cleans up the executor after processing.
153      *
154      * @throws StateMachineException thrown when cleanup of the executor fails
155      */
156     public void cleanUp() throws StateMachineException {
157         if (!executorThread.isAlive()) {
158             throw new StateMachineException("cleanup failed, executor " + subjectKey.getId() + " is not running");
159         }
160
161         executorThread.interrupt();
162
163         try {
164             if (!shutdownLatch.await(60, TimeUnit.SECONDS)) {
165                 LOGGER.warn("JavascriptExecutor {}, shutdown timed out", subjectKey.getId());
166             }
167         } catch (InterruptedException e) {
168             LOGGER.debug("JavascriptExecutor {} interrupted on execution clkeanup wait", subjectKey.getId(), e);
169             Thread.currentThread().interrupt();
170         }
171
172         if (executorException.get() != null) {
173             clearAndThrowExecutorException();
174         }
175     }
176
177     @Override
178     public void run() {
179         LOGGER.debug("JavascriptExecutor {} initializing ... ", subjectKey.getId());
180
181         try {
182             initExecutor();
183         } catch (StateMachineException sme) {
184             LOGGER.warn("JavascriptExecutor {} initialization failed", sme);
185             executorException.set(sme);
186             intializationLatch.countDown();
187             return;
188         }
189
190         intializationLatch.countDown();
191
192         LOGGER.debug("JavascriptExecutor {} executing ... ", subjectKey.getId());
193
194         // Take jobs from the execution queue of the worker and execute them
195         while (!executorThread.isInterrupted()) {
196             try {
197                 Object contextObject = executionQueue.take();
198                 if (contextObject == null) {
199                     break;
200                 }
201
202                 resultQueue.add(executeScript(contextObject));
203             } catch (final InterruptedException e) {
204                 LOGGER.debug("execution was interruped for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
205                 resultQueue.add(false);
206                 Thread.currentThread().interrupt();
207             } catch (StateMachineException sme) {
208                 executorException.set(sme);
209                 resultQueue.add(false);
210             }
211         }
212
213         try {
214             Context.exit();
215         } catch (final Exception e) {
216             executorException.set(new StateMachineException(
217                 "executor close failed to close for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e));
218         }
219
220         shutdownLatch.countDown();
221
222         LOGGER.debug("JavascriptExecutor {} completed processing", subjectKey.getId());
223     }
224
225     private void initExecutor() throws StateMachineException {
226         try {
227             // Create a Javascript context for this thread
228             javascriptContext = Context.enter();
229
230             // Set up the default values of the context
231             javascriptContext.setOptimizationLevel(DEFAULT_OPTIMIZATION_LEVEL);
232             javascriptContext.setLanguageVersion(Context.VERSION_1_8);
233
234             script = javascriptContext.compileString(javascriptCode, subjectKey.getId(), 1, null);
235         } catch (Exception e) {
236             Context.exit();
237             throw new StateMachineException(
238                 "logic failed to compile for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
239         }
240     }
241
242     private boolean executeScript(final Object executionContext) throws StateMachineException {
243         Object returnObject = null;
244
245         try {
246             // Pass the subject context to the Javascript engine
247             Scriptable javascriptScope = javascriptContext.initStandardObjects();
248             javascriptScope.put("executor", javascriptScope, executionContext);
249
250             // Run the script
251             returnObject = script.exec(javascriptContext, javascriptScope);
252         } catch (final Exception e) {
253             throw new StateMachineException(
254                 "logic failed to run for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
255         }
256
257         if (!(returnObject instanceof Boolean)) {
258             throw new StateMachineException(
259                 "execute: logic for " + subjectKey.getId() + " returned a non-boolean value " + returnObject);
260         }
261
262         return (boolean) returnObject;
263     }
264
265     private void clearAndThrowExecutorException() throws StateMachineException {
266         StateMachineException exceptionToThrow = executorException.getAndSet(null);
267         if (exceptionToThrow != null) {
268             throw exceptionToThrow;
269         }
270     }
271
272     protected Thread getExecutorThread() {
273         return executorThread;
274     }
275 }