This page describes the Rexec client library, scidx-rexec 0.0.9, used through ndp-ep 0.8.1. Items marked Upcoming are not available in this release.
Overview
Rexec turns a decorated Python function into a remote call. You get one dedicated server on the cluster. Each time you call the function, its code and arguments are serialized on your machine, routed through the broker to your server, and run there in a new process.
@remote_func serializes the function and its arguments with dill.
Checks your token on every message and routes the call to your server by user ID.
One per user. Runs calls one at a time, in the order they arrive.
Runs the function, streams stdout and stderr, then sends the return value or the error.
Summary of Execution Semantics
The following nine properties characterize how Rexec executes user code. Each is labelled either Guaranteed, a property of the design that users may rely on, or Limitation, a property of the current release that may change in a future one. Each property is examined in detail in the section indicated.
- Guaranteed Each user is assigned a single server, shared across all sessions. All notebooks and scripts authenticated under the same user identity dispatch their calls to the same server instance. See Your Environment.
- Limitation The Python version of the server must match that of the client. Functions are transmitted as Python bytecode, the format of which varies between interpreter versions. The version should therefore be pinned in the requirements specification (e.g., python==3.11). See Your Environment.
- Guaranteed A call transmits only the function and its arguments. Module-level imports, global variables, and helper functions defined in the calling notebook are not transmitted. They must be defined within the function body or supplied as arguments. See Remote Calls.
- Limitation Only positional arguments are supported. A call of the form
f(16)succeeds, whereasf(x=16)raises TypeError. See Remote Calls. - Guaranteed Standard output is streamed during execution. The return value is delivered upon completion. Exceptions raised on the server are reported to the client as RuntimeError. See Live output streaming.
- Guaranteed Calls are executed sequentially, in order of arrival. A subsequent call does not begin until the preceding call has completed. See State Between Calls.
- Guaranteed Each call is executed in a new process. In-memory state does not persist between calls. Files written to the server's local disk persist until the server is restarted, replaced, or terminated. See State Between Calls.
- Limitation Calls are neither retried nor subject to a timeout. If the server or the broker fails during execution, the call blocks until the user interrupts it. See When Things Fail.
- Limitation A server remains active until it is explicitly terminated. No idle-shutdown mechanism is provided yet. Users should call
terminate_rexec_environment()upon completion of their work. See Lifecycle.
Remote Calls
Decorating a function with @remote_func replaces it with a remote call. Calling it
serializes the function and its arguments with dill,
sends them to the broker with your token, and waits for your server to answer.
Contents of a remote call
| Element | Description |
|---|---|
| Transmitted to the server | |
| Function code | Serialized by value if the function is defined in the executing notebook or script. |
| Positional arguments | Keyword arguments are not supported. |
| Default argument values and closure variables | Captured at the time the function is defined. |
| Authentication token | Verified by the broker on every message. |
| Not transmitted | |
| Module-level imports | Must be placed within the function body. |
| Global variables and helper functions | Must be defined within the function body or supplied as arguments. |
| Functions from local modules | The server cannot import modules that exist only on the client. |
| Local files and environment variables | Not accessible from the server. |
import math
SCALE = 3
@remote_func
def bad(x):
return math.sqrt(x) * SCALE
@remote_func
def good(x, scale=SCALE):
import math
return math.sqrt(x) * scale
good(16)
good(x=16)
- A module-level import. It is not transmitted to the server.
- A global variable. It is not transmitted to the server.
- Raises
NameErroron the server, since neithermathnorSCALEis defined there. - The value of
SCALEis captured as a default argument when the function is defined. - An import within the function body, which therefore executes on the server.
- Returns
12.0. - Raises
TypeError: keyword arguments are not supported.
Live output streaming
Output that a remote function writes to standard output or standard error is forwarded to the client as it is produced, rather than collected and delivered when the call completes. Progress messages, logs, and diagnostics therefore appear in the notebook while a long-running function is still executing, and all of a call's output arrives before its return value.
@remote_func
def train(epochs):
import time
for epoch in range(epochs):
time.sleep(5)
print(f"epoch {epoch}: done")
return epochs
train(3)
- Each line appears in the notebook as soon as it is printed, at intervals of about five seconds.
- Returns
3after the last line has been printed.
- Each write is forwarded immediately.
- Standard output is printed in the notebook. Standard error is written to the client's standard error.
- Only output written through Python's
sys.stdoutandsys.stderris streamed. Output written directly to the process's file descriptors, for example by a subprocess or a compiled extension, is not forwarded and appears only in the server's logs.
What comes back
| On the server | What you see locally |
|---|---|
| stdout | Printed in your notebook while the function runs. |
| stderr | Written to your local stderr while the function runs. |
| return value | Returned by the call when the function finishes. A function that returns nothing gives None. |
| raise | RuntimeError("An unexpected error occurred: <message>"), raised after any streamed output. The original exception type and traceback stay in the server's logs. |
| yield | Not supported. Yielded values are dropped and the call returns None. |
Where packages must be installed
Where a package must be installed depends on how it is used. Code inside the function runs only on the server. Arguments and return values, however, are serialized on one side and reconstructed on the other, and reconstruction requires importing the package that defines their type.
| Package used by | Server | Client |
|---|---|---|
Code inside the function ( e.g. import sklearn in the function body ) | Required | Not required |
Arguments or return values ( e.g. a returned pandas DataFrame ) | Required | Required |
| Rexec and its dependencies ( dill, pyzmq ) | Included in the server image | Required ( installed with pip install ndp_ep[rexec] ) |
@remote_func
def sklearn_version():
import sklearn
return sklearn.__version__
@remote_func
def load(path):
import pandas as pd
return pd.read_csv(path)
- Executes on the server only;
sklearnis not required on the client. - Returns a
str, so no additional package is required on the client. - Returns a
DataFrame, so pandas is required on both the server and the client.
Installed versions should also be compatible, since an object pickled by one version of a package
is not guaranteed to load in another. Return values must, in addition, be picklable with the
standard pickle module: a return value that cannot be pickled is dropped, and the call
returns None without raising an error.
Interrupting a call
Stopping the cell (Ctrl-C, or the stop button in Jupyter) cancels the call. The server terminates the process running it (SIGTERM, then SIGKILL after half a second) and KeyboardInterrupt is raised locally.
Your Environment
setup_rexec_environment() asks the Deploy API for a server built from your requirements.
The server is identified by your user ID (the sub claim in your token), so you have
exactly one, and every session signed in as you uses it.
What's installed
- The official
python:<version>container image, for the version you pin. scidx-rexecand its dependencies (dill, pyzmq, dxspaces, packaging, cryptography).- Your requirements, installed with
pip install -r.
Requirements file rules
- Pin Python exactly with one
python==X.Yline, matching your local Python's major and minor version. - Use standard pip requirement lines. Direct URL references (
pkg @ https://…) are rejected. - Full-line
#comments are fine. Comments at the end of a line are not, and makesetup_rexec_environment()raiseInvalidRequirement. - Line order doesn't matter: the same set of lines is the same environment.
# requirements.txt
python==3.11
numpy==2.1.3
pandas>=2.2
Invalid function payload: …
and returns None, or terminates the server process, in which case the call does not return.
What setup_rexec_environment() does
| Your server | You ask for | Result |
|---|---|---|
| None yet | Any requirements | Created. A new pod starts in namespace rexec-server-<user-id>. |
| Running | The same requirements | No change. The running server and its files are untouched. |
| Running | Different requirements | Conflict (HTTP 409). Nothing changes. |
| Running | Different requirements with force=True Upcoming | Replaced. A new pod replaces the old one. A call the old pod was running is interrupted. |
| Any | Invalid requirements | Error (HTTP 500) with the reason, for example a missing python== line. |
setup_rexec_environment() returns the Deploy API's response whatever happened.
Its Status tells you which of the outcomes above you got. A login or permission
problem comes back as detail instead.
resp = ndp_ep_client.setup_rexec_environment(requirements="requirements.txt")
print(resp)
- Prints either
{"Status": "...", ...}or{"detail": "..."}.
Starting takes a while
setup_rexec_environment() returns as soon as Kubernetes accepts the new server, before the
pod has pulled its image and installed your packages. Until the server connects to the broker,
calls print Server not ready/available for user <id>. and return None.
To wait for it:
import time
@remote_func
def ping():
return "ready"
while ping() != "ready":
time.sleep(10)
- Prints
Server not ready…on each attempt until the pod is running.
State Between Calls
| On your server | Carries over to the next call? |
|---|---|
| Variables, imported modules, objects in memory | No. Each call runs in a new process. |
| Files your function writes | Yes, until the server restarts, is replaced or is terminated. They share the pod's 2 GiB disk limit with your installed packages. |
| Installed packages | Yes, until you replace the server with different requirements. |
Order
Your server runs one call at a time, in the order they arrive. A call from another cell, notebook or script waits until the current one finishes.
Lifecycle
Rexec doesn't report these states yet (there is no status API). They describe what is happening at each stage.
Server states
| State | What it means | What comes next |
|---|---|---|
starting | The pod pulls the Python image, installs packages and starts the Rexec server. Calls print "Server not ready". | ready, once it connects to the broker |
ready | Connected to the broker and idle. | busy, when a call arrives |
busy | Running a call. Other calls wait in line. | ready, when the queue is empty |
restarting | The container crashed or was evicted and Kubernetes restarts it. Files are lost; packages reinstall. | starting |
replaced | You called setup_rexec_environment() with new requirements and force=True. Upcoming | starting, as a new pod |
terminated | You called terminate_rexec_environment(). Your namespace and everything in it are deleted. | None. Call setup_rexec_environment() to start again. |
There is no idle shutdown and no time limit. A server keeps running, and keeps its resource reservation, until you terminate it. Terminating also kills any call it is running.
ndp_ep_client.terminate_rexec_environment()
Call states
| State | What it means |
|---|---|
queued | Your server is still busy with an earlier call. |
running | Executing in a fresh process. Output streams back. |
completed | The function returned. The call returns its value, or None. |
failed | The function raised, or its process died. The call raises RuntimeError. |
cancelled | You interrupted it. The process is terminated and KeyboardInterrupt is raised locally. |
When Things Fail
| What happens | What you see | What to do |
|---|---|---|
| Your function raises | RuntimeError: An unexpected error occurred: … | Fix the function. The server keeps running. |
| Your function's process dies, for example killed for using too much memory | RuntimeError: Execution process exited unexpectedly with code N. | Reduce memory use. The server keeps running. |
| The server pod crashes or restarts during a call | Nothing. The call waits with no result and no error. | Interrupt the cell. Call again once the server is back (packages reinstall; files are lost). |
| The broker restarts during a call | Nothing. The call waits. | Interrupt the cell and call again. |
| No server is running, or it is still starting | Prints Server not ready/available for user <id>. and returns None. | Run setup_rexec_environment(), or wait for the server. |
| Your token is invalid or expired | Prints Token validation failed. and returns None. A call that is already running still returns its result. | Get a fresh token. |
| The server's Python version doesn't match yours | Prints Invalid function payload: …, or the server crashes and the call waits. | Pin your local version in the requirements and replace the server. |
Isolation & Limits
Guaranteed today
- Your server runs in its own Kubernetes namespace,
rexec-server-<user-id>, and each call runs in its own process. - Every message to the broker, and every request to create or delete a server, is checked against the identity provider.
- Your requirements reach the pod as a file for
pip install -r, never through a shell command. - Each server pod has fixed resource limits:
| Resource | Reserved | Maximum | Over the maximum |
|---|---|---|---|
| CPU | 0.25 core | 2 cores | Slowed down (throttled) |
| Memory | 512 MiB | 4 GiB | The function's process is killed (RuntimeError), or the whole server restarts, depending on the cluster |
| Disk | 512 MiB | 2 GiB | The pod is evicted and restarted; files are lost |
These are the Deploy API's defaults; your sysadmin can change them. You can't request a different size.
Not guaranteed yet
- Network isolation. Rexec doesn't create a network policy for server pods, so what they can reach, inside the cluster and on the internet, depends entirely on the cluster's own policies. By default, Kubernetes allows all of it.
- Unprivileged execution. The server runs as root inside its container.
- Encrypted transport. Traffic between your machine and the broker is plain TCP. Rexec doesn't encrypt your token, code or results on that link.
Not Yet Supported
| Feature | Today |
|---|---|
| Upcoming | |
| Idle shutdown and maximum run time | Servers run until you terminate them. |
| Server readiness check | No API. only limited way like a test call like ping() above tells you whether the server is ready. |
| Keyword arguments | Raise TypeError. Pass arguments by position. |
| Remote exception type and traceback | Every error arrives as RuntimeError with the message only. |
| Call timeouts | A call waits until it gets an answer or you interrupt it. |
| Generator functions | Yielded values are dropped; the call returns None. |
| Envisioned | |
| Scheduled or event-triggered calls | Every call is made directly from your code. |
DataSpaces-backed arguments (DSDataObj) | Server pods aren't started with a DataSpaces address, so these arrive as None. |
| Amazon S3 object references | No built-in reference type. Pass the object's location (for example an s3:// URI) and any credentials as arguments, and read the object inside the function. Local environment variables, including AWS credentials, are not transmitted. |
| NDP data object references | No built-in reference type. Pass the dataset or resource identifier as an argument and retrieve the data inside the function; any library and token this requires must be supplied through the requirements file and the arguments. |
Remote Execution
GitHub