Skip to content

Commit 9ed2ec2

Browse files
committed
Add support for RunTask.script (JavaScript) task
Signed-off-by: Matheus Cruz <matheuscruz.dev@gmail.com>
1 parent 8a44a70 commit 9ed2ec2

File tree

14 files changed

+594
-1
lines changed

14 files changed

+594
-1
lines changed

impl/core/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,15 @@
2424
<groupId>de.huxhorn.sulky</groupId>
2525
<artifactId>de.huxhorn.sulky.ulid</artifactId>
2626
</dependency>
27+
<dependency>
28+
<groupId>org.graalvm.js</groupId>
29+
<artifactId>js</artifactId>
30+
<type>pom</type>
31+
</dependency>
32+
<dependency>
33+
<groupId>org.graalvm.sdk</groupId>
34+
<artifactId>graal-sdk</artifactId>
35+
<type>pom</type>
36+
</dependency>
2737
</dependencies>
2838
</project>
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
/*
2+
* Copyright 2020-Present The Serverless Workflow Specification Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.serverlessworkflow.impl.executors;
17+
18+
import io.serverlessworkflow.api.types.RunScript;
19+
import io.serverlessworkflow.api.types.RunTaskConfiguration;
20+
import io.serverlessworkflow.api.types.Script;
21+
import io.serverlessworkflow.api.types.ScriptUnion;
22+
import io.serverlessworkflow.impl.TaskContext;
23+
import io.serverlessworkflow.impl.WorkflowApplication;
24+
import io.serverlessworkflow.impl.WorkflowContext;
25+
import io.serverlessworkflow.impl.WorkflowDefinition;
26+
import io.serverlessworkflow.impl.WorkflowError;
27+
import io.serverlessworkflow.impl.WorkflowException;
28+
import io.serverlessworkflow.impl.WorkflowModel;
29+
import io.serverlessworkflow.impl.WorkflowModelFactory;
30+
import io.serverlessworkflow.impl.WorkflowUtils;
31+
import io.serverlessworkflow.impl.expressions.ExpressionUtils;
32+
import io.serverlessworkflow.impl.resources.ResourceLoaderUtils;
33+
import java.io.ByteArrayOutputStream;
34+
import java.util.Arrays;
35+
import java.util.HashMap;
36+
import java.util.Map;
37+
import java.util.concurrent.CompletableFuture;
38+
import java.util.concurrent.ExecutorService;
39+
import org.graalvm.polyglot.Context;
40+
import org.graalvm.polyglot.PolyglotException;
41+
import org.graalvm.polyglot.Value;
42+
43+
public class RunScriptExecutor implements RunnableTask<RunScript> {
44+
45+
enum ScriptLanguage {
46+
JS("js"),
47+
PYTHON("python");
48+
49+
private final String lang;
50+
51+
ScriptLanguage(String lang) {
52+
this.lang = lang;
53+
}
54+
55+
public String getLang() {
56+
return lang;
57+
}
58+
59+
public static boolean isSupported(String lang) {
60+
for (ScriptLanguage l : ScriptLanguage.values()) {
61+
if (l.getLang().equalsIgnoreCase(lang)) {
62+
return true;
63+
}
64+
}
65+
return false;
66+
}
67+
}
68+
69+
@FunctionalInterface
70+
private interface FnExecutor {
71+
WorkflowModel apply(WorkflowContext workflowContext, TaskContext taskContext);
72+
}
73+
74+
private FnExecutor fnExecutor;
75+
76+
@Override
77+
public void init(RunScript taskConfiguration, WorkflowDefinition definition) {
78+
ScriptUnion scriptUnion = taskConfiguration.getScript();
79+
Script script = scriptUnion.get();
80+
String language = script.getLanguage();
81+
boolean isAwait = taskConfiguration.isAwait();
82+
83+
WorkflowApplication application = definition.application();
84+
if (language == null || !ScriptLanguage.isSupported(language)) {
85+
throw new IllegalArgumentException(
86+
"Unsupported script language: "
87+
+ language
88+
+ ". Supported languages are: "
89+
+ Arrays.toString(
90+
Arrays.stream(ScriptLanguage.values()).map(ScriptLanguage::getLang).toArray()));
91+
}
92+
93+
fnExecutor =
94+
(workflowContext, taskContext) -> {
95+
String source;
96+
if (scriptUnion.getInlineScript() != null) {
97+
source = scriptUnion.getInlineScript().getCode();
98+
} else if (scriptUnion.getExternalScript() == null) {
99+
throw new WorkflowException(
100+
WorkflowError.runtime(
101+
taskContext, new IllegalStateException("No script source defined."))
102+
.build());
103+
} else {
104+
source =
105+
definition
106+
.resourceLoader()
107+
.load(
108+
scriptUnion.getExternalScript().getSource(),
109+
ResourceLoaderUtils::readString,
110+
workflowContext,
111+
taskContext,
112+
taskContext.input());
113+
}
114+
115+
ByteArrayOutputStream stderr = new ByteArrayOutputStream();
116+
ByteArrayOutputStream stdout = new ByteArrayOutputStream();
117+
118+
String lowerLang = language.toLowerCase();
119+
120+
Map<String, String> envs = new HashMap<>();
121+
if (script.getEnvironment() != null) {
122+
for (Map.Entry<String, Object> entry :
123+
script.getEnvironment().getAdditionalProperties().entrySet()) {
124+
String value =
125+
ExpressionUtils.isExpr(entry.getValue())
126+
? WorkflowUtils.buildStringResolver(
127+
application,
128+
entry.getValue().toString(),
129+
taskContext.input().asJavaObject())
130+
.apply(workflowContext, taskContext, taskContext.input())
131+
: entry.getValue().toString();
132+
envs.put(entry.getKey(), value);
133+
}
134+
}
135+
136+
Map<String, Object> args = new HashMap<>();
137+
if (script.getArguments() != null) {
138+
for (Map.Entry<String, Object> entry :
139+
script.getArguments().getAdditionalProperties().entrySet()) {
140+
String value =
141+
ExpressionUtils.isExpr(entry.getValue())
142+
? WorkflowUtils.buildStringResolver(
143+
application,
144+
entry.getValue().toString(),
145+
taskContext.input().asJavaObject())
146+
.apply(workflowContext, taskContext, taskContext.input())
147+
: entry.getValue().toString();
148+
args.put(entry.getKey(), value);
149+
}
150+
}
151+
152+
try (Context context =
153+
Context.newBuilder(lowerLang)
154+
.err(stderr)
155+
.out(stdout)
156+
.environment(envs)
157+
.useSystemExit(true)
158+
.option(
159+
"engine.WarnInterpreterOnly",
160+
"false") // disabling it due to warnings in stderr
161+
.build()) {
162+
163+
ExecutorService executorService = application.executorService();
164+
165+
args.forEach(
166+
(arg, val) -> {
167+
context.getBindings(lowerLang).putMember(arg, val);
168+
});
169+
170+
// configure process.env for js environment variables
171+
if (language.equalsIgnoreCase(ScriptLanguage.JS.lang)) {
172+
configureProcessEnv(context, envs);
173+
}
174+
175+
if (!isAwait) {
176+
executorService.submit(
177+
() -> {
178+
context.eval(scriptUnion.getInlineScript().getLanguage(), source);
179+
});
180+
return application.modelFactory().fromAny(taskContext.input());
181+
}
182+
183+
context.eval(lowerLang, source);
184+
185+
WorkflowModelFactory modelFactory = application.modelFactory();
186+
187+
// GraalVM does not provide exit code, assuming 0 for successful execution
188+
int statusCode = 0;
189+
190+
return switch (taskConfiguration.getReturn()) {
191+
case ALL ->
192+
modelFactory.fromAny(
193+
new ProcessResult(statusCode, stdout.toString(), stderr.toString()));
194+
case NONE -> modelFactory.fromNull();
195+
case CODE -> modelFactory.from(statusCode);
196+
case STDOUT -> modelFactory.from(stdout.toString().trim());
197+
case STDERR -> modelFactory.from(stderr.toString().trim());
198+
};
199+
} catch (PolyglotException e) {
200+
if (e.getExitStatus() != 0 || e.isSyntaxError()) {
201+
throw new WorkflowException(WorkflowError.runtime(taskContext, e).build());
202+
} else {
203+
WorkflowModelFactory modelFactory = definition.application().modelFactory();
204+
return switch (taskConfiguration.getReturn()) {
205+
case ALL ->
206+
modelFactory.fromAny(
207+
new ProcessResult(
208+
e.getExitStatus(),
209+
stdout.toString().trim(),
210+
getStderrMessage(e, stderr)));
211+
case NONE -> modelFactory.fromNull();
212+
case CODE -> modelFactory.from(e.getExitStatus());
213+
case STDOUT -> modelFactory.from(stdout.toString().trim());
214+
case STDERR -> modelFactory.from(getStderrMessage(e, stderr));
215+
};
216+
}
217+
}
218+
};
219+
}
220+
221+
/**
222+
* Gets the stderr message from the PolyglotException or the stderr stream.
223+
*
224+
* @param e the {@link PolyglotException} thrown during script execution
225+
* @param stderr the stderr stream
226+
* @return the stderr message
227+
*/
228+
private String getStderrMessage(PolyglotException e, ByteArrayOutputStream stderr) {
229+
String err = stderr.toString();
230+
return err.isBlank() ? e.getMessage() : err.trim();
231+
}
232+
233+
/**
234+
* Configures the process.env object in the JavaScript context with the provided environment
235+
* variables.
236+
*
237+
* @param context the GraalVM context
238+
* @param envs the environment variables to set
239+
*/
240+
private void configureProcessEnv(Context context, Map<String, String> envs) {
241+
String js = ScriptLanguage.JS.lang;
242+
Value bindings = context.getBindings(js);
243+
Value process = context.eval(js, "({ env: {} })");
244+
245+
for (var entry : envs.entrySet()) {
246+
process.getMember("env").putMember(entry.getKey(), entry.getValue());
247+
}
248+
bindings.putMember("process", process);
249+
}
250+
251+
@Override
252+
public CompletableFuture<WorkflowModel> apply(
253+
WorkflowContext workflowContext, TaskContext taskContext, WorkflowModel input) {
254+
return CompletableFuture.supplyAsync(() -> this.fnExecutor.apply(workflowContext, taskContext));
255+
}
256+
257+
@Override
258+
public boolean accept(Class<? extends RunTaskConfiguration> clazz) {
259+
return RunScript.class.equals(clazz);
260+
}
261+
}
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
io.serverlessworkflow.impl.executors.RunWorkflowExecutor
2-
io.serverlessworkflow.impl.executors.RunShellExecutor
2+
io.serverlessworkflow.impl.executors.RunShellExecutor
3+
io.serverlessworkflow.impl.executors.RunScriptExecutor

impl/pom.xml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<version.jakarta.ws.rs>4.0.0</version.jakarta.ws.rs>
1515
<version.net.thisptr>1.6.0</version.net.thisptr>
1616
<version.org.glassfish.jersey>3.1.11</version.org.glassfish.jersey>
17+
<version.org.graalvm.sdk>24.1.1</version.org.graalvm.sdk>
1718
</properties>
1819
<dependencyManagement>
1920
<dependencies>
@@ -92,6 +93,18 @@
9293
<artifactId>serverlessworkflow-impl-openapi</artifactId>
9394
<version>${project.version}</version>
9495
</dependency>
96+
<dependency>
97+
<groupId>org.graalvm.sdk</groupId>
98+
<artifactId>graal-sdk</artifactId>
99+
<version>${version.org.graalvm.sdk}</version>
100+
<type>pom</type>
101+
</dependency>
102+
<dependency>
103+
<groupId>org.graalvm.js</groupId>
104+
<artifactId>js</artifactId>
105+
<version>${version.org.graalvm.sdk}</version>
106+
<type>pom</type>
107+
</dependency>
95108
<dependency>
96109
<groupId>net.thisptr</groupId>
97110
<artifactId>jackson-jq</artifactId>

0 commit comments

Comments
 (0)