This page defines the repository's current runtime contract for the Python-first execution path.
It is intentionally narrower than a full platform specification. The goal is to make the current behavior explicit enough for users, contributors, and release reviews.
The semantics below describe what the repository currently treats as the supported runtime model for:
runtime/polyglot-adapterruntime/polyglot-spring-boot-starter- Python bindings driven by
PyExecutor
JavaScript support remains narrower and is documented separately in runtime.md.
The default Python runtime convention is:
- Java interface name:
ForecastService - Python module name:
forecast_service.py - exported Python value:
ForecastService
This remains the primary, documented contract model for the project.
- Source cache entries are keyed by Java interface type.
- Source cache does not track file versions, checksums, or live source mutation.
- Script changes are not detected automatically after an interface has been resolved.
clearSourceCache()removes cachedSourceobjects. For PythonBY_METHOD_NAME, it also clears the executor's module-evaluated markers, so a later call re-evaluates those modules. It does not invalidate default-convention Python instances.
Implication:
- if the underlying script changes, callers must invalidate or reload the affected contract, or recreate the executor.
- Resolved Python targets are cached per Java interface type.
- Python targets are stored via weak references.
- A cached Python target may disappear after garbage collection.
- If a weakly cached target is reclaimed, the executor may recreate it on the next invocation.
Implication:
- the project guarantees reuse when the cached target is still reachable;
- it does not guarantee process-wide singleton lifecycle for exported Python objects.
- Preload is raw script evaluation only.
- Preload does not bind Java interfaces.
- Preload does not hydrate per-interface source or instance caches.
- Later binding may evaluate the same script again.
- Repeated script side effects remain possible when preload and later binding both evaluate the same source.
Implication:
- preload is a startup warmup/visibility feature, not a hot-reload or prebinding feature.
invalidateContractCache(iface)evicts source and Python instance cache entries for one contract.reloadContract(iface)evicts one contract and eagerly re-validates it.reloadContracts(...)performs the same operation for multiple contracts in one serialized operation.clearAllCaches()is a coarse reset, not a source-version management feature.- Python
BY_METHOD_NAMEevaluates a contract module once per Java contract and executor after a successful evaluation. Its module globals, including mutable state changed by calls, persist between calls. A failed evaluation is retried on the next call.clearSourceCache()permits all such modules to be evaluated again;invalidateContractCache(iface)orreloadContract(iface, Convention.BY_METHOD_NAME)permits one contract to be evaluated again. These operations do not roll back Python globals already created in the Context; re-evaluated module code runs in the existing language context.
Compatibility note: earlier versions re-evaluated the source on each BY_METHOD_NAME invocation.
Applications that relied on module-level side effects repeating on each call must move that work
into the Python function or explicitly invalidate/reload the contract. The default Python
export-based convention is unchanged.
reloadContract(iface, convention) reloads the logical source derived from the interface name,
not an arbitrary entry in Python's sys.modules. Existing proxies, including Spring clients,
continue to use the same executor. No bean recreation or Context replacement is required.
Reload is serialized with guest invocation, result conversion and shutdown. An in-flight call finishes first; the next call after a successful reload resolves the replacement. Reentrant reload from guest callbacks or result mappings is rejected. Calls after executor closure fail. If module initialization closes the executor through a host callback, reload fails and does not restore cache entries into that closed executor. Borrowed Context ownership is preserved.
The source is read afresh and parsed before caches or supported exports change. The selected contract's expected export (DEFAULT/BY_INTERFACE_EXPORT) or functions (BY_METHOD_NAME) are removed from both binding scopes before evaluation, so removed symbols cannot be satisfied by old code. A successful reload resets the selected DEFAULT instance and retains the new source/loaded marker. Other Java contract cache entries are untouched. Multiple proxies of the same interface observe the replacement. A batch reload is sequential under one monitor, not an all-or-nothing transaction.
If evaluation or contract validation fails, the previous supported binding references, Source, instance and loaded marker are restored. This is not a Python-state rollback. Module-level side effects, mutations of shared globals/objects, imports and external effects may already have occurred. If binding restoration itself fails, that failure is retained as a suppressed exception on the cause; an unusable or externally closed Context cannot provide recovery guarantees. The failure explicitly warns about this; recreate the executor when clean guest state is required. Missing sources and syntax errors are rejected before module execution. No business method is called for validation, but module initialization and exported constructors can execute. Validation checks export/callable existence, not Python argument arity, result types or business correctness. Signature/conversion incompatibilities retain ordinary invocation-time behavior; use the opt-in build-time contract validator for its supported static signature subset.
Module globals retain ordinary evaluation semantics: explicitly assigned globals are replaced; other globals and imported modules persist. Dependencies are not recursively reloaded. Contract names/functions share Context bindings: unrelated modules must not publish conflicting names; reload cannot provide namespace isolation. Different Java types with the same simple name resolve the same logical file but retain separate caches; reload each affected contract explicitly. Existing guest-backed Java views or Values already returned to callers are not rebound.
Cache invalidation remains lazy, not equivalent to explicit reload or transactional validation.
clearSourceCache() still does not reset DEFAULT instances. Engine-owned compiled-code caching
is separate and is neither cleared nor claimed to be purged by these operations. No filesystem
checks or reload-version checks are added to ordinary cached invocations.
- Executor access to the underlying GraalVM
Contextis serialized. - The current runtime model optimizes for safety of a shared executor instance.
- It does not provide high-throughput parallel execution on one shared context.
- It does not provide multi-version source isolation.
Implication:
- the runtime is safe-by-serialization, not designed as a parallel execution engine.
- Contexts created by executor factory methods such as
PyExecutor.create(...)are owned by the executor and are closed when the executor is closed. - The legacy
createWithContext(context, scriptSource)factories and public constructors preserve their historical behavior: the executor treats the caller-provided context as owned and closes it onclose(). - Callers that share or externally manage a context should use the overload that accepts
ContextOwnership.BORROWED. In that mode,close()closes the executor and releases its internal caches, but leaves the supplied context open for the caller or another executor. close()is idempotent. After an executor is closed, evaluation, binding, cache invalidation, and reload operations fail withIllegalStateException.
Sharing a caller-managed Engine across separate Contexts is opt-in through existing customizers; see the shared Engine recipe. Closing one executor-owned Context does not close an explicitly supplied Engine. The Engine owner closes it only after every Context using it is closed. Engine sharing does not merge guest globals, executor caches or Context locks, and does not change the default creation path. It is distinct from borrowing one Context.
validateBinding(...)is the authoritative eager binding check.- In the Spring starter,
polyglot.core.fail-fast=trueeagerly instantiates discovered@PolyglotClientbeans during startup. - Validation resolves scripts and exports without invoking bound business methods; evaluating a Python module and constructing an exported class can still run application initialization code.
- This runtime check is distinct from the Maven
validate-contractsgoal, which statically checks a documented Python subset without initializing GraalVM or executing Python. - Startup failure is expected behavior when documented binding requirements are not met.
GraalVM contexts created by PolyglotHelper use allowAllAccess(true). This grants the
guest-language environment unrestricted access to host Java types, host I/O, and host environment
variables.
This is only safe when the scripts being executed are trusted application code under the
operator's control. Running untrusted or user-supplied scripts with allowAllAccess(true) is
not safe and is not a supported use case for this project.
Implications:
- Do not embed guest code from untrusted sources without separate sandboxing outside the adapter.
- The adapter is designed for application-owned scripts deployed alongside the service, not for general-purpose script execution from external input.
- Python contexts additionally use a GraalPy virtual file system; this constrains physical filesystem access but does not restrict host API access.
Setting polyglot.python.safe-defaults=false results in a more minimal context and allows
providing a custom PolyglotContextCustomizer, but it does not change the allowAllAccess(true)
contract established by the default helper. Callers taking full control of context creation are
responsible for their own access policy.
Exceptions from guest-language execution propagate through the proxy method call to the caller.
The adapter defines its own exception hierarchy for structured error reporting:
ScriptNotFoundException: the requested script could not be found throughScriptSourceEvaluationException: the script could not be loaded (I/O failure)BindingException: the guest function or export could not be resolved or is not executableInvocationException: a runtime failure occurred during guest code execution or inline evaluation
PolyglotException thrown during bound invocation is wrapped as InvocationException.
This covers both proxy method calls (via bind(...)) and direct preloadScript(...) calls.
The original PolyglotException is preserved as the cause.
If the PolyglotException signals a thread interrupt (isInterrupted() == true), the adapter
re-sets the thread interrupt flag before throwing InvocationException, so callers that check
Thread.interrupted() will observe the interrupt. For bound calls, this also applies when Python
execution has already wrapped the guest exception in InvocationException, or GraalVM exposes a
host InterruptedException through PolyglotException.asHostException(). Existing exception
wrappers and causes are retained. The invocation monitor is released on failure; acquiring that
monitor remains non-interruptible, as with any Java synchronized block.
Explicit reload has separate failure semantics: missing sources throw ScriptNotFoundException;
syntax preflight preserves PolyglotException; evaluation/validation failure throws BindingException
with its original cause and the Python-side-effects warning. Interrupt status is preserved during
reload as well. These failures are unchecked; handling is recommended for resilience.
Proxy methods with void return type execute the guest function normally and return without
attempting result conversion. Any guest exception still propagates to the caller.
The current runtime contract does not guarantee:
- hostile sandboxing or restriction of guest access to host APIs
- automatic hot reload or transactional Python-state rollback
- source version tracking
- automatic pickup of changed guest-language code
- JavaScript parity with Python lifecycle behavior
- parallel throughput scaling from one shared executor instance