How Rexec Works

What happens when you call a remote function: what gets sent, where it runs, what carries over between calls, and what you'll see when something fails.

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.

Your notebook

@remote_func serializes the function and its arguments with dill.

Broker

Checks your token on every message and routes the call to your server by user ID.

Your server pod

One per user. Runs calls one at a time, in the order they arrive.

New process for each call

Runs the function, streams stdout and stderr, then sends the return value or the error.

Output travels back the same way: printed output while the function runs, then the result.

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.

  1. 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.
  2. 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.
  3. 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.
  4. Limitation Only positional arguments are supported. A call of the form f(16) succeeds, whereas f(x=16) raises TypeError. See Remote Calls.
  5. 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.
  6. 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.
  7. 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.
  8. 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.
  9. 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

ElementDescription
Transmitted to the server
Function codeSerialized by value if the function is defined in the executing notebook or script.
Positional argumentsKeyword arguments are not supported.
Default argument values and closure variablesCaptured at the time the function is defined.
Authentication tokenVerified by the broker on every message.
Not transmitted
Module-level importsMust be placed within the function body.
Global variables and helper functionsMust be defined within the function body or supplied as arguments.
Functions from local modulesThe server cannot import modules that exist only on the client.
Local files and environment variablesNot accessible from the server.

On the server, anything the function neither defines nor receives fails with NameError, or ModuleNotFoundError for a local module.

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)
  1. A module-level import. It is not transmitted to the server.
  2. A global variable. It is not transmitted to the server.
  3. Raises NameError on the server, since neither math nor SCALE is defined there.
  4. The value of SCALE is captured as a default argument when the function is defined.
  5. An import within the function body, which therefore executes on the server.
  6. Returns 12.0.
  7. 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)
  1. Each line appears in the notebook as soon as it is printed, at intervals of about five seconds.
  2. Returns 3 after the last line has been printed.

What comes back

On the serverWhat you see locally
stdoutPrinted in your notebook while the function runs.
stderrWritten to your local stderr while the function runs.
return valueReturned by the call when the function finishes. A function that returns nothing gives None.
raiseRuntimeError("An unexpected error occurred: <message>"), raised after any streamed output. The original exception type and traceback stay in the server's logs.
yieldNot 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 byServerClient
Code inside the function ( e.g. import sklearn in the function body )RequiredNot required
Arguments or return values ( e.g. a returned pandas DataFrame )RequiredRequired
Rexec and its dependencies ( dill, pyzmq )Included in the server imageRequired ( 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)
  1. Executes on the server only; sklearn is not required on the client.
  2. Returns a str, so no additional package is required on the client.
  3. 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

Requirements file rules

# requirements.txt
python==3.11
numpy==2.1.3
pandas>=2.2
A Python version mismatch prevents execution. A function serialized under Python 3.13 cannot be executed by a server running Python 3.11. Depending on the versions involved, the call either prints 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 serverYou ask forResult
None yetAny requirementsCreated. A new pod starts in namespace rexec-server-<user-id>.
RunningThe same requirementsNo change. The running server and its files are untouched.
RunningDifferent requirementsConflict (HTTP 409). Nothing changes.
RunningDifferent requirements with force=True UpcomingReplaced. A new pod replaces the old one. A call the old pod was running is interrupted.
AnyInvalid requirementsError (HTTP 500) with the reason, for example a missing python== line.
Conflicts and server-side errors don't raise. 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)
  1. 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)
  1. Prints Server not ready… on each attempt until the pod is running.

State Between Calls

On your serverCarries over to the next call?
Variables, imported modules, objects in memoryNo. Each call runs in a new process.
Files your function writesYes, until the server restarts, is replaced or is terminated. They share the pod's 2 GiB disk limit with your installed packages.
Installed packagesYes, 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.

Interrupting affects the shared server. Interrupting a call stops whichever call is running on your server at that moment, even if another of your sessions started it.

Lifecycle

Rexec doesn't report these states yet (there is no status API). They describe what is happening at each stage.

Server states

StateWhat it meansWhat comes next
startingThe pod pulls the Python image, installs packages and starts the Rexec server. Calls print "Server not ready".ready, once it connects to the broker
readyConnected to the broker and idle.busy, when a call arrives
busyRunning a call. Other calls wait in line.ready, when the queue is empty
restartingThe container crashed or was evicted and Kubernetes restarts it. Files are lost; packages reinstall.starting
replacedYou called setup_rexec_environment() with new requirements and force=True. Upcomingstarting, as a new pod
terminatedYou 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

StateWhat it means
queuedYour server is still busy with an earlier call.
runningExecuting in a fresh process. Output streams back.
completedThe function returned. The call returns its value, or None.
failedThe function raised, or its process died. The call raises RuntimeError.
cancelledYou interrupted it. The process is terminated and KeyboardInterrupt is raised locally.

When Things Fail

What happensWhat you seeWhat to do
Your function raisesRuntimeError: An unexpected error occurred: …Fix the function. The server keeps running.
Your function's process dies, for example killed for using too much memoryRuntimeError: Execution process exited unexpectedly with code N.Reduce memory use. The server keeps running.
The server pod crashes or restarts during a callNothing. 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 callNothing. The call waits.Interrupt the cell and call again.
No server is running, or it is still startingPrints Server not ready/available for user <id>. and returns None.Run setup_rexec_environment(), or wait for the server.
Your token is invalid or expiredPrints 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 yoursPrints Invalid function payload: …, or the server crashes and the call waits.Pin your local version in the requirements and replace the server.
ⓘ
Rexec never retries. Each call runs at most once. If you call again after a hang, the first attempt may already have run; only its result was lost. Keep that in mind for functions that write files or send data elsewhere.

Isolation & Limits

Guaranteed today

ResourceReservedMaximumOver the maximum
CPU0.25 core2 coresSlowed down (throttled)
Memory512 MiB4 GiBThe function's process is killed (RuntimeError), or the whole server restarts, depending on the cluster
Disk512 MiB2 GiBThe 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

Not Yet Supported

FeatureToday
Upcoming
Idle shutdown and maximum run timeServers run until you terminate them.
Server readiness checkNo API. only limited way like a test call like ping() above tells you whether the server is ready.
Keyword argumentsRaise TypeError. Pass arguments by position.
Remote exception type and tracebackEvery error arrives as RuntimeError with the message only.
Call timeoutsA call waits until it gets an answer or you interrupt it.
Generator functionsYielded values are dropped; the call returns None.
Envisioned
Scheduled or event-triggered callsEvery 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 referencesNo 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 referencesNo 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.