Skip to content

[SCXML-290] Make JSEvaluator.initGlobalsScript volatile - #396

Open
akashchamp wants to merge 1 commit into
apache:masterfrom
akashchamp:SCXML-290-initGlobalsScript-visibility
Open

akashchamp wants to merge 1 commit into
apache:masterfrom
akashchamp:SCXML-290-initGlobalsScript-visibility

Conversation

@akashchamp

Copy link
Copy Markdown

Fixes SCXML-290: https://issues.apache.org/jira/browse/SCXML-290

Problem

JSEvaluator#initGlobalsScript is a private static String that caches
the contents of the init_global.js classpath resource. It is written
exactly once, inside the per-instance synchronized initEngine() method:

protected synchronized void initEngine() {
    if (engine == null) {
        engine = new ScriptEngineManager().getEngineByName("JavaScript");
        if (initGlobalsScript == null) {
            initGlobalsScript = IOUtils.toString(...);
        }
    }
}

but it is read without any synchronization elsewhere, e.g. in
getScriptContext():

getEngine().eval(initGlobalsScript, scriptContext);

initEngine() synchronizes on the individual JSEvaluator instance
(this). Separate JSEvaluator instances (one per SCXML instance, per
the class's own Javadoc) are routinely constructed and used concurrently
by different threads, so the write on one instance's monitor and a read
via another instance's getScriptContext()/initEngine() do not share a
common monitor. The Java Memory Model therefore does not guarantee that a
thread will observe the script content published by another thread's
initEngine() call, and may see a stale or null value.

Fix

Mark initGlobalsScript volatile. This establishes the required
happens-before edge between any write and any subsequent read of the
field, regardless of which instance or monitor performed the write,
closing the visibility gap directly at its source without changing the
existing per-instance locking structure.

Tests

Added JSEvaluatorTest#testConcurrentEvaluatorInitialization, which
constructs and evaluates 32 JSEvaluator instances concurrently from a
thread pool, exercising the previously-racy lazy-initialization path
under real thread contention and failing loudly on any exception or
incorrect result.

Note: a Java Memory Model visibility bug like this one is inherently
non-deterministic to reproduce as a hard test failure in a single JVM run
(it depends on the JIT, CPU cache behavior, and scheduling, none of which
a portable unit test controls). The added test provides concurrency
coverage of the affected code path but cannot itself prove the pre-fix
race; the fix is otherwise a direct, minimal application of the standard
JMM remedy for this exact pattern (write under one lock, read without a
shared lock).

Verification

  • mvn clean verify — BUILD SUCCESS, 241 tests run, 0 failures, 0 errors, 0 skipped (includes RAT license check and Jacoco coverage check).
  • mvn test -Dtest=JSEvaluatorTest — 12/12 tests pass (11 existing + 1 new).

  • Read the contribution guidelines for this project.
  • Read the ASF Generative Tooling Guidance if you use Artificial Intelligence (AI).
  • I used AI to create any part of, or all of, this pull request. Which AI tool was used to create this pull request, and to what extent did it contribute? — Claude (Anthropic), model Claude Sonnet 5, operated under my direction end-to-end: root-cause analysis, the code and test changes, and this description. I reviewed the diff and ran the verification above myself before opening this PR.
  • Run a successful build using the default Maven goal with mvn; that's mvn on the command line by itself.
  • Write unit tests that match behavioral changes, where the tests fail if the changes to the runtime are not applied. This may not always be possible, but it is a best practice. (See the Tests/Verification notes above on the limits of testing a JMM visibility bug deterministically.)
  • Write a pull request description that is detailed enough to understand what the pull request does, how, and why.
  • Each commit in the pull request should have a meaningful subject line and body. Note that a maintainer may squash commits during the merge process.

JSEvaluator#initGlobalsScript is a private static String that caches the
contents of the init_global.js classpath resource. It is written exactly
once, inside the per-instance synchronized initEngine() method, but is
read without any synchronization from getScriptContext() (and by every
other JSEvaluator instance's initEngine() null-check).

Because initEngine() synchronizes on the individual JSEvaluator instance
(this), and separate instances are commonly constructed concurrently
(one per SCXML instance), the write to the static field and a read of it
on another instance do not share a common monitor. The JMM therefore does
not guarantee that a thread will observe the script content published by
another thread's initEngine() call; it may observe a stale or null value.

Marking the field volatile establishes the required happens-before edge
between any write and any subsequent read, independent of which instance
or monitor performed the write, fixing the visibility gap directly at its
source.

Also adds a JSEvaluatorTest that constructs and evaluates many JSEvaluator
instances concurrently, exercising the previously-racy initialization path
under real thread contention.

Generated-by: Claude Sonnet 5 -- https://www.apache.org/legal/generative-tooling.html

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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.

1 participant