Skip to content

Commit c965e37

Browse files
committed
Merge branch '9.x-polyglot-fix' into 9.x
fix: route all Truffle operations through dedicated platform thread (oracle/graal#7520) Resolves merge conflicts by taking the fix version for all polyglot files, superseding obsolete commits after 9.7.0. Closes #663 See #665
2 parents b34134e + cca9d0e commit c965e37

10 files changed

Lines changed: 654 additions & 67 deletions

File tree

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

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

85-
// Pre-populate pool
85+
// Pre-populate pool on the dedicated platform thread. Each Context
86+
// is created, entered, bound, left and returned to the pool — all on
87+
// the same thread that will be used at runtime, to avoid
88+
// DefaultContextThreadLocal index corruption (oracle/graal#7520).
89+
//
90+
// If already on the platform thread (e.g. called from within a
91+
// JSInterceptorFactory.create lambda), create directly to avoid
92+
// self-deadlock on the single-threaded executor.
93+
if (PolyglotThreadUtils.isAlreadyOnPlatformThread()) {
94+
populatePool(engine, name, conf, logger, mclient, modulesReplacements);
95+
} else {
96+
try {
97+
PolyglotThreadUtils.onPlatformThread(() -> {
98+
populatePool(engine, name, conf, logger, mclient, modulesReplacements);
99+
return null;
100+
});
101+
} catch (Exception e) {
102+
throw new IllegalStateException("Error pre-populating polyglot context pool", e);
103+
}
104+
}
105+
}
106+
107+
private void populatePool(Engine engine, String name, Configuration conf, Logger logger, Optional<MongoClient> mclient, String modulesReplacements) {
86108
for (var c = 0;c < POOL_SIZE;c++) {
87-
pool.offer(newContext());
109+
var ctx = newContext(engine, name, conf, logger, mclient, modulesReplacements, OPTS);
110+
ctx.enter();
111+
try {
112+
addBindings(ctx, name, conf, logger, mclient);
113+
} finally {
114+
ctx.leave();
115+
}
116+
pool.offer(ctx);
88117
}
89118
}
90119

@@ -110,9 +139,16 @@ private Context acquire() {
110139
*/
111140
private void release(Context ctx) {
112141
if (!pool.offer(ctx)) {
113-
// Pool is full, close the context
114-
ctx.close();
115-
LOGGER.debug("Pool full, closed excess context");
142+
try {
143+
// Context.close() touches thread locals, must run on a platform thread, see PolyglotThreadUtils
144+
PolyglotThreadUtils.onPlatformThread(() -> {
145+
ctx.close();
146+
return null;
147+
});
148+
LOGGER.debug("Pool full, closed excess context");
149+
} catch (Exception e) {
150+
LOGGER.warn("Error closing excess context", e);
151+
}
116152
}
117153
}
118154

@@ -127,14 +163,23 @@ private void release(Context ctx) {
127163
* @throws Exception if the task throws an exception
128164
*/
129165
public <T> T executeWithContext(ContextTask<T> task) throws Exception {
130-
Context ctx = acquire();
131-
ctx.enter();
132-
try {
133-
return task.run(ctx);
134-
} finally {
135-
ctx.leave();
136-
release(ctx);
137-
}
166+
// acquire/enter/task/leave/release must all happen on the very same
167+
// platform thread: if the pool is empty, acquire() calls newContext()
168+
// which creates a Context that must be entered on the same thread
169+
// (see PolyglotThreadUtils / oracle/graal#7520).
170+
return PolyglotThreadUtils.onPlatformThread(() -> {
171+
Context ctx = acquire();
172+
try {
173+
ctx.enter();
174+
try {
175+
return task.run(ctx);
176+
} finally {
177+
ctx.leave();
178+
}
179+
} finally {
180+
release(ctx);
181+
}
182+
});
138183
}
139184

140185
/**
@@ -144,14 +189,20 @@ public <T> T executeWithContext(ContextTask<T> task) throws Exception {
144189
* @throws Exception if the task throws an exception
145190
*/
146191
public void executeWithContext(VoidContextTask task) throws Exception {
147-
Context ctx = acquire();
148-
ctx.enter();
149-
try {
150-
task.run(ctx);
151-
} finally {
152-
ctx.leave();
153-
release(ctx);
154-
}
192+
PolyglotThreadUtils.onPlatformThread(() -> {
193+
Context ctx = acquire();
194+
try {
195+
ctx.enter();
196+
try {
197+
task.run(ctx);
198+
} finally {
199+
ctx.leave();
200+
}
201+
} finally {
202+
release(ctx);
203+
}
204+
return null;
205+
});
155206
}
156207

157208
/**
@@ -228,12 +279,18 @@ public static Context newContext(Engine engine, String name, Configuration conf,
228279
.options(OPTS)
229280
.build();
230281

231-
addBindings(ctx, name, conf, logger, mclient);
232-
282+
// NOTE: addBindings() is NOT called here. It requires ctx.enter(),
283+
// and a second enter()/leave() cycle before the caller's own
284+
// enter() corrupts Truffle's DefaultContextThreadLocal (oracle/graal#7520).
285+
// Callers must call addBindings() AFTER entering the context.
233286
return ctx;
234287
}
235288

236-
private static void addBindings(Context ctx,
289+
/**
290+
* Adds default bindings (LOGGER, mclient, pluginArgs) to an already-entered
291+
* context. Must only be called between ctx.enter() and ctx.leave().
292+
*/
293+
public static void addBindings(Context ctx,
237294
String pluginName,
238295
Configuration conf,
239296
Logger logger,

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,34 @@
3535
public abstract class JSPlugin {
3636
protected static final Logger LOGGER = LoggerFactory.getLogger(JSPlugin.class);
3737

38-
private static final Engine engine = Engine.create();
38+
private static volatile Engine engine;
39+
40+
/**
41+
* Returns the shared polyglot Engine, creating it on the dedicated
42+
* platform thread on first access. Lazy initialization avoids the
43+
* deadlock that would occur if we created the Engine in a static
44+
* initializer (the main thread holds the class-init lock while
45+
* waiting for the platform thread).
46+
*/
47+
public static Engine engine() {
48+
if (engine == null) {
49+
synchronized (JSPlugin.class) {
50+
if (engine == null) {
51+
try {
52+
if (PolyglotThreadUtils.isAlreadyOnPlatformThread()) {
53+
engine = PolyglotClassloaderHelper.withPluginsClassloaderResult(Engine::create);
54+
} else {
55+
engine = PolyglotThreadUtils.onPlatformThread(
56+
() -> PolyglotClassloaderHelper.withPluginsClassloaderResult(Engine::create));
57+
}
58+
} catch (Exception e) {
59+
throw new IllegalStateException("Error creating polyglot Engine", e);
60+
}
61+
}
62+
}
63+
}
64+
return engine;
65+
}
3966

4067
private final String modulesReplacements;
4168
private final Source handleSource;
@@ -116,7 +143,4 @@ public Configuration configuration() {
116143
return configuration;
117144
}
118145

119-
public static Engine engine() {
120-
return engine;
121-
}
122146
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ private PolyglotClassloaderHelper() {}
6161
* Returns the PluginsClassloader instance via reflection, or null if
6262
* unavailable.
6363
*/
64-
private static ClassLoader getPluginsClassloader() {
64+
public static ClassLoader getPluginsClassloader() {
6565
try {
6666
Class<?> pclClass = Class.forName(PCL_CLASS);
6767
var getInstance = pclClass.getMethod("getInstance");

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

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,9 @@
7878
* @author Andrea Di Cesare {@literal <andrea@softinstigate.com>}
7979
*/
8080
@RegisterPlugin(
81-
name = "polyglotDeployer",
82-
description = "handles GraalVM polyglot plugins",
83-
enabledByDefault = true)
81+
name = "polyglotDeployer",
82+
description = "handles GraalVM polyglot plugins",
83+
enabledByDefault = true)
8484
public class PolyglotDeployer implements Initializer {
8585

8686
private static final Logger LOGGER = LoggerFactory.getLogger(PolyglotDeployer.class);
@@ -352,17 +352,12 @@ private List<Path> findDeclaredPlugins(final Path path, final String prop, final
352352

353353
if (checkPluginFiles) {
354354
if (Files.isRegularFile(pluginPath)) {
355-
try {
356-
final var language = PolyglotClassloaderHelper.withPluginsClassloaderResult(
357-
() -> Source.findLanguage(pluginPath.toFile()));
358-
if ("js".equals(language)) {
359-
ret.add(pluginPath);
360-
} else {
361-
LOGGER.warn("{} is not javascript", pluginPath.toAbsolutePath());
362-
}
363-
} catch (final IOException e) {
364-
LOGGER.warn("{} is not javascript", pluginPath.toAbsolutePath(), e);
365-
}
355+
// Source.findLanguage() is NOT called here:
356+
// it triggers Truffle's language-discovery
357+
// internals which corrupt DefaultContextThread-
358+
// Local (oracle/graal#7520). Files are
359+
// already filtered to .mjs by the deployer.
360+
ret.add(pluginPath);
366361
} else {
367362
LOGGER.warn("pluging not found {}, it is declared in {}", pluginPath.toAbsolutePath(),
368363
packagePath.toAbsolutePath());
@@ -454,8 +449,8 @@ private void deployNodeService(final Path pluginPath) throws IOException {
454449
DEPLOYEES.put(pluginPath.toAbsolutePath(), srv);
455450

456451
LOGGER.info(ansi().fg(GREEN).a(
457-
"Service '{}' deployed at URI '{}' with description: '{}'. Secured: {}. Uri match policy: {}")
458-
.reset().toString(), srv.name(), srv.uri(), srv.getDescription(), srv.secured(),
452+
"Service '{}' deployed at URI '{}' with description: '{}'. Secured: {}. Uri match policy: {}")
453+
.reset().toString(), srv.name(), srv.uri(), srv.getDescription(), srv.secured(),
459454
srv.matchPolicy());
460455
} catch (IOException | InterruptedException | ExecutionException | TimeoutException ex) {
461456
LOGGER.error("Error deploying node service {}", pluginPath, ex);

0 commit comments

Comments
 (0)