Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 3 additions & 7 deletions container-runner/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,17 @@ path = "src/main.rs"

[dependencies]
anyhow.workspace = true
axum.workspace = true
bytes.workspace = true
async-trait.workspace = true
clap = { workspace = true, features = ["env"] }
futures.workspace = true
futures-util.workspace = true
http.workspace = true
nix = { workspace = true, features = ["process"] }
reqwest = { workspace = true, features = ["stream"] }
rivet-envoy-client = { workspace = true, features = ["native-transport"] }
rivetkit.workspace = true
scc.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tokio-stream.workspace = true
tokio-tungstenite.workspace = true
tokio-util = { workspace = true, features = ["rt"] }
tracing.workspace = true
tracing-subscriber.workspace = true
url.workspace = true
13 changes: 9 additions & 4 deletions container-runner/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
# Container Runner

`rivet-container-runner` is a Rivet serverless runner that hosts one actor by spawning a
child game-server process and proxying Rivet's tunneled HTTP/WebSocket traffic to it.
Wrap any dedicated server (Unity, Godot, a plain Node process) in a container with this
binary as the entrypoint and Rivet Compute can cold-start and route to it.
`rivet-container-runner` is a RivetKit (Rust) serverless app that hosts one actor by
spawning a child game-server process and proxying Rivet's tunneled HTTP/WebSocket
traffic to it. Wrap any dedicated server (Unity, Godot, a plain Node process) in a
container with this binary as the entrypoint and Rivet Compute can cold-start and route
to it.

The actor `input` payload (CBOR-encoded, RivetKit convention) can override the child
command, args, env, and port per actor. WebSocket clients connect at the bare gateway
path; raw HTTP reaches the child under the `/request/*` prefix on the actor surface.

`examples/` contains a Unity + FishNet demo project, a lightweight Node test server, and
an end-to-end test harness. All commands below run from the rivet repo root.
Expand Down
12 changes: 8 additions & 4 deletions container-runner/examples/e2e-test/create-actor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// POST /actors?namespace=default (no auth)
// The opaque `input` (base64) carries the child launch params decoded by container-runner.
import { writeFileSync } from "node:fs";
import { encode as cborEncode } from "cbor-x";
import {
ENGINE,
ENGINE_PUBLIC,
Expand All @@ -18,12 +19,13 @@ import {
const key = process.env.ACTOR_KEY || "match-1";
const outputJson = process.argv.includes("--json") || process.env.CREATE_ACTOR_OUTPUT === "json";

// container-runner decodes this JSON from ActorConfig.input. Override the whole object
// via ACTOR_INPUT (JSON) to exercise command/args/env/port; defaults to { port: 7770 }.
// container-runner decodes this from the actor input as CBOR (the RivetKit wire
// encoding). Override the whole object via ACTOR_INPUT (JSON) to exercise
// command/args/env/port; defaults to { port: 7770 }.
const inputJson = process.env.ACTOR_INPUT
? JSON.parse(process.env.ACTOR_INPUT)
: { port: 7770 };
const input = Buffer.from(JSON.stringify(inputJson)).toString("base64");
const input = Buffer.from(cborEncode(inputJson)).toString("base64");

const body = {
name: ACTOR_NAME,
Expand Down Expand Up @@ -59,7 +61,9 @@ if (!actorId) {
process.exit(1);
}
const gatewayPath = actorGatewayPath(actorId);
const gatewayHttpUrl = actorGatewayHttpUrl(actorId);
// Raw HTTP reaches the child under the /request/* prefix on the actor surface;
// WebSockets connect at the bare path.
const gatewayHttpUrl = actorGatewayHttpUrl(actorId, "/request/");
const gatewayWsUrl = actorGatewayWsUrl(actorId);

writeFileSync(new URL("./.last-actor-id", import.meta.url), actorId);
Expand Down
5 changes: 3 additions & 2 deletions container-runner/examples/e2e-test/create-cloud-actor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
// Game clients cannot send an Authorization header (Bayou/browser WebSockets), so the
// token is moved into the gateway path as `<actor_id>@<token>` instead.
import { writeFileSync } from "node:fs";
import { encode as cborEncode } from "cbor-x";

const raw = process.env.RIVET_URL;
if (!raw) {
Expand Down Expand Up @@ -38,8 +39,8 @@ const actorName = process.env.RIVET_ACTOR_NAME || "game";
// The managed pool the Rivet CLI creates is named `default`, not the actor name.
const runnerName = process.env.RIVET_RUNNER_NAME || "default";

// container-runner decodes this from ActorConfig.input to launch its child.
const input = Buffer.from(JSON.stringify({ port: 7770 })).toString("base64");
// container-runner decodes this from the actor input as CBOR to launch its child.
const input = Buffer.from(cborEncode({ port: 7770 })).toString("base64");

const res = await fetch(`${origin}/actors?namespace=${encodeURIComponent(namespace)}`, {
method: "POST",
Expand Down
10 changes: 6 additions & 4 deletions container-runner/examples/e2e-test/host/run-host-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@ L="$LOGS/s1.log"; start_runner "$L"
[ -z "${POOL_CONFIGURED:-}" ] && { node "$E2E/configure-serverless.mjs" >/dev/null; POOL_CONFIGURED=1; }
create_actor "fetch-$$" '{"port":7770}'; wait_ready "$L"
# The actor becomes guard-routable shortly after it reports ready; retry briefly.
# rivetkit's actor surface delivers raw HTTP to the child only under the
# /request/* prefix (stripped before proxying); bare paths are framework routes.
for _ in $(seq 1 15); do
[ "$(curl -s "$ENGINE_URL/gateway/$ACTOR_ID/health")" = "healthy" ] && break; sleep 1
[ "$(curl -s "$ENGINE_URL/gateway/$ACTOR_ID/request/health")" = "healthy" ] && break; sleep 1
done
H=$(curl -s "$ENGINE_URL/gateway/$ACTOR_ID/health")
R=$(curl -s "$ENGINE_URL/gateway/$ACTOR_ID/")
P=$(curl -s -X POST --data 'hello-body' "$ENGINE_URL/gateway/$ACTOR_ID/reflect")
H=$(curl -s "$ENGINE_URL/gateway/$ACTOR_ID/request/health")
R=$(curl -s "$ENGINE_URL/gateway/$ACTOR_ID/request/")
P=$(curl -s -X POST --data 'hello-body' "$ENGINE_URL/gateway/$ACTOR_ID/request/reflect")
[ "$H" = "healthy" ] && ok "GET /health -> healthy" || bad "GET /health -> '$H'"
[ "$R" = "ok" ] && ok "GET / -> ok" || bad "GET / -> '$R'"
echo "$P" | grep -q '"method":"POST"' && ok "POST method proxied" || bad "POST method ('$P')"
Expand Down
3 changes: 2 additions & 1 deletion container-runner/examples/e2e-test/load-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
// RIVET_TOKEN Bearer for the engine API (cloud) (default: none / local)
// ENGINE_URL, ENGINE_PUBLIC_URL, RIVET_NAMESPACE, RIVET_ACTOR_NAME, RIVET_RUNNER_NAME — see common.mjs
import WebSocket from "ws";
import { encode as cborEncode } from "cbor-x";
import { ENGINE, ENGINE_PUBLIC, NAMESPACE, ACTOR_NAME, RUNNER_NAME } from "./common.mjs";

// ---- helpers ----
Expand Down Expand Up @@ -80,7 +81,7 @@ async function createActor(i) {
const body = {
name: ACTOR_NAME,
key: `load-${RUN_ID}-${i}`,
input: Buffer.from(JSON.stringify(input)).toString("base64"),
input: Buffer.from(cborEncode(input)).toString("base64"),
runner_name_selector: POOL_PREFIX ? `${POOL_PREFIX}-${i}` : RUNNER_NAME,
crash_policy: "destroy",
...(DATACENTER ? { datacenter: DATACENTER } : {}),
Expand Down
135 changes: 135 additions & 0 deletions container-runner/examples/e2e-test/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions container-runner/examples/e2e-test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"description": "Scripts to drive the local Rivet <-> container-runner end-to-end test.",
"dependencies": {
"cbor-x": "^1.6.0",
"ws": "^8.18.0"
}
}
Loading
Loading