489489ff0aa7e6a105c264f3ff7a4139adcd7aaa
[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 lombok.AccessLevel;
30 import lombok.Getter;
31 import lombok.NonNull;
32 import lombok.Setter;
33
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;
42
43 /**
44  * The Class JavascriptExecutor is the executor for task logic written in Javascript.
45  *
46  * @author Liam Fallon (liam.fallon@ericsson.com)
47  */
48 public class JavascriptExecutor implements Runnable {
49     private static final XLogger LOGGER = XLoggerFactory.getXLogger(JavascriptExecutor.class);
50
51     public static final int DEFAULT_OPTIMIZATION_LEVEL = 9;
52
53     // Token passed to executor thread to stop execution
54     private static final Object STOP_EXECUTION_TOKEN = "*** STOP EXECUTION ***";
55
56     // Recurring string constants
57     private static final String WITH_MESSAGE = " with message: ";
58     private static final String JAVASCRIPT_EXECUTOR = "JavascriptExecutor ";
59
60     @Setter(AccessLevel.PROTECTED)
61     private static TimeUnit timeunit4Latches = TimeUnit.SECONDS;
62     @Setter(AccessLevel.PROTECTED)
63     private static int intializationLatchTimeout = 60;
64     @Setter(AccessLevel.PROTECTED)
65     private static int cleanupLatchTimeout = 60;
66
67     // The key of the subject that wants to execute Javascript code
68     final AxKey subjectKey;
69
70     private String javascriptCode;
71     private Context javascriptContext;
72     private Script script;
73
74     private final BlockingQueue<Object> executionQueue = new LinkedBlockingQueue<>();
75     private final BlockingQueue<Boolean> resultQueue = new LinkedBlockingQueue<>();
76
77     @Getter(AccessLevel.PROTECTED)
78     private Thread executorThread;
79     private CountDownLatch intializationLatch;
80     private CountDownLatch cleanupLatch;
81     private AtomicReference<StateMachineException> executorException = new AtomicReference<>(null);
82
83     /**
84      * Initializes the Javascript executor.
85      *
86      * @param subjectKey the key of the subject that is requesting Javascript execution
87      */
88     public JavascriptExecutor(final AxKey subjectKey) {
89         this.subjectKey = subjectKey;
90     }
91
92     /**
93      * Prepares the executor for processing and compiles the Javascript code.
94      *
95      * @param javascriptCode the Javascript code to execute
96      * @throws StateMachineException thrown when instantiation of the executor fails
97      */
98     public synchronized void init(@NonNull final String javascriptCode) throws StateMachineException {
99         LOGGER.debug("JavascriptExecutor {} starting ... ", subjectKey.getId());
100
101         if (executorThread != null) {
102             throw new StateMachineException("initiation failed, executor " + subjectKey.getId()
103                 + " already initialized, run cleanUp to clear executor");
104         }
105
106         if (StringUtils.isBlank(javascriptCode)) {
107             throw new StateMachineException("initiation failed, no logic specified for executor " + subjectKey.getId());
108         }
109
110         this.javascriptCode = javascriptCode;
111
112         executorThread = new Thread(this);
113         executorThread.setName(this.getClass().getSimpleName() + ":" + subjectKey.getId());
114         intializationLatch = new CountDownLatch(1);
115         cleanupLatch = new CountDownLatch(1);
116
117         try {
118             executorThread.start();
119         } catch (IllegalThreadStateException e) {
120             throw new StateMachineException("initiation failed, executor " + subjectKey.getId() + " failed to start",
121                 e);
122         }
123
124         try {
125             if (!intializationLatch.await(intializationLatchTimeout, timeunit4Latches)) {
126                 executorThread.interrupt();
127                 throw new StateMachineException(JAVASCRIPT_EXECUTOR + subjectKey.getId()
128                     + " initiation timed out after " + intializationLatchTimeout + " " + timeunit4Latches);
129             }
130         } catch (InterruptedException e) {
131             LOGGER.debug("JavascriptExecutor {} interrupted on execution thread startup", subjectKey.getId(), e);
132             Thread.currentThread().interrupt();
133         }
134
135         checkAndThrowExecutorException();
136
137         LOGGER.debug("JavascriptExecutor {} started ... ", subjectKey.getId());
138     }
139
140     /**
141      * Execute a Javascript script.
142      *
143      * @param executionContext the execution context to use for script execution
144      * @return true if execution was successful, false otherwise
145      * @throws StateMachineException on execution errors
146      */
147     public synchronized boolean execute(final Object executionContext) throws StateMachineException {
148         if (executorThread == null) {
149             throw new StateMachineException("execution failed, executor " + subjectKey.getId() + " is not initialized");
150         }
151
152         if (!executorThread.isAlive()) {
153             throw new StateMachineException("execution failed, executor " + subjectKey.getId()
154                 + " is not running, run cleanUp to clear executor and init to restart executor");
155         }
156
157         executionQueue.add(executionContext);
158
159         boolean result = false;
160
161         try {
162             result = resultQueue.take();
163         } catch (final InterruptedException e) {
164             executorThread.interrupt();
165             Thread.currentThread().interrupt();
166             throw new StateMachineException(
167                 JAVASCRIPT_EXECUTOR + subjectKey.getId() + "interrupted on execution result wait", e);
168         }
169
170         checkAndThrowExecutorException();
171
172         return result;
173     }
174
175     /**
176      * Cleans up the executor after processing.
177      *
178      * @throws StateMachineException thrown when cleanup of the executor fails
179      */
180     public synchronized void cleanUp() throws StateMachineException {
181         if (executorThread == null) {
182             throw new StateMachineException("cleanup failed, executor " + subjectKey.getId() + " is not initialized");
183         }
184
185         if (executorThread.isAlive()) {
186             executionQueue.add(STOP_EXECUTION_TOKEN);
187
188             try {
189                 if (!cleanupLatch.await(cleanupLatchTimeout, timeunit4Latches)) {
190                     executorException.set(new StateMachineException(JAVASCRIPT_EXECUTOR + subjectKey.getId()
191                         + " cleanup timed out after " + cleanupLatchTimeout + " " + timeunit4Latches));
192                 }
193             } catch (InterruptedException e) {
194                 LOGGER.debug("JavascriptExecutor {} interrupted on execution cleanup wait", subjectKey.getId(), e);
195                 Thread.currentThread().interrupt();
196             }
197         }
198
199         executorThread = null;
200         executionQueue.clear();
201         resultQueue.clear();
202
203         checkAndThrowExecutorException();
204     }
205
206     @Override
207     public void run() {
208         LOGGER.debug("JavascriptExecutor {} initializing ... ", subjectKey.getId());
209
210         try {
211             initExecutor();
212         } catch (StateMachineException sme) {
213             LOGGER.warn("JavascriptExecutor {} initialization failed", subjectKey.getId(), sme);
214             executorException.set(sme);
215             intializationLatch.countDown();
216             cleanupLatch.countDown();
217             return;
218         }
219
220         intializationLatch.countDown();
221
222         LOGGER.debug("JavascriptExecutor {} executing ... ", subjectKey.getId());
223
224         // Take jobs from the execution queue of the worker and execute them
225         while (!Thread.currentThread().isInterrupted()) {
226             try {
227                 Object contextObject = executionQueue.take();
228                 if (STOP_EXECUTION_TOKEN.equals(contextObject)) {
229                     LOGGER.debug("execution close was ordered for  " + subjectKey.getId());
230                     break;
231                 }
232                 resultQueue.add(executeScript(contextObject));
233             } catch (final InterruptedException e) {
234                 LOGGER.debug("execution was interruped for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
235                 executionQueue.add(STOP_EXECUTION_TOKEN);
236                 Thread.currentThread().interrupt();
237             } catch (StateMachineException sme) {
238                 executorException.set(sme);
239                 resultQueue.add(false);
240             }
241         }
242
243         resultQueue.add(false);
244
245         try {
246             Context.exit();
247         } catch (final Exception e) {
248             executorException.set(new StateMachineException(
249                 "executor close failed to close for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e));
250         }
251
252         cleanupLatch.countDown();
253
254         LOGGER.debug("JavascriptExecutor {} completed processing", subjectKey.getId());
255     }
256
257     private void initExecutor() throws StateMachineException {
258         try {
259             // Create a Javascript context for this thread
260             javascriptContext = Context.enter();
261
262             // Set up the default values of the context
263             javascriptContext.setOptimizationLevel(DEFAULT_OPTIMIZATION_LEVEL);
264             javascriptContext.setLanguageVersion(Context.VERSION_1_8);
265
266             script = javascriptContext.compileString(javascriptCode, subjectKey.getId(), 1, null);
267         } catch (Exception e) {
268             Context.exit();
269             throw new StateMachineException(
270                 "logic failed to compile for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
271         }
272     }
273
274     private boolean executeScript(final Object executionContext) throws StateMachineException {
275         Object returnObject = null;
276
277         try {
278             // Pass the subject context to the Javascript engine
279             Scriptable javascriptScope = javascriptContext.initStandardObjects();
280             javascriptScope.put("executor", javascriptScope, executionContext);
281
282             // Run the script
283             returnObject = script.exec(javascriptContext, javascriptScope);
284         } catch (final Exception e) {
285             throw new StateMachineException(
286                 "logic failed to run for " + subjectKey.getId() + WITH_MESSAGE + e.getMessage(), e);
287         }
288
289         if (!(returnObject instanceof Boolean)) {
290             throw new StateMachineException(
291                 "execute: logic for " + subjectKey.getId() + " returned a non-boolean value " + returnObject);
292         }
293
294         return (boolean) returnObject;
295     }
296
297     private void checkAndThrowExecutorException() throws StateMachineException {
298         StateMachineException exceptionToThrow = executorException.getAndSet(null);
299         if (exceptionToThrow != null) {
300             throw exceptionToThrow;
301         }
302     }
303 }