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