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