Skip to content

fix(jsonata): catch StackOverflowError to prevent worker crash on Windows#79

Open
fdelbrayelle wants to merge 2 commits intomainfrom
fix/jsonata-stackoverflow-crashes-windows-worker
Open

fix(jsonata): catch StackOverflowError to prevent worker crash on Windows#79
fdelbrayelle wants to merge 2 commits intomainfrom
fix/jsonata-stackoverflow-crashes-windows-worker

Conversation

@fdelbrayelle
Copy link
Copy Markdown
Member

@fdelbrayelle fdelbrayelle commented Apr 24, 2026

Fixes Pylon #1703

Root cause

Jsonata$Frame.lookup() traverses the frame parent chain recursively. With the old default maxDepth=1000, a recursive JSONata function can build a 1000-deep frame chain, causing lookup() to recurse 1000 levels. The Windows JVM default thread stack (~256 KB) overflows well before Linux (~512 KB) does. Reactor's throwIfFatal treats StackOverflowError as a JVM fatal, re-throws it, and ThreadUncaughtExceptionHandler shuts down the entire worker process.

What changed

1. Catch StackOverflowError → graceful task failure instead of worker crash

2. Re-parse expression after catchingJsonata has mutable instance fields (errors, environment) written during evaluate(). A partially-modified Jsonata instance would cause every subsequent evaluation on the same task (e.g. all remaining items in a TransformItems batch) to fail immediately on the errors list check at evaluate() entry. Re-parsing gives a clean instance.

3. Lower default maxDepth from 1000 → 200 — makes JSONata's own JException depth guard fire before the JVM stack overflows on Windows. Users who genuinely need deeper recursion can raise it explicitly in their flow YAML.

Breaking change

Flows using deeply recursive JSONata expressions (depth > 200) will now fail with JException instead of working. Those users must add maxDepth: <N> to their task. This is intentional — expressions that previously crashed Windows workers will now fail gracefully on all platforms.

Immediate workaround (before deploying this fix)

Add maxDepth: 100 to affected tasks in the flow YAML. This makes JSONata's depth guard fire before the JVM stack overflows.

Test plan

  • TransformValueTest and TransformItemsTest pass
  • Deploy to a Windows worker — flows run correctly, recursive expressions fail with clear error instead of crashing the service
  • Verify maxDepth can be raised in flow YAML for legitimate deep-recursion use cases

@fdelbrayelle fdelbrayelle self-assigned this Apr 24, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 2026

📦 Artifacts

Name Size Updated Expiration
jar 2.96 MB Apr 24, 26, 1:31:40 PM UTC May 1, 26, 1:31:39 PM UTC

🧪 Java Unit Tests

TestsPassed ✅Skipped ⚠️FailedTime ⏱
Java Tests Report136 ran135 ✅1 ⚠️0 ❌44s 154ms

🔁 Unreleased Commits

4 commits since v1.5.0

SHA Title Author Date
87fa379 docs: add/update AGENTS.md with clear Why/What François Delbrayelle Apr 18, 26, 7:14:01 AM UTC
253e91b docs: rewrite AGENTS.md Why section François Delbrayelle Apr 19, 26, 7:53:20 AM UTC
6e7c3ed docs: normalize README with Why and What François Delbrayelle Apr 19, 26, 8:12:42 AM UTC
c6a2b00 chore(deps): bump com.github.ben-manes.versions from 0.53.0 to 0.54.0 (#77) dependabot[bot] Apr 24, 26, 6:42:10 AM UTC

@fdelbrayelle fdelbrayelle force-pushed the fix/jsonata-stackoverflow-crashes-windows-worker branch from 4a22e96 to 2f13371 Compare April 24, 2026 13:05
…SONata 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.
@fdelbrayelle fdelbrayelle force-pushed the fix/jsonata-stackoverflow-crashes-windows-worker branch from 2f13371 to 23439c0 Compare April 24, 2026 13:06
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants