Class DefaultScriptEngineResolver

java.lang.Object
org.camunda.bpm.engine.impl.scripting.engine.DefaultScriptEngineResolver
All Implemented Interfaces:
ScriptEngineResolver

public class DefaultScriptEngineResolver extends Object implements ScriptEngineResolver
  • Field Details

    • scriptEngineManager

      protected final ScriptEngineManager scriptEngineManager
    • cachedEngines

      protected final Map<String,ScriptEngine> cachedEngines
    • threadLocalEngines

      protected final ThreadLocal<Map<String,ScriptEngine>> threadLocalEngines
      Per-thread cache for script engines that enforce thread affinity (e.g. GraalJS). GraalJS binds its Polyglot Context to the creating thread and rejects eval() calls from any other thread. A global cache is therefore unsafe. Instead, each thread gets its own engine instance, reused across multiple script task executions on that thread. This bounds the number of live engines to the size of the thread pool (e.g. Tomcat's HTTP connector) and prevents the per-instance memory leak seen in GraalJS 25.x.

      Container lifecycle note: ThreadLocal entries are retained by the application server's worker threads. Call clearThreadLocalResources() from each thread to release cached engines during engine shutdown / application undeploy. In typical Camunda deployments, the thread pool is destroyed together with the application, so this is only relevant for hot-redeploy scenarios.

      See Also:
    • threadLocalConfigSnapshots

      protected final ThreadLocal<Map<String,DefaultScriptEngineResolver.GraalJsConfigSnapshot>> threadLocalConfigSnapshots
      Stores the GraalJS configuration snapshot that was active when each thread-local engine was created. Used for staleness detection: if the current config no longer matches the snapshot, the cached engine must be discarded and recreated.

      We cannot read polyglot options back from the ScriptContext because GraalJS consumes them when building its internal polyglot context. Instead, we record what was configured at creation time.

  • Constructor Details

    • DefaultScriptEngineResolver

      public DefaultScriptEngineResolver(ScriptEngineManager scriptEngineManager)
  • Method Details

    • addScriptEngineFactory

      public void addScriptEngineFactory(ScriptEngineFactory scriptEngineFactory)
      Specified by:
      addScriptEngineFactory in interface ScriptEngineResolver
    • getScriptEngineManager

      public ScriptEngineManager getScriptEngineManager()
      Specified by:
      getScriptEngineManager in interface ScriptEngineResolver
    • getScriptEngine

      public ScriptEngine getScriptEngine(String language, boolean resolveFromCache)
      Returns a cached script engine or creates a new script engine if no such engine is currently cached.
      Specified by:
      getScriptEngine in interface ScriptEngineResolver
      Parameters:
      language - the language (such as 'groovy' for the script engine)
      Returns:
      the cached engine or null if no script engine can be created for the given language
    • getThreadLocalEngine

      protected ScriptEngine getThreadLocalEngine(String language)
      Returns a thread-local cached engine for the given language, or null if none is cached or the cache entry is stale. A ThreadLocal entry is considered stale when:
      • The JavaScript engine name has been reconfigured (e.g. via ProcessEngineConfiguration.setScriptEngineNameJavaScript()).
      • GraalJS-specific context options (allowIO, allowHostAccess, nashorn-compat) no longer match the current process engine configuration. These options are baked into the polyglot context at creation time and cannot be changed afterwards, so the cached engine must be discarded.
    • invalidateThreadLocalEntry

      protected void invalidateThreadLocalEntry(String cacheKey)
      Removes a stale entry from both the engine cache and the config snapshot cache.
    • clearThreadLocalResources

      public void clearThreadLocalResources()
      Releases all thread-local cached engines and config snapshots for the calling thread. This should be called during engine shutdown to prevent classloader leaks in container hot-redeploy scenarios where worker threads outlive the application.

      Since ThreadLocal values are per-thread, this method only clears the cache for the current thread. In application server environments, each worker thread that executed script tasks should call this method (e.g., via a shutdown listener or filter).

    • isJsEngineNameStale

      protected boolean isJsEngineNameStale(ScriptEngine cached, ProcessEngineConfigurationImpl config)
      Returns true if the cached engine's factory no longer matches the effective JavaScript engine name for the current configuration.

      The effective name is determined as follows:

      1. If scriptEngineNameJavaScript is explicitly configured, that name is used.
      2. Otherwise, if the default JavaScript engine (ScriptingEngines.DEFAULT_JS_SCRIPTING_LANGUAGE, i.e. GraalJS) is available on the classpath, that is the effective name — a cached engine from a previous explicit configuration that has since been reset to null is therefore correctly detected as stale.
      3. Otherwise (default engine unavailable — fallback path in getJavaScriptScriptEngine(java.lang.String) is in effect), the cached fallback engine is treated as not stale to prevent unnecessary recreation on every lookup.

      If config is null (no process engine context available), the entry is conservatively treated as not stale, mirroring the pre-existing behavior for this edge case.

    • isDefaultJsEngineAvailable

      protected boolean isDefaultJsEngineAvailable()
      Returns true if the default JavaScript engine (ScriptingEngines.DEFAULT_JS_SCRIPTING_LANGUAGE, i.e. GraalJS) appears in the JDK ServiceLoader/SPI metadata already visible to the ScriptEngineManager, without instantiating a new engine. Used by isJsEngineNameStale(javax.script.ScriptEngine, org.camunda.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl) to correctly distinguish "no explicit engine configured, use the default" from "default unavailable, fallback is in effect".
    • isGraalJsConfigStale

      protected boolean isGraalJsConfigStale(String language, ProcessEngineConfigurationImpl config)
      Checks whether the GraalJS-specific configuration has changed since the engine was cached. Compares the stored DefaultScriptEngineResolver.GraalJsConfigSnapshot against the current process engine configuration.

      We do NOT read polyglot attributes back from the ScriptContext because GraalJS consumes them when building its internal polyglot context. Instead, we compare against the snapshot recorded at creation time.

      Returns:
      true if the cached engine is stale and should be discarded.
    • captureGraalJsConfig

      protected DefaultScriptEngineResolver.GraalJsConfigSnapshot captureGraalJsConfig()
      Captures the current GraalJS-relevant configuration into an immutable snapshot.
    • isJavaScriptAlias

      protected boolean isJavaScriptAlias(String language)
    • normalizeLanguageKey

      protected String normalizeLanguageKey(String language)
      Returns a canonical cache key for the given language. JavaScript aliases ("javascript", "ecmascript") are normalized to ScriptingEngines.JAVASCRIPT_SCRIPTING_LANGUAGE so that both aliases resolve to the same thread-local engine instance, keeping the intended bound of one GraalJS engine per thread.
    • getScriptEngine

      protected ScriptEngine getScriptEngine(String language)
    • getJavaScriptScriptEngine

      protected ScriptEngine getJavaScriptScriptEngine(String language)
    • isCachable

      protected boolean isCachable(ScriptEngine scriptEngine)
      Allows checking whether the script engine can be cached.

      GraalJS reports THREADING = null via the JSR-223 factory parameter, which would normally prevent caching. However, GraalJS 25.x retains non-reclaimable native memory for every discarded ScriptEngine instance, causing a slow heap leak when the engine is re-created on every script task execution. Therefore GraalJS is explicitly allowed to be cached (per-thread, due to thread affinity) regardless of its THREADING parameter value.

      Parameters:
      scriptEngine - the script engine to check.
      Returns:
      true if the script engine may be cached.
    • isThreadAffine

      protected boolean isThreadAffine(ScriptEngine scriptEngine)
      Returns true if the script engine enforces thread affinity (i.e. its Polyglot Context is bound to the creating thread and rejects eval() from other threads). Such engines must be cached per-thread via threadLocalEngines instead of in the global cachedEngines map.

      Currently only GraalJS exhibits this behavior.

    • isGraalJs

      protected boolean isGraalJs(ScriptEngine scriptEngine)
    • configureScriptEngines

      protected void configureScriptEngines(String language, ScriptEngine scriptEngine)
    • configureGroovyScriptEngine

      protected void configureGroovyScriptEngine(ScriptEngine scriptEngine)
      Allows providing custom configuration for the groovy script engine.
      Parameters:
      scriptEngine - the groovy script engine to configure.
    • configureGraalJsScriptEngine

      protected void configureGraalJsScriptEngine(ScriptEngine scriptEngine)
      Allows providing custom configuration for the Graal JS script engine.
      Parameters:
      scriptEngine - the Graal JS script engine to configure.