Skip to content

Commit 4fcecf4

Browse files
authored
fix(jsonata): lower default maxDepth to prevent Windows worker crash (#79)
* fix(jsonata): prevent Windows worker crash on StackOverflowError in JSONata evaluation Root cause: Jsonata$Frame.lookup() traverses the frame parent chain recursively. With the previous default maxDepth=1000, a recursive JSONata function can create a 1000-deep frame chain, causing Frame.lookup() to recurse 1000 levels. Windows JVM default thread stack (~256 KB) overflows before the ~512 KB Linux default does. Reactor's throwIfFatal re-throws the Error, and ThreadUncaughtExceptionHandler shuts down the entire worker process. Three changes: 1. Catch StackOverflowError in evaluateExpression() and throw RuntimeException so the task fails gracefully instead of crashing the worker. 2. Re-parse the expression after catching to get a clean Jsonata instance. The Jsonata class has mutable fields (errors, environment) written during evaluate(); leaving a partially-modified instance would cause all subsequent evaluations on the same task instance (e.g. TransformItems batches) to fail immediately on the errors-list check at evaluate() entry. 3. Lower default maxDepth from 1000 to 200. This makes JSONata's own JException depth guard fire before the JVM stack overflows on Windows, while still supporting non-trivial recursive expressions. Users with proven deep-recursion needs can raise it explicitly in their flow YAML. * refactor(jsonata): drop StackOverflowError catch, rely on JException depth guard Catching StackOverflowError (a VirtualMachineError) is unsafe: the JVM gives no recovery guarantee, and allocating inside the handler (re-parsing the JSONata AST) can trigger further failures on a barely-unwound stack. The Dashjoin JSONata engine already exposes a deterministic depth guard via Frame.setRuntimeBounds(timeoutMillis, maxDepth). With maxDepth=200 (lowered in the prior commit), the engine throws a clean JException before the Windows 256 KB thread stack overflows — making the StackOverflowError catch both unsafe and unnecessary. Remove the catch block and the parsedExpressionSource field that was only needed to re-parse inside that handler.
1 parent c6a2b00 commit 4fcecf4

2 files changed

Lines changed: 5 additions & 2 deletions

File tree

plugin-transform-json/src/main/java/io/kestra/plugin/transform/jsonata/JSONataInterface.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ public interface JSONataInterface {
1212
@PluginProperty(group = "main")
1313
Property<String> getExpression();
1414

15-
@Schema(title = "The maximum number of recursive calls allowed for the JSONata transformation.")
15+
@Schema(
16+
title = "The maximum number of recursive calls allowed for the JSONata transformation.",
17+
description = "Limits recursive JSONata function call depth. Each recursive call adds a frame to the chain traversed by variable lookup, so high values can cause a JVM StackOverflowError on platforms with small default thread stacks (e.g. Windows ~256 KB vs Linux ~512 KB). Raise only for expressions with proven deep recursion needs."
18+
)
1619
@NotNull
1720
@PluginProperty(group = "main")
1821
Property<Integer> getMaxDepth();

plugin-transform-json/src/main/java/io/kestra/plugin/transform/jsonata/Transform.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public abstract class Transform<T extends Output> extends Task implements JSONat
3737
private Property<String> expression;
3838

3939
@Builder.Default
40-
private Property<Integer> maxDepth = Property.ofValue(1000);
40+
private Property<Integer> maxDepth = Property.ofValue(200);
4141

4242
@Getter(AccessLevel.PRIVATE)
4343
private Jsonata parsedExpression;

0 commit comments

Comments
 (0)