Function Runtime Server Contract
Status: Accepted; implementation in progress
This document defines the internal gRPC contract for function-mode Runs. It refines Function Mode Lifecycle and Invoke Dataplane without changing the public Run API or exposing the Runtime Server outside its Runtime Pod.
Scope
The base Runtime service supports one-shot execution. A Runtime Server that
supports function mode additionally registers the optional FunctionRuntime
service, which provides four Pod-local operations called only by runtimed:
RegisterFunctioncreates or resumes one callable function registration.FunctionStatusreads local readiness, activity, and fatal state.InvokeFunctionexecutes one bounded invocation.UnregisterFunctiondrains or cancels work and removes local state.
The Runtime Server does not read Kubernetes objects, authenticate callers, route gateway requests, upload artifacts, or schedule capacity. Those concerns remain with runtimed, the Runtime gateway, and the control plane. Invocation artifacts are out of scope for v0.x.
The same FunctionRuntime service is also implemented by runtimed on the
Runtime Service port for gateway traffic. For that proxy hop only,
InvokeFunctionRequest.registration supplies run_uid with an empty
registration_id. runtimed resolves the current assigned owner and its private
active registration, fills the opaque ID, and calls the colocated Runtime
Server. A Runtime Server itself always requires a non-empty registration ID;
the gateway neither sees nor supplies it.
Function support is opt-in for custom Runtimes: a Runtime that supports only
one-shot execution implements and registers Runtime only. The exact shape
and semantics below require review before runtime.proto, generated stubs, or
built-in Runtime implementations change.
Registration Identity
For a one-shot Run, Run.status.attempt counts execution attempts. For a
function Run, it counts registration lifecycle attempts: the initial registration is
1, and the shared retry engine increments it only after a registration
failure enters retry or reassignment. Retrying an uncertain Pod-local
RegisterFunction RPC for the same registration attempt is idempotent and
does not change Run status.
registration_attempt is not an invocation counter. invocation_id is
optional correlation data for one function call.
RegisterFunction uses Run UID plus registration_attempt to establish a
new local registration generation. It returns an opaque registration_id.
All later Pod-local operations use that ID instead of repeating the attempt.
The Runtime Server binds the ID to its Run UID and registration attempt, and a
stale ID must never change, invoke, or remove a newer registration.
registration_digest is a lowercase SHA-256 digest calculated by runtimed
from canonical, immutable registration inputs: resolved source identity,
handler, environment, and runtime-visible registration settings. It excludes
the transient working-directory path. It is an idempotency check, not a
credential.
Example Calls and Registration Fence
The following illustrates the Pod-local calls made by runtimed. It is an example of the proposed protocol, not a public gateway command.
A function Run with UID
2b5d...starts its first registration on Runtime Pod A.Run.status.attemptis1, so runtimed A registers the function:grpcurl -plaintext -d '{ "runUid": "2b5d...", "registrationAttempt": 1, "workingDir": "/workspace/runs/2b5d", "handler": "handler.handle", "idleTimeoutSeconds": 300, "registrationDigest": "sha256:..." }' 127.0.0.1:9090 executor.v1.FunctionRuntime/RegisterFunctionA successful response contains a server-generated registration reference:
{ "registration": { "runUid": "2b5d...", "registrationId": "reg_01J..." }, "state": "FUNCTION_REGISTRATION_STATE_READY" }A gateway request is routed to runtimed A. It assigns an invocation ID and sends the payload without creating another Kubernetes object:
grpcurl -plaintext -d '{ "registration": { "runUid": "2b5d...", "registrationId": "reg_01J..." }, "invocationId": "01J...", "contentType": "application/json", "input": "eyJjb21tYW5kIjoic3RhdHVzIn0=" }' 127.0.0.1:9090 executor.v1.FunctionRuntime/InvokeFunctionIn protobuf JSON,
bytesis base64-encoded; the decoded input is{"command":"status"}. Another call gets anotherinvocation_id, but uses the same registration ID while this registration remains active.If the registration fails and the retry policy allows recovery, the shared retry engine advances
Run.status.attemptto2before the scheduler assigns or reassigns a Runtime Pod. The nextRegisterFunctionusesregistration_attempt: 2and receives a new registration ID. An invoke or unregister request carryingreg_01J...must returnFailedPreconditionafter the newer registration supersedes it; it cannot affect the new registration.
The gateway and runtimed also fence routing using assigned Pod identity. The registration ID protects the Runtime Server’s local registration state; it does not make an invocation exactly once.
Proposed Protobuf API
The base executor.v1.Runtime service remains for one-shot execution. The
following optional service is registered alongside it by a function-capable
Runtime Server:
service FunctionRuntime {
// Creates or resumes one local function registration.
rpc RegisterFunction(RegisterFunctionRequest) returns (RegisterFunctionResponse);
// Returns local readiness, activity, and fatal state.
rpc FunctionStatus(FunctionStatusRequest) returns (FunctionStatusResponse);
// Executes one bounded function invocation.
rpc InvokeFunction(InvokeFunctionRequest) returns (InvokeFunctionResponse);
// Drains or cancels work and removes local state.
rpc UnregisterFunction(UnregisterFunctionRequest) returns (UnregisterFunctionResponse);
}
message FunctionRegistration {
// Kubernetes Run UID, never its mutable name.
string run_uid = 1;
// Opaque Runtime Server-generated ID for one local registration generation.
string registration_id = 2;
}
message RegisterFunctionRequest {
string run_uid = 1;
int32 registration_attempt = 2;
string working_dir = 3;
string handler = 4;
map<string, string> env = 5;
int64 idle_timeout_seconds = 6;
string registration_digest = 7;
}
message RegisterFunctionResponse {
FunctionRegistration registration = 1;
FunctionRegistrationState state = 2;
}
message FunctionStatusRequest { FunctionRegistration registration = 1; }
message FunctionStatusResponse {
FunctionRegistration registration = 1;
FunctionRegistrationState state = 2;
int32 in_flight = 3;
int64 last_activity_unix_nano = 4;
string fatal_error = 5;
}
message InvokeFunctionRequest {
FunctionRegistration registration = 1;
string invocation_id = 2;
bytes input = 3;
string content_type = 4;
int64 timeout_millis = 5;
}
message InvokeFunctionResponse {
FunctionRegistration registration = 1;
string invocation_id = 2;
bytes output = 3;
string content_type = 4;
map<string, string> outputs = 5;
}
message UnregisterFunctionRequest {
FunctionRegistration registration = 1;
bool cancel_in_flight = 2;
int64 drain_timeout_millis = 3;
}
message UnregisterFunctionResponse { FunctionRegistration registration = 1; }
enum FunctionRegistrationState {
FUNCTION_REGISTRATION_STATE_UNSPECIFIED = 0;
FUNCTION_REGISTRATION_STATE_REGISTERING = 1;
FUNCTION_REGISTRATION_STATE_READY = 2;
FUNCTION_REGISTRATION_STATE_DRAINING = 3;
FUNCTION_REGISTRATION_STATE_FAILED = 4;
}
The gateway initially accepts JSON and sets content_type to
application/json. The local protocol uses opaque bytes so trusted custom
Runtimes can use another representation later. Inputs and response bytes are
never written to Run.status.
Registration and Status Semantics
RegisterFunction validates its working directory, handler, environment,
timeout, digest, and one-based registration_attempt before accepting work.
- Repeating the same Run UID, registration attempt, and digest returns the current registration reference without reinitializing the function.
- A different digest for the same Run UID and registration attempt returns
AlreadyExistsand does not replace the registration. - A higher registration attempt supersedes an older local registration and
creates a new opaque registration ID. A lower attempt returns
FailedPrecondition. - Permanent initialization failure is surfaced as
FAILEDwith a boundedfatal_errorthroughFunctionStatus.
FunctionStatus only reads local Runtime Server state. last_activity_unix_nano
is initialized when the registration becomes ready, then updated when an
invocation starts or completes. This makes a registration with no invocations
subject to its idle timeout. fatal_error is bounded diagnostic text, not logs.
NotFound means no registration has this Run UID; FailedPrecondition means
its registration ID is stale, draining, or unready. runtimed polls it at a
bounded cadence for health and idle timeout, never writing each activity update
to Kubernetes.
If the assigned Runtime Pod does not register FunctionRuntime, runtimed
receives Unimplemented. This is a permanent configuration failure: the Run
cannot fall back to one-shot execution and is handled through the normal terminal or
retry policy for an incompatible Runtime.
Invocation Semantics
InvokeFunction requires a READY registration for the supplied registration
reference. v0.x
allows one in-flight invocation per function Run and does not queue requests.
invocation_idis optional correlation data, limited to 128 bytes. The caller may supply one for cross-system tracing. When it is empty, the Runtime Server generates an opaque ID; the response always returns the ID in use.- It is not a deduplication key. Retrying after an unknown result can execute work again; no component automatically retries after dispatch.
timeout_millisis bounded by runtimed to the remaining Run lifetime and gateway deadline. Zero means the bounded gateway default, never unlimited runtime work.ResourceExhaustedmaps to gateway HTTP 429.DeadlineExceededaffects only this invocation, not the function Run lifecycle.FailedPreconditionmaps to HTTP 503 for draining, stale, or unready registration.
outputs follow the key, count, and value bounds used by Run.status.outputs.
Function invocations do not produce artifact declarations or ArtifactRef
values in v0.x. A future artifact design must define lifecycle, retention, and
storage boundaries before extending this local protocol.
Runtime logs are structured by Run UID and invocation ID. Adapter-captured
function output populates the RPC output field and is not automatically
logged. Built-in Bash uses handler stdout as function output and stderr as
structured logs; neither is written to Run.status.message.
Unregistration
UnregisterFunction first moves the registration to DRAINING, rejecting new
invokes. With cancel_in_flight=false, it waits no longer than
drain_timeout_millis for the active invocation. With cancel_in_flight=true,
it cancels immediately, then releases registration-local state.
Unregistering an absent registration succeeds. Unregistering a stale
registration ID returns FailedPrecondition and cannot delete a newer
registration for the same Run UID.
Limits and Error Mapping
| Value | Initial limit | Enforcement |
|---|---|---|
| Request body | 1 MiB | Gateway and runtimed |
| Registration ID | 128 bytes | Runtime Server and runtimed |
| Invocation ID | 128 bytes | Gateway and runtimed |
| Response body | 1 MiB | runtimed |
| Outputs | Existing Run output limits | runtimed |
| In-flight calls | One per function Run | Runtime Server |
fatal_error | 4 KiB | Runtime Server |
| gRPC code | Meaning | Gateway result |
|---|---|---|
InvalidArgument | Invalid handler, path, payload, or limit | HTTP 400 |
NotFound | Unknown registration | HTTP 404 or 503 after cache recheck |
AlreadyExists | Same registration attempt, different digest | Registration failure |
FailedPrecondition | Stale registration ID or unready registration | HTTP 503 |
ResourceExhausted | Invocation already active | HTTP 429 |
DeadlineExceeded | Invocation deadline elapsed | HTTP 504 |
Unavailable | Runtime Server cannot accept work | HTTP 503 and lifecycle recovery |
Built-in Runtime Requirements
Python imports module.function, passes decoded JSON input, and encodes its
return value as JSON output. Bash follows the
AWS Lambda custom-runtime
handler model
:
its handler is file.function, where file names a .sh file relative
to the registered working directory. During registration, the Bash Runtime
sources file.sh and validates that function exists. For an
application/json invocation, it calls that function with the payload as one
quoted positional argument and captures its stdout as the response output. It
never evaluates either the handler or request payload as shell source, and it
does not interpolate request data into a command string. Both adapters operate
beneath the registered working directory, honor context cancellation, and
permit only one active invocation per registration.
Existing one-shot Runtime Servers remain valid. Function mode is enabled only
after a future compatibility/health handshake confirms support for these RPCs;
there is no fallback that emulates function invocation through Execute.
Review Decisions Requested
- Use
Run.status.attemptas the function registration lifecycle attempt; use the Runtime Server-generated registration ID for subsequent local calls and stale-operation fencing. - Use opaque bytes plus content type for local invoke payloads; JSON is the first gateway encoding.
- Keep invocation artifacts out of scope for v0.x.
- Do not promise invocation-ID deduplication or automatic execution retry.
- Limit v0.x to one in-flight invocation per registered function Run.
After approval, implementation can be split into protobuf/stub generation, Bash and Python adapters, and runtimed registration lifecycle/gateway work.