python -m blockx_executor) in the Function Executor Pool local to the Worker. It receives ExecuteCall from the Worker, loads and runs the user function, translates the BlockDB / RPC / subfunction calls made by the user function into CallWaiting messages sent back to the Worker, and restores the original execution stack when ResumeCall arrives. See Architecture overview for where it sits in the system, and Call execution subsystem for its Worker-side counterpart (dispatcher, executor adapter, subfunctions).
Responsibilities and boundaries
Responsible for:- Receiving
ExecuteCall/ResumeCall/CancelCall/HeartbeatAckand emittingCallWaiting/CallCompleted/CallFailed/Heartbeatover a single long-lived UDS connection. - Compiling and caching function modules by source digest, isolating module namespaces per task, and fetching source from the Worker by digest when it is missing.
- Using
greenletto suspend synchronous-style user code at SDK call points and resume it afterResumeCall. - Running cancellation and deadline / budget checks at safe points, and preempting pure-CPU loops with a
SIGALRMtimer. - Reporting process metrics (RSS, CPU utilization, cumulative CPU seconds, call counts per state) every heartbeat interval.
- Deciding the call dispatch order or which Executor to pick (that is the Worker dispatcher’s job).
- Accessing BlockDB / node RPC / Function Code View directly. All SDK IO is intercepted and goes out through the Worker.
- Persisting or recovering any call across processes. After a process restart, all context is rebuilt.
- User code runs only in greenlets on the main thread, one at a time; the UDS reader / writer / heartbeat threads are separate and never switch greenlets.
- An
ExecContexthas at most one pendingrequestIdat any time; aResumeCallmust matchcallId, theresumeKindcorresponding to the state, andrequestIdall at once, otherwise it is dropped as stale. - A second
ExecuteCallfor the samecallIdis treated as a new attempt: the old greenlet is marked stale and terminated without emitting a terminal state; the new attempt gets a new greenlet. - Every discarded context emits exactly one terminal message (
CallCompletedorCallFailed), except on attempt replacement. - Compiled artifacts are shared across tasks by digest; exec’d module namespaces are isolated per
task_idand released when the task’s last call finishes.
Code location
blockx_executor module file list
blockx_executor module file list
Core types and interfaces
The
Provider protocol verbatim:
User function programming model
A function module is a piece of Python source whose entry point is a top-leveldef. The entry-name resolution rules are in resolve_entry_name in blockx_audit/tables.py: prefer the top-level def whose name matches the last segment of functionId, then def _ (or a _ = some_def alias), and finally fall back to the first public def. In practice, just write def _(...).
When ExecuteCall.payload.args is a JSON array it is unpacked positionally; otherwise it is passed as a single argument (_run_callable in runtime.py: target(*input_payload) if isinstance(input_payload, list) else target(input_payload)). The return value is row data directly: a dict is one row, a list[dict] is multiple rows, and None means no output (see Plugin system for details). Return values are serialized by uds_codec.dumps_wire: Python int values beyond 64 bits (e.g. uint256) can be returned directly, datetime / date become RFC 3339 strings, dataclasses are converted through asdict as a fallback (with a one-time warning), and other types such as Decimal are not serializable and must be converted to str first (which is why the example below uses str(total)).
Below is a real case from cmd/worker/worker_process_subcall_test.go. It has no import at all, because the unrestricted namespace is preloaded with tables.PRELUDE_IMPORTS (json, Decimal, datetime, date, blockdb.Table / BlockTable / TimeTable / Block, blockx.function, leafage.ChainState, and so on):
e2e/perf/s4_noderpc_batch_test.go:
The relationship between
blockx_sdk and blockx-py: blockx-py / blockdb-py / leafage-py are the SDKs users actually face, and they can also connect to the services directly outside the Executor; blockx_sdk is not a replacement for them but the seam inside the Executor process: blockx_sdk.runtime keeps the current Provider in contextvars, and blockx-py’s function.call lazily imports it when it detects the Executor environment. The early blockx_sdk.db / rpc / call_subfunc entry points now mainly appear in unit tests, Go-side E2E fixtures, and the load functions generated by cmd/bundle-loadgen.
When auditing is enabled (
ExecuteCall.payload.audit is present), the module is exec’d inside restricted_runtime.restricted_namespace: __builtins__ contains only SAFE_BUILTINS, import only yields facades of whitelisted modules, and blockx exposes only function and the capability classes. See docs/specs/function-code-python-whitelist.md in the blockx repo for which syntax and APIs are whitelisted.Data flow / execution flow
The main loop lives in__main__.py: while session.is_connected(): runtime.run_until_idle(); session.wait_for_message(timeout=runtime.next_scheduler_delay(None)). Each round of run_until_idle first wakes sleepers that are due, refreshes the connection state, and processes inbound messages, then takes one call_id from _sleep_ready (preferred, yielding after 64 consecutive picks) or _runnable and hands it to _step_context. _step_context has only three branches: if cancel_reason is set, _terminate_context; if resume_payload is set, switch(payload) back to the wait point; otherwise the first switch() enters _run_callable.
At startup, ExecutorRuntime.__init__ performs the one-time hook installation: install_global_sleep_patch(), _install_blockdb_bridge (blockdb’s mode.set_mock(False) + set_channel_factory), _install_noderpc_bridge, _install_localtest_bridge (blockx-py’s channel factory, used by the Python clients of capability backends), and sets BLOCKX_EXECUTOR_RUNTIME=1. SdkBridgeProvider is not installed globally once; instead set_provider runs on every entry into _run_callable and reset_provider on exit.
The SDK hooks combine two techniques:
blockx_sdk uses provider injection; blockdb-py and blockx-py are replaced at the gRPC channel layer through the set_channel_factory seam they expose; leafage-py is patched by replacing W3_DICT entries. There are no hooks at the socket layer.MessageEnvelope.binary_payload carries the raw protobuf / JSON-RPC bytes, and uds_codec.encode_frame packs them into a BXB1-tagged frame; the Worker’s ResumeCall reply also carries the response bytes in a sidecar, runtime._handle_inbound_message_typed puts them into result["Data"], and the bridge hands them to the gRPC stub for deserialization. blockdb-py additionally caches mode / channel / stub on its side. There is no Python-local L1 read cache and no protocol-level batching inside the Executor process.
State and lifecycle
The actual values ofExecState (context.py):
A few additional notes:
- On resume, the state does not first go back to
RUNNABLE:resume_callonly writesresume_payloadand enqueues withqueued=True, and_perform_waitswitches directly toRUNNINGafterswitch()returns. Whether a waiting context is already queued is expressed by thequeuedflag, not by the state. - A
DONEcontext is removed from_contextsimmediately, soinflight_count()is justlen(_contexts). WAITING_FUNCTION_CODEis used for fetching source by digest. Concurrent misses on the same digest are coalesced locally (singleflight) inSdkBridge._function_code_fetches; only the leader emitsCallWaiting(waitKind=function_code).
_replace_existing_attempt runs inside submit_call: the old context is marked stale, removed from the queues, and terminated with _terminate_context(..., suppress_terminal_emit=True); attempt_seq comes from ExecuteCall.payload.attemptSeq (default 1), not from a local counter. requestId has the form req-<processInstanceId>-<seq> (next_request_id); the process-instance-id prefix ensures a restarted process never collides with the old process’s pending requests, and seq increases monotonically within the process. Stale resumes / cancels increment _stale_resume_dropped / _stale_cancel_dropped respectively.
Cancellation. For WAITING_* states, cancel_call runs _terminate_context immediately (throw(CallCancelledError) into the greenlet, then GreenletExit); for RUNNING / RUNNABLE it only sets cancel_reason and enqueues, and check_cancelled raises at the next safe point. CancelCall.payload.reason defaults to worker_shutdown; the local timeout latch uses call_timeout, and the terminal state is classified as deadline_exceeded rather than cancelled.
Deadline and budget. ExecContext.deadline_expired_reason() is the single decision point: budget_ms (callBudgetMs, which counts only CPU + IO backend + subfunction + sleep time, not queueing) takes precedence over the absolute call_deadline_ms. The safe points are: before entering the user function, before sending and after resuming in every _perform_wait, after waking from sleep, and before returning. In addition, _switch_greenlet_with_deadline calls signal.setitimer(ITIMER_REAL, remaining, 0.1) before every switch into the greenlet, and _on_deadline_signal raises _DeadlineSignalExpired only when that call is RUNNING and has actually expired. This SIGALRM path can preempt pure-CPU loops; loops that swallow BaseException or long C calls still rely on the Worker’s wedge handling.
Heartbeat and connection. _heartbeat_loop is a separate thread that sends Heartbeat every heartbeat_interval_s; the payload contains runnableCount, inflightCount, availableContexts (max(0, max_inflight - inflight); 1<<30 when unlimited), memoryBytes, executorCpuUtilization, executorCpuSecondsTotal, executorProcessInstanceId, lastSchedulerActiveAtMs, runningCallId, callStateCounts, sentAtMs. ProcessMetricsSampler performs a capability probe with retries when ExecutorRuntime is constructed, and process startup fails if the probe fails; sampling failures at runtime keep the previous values. After the UDS disconnects, _refresh_transport_state marks every active context transport_lost and enqueues it to converge, and submit_call rejects new calls.
Admission. A top-level call raises ExecutorCapacityExhausted when inflight_count() >= executor_max_inflight (replied as CallFailed(errorKind=executor_capacity_exhausted, retryable=true)); subfunctions skip this gate and only require the parent context to be resident in this process, otherwise subcall_parent_not_resident (not retryable). executor_max_runnable is only received and stored; it is not enforced locally.
Configuration
Command-line arguments (blockx_executor/__main__.py, passed in by executorRuntimeArgs in internal/worker/adapters/executor/spawner.go):
Environment variables:
In sandbox mode (
EXECUTOR_SPAWN_MODE=sandbox), the Go side only passes through the variables in the childEnvKeys whitelist in internal/worker/adapters/executor/pool.go (everything in the table above plus LEAFAGE_ENDPOINT and PROXY_SOCKET_PATH); bare-process mode still inherits the entire parent environment. The container has an empty netns, so a direct OTLP connection is unavailable. The Python process itself is unaware of the spawn mode; see docs/specs/executor-sandbox-isolation.md in the blockx repo.
Extension points
- Connect a new external SDK to the Worker IO channel: write a bridge module (see
blockdb_bridge.py/noderpc_bridge.pyfor reference) that replaces the SDK’s transport seam with calls toprovider.read_req/write_req, and install it inExecutorRuntime.__init__; the backend name must match the backend registered in the Worker-side IO subsystem (see IO access subsystem). If the SDK is gRPC unary, reuseBridgeChannel(provider_getter, backend=..., streaming_methods=frozenset())directly. - Add a new SDK entry point for user code: only add a thin wrapper under
blockx_sdkthat callsget_provider(); do not hold connections in the SDK. For restricted mode, also update the whitelist inblockx_audit/tables.pyand therestricted_runtimefacade. - Add a new
ResumeCallerror type: add aRemoteCallErrorsubclass insdk_bridge.pyand extend_map_resume_error; useregister_io_error_kindsfor backend-specific mappings. - Add a heartbeat field or message type: add a constructor in
messages.py, a constant inwire_keys.py, a branch inruntime._handle_inbound_message_typed, and updateapi/uds/types.goin step. - Change safe points or accounting: all expiry decisions go through
ExecContext.deadline_expired_reasonandraise_if_deadline_expired; do not write separate checks elsewhere.
Testing
python/tests/ is split into files by implementation boundary (test_<boundary>.py), and each file is split into TestCases by logical block (<Boundary><Theme>Test); the convention is in docs/specs/python-test-organization.md in the blockx repo. The main boundaries:
- Runtime:
test_executor_runtime_scheduler.py,test_executor_runtime_termination.py,test_executor_runtime_attempt.py,test_executor_budget.py,test_executor_subcall.py,test_executor_oom_hardening.py,test_terminal_bounds.py. - Protocol:
test_executor_protocol_contract.py(ExecuteCallvalidation, inbound control messages, heartbeat fields),test_uds_codec.py,test_uds_session.py,test_envelope_*.py. - SDK hooks:
test_executor_sdk_hook.py,test_sdk_bridge_provider.py,test_sdk_bridge_capability.py,test_perform_wait_emit_failure.py,test_blockdb_bridge.py,test_noderpc_bridge.py,test_sleep_patch.py. - Loading and isolation:
test_module_registry.py,test_restricted_runtime.py,test_executor_audit_gate.py. - Context and metrics:
test_exec_context_*.py,test_process_metrics_sampler.py,test_logging_setup.py,test_tracing_setup.py,test_debug_capture.py. - Auditor:
test_audit_*.py.
ExecutorRuntime("exec-1") is constructed without a session, messages go to drain_outbox(); feed inbound messages with handle_inbound_message(dict) and advance with run_until_idle().
E2E tests live on the Go side: cmd/worker/worker_process_*_test.go and cmd/syncinvoker/*_test.go spawn a real Worker + python/.venv/bin/python -m blockx_executor; e2e/system brings up the whole system; e2e/perf runs under systemd-run cgroup limits through test.sh at the repo root. See Testing for the commands.
Related docs
blockx repo specs:docs/specs/python-function-executor.md— the in-process design of the Executor: scheduling, states, attempt / requestId, cancellation and deadlines, resource bounds (the key one).docs/specs/worker-executor-connection-and-python-sdk-hook.md§6–§8 — the SDK hook approach, theSdkBridgesynchronous contract, the suspend / resume sequence.docs/specs/python-executor-process-metrics.md— sampling and degradation of process metrics in heartbeats.docs/specs/2026-07-23-execute-call-function-code-reference.md— the protocol for fetching source by digest.docs/specs/executor-sandbox-isolation.md— the containerd sandbox spawn mode.docs/specs/2026-07-30-python-executor-cpu-optimization.md— the CPU optimization plan.docs/specs/python-test-organization.md— the unit test organization standard.docs/specs/function-code-python-whitelist.md,docs/specs/function-code-audit-design.md— user code whitelist and auditing.
- Call execution subsystem — Worker-side dispatcher, executor adapter, subfunctions.
- Protocols and interfaces — UDS frame format and message fields.
- Function Code View — where source comes from and
blockx_audit. - IO access subsystem — where
CallWaiting(waitKind=io)goes on the Worker side. - Observability — log fields, metrics, tracing.