Skip to content

Latest commit

 

History

History
236 lines (178 loc) · 12.8 KB

runtime-semantics.md

File metadata and controls

236 lines (178 loc) · 12.8 KB

Runtime Semantics

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.

Scope

The semantics below describe what the repository currently treats as the supported runtime model for:

  • runtime/polyglot-adapter
  • runtime/polyglot-spring-boot-starter
  • Python bindings driven by PyExecutor

JavaScript support remains narrower and is documented separately in runtime.md.

Contract Naming

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 Semantics

  • 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 cached Source objects. For Python BY_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.

Python Instance Cache Semantics

  • 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.

Evaluation and Binding Semantics

  • 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.

Reload and Invalidation

  • 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_NAME evaluates 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) or reloadContract(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.

Explicit reload consistency

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.

Concurrency Model

  • Executor access to the underlying GraalVM Context is 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.

Context Ownership And Close Semantics

  • 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 on close().
  • 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 with IllegalStateException.

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.

Fail-Fast Semantics

  • validateBinding(...) is the authoritative eager binding check.
  • In the Spring starter, polyglot.core.fail-fast=true eagerly instantiates discovered @PolyglotClient beans 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-contracts goal, 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.

Security Contract

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.

Exception Contract

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 through ScriptSource
  • EvaluationException: the script could not be loaded (I/O failure)
  • BindingException: the guest function or export could not be resolved or is not executable
  • InvocationException: 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.

Explicit Non-Goals

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