Background
Issue #663 required routing all Truffle operations through a single dedicated platform thread ("RH JS PLT") to work around DefaultContextThreadLocal.fastGet() throwing ArrayIndexOutOfBoundsException: Index -1 when virtual threads are used (oracle/graal#7520).
This serializes all JS plugin execution onto one thread, which reduces parallelism under concurrent load.
Upstream dependency
What to monitor
- Truffle CHANGELOG for any mention of virtual thread support (
GR-40931 was partial support in 24.1)
- GraalVM release notes for
DefaultContextThreadLocal changes
- Whether
truffle-api changes its fastGet() to handle virtual thread IDs correctly
How to test if upstream fix works
When a new GraalVM version ships with the fix:
-
Update graalvm.version in root pom.xml to the new version
-
Run the existing regression tests (they should still pass):
./mvnw test -pl polyglot -DskipUpdateLicense=true
Key tests:
ContextQueueVirtualThreadTest — creates/enters/evals Context from virtual threads
ContextLifecycleTest.pooledContextWorksFromVirtualThread — pooled Context from virtual thread
ContextLifecycleTest.concurrentVirtualThreadsUseSameContextQueue — 20 concurrent virtual threads
-
Disable the platform thread workaround and verify tests still pass:
./mvnw test -pl polyglot -DskipUpdateLicense=true \
-Drestheart.polyglot.force-platform-threads=false
All 28 tests must pass with force-platform-threads=false. If they do, Truffle's virtual thread support is working.
-
Docker end-to-end test with the workaround disabled:
docker run -d -p 8080:8080 \
-e JAVA_TOOL_OPTIONS="-Drestheart.polyglot.force-platform-threads=false" \
-v ./examples/js-plugin:/opt/restheart/plugins/js-plugin \
softinstigate/restheart-snapshot:latest
curl http://localhost:8080/hello
# Expected: {"msg":"Hello World! from Italy with Love","note":"..."}
Both services and interceptors must work.
-
Performance test: compare JS plugin throughput with force-platform-threads=true (current) vs false (virtual threads). With false, concurrent JS requests should execute in parallel rather than serializing.
What to change when removing the workaround
If upstream fix is confirmed, these changes can be reverted/simplified:
| File |
What to change |
PolyglotThreadUtils.java |
Remove getPlatformExecutor(), isAlreadyOnPlatformThread(). Simplify onPlatformThread() to always call directly. Set FORCE_PLATFORM default to false |
JSPlugin.java |
Simplify engine() to direct Engine.create() (no lazy init, no platform thread dispatch) |
ContextQueue.java |
Constructor creates pool directly (no onPlatformThread). newContext() restores addBindings() call. Remove populatePool() helper |
JSStringService.java |
Remove onPlatformThreadIO() wrapper. Restore Source.findLanguage() if useful |
JSInterceptorFactory.java |
Same as JSStringService |
Performance implications of the workaround
With force-platform-threads=true (current):
- All JS plugin execution serializes on one
"RH JS PLT" thread
- Under concurrent load, JS latency grows linearly with concurrency
- REST request handling (non-JS) continues on virtual threads — no impact
With force-platform-threads=false (after upstream fix):
- JS plugin execution runs on virtual threads in parallel
- Full concurrency restored
- No platform thread overhead
Decision criteria
Remove the workaround when ALL of these are true:
Background
Issue #663 required routing all Truffle operations through a single dedicated platform thread (
"RH JS PLT") to work aroundDefaultContextThreadLocal.fastGet()throwingArrayIndexOutOfBoundsException: Index -1when virtual threads are used (oracle/graal#7520).This serializes all JS plugin execution onto one thread, which reduces parallelism under concurrent load.
Upstream dependency
What to monitor
GR-40931was partial support in 24.1)DefaultContextThreadLocalchangestruffle-apichanges itsfastGet()to handle virtual thread IDs correctlyHow to test if upstream fix works
When a new GraalVM version ships with the fix:
Update
graalvm.versionin rootpom.xmlto the new versionRun the existing regression tests (they should still pass):
./mvnw test -pl polyglot -DskipUpdateLicense=trueKey tests:
ContextQueueVirtualThreadTest— creates/enters/evals Context from virtual threadsContextLifecycleTest.pooledContextWorksFromVirtualThread— pooled Context from virtual threadContextLifecycleTest.concurrentVirtualThreadsUseSameContextQueue— 20 concurrent virtual threadsDisable the platform thread workaround and verify tests still pass:
./mvnw test -pl polyglot -DskipUpdateLicense=true \ -Drestheart.polyglot.force-platform-threads=falseAll 28 tests must pass with
force-platform-threads=false. If they do, Truffle's virtual thread support is working.Docker end-to-end test with the workaround disabled:
Both services and interceptors must work.
Performance test: compare JS plugin throughput with
force-platform-threads=true(current) vsfalse(virtual threads). Withfalse, concurrent JS requests should execute in parallel rather than serializing.What to change when removing the workaround
If upstream fix is confirmed, these changes can be reverted/simplified:
PolyglotThreadUtils.javagetPlatformExecutor(),isAlreadyOnPlatformThread(). SimplifyonPlatformThread()to always call directly. SetFORCE_PLATFORMdefault tofalseJSPlugin.javaengine()to directEngine.create()(no lazy init, no platform thread dispatch)ContextQueue.javaonPlatformThread).newContext()restoresaddBindings()call. RemovepopulatePool()helperJSStringService.javaonPlatformThreadIO()wrapper. RestoreSource.findLanguage()if usefulJSInterceptorFactory.javaPerformance implications of the workaround
With
force-platform-threads=true(current):"RH JS PLT"threadWith
force-platform-threads=false(after upstream fix):Decision criteria
Remove the workaround when ALL of these are true:
force-platform-threads=falseforce-platform-threads=falseArrayIndexOutOfBoundsExceptioninDefaultContextThreadLocal.fastGet()