Runtime#
Responsibilities#
- Load and execute user-defined handler
- Process messages received from sidecar
- Return results to sidecar
- Handle errors gracefully
How It Works#
- Listen on Unix socket at
/var/run/asya/asya-runtime.sock - Receive message from sidecar
- Load user handler (function or class)
- Execute handler with payload
- Return result to sidecar
Deployment#
User defines container with Python code. The Crossplane composition renders asya_runtime.py into the pod:
containers:
- name: asya-runtime
image: my-handler:v1
command: ["python3", "/opt/asya/asya_runtime.py"] # Rendered by Crossplane
env:
- name: ASYA_HANDLER
value: "my_module.MyClass.process"
- name: ASYA_SOCKET_DIR
value: /var/run/asya # Rendered by Crossplane
volumeMounts:
- name: asya-runtime # ConfigMap mounted by Crossplane
mountPath: /opt/asya/asya_runtime.py
subPath: asya_runtime.py
readOnly: true
- name: socket-dir # Rendered by Crossplane
mountPath: /var/run/asya
Python Executable Resolution#
The Crossplane composition sets the runtime command to python3 /opt/asya/asya_runtime.py. The bare python3 is resolved via the container's PATH at runtime — the same mechanism that conda, virtualenv, and pyenv all use.
Most users do not need to configure anything. As long as python3 is on your container's PATH, it just works.
How Python is found#
- The Crossplane composition sets the runtime command to
["python3", "/opt/asya/asya_runtime.py"] - Kubernetes resolves
python3via the container'sPATHwhen starting the process - If
ASYA_PYTHONEXECUTABLEis set on the runtime container, its value replacespython3in the command
Standard approach: ensure python3 is on PATH#
This is how the Python ecosystem works — tools like conda, virtualenv, and pyenv all configure PATH so that python3 resolves to the right binary.
| Image type | python3 on PATH? |
Action needed |
|---|---|---|
python:3.x |
Yes (/usr/local/bin/python3) |
None |
pytorch/pytorch, tensorflow/tensorflow |
Yes (conda's python3) |
None |
| Conda image with activated env | Yes (/opt/conda/bin/python3) |
None |
Custom image with python3 installed |
Yes | None |
Minimal image with only python (no python3) |
No | Set ASYA_PYTHONEXECUTABLE |
Custom install without python3 symlink |
No | Set ASYA_PYTHONEXECUTABLE |
Last resort: ASYA_PYTHONEXECUTABLE#
If your container does not have a python3 binary on PATH, set ASYA_PYTHONEXECUTABLE to the full path of the Python binary:
containers:
- name: asya-runtime
image: my-custom-image:latest
env:
- name: ASYA_PYTHONEXECUTABLE
value: "/opt/conda/envs/inference/bin/python"
- name: ASYA_HANDLER
value: "ml_model.predict"
Python environment variables reference#
| Variable | Purpose | Set by Asya? |
|---|---|---|
PATH |
OS-level executable search path; python3 is resolved via this |
No — configured in your Dockerfile or container spec |
PYTHONPATH |
Tells Python where to find extra modules/packages | No — set it if your handler is not on the default module path |
PYTHONHOME |
Tells Python where its standard library is located | No — rarely needed, managed by conda/venv automatically |
VIRTUAL_ENV |
Indicates the active virtual environment path | No — informational, does not affect which binary runs |
ASYA_PYTHONEXECUTABLE |
Overrides the Python binary used to launch the runtime | Yes — only needed if python3 is not on PATH |
Quick decision guide#
- Standard Python image (
python:3.x,pytorch/pytorch, etc.) → do nothing - Conda environment → ensure the env is activated in your Dockerfile (
conda activatesetsPATH); if not, setASYA_PYTHONEXECUTABLE - Virtual environment → ensure the venv is activated in your Dockerfile; if not, set
ASYA_PYTHONEXECUTABLE - Image has
pythonbut notpython3→ setASYA_PYTHONEXECUTABLE=python - Custom handler import path → set
PYTHONPATH(separate from the executable)
Python Compatibility#
Requires Python 3.10+. The runtime uses modern type syntax (dict[str, Any], X | Y unions) and is tested on Python 3.13.
Async Support#
The runtime transparently supports async def handlers via asyncio.run(). Async is the preferred pattern for AI workloads where handlers make long-running async calls (LLM APIs, HTTP clients, database queries).
- Async handlers (
async def) are executed viaasyncio.run()automatically - Sync handlers (
def) continue to work unchanged with zero overhead - Detection uses
inspect.iscoroutinefunction()at call time - No configuration needed — the runtime auto-detects async vs sync
Sync handlers remain fully supported for backward compatibility.
Handler Types#
Function Handler#
Configuration: ASYA_HANDLER=module.function
Example (async, preferred for AI workloads):
# handler.py
async def process(payload: dict) -> dict:
result = await llm.generate(payload["prompt"])
return {"result": result}
Example (sync, still fully supported):
# handler.py
def process(payload: dict) -> dict:
return {"result": payload["value"] * 2}
Class Handler#
Configuration: ASYA_HANDLER=module.Class.method
Example:
# handler.py
class Processor:
def __init__(self, model_path: str = "/models/default"):
self.model = load_model(model_path) # Init once, always sync
async def process(self, payload: dict) -> dict:
return {"result": await self.model.predict(payload)}
Benefits: Stateful initialization (model loading, preprocessing setup)
Important: All __init__ parameters must have default values for zero-arg instantiation. __init__ is always synchronous — only the handler method can be async.
# ✅ Correct - all params have defaults
class Processor:
def __init__(self, model_path: str = "/models/default"):
self.model = load_model(model_path)
# ❌ Wrong - param without default
class Processor:
def __init__(self, model_path: str): # Missing default!
self.model = load_model(model_path)
Response Patterns#
Single Response#
return {"processed": True}
Sidecar creates one message, routes to next actor.
Fan-Out (Generator)#
async def process(payload):
for item in payload["items"]:
yield {"item": item}
Each yield produces a separate downstream envelope. Returning a list does NOT trigger fan-out — it is treated as a single payload value.
Abort#
return None
Sidecar routes message to x-sink (no more processing).
Error#
raise ValueError("Invalid input")
Runtime catches exception, creates error response with detailed traceback:
[{
"error": "processing_error",
"details": {
"message": "Invalid input",
"type": "ValueError",
"traceback": "Traceback (most recent call last):\n File ..."
}
}]
Error codes:
processing_error: Handler exception (any unhandled error)msg_parsing_error: Invalid JSON or message structureconnection_error: Socket/network issues
Sidecar receives error response and applies resiliency policy: retries if configured, or routes to x-sink (phase: failed) if exhausted or non-retryable. x-sink then dispatches through hooks to x-sump.
asya_runtime.py via ConfigMap#
Source: src/asya-runtime/asya_runtime.py (single file, no dependencies)
Deployment:
1. Crossplane chart embeds asya_runtime.py in a ConfigMap
2. Crossplane composition mounts the ConfigMap into actor pods at /opt/asya/asya_runtime.py
Readiness Probe#
Runtime signals readiness via separate mechanism:
readinessProbe:
exec:
command: ["sh", "-c", "test -S /var/run/asya/asya-runtime.sock && test -f /var/run/asya/runtime-ready"]
Runtime creates /var/run/asya/runtime-ready file after handler initialization.
Configuration#
| Variable | Default | Description |
|---|---|---|
ASYA_HANDLER |
(required) | Handler path (module.Class.method) |
ASYA_PYTHONEXECUTABLE |
python3 |
Python binary path for launching the runtime |
ASYA_SOCKET_DIR |
/var/run/asya |
Unix socket directory (internal testing only) |
ASYA_SOCKET_NAME |
asya-runtime.sock |
Socket filename (internal testing only) |
ASYA_SOCKET_CHMOD |
0o666 |
Socket permissions in octal (empty = skip chmod) |
ASYA_CHUNK_SIZE |
65536 |
Socket read chunk size in bytes |
ASYA_LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
Note: ASYA_SOCKET_DIR and ASYA_SOCKET_NAME are for internal testing only. DO NOT set in production - socket path is managed by the Crossplane composition.
Examples#
Data processing (async):
async def process(payload: dict) -> dict:
data = await fetch_data(payload["id"])
return {**payload, "data": data}
AI inference (async class handler):
class LLMInference:
def __init__(self):
self.model = load_llm("/models/llama3") # Init is always sync
async def process(self, payload: dict) -> dict:
response = await self.model.generate(payload["prompt"])
return {**payload, "response": response}
Simple sync handler (still supported):
def process(payload: dict) -> dict:
return {"result": payload["value"] * 2}