Skip to content

Commit 0d92616

Browse files
committed
fix: create contexts directly on the calling thread to prevent thread-local issues
1 parent 023a9a2 commit 0d92616

6 files changed

Lines changed: 104 additions & 120 deletions

File tree

polyglot/src/main/java/org/restheart/polyglot/ContextQueue.java

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,10 @@ public ContextQueue(Engine engine, String name, Configuration conf, Logger logge
8282
this.modulesReplacements = modulesReplacements;
8383
this.OPTS = OPTS;
8484

85-
// Pre-populate pool: each Context must be created on the same dedicated
86-
// platform thread that will later enter it (see PolyglotThreadUtils).
85+
// Pre-populate pool: create Contexts directly on the calling thread.
86+
// During startup this is the JVM main thread (platform thread).
8787
for (var c = 0;c < POOL_SIZE;c++) {
88-
try {
89-
pool.offer(PolyglotThreadUtils.onPlatformThread(
90-
() -> newContext(engine, name, conf, logger, mclient, modulesReplacements, OPTS)));
91-
} catch (Exception e) {
92-
throw new IllegalStateException("Error pre-creating polyglot context", e);
93-
}
88+
pool.offer(newContext(engine, name, conf, logger, mclient, modulesReplacements, OPTS));
9489
}
9590
}
9691

polyglot/src/main/java/org/restheart/polyglot/JSPlugin.java

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,11 @@ public abstract class JSPlugin {
3939

4040
static {
4141
try {
42-
// ALL Truffle operations (Engine.create, Context build, enter, eval,
43-
// leave) must happen on the very same dedicated platform thread to
44-
// avoid DefaultContextThreadLocal cross-thread corruption
45-
// (see PolyglotThreadUtils / oracle/graal#7520).
46-
//
47-
// Must be a plain method reference to a method on a DIFFERENT class:
48-
// a lambda body here would become a private synthetic method of
49-
// JSPlugin, and invoking it from the platform thread would require
50-
// JSPlugin's own <clinit> to finish, deadlocking this thread.
51-
engine = PolyglotThreadUtils.onPlatformThread(PolyglotThreadUtils::createEngine);
42+
// Engine.create() runs directly on the calling thread. During
43+
// startup this is the JVM main thread (a genuine platform thread),
44+
// so no executor dispatch is needed. Must be a plain method
45+
// reference to a method on a DIFFERENT class to avoid deadlock.
46+
engine = PolyglotClassloaderHelper.withPluginsClassloaderResult(Engine::create);
5247
} catch (Exception e) {
5348
throw new IllegalStateException("Error creating polyglot Engine", e);
5449
}

polyglot/src/main/java/org/restheart/polyglot/PolyglotDeployer.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -353,12 +353,10 @@ private List<Path> findDeclaredPlugins(final Path path, final String prop, final
353353
if (checkPluginFiles) {
354354
if (Files.isRegularFile(pluginPath)) {
355355
try {
356-
// Source.findLanguage() touches Truffle internals;
357-
// this lambda may run on a virtual thread (file watcher),
358-
// so dispatch to a platform thread (see PolyglotThreadUtils)
359-
final var language = PolyglotThreadUtils.onPlatformThread(
360-
() -> PolyglotClassloaderHelper.withPluginsClassloaderResult(
361-
() -> Source.findLanguage(pluginPath.toFile())));
356+
// Source.findLanguage() runs directly on the calling thread.
357+
// During startup this is the JVM main thread (platform thread).
358+
final var language = PolyglotClassloaderHelper.withPluginsClassloaderResult(
359+
() -> Source.findLanguage(pluginPath.toFile()));
362360
if ("js".equals(language)) {
363361
ret.add(pluginPath);
364362
} else {

polyglot/src/main/java/org/restheart/polyglot/interceptors/JSInterceptorFactory.java

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,8 @@ public JSInterceptorFactory(Optional<MongoClient> mclient, Configuration config)
9292
LOGGER.debug("Enabling require for interceptor {} with require-cwd {} ", pluginPath, requireCwdPath);
9393
}
9494

95-
// Source.findLanguage() and all Context/Value operations must run on a
96-
// platform thread (see PolyglotThreadUtils). The whole block is dispatched
97-
// as a single task so eval() and subsequent Value member access happen on
98-
// the very same thread that entered the context.
99-
try {
100-
return PolyglotThreadUtils.onPlatformThread(() -> {
95+
// During startup the calling thread is the JVM main thread (a genuine
96+
// platform thread), so no executor dispatch is needed.
10197

10298
// check that the plugin script is js (use PluginsClassloader so js-language is visible)
10399
final var language = PolyglotClassloaderHelper.withPluginsClassloaderResult(
@@ -112,6 +108,7 @@ public JSInterceptorFactory(Optional<MongoClient> mclient, Configuration config)
112108
var sindexPath = pluginPath.toUri().toString();
113109
LOGGER.debug("Resolved interceptor path: {}", sindexPath);
114110

111+
try {
115112
var ctx = ContextQueue.newContext(engine, "foo", config, LOGGER, mclient, "", contextOptions);
116113
ctx.enter();
117114
try {
@@ -359,15 +356,11 @@ public JSInterceptorFactory(Optional<MongoClient> mclient, Configuration config)
359356
ctx.leave();
360357
ctx.close();
361358
}
362-
});
363-
} catch (RuntimeException re) {
364-
throw re;
365-
} catch (IOException ioe) {
366-
throw ioe;
367-
} catch (InterruptedException ie) {
368-
throw ie;
369-
} catch (Exception e) {
370-
throw new IllegalStateException("Error evaluating js interceptor " + pluginPath.toAbsolutePath(), e);
359+
} catch (Throwable t) {
360+
// DIAGNOSTIC: log full stack trace including PolyglotException chain
361+
LOGGER.error("DIAGNOSTIC: full exception chain for {} [thread={}, class={}]:",
362+
pluginPath, Thread.currentThread().getName(), t.getClass().getName(), t);
363+
throw t;
371364
}
372365
}
373366

polyglot/src/main/java/org/restheart/polyglot/services/JSStringService.java

Lines changed: 75 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -96,88 +96,83 @@ private static JSServiceArgs args(Path pluginPath, Optional<MongoClient> mclient
9696
LOGGER.trace("Enabling require for service {} with require-cwd {} ", pluginPath, requireCwdPath);
9797
}
9898

99-
// Context creation, eval and all Value member access must happen on the very
100-
// same platform thread: a Value returned by eval() is only safely readable on
101-
// the thread the context is entered on (see PolyglotThreadUtils), so the whole
102-
// block below -- not just the eval() calls -- is dispatched as a single task.
99+
// During startup the calling thread is the JVM main thread (a genuine
100+
// platform thread), so no executor dispatch is needed.
103101
try {
104-
return PolyglotThreadUtils.onPlatformThread(() -> {
105-
var ctx = ContextQueue.newContext(engine(), "foo", config, LOGGER, mclient, "", contextOptions);
106-
ctx.enter();
107-
try {
108-
// check that the plugin script is js (use PluginsClassloader so js-language is visible)
109-
final var language = PolyglotClassloaderHelper.withPluginsClassloaderResult(
110-
() -> Source.findLanguage(pluginPath.toFile()));
111-
112-
if (!"js".equals(language)) {
113-
throw new IllegalArgumentException("wrong js plugin, not javascript");
114-
}
115-
116-
var sindexPath = pluginPath.toUri().toString();
117-
LOGGER.debug("Resolved plugin path for import: {}", sindexPath);
118-
var optionsScript = "import { options } from '" + sindexPath + "'; options;";
119-
var optionsSource = Source.newBuilder(language, optionsScript, "optionsScript").mimeType("application/javascript+module").build();
120-
121-
Value options;
122-
123-
try {
124-
options = ctx.eval(optionsSource);
125-
} catch (Throwable t) {
126-
if (t.getMessage() != null && t.getMessage().contains("Cannot load CommonJS module")) {
127-
throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ": " + t.getMessage());
128-
} else if (t.getMessage() != null && t.getMessage().contains("Access to host class")) {
129-
throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ": " + t.getMessage());
130-
} else {
131-
throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ": " + t.getMessage() + ", " + PACKAGE_HINT);
132-
}
133-
}
134-
135-
checkOptions(options, pluginPath);
136-
137-
var name = options.getMember("name").asString();
138-
var description = options.getMember("description").asString();
139-
var uri = options.getMember("uri").asString();
140-
var secured = !options.getMemberKeys().contains("secured") ? false : options.getMember("secured").asBoolean();
141-
var matchPolicy = !options.getMemberKeys().contains("matchPolicy") ? MATCH_POLICY.PREFIX : MATCH_POLICY.valueOf(options.getMember("matchPolicy").asString());
142-
String modulesReplacements = null;
143-
144-
if (options.getMemberKeys().contains("modulesReplacements")) {
145-
var sb = new StringBuilder();
146-
147-
options.getMember("modulesReplacements").getMemberKeys().stream()
148-
.forEach(k -> sb.append(k).append(":")
149-
.append(options.getMember("modulesReplacements").getMember(k))
150-
.append(","));
151-
152-
modulesReplacements = sb.toString();
153-
}
154-
155-
// ******** evaluate and check handle
156-
var _handleScript = "import { handle } from '" + sindexPath + "'; handle;";
157-
var handleSource = Source.newBuilder(language, _handleScript, "handleScript").mimeType("application/javascript+module").build();
158-
159-
Value handle;
160-
161-
try {
162-
handle = ctx.eval(handleSource);
163-
} catch (Throwable t) {
164-
throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ", " + t.getMessage());
165-
}
166-
167-
checkHandle(handle, pluginPath);
168-
169-
return new JSServiceArgs(name, description, uri, secured, modulesReplacements, matchPolicy, handleSource, config, mclient, contextOptions);
170-
} finally {
171-
ctx.leave();
172-
ctx.close();
102+
var ctx = ContextQueue.newContext(engine(), "foo", config, LOGGER, mclient, "", contextOptions);
103+
ctx.enter();
104+
try {
105+
// check that the plugin script is js (use PluginsClassloader so js-language is visible)
106+
final var language = PolyglotClassloaderHelper.withPluginsClassloaderResult(
107+
() -> Source.findLanguage(pluginPath.toFile()));
108+
109+
if (!"js".equals(language)) {
110+
throw new IllegalArgumentException("wrong js plugin, not javascript");
111+
}
112+
113+
var sindexPath = pluginPath.toUri().toString();
114+
LOGGER.debug("Resolved plugin path for import: {}", sindexPath);
115+
var optionsScript = "import { options } from '" + sindexPath + "'; options;";
116+
var optionsSource = Source.newBuilder(language, optionsScript, "optionsScript").mimeType("application/javascript+module").build();
117+
118+
Value options;
119+
120+
try {
121+
options = ctx.eval(optionsSource);
122+
} catch (Throwable t) {
123+
if (t.getMessage() != null && t.getMessage().contains("Cannot load CommonJS module")) {
124+
throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ": " + t.getMessage());
125+
} else if (t.getMessage() != null && t.getMessage().contains("Access to host class")) {
126+
throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ": " + t.getMessage());
127+
} else {
128+
throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ": " + t.getMessage() + ", " + PACKAGE_HINT);
173129
}
174-
});
175-
} catch (RuntimeException re) {
176-
throw re;
177-
} catch (IOException ioe) {
178-
throw ioe;
179-
} catch (Exception e) {
180-
throw new IllegalStateException("Error evaluating js service " + pluginPath.toAbsolutePath(), e);
130+
}
131+
132+
checkOptions(options, pluginPath);
133+
134+
var name = options.getMember("name").asString();
135+
var description = options.getMember("description").asString();
136+
var uri = options.getMember("uri").asString();
137+
var secured = !options.getMemberKeys().contains("secured") ? false : options.getMember("secured").asBoolean();
138+
var matchPolicy = !options.getMemberKeys().contains("matchPolicy") ? MATCH_POLICY.PREFIX : MATCH_POLICY.valueOf(options.getMember("matchPolicy").asString());
139+
String modulesReplacements = null;
140+
141+
if (options.getMemberKeys().contains("modulesReplacements")) {
142+
var sb = new StringBuilder();
143+
144+
options.getMember("modulesReplacements").getMemberKeys().stream()
145+
.forEach(k -> sb.append(k).append(":")
146+
.append(options.getMember("modulesReplacements").getMember(k))
147+
.append(","));
148+
149+
modulesReplacements = sb.toString();
150+
}
151+
152+
// ******** evaluate and check handle
153+
var _handleScript = "import { handle } from '" + sindexPath + "'; handle;";
154+
var handleSource = Source.newBuilder(language, _handleScript, "handleScript").mimeType("application/javascript+module").build();
155+
156+
Value handle;
157+
158+
try {
159+
handle = ctx.eval(handleSource);
160+
} catch (Throwable t) {
161+
throw new IllegalArgumentException("wrong js service " + pluginPath.toAbsolutePath() + ", " + t.getMessage());
162+
}
163+
164+
checkHandle(handle, pluginPath);
165+
166+
return new JSServiceArgs(name, description, uri, secured, modulesReplacements, matchPolicy, handleSource, config, mclient, contextOptions);
167+
} finally {
168+
ctx.leave();
169+
ctx.close();
170+
}
171+
} catch (Throwable t) {
172+
// DIAGNOSTIC: log full stack trace including PolyglotException chain
173+
LOGGER.error("DIAGNOSTIC: full exception chain for {} [thread={}, class={}]:",
174+
pluginPath, Thread.currentThread().getName(), t.getClass().getName(), t);
175+
throw t;
181176
}
182177
}
183178

polyglot/src/main/resources/META-INF/native-image/org.restheart/restheart-polyglot/reachability-metadata.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,20 @@
22
"reflection": [
33
{
44
"type": "org.restheart.polyglot.JSPlugin",
5+
"fields": [
6+
{
7+
"name": "name"
8+
}
9+
]
10+
},
11+
{
12+
"type": "org.restheart.polyglot.interceptors.JSInterceptor",
513
"fields": [
614
{
715
"name": "interceptPoint"
816
},
917
{
10-
"name": "name"
18+
"name": "pluginClass"
1119
}
1220
]
1321
},

0 commit comments

Comments
 (0)