Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .github/workflows/benchmark-smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
- '.github/workflows/benchmark-live.yml'
- '.github/workflows/benchmark-smoke.yml'
- 'docs/relayer/benchmark-ci-setup.md'
- 'docs/relayer/k6-stress-tests.md'
push:
branches:
- main
Expand All @@ -17,6 +18,7 @@ on:
- '.github/workflows/benchmark-live.yml'
- '.github/workflows/benchmark-smoke.yml'
- 'docs/relayer/benchmark-ci-setup.md'
- 'docs/relayer/k6-stress-tests.md'
workflow_dispatch:

concurrency: ${{ github.workflow }}-${{ github.ref }}
Expand Down Expand Up @@ -66,3 +68,7 @@ jobs:
working-directory: services/server/scripts
run: |
./node_modules/.bin/tsx bench-recall-latency.ts --help

- name: Build k6 relayer bundle
working-directory: services/server/scripts
run: npm run k6:build
3 changes: 2 additions & 1 deletion docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@
"relayer/overview",
"relayer/public-relayer",
"relayer/self-hosting",
"relayer/api-reference"
"relayer/api-reference",
"relayer/k6-stress-tests"
]
}
]
Expand Down
10 changes: 6 additions & 4 deletions docs/relayer/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,19 +20,21 @@ All `/api/*` routes require signed headers. The SDK handles this automatically.
| `x-public-key` | Hex-encoded Ed25519 public key (32 bytes) |
| `x-signature` | Hex-encoded Ed25519 signature (64 bytes) |
| `x-timestamp` | Unix timestamp in seconds (5-minute validity window) |
| `x-nonce` | UUID nonce, unique per request, used for replay protection |
| `x-account-id` | MemWalAccount object ID; included in the signed message |

### Optional Headers

| Header | Description |
|--------|-------------|
| `x-account-id` | MemWalAccount object ID hint — speeds up account resolution when not cached |
| `x-delegate-key` | Delegate private key (hex) — used by the default SDK for SEAL decrypt flows |
| `x-seal-session` | Client-built SEAL SessionKey for server-side decrypt flows |
| `x-delegate-key` | Legacy delegate private key (hex or `suiprivkey`) fallback for SEAL decrypt flows |

### Signature Format

The signed message is: `{timestamp}.{method}.{path}.{body_sha256}`
The signed message is: `{timestamp}.{method}.{path_and_query}.{body_sha256}.{nonce}.{account_id}`

The relayer verifies the Ed25519 signature, then resolves the owner by looking up the public key in onchain `MemWalAccount.delegate_keys`.
The relayer verifies the Ed25519 signature, checks the nonce, then resolves the owner by looking up the public key in onchain `MemWalAccount.delegate_keys`.

## Public Routes

Expand Down
17 changes: 17 additions & 0 deletions docs/relayer/benchmark-ci-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ deployment.
- Runs `cargo check`.
- Typechecks `bench-recall-latency.ts`.
- Runs a `--help` smoke check for the recall benchmark CLI.
- Builds the k6 relayer bundle from `services/server/scripts/k6/relayer.ts`.
- Does not need secrets and does not call Sui, Walrus, SEAL, or OpenAI.

- `.github/workflows/benchmark-live.yml`
Expand Down Expand Up @@ -84,3 +85,19 @@ cd services/server/scripts
--remember-text "benchmark memory" \
--query "benchmark memory"
```

k6 smoke against staging:

```bash
cd services/server/scripts
npm run k6:build

BENCH_SERVER_URL=https://relayer.staging.memwal.ai \
BENCH_ACCOUNT_ID="$BENCH_ACCOUNT_ID" \
BENCH_DELEGATE_KEY="$BENCH_DELEGATE_KEY" \
MEMWAL_NAMESPACE=benchmark \
K6_PROFILE=smoke \
k6 run dist/k6/relayer.js
```

For load, stress, and spike profiles, see [k6 Stress Tests](/relayer/k6-stress-tests).
159 changes: 159 additions & 0 deletions docs/relayer/k6-stress-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: "k6 Stress Tests"
---

The relayer k6 harness lives in `services/server/scripts/k6/relayer.ts`.
It signs protected requests with the same canonical message as the Rust auth
middleware:

```text
{timestamp}.{method}.{path_and_query}.{body_sha256}.{nonce}.{account_id}
```

The bundle sends `x-nonce`, `x-account-id`, and the legacy `x-delegate-key`
header so relayer-mode `recall`, `ask`, and `restore` can decrypt through SEAL
without needing SDK SessionKey construction inside k6.

## Build

```bash
cd services/server/scripts
npm ci
npm run k6:build
```

This writes `dist/k6/relayer.js`, which is the file k6 runs.

## Required Environment

| Variable | Notes |
| --- | --- |
| `MEMWAL_SERVER_URL` | Relayer base URL, for example `http://localhost:8000` |
| `MEMWAL_ACCOUNT_ID` | MemWal account object ID used by `x-account-id` |
| `MEMWAL_DELEGATE_KEY` | Ed25519 delegate key, as `suiprivkey`, 64-char hex, or `0x` hex |
| `MEMWAL_NAMESPACE` | Namespace to read/write. Defaults to `k6` |

The harness also accepts the existing benchmark variable names:
`BENCH_SERVER_URL`, `BENCH_ACCOUNT_ID`, and `BENCH_DELEGATE_KEY`.

## Profiles

Run from `services/server/scripts` after building:

```bash
# Health only, no auth required
K6_PROFILE=health k6 run dist/k6/relayer.js

# One end-to-end remember -> poll -> recall pass
K6_PROFILE=smoke \
MEMWAL_SERVER_URL=http://localhost:8000 \
MEMWAL_ACCOUNT_ID=0x... \
MEMWAL_DELEGATE_KEY=suiprivkey1... \
k6 run dist/k6/relayer.js

# Steady load. Defaults to 1 iteration/sec for 5 minutes.
K6_PROFILE=load K6_RATE=2 K6_DURATION=10m k6 run dist/k6/relayer.js

# Gradual ramp. Override stage rates/durations as needed.
K6_PROFILE=stress \
K6_STRESS_STAGE_1_RATE=2 \
K6_STRESS_STAGE_2_RATE=5 \
K6_STRESS_STAGE_3_RATE=10 \
k6 run dist/k6/relayer.js

# Sudden traffic burst.
K6_PROFILE=spike K6_SPIKE_RATE=30 k6 run dist/k6/relayer.js
```

The default `mixedFlow` is conservative:

| Flow | Default weight | Main bottlenecks exercised |
| --- | ---: | --- |
| `POST /api/recall` | `0.75` | query embedding, pgvector search, Walrus download/cache, SEAL decrypt |
| `POST /api/remember` + status polling | `0.25` | enqueue latency, background embedding, SEAL encrypt, Walrus upload, DB insert |
| `POST /api/ask` | `0` | recall path plus LLM completion |
| `POST /api/restore` | `0` | onchain blob query, Walrus download, SEAL decrypt, re-embedding, DB insert |

Enable expensive flows explicitly:

```bash
MEMWAL_ASK_WEIGHT=0.05 MEMWAL_RESTORE_WEIGHT=0.01 K6_PROFILE=load k6 run dist/k6/relayer.js
```

To isolate one endpoint family, set `K6_EXEC`:

```bash
K6_PROFILE=load K6_EXEC=recallOnly k6 run dist/k6/relayer.js
K6_PROFILE=load K6_EXEC=rememberOnly k6 run dist/k6/relayer.js
K6_PROFILE=load K6_EXEC=askOnly k6 run dist/k6/relayer.js
K6_PROFILE=load K6_EXEC=restoreOnly k6 run dist/k6/relayer.js
```

## Metrics

k6 already reports `http_req_duration`, `http_reqs`, `http_req_failed`, and
request throughput. The harness adds:

| Metric | Meaning |
| --- | --- |
| `memwal_remember_enqueue_duration` | Time for `POST /api/remember` to return `202` |
| `memwal_remember_worker_duration` | Time from enqueue to terminal remember job status |
| `memwal_recall_duration` | End-to-end `POST /api/recall` latency |
| `memwal_ask_duration` | End-to-end `POST /api/ask` latency |
| `memwal_restore_duration` | End-to-end `POST /api/restore` latency |
| `memwal_timeout_rate` | Requests or worker polls that timed out |
| `memwal_remember_job_failed_rate` | Remember jobs that ended failed or timed out |
| `memwal_http_errors` | HTTP status `0` or `>=400`, tagged by endpoint |

Thresholds include p95/p99 latency checks for health, remember enqueue, recall,
timeout rate, and remember job failure rate.

## Local Runs

For local capacity testing, start the relayer with the benchmark rate-limit
escape hatch so polling does not dominate the results:

```bash
RATE_LIMIT_DISABLED=1 cargo run --release
```

Use this only on local benchmark instances. Keep normal rate limits enabled for
shared staging unless the environment is isolated for a run.

## Staging Runs

Use the existing benchmark secrets:

```bash
cd services/server/scripts
npm run k6:build

BENCH_SERVER_URL=https://relayer.staging.memwal.ai \
BENCH_ACCOUNT_ID="$BENCH_ACCOUNT_ID" \
BENCH_DELEGATE_KEY="$BENCH_DELEGATE_KEY" \
MEMWAL_NAMESPACE=benchmark \
K6_PROFILE=smoke \
k6 run dist/k6/relayer.js
```

Before running load, stress, or spike against staging, seed the namespace with
several memories and agree on rate limits for OpenAI, SEAL, Walrus, and Sui RPC.
For recall-only tests, a namespace with no memories still exercises query
embedding and pgvector search, but it does not exercise Walrus download or SEAL
decrypt.

## RPC Self-Hosting

The Rust relayer honors `SUI_RPC_URL`; the sidecar now uses the same env var
before falling back to `getJsonRpcFullnodeUrl(SUI_NETWORK)`. If public Sui RPC is
blocked or rate-limited, self-hosting is viable as long as the relayer and
sidecar are both configured with the same private or paid RPC endpoint:

```bash
SUI_NETWORK=testnet
SUI_RPC_URL=https://your-sui-fullnode.example.com
```

Walrus publisher, aggregator, and upload relay are separate from Sui RPC. If
those endpoints are blocked too, override `WALRUS_PUBLISHER_URL`,
`WALRUS_AGGREGATOR_URL`, and `WALRUS_UPLOAD_RELAY_URL` separately.
62 changes: 62 additions & 0 deletions services/server/scripts/k6/k6.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
declare module "k6/http" {
export type Response = {
status: number;
body: unknown;
timings: { duration: number };
json: () => unknown;
};

type Params = {
headers?: Record<string, string>;
tags?: Record<string, string>;
timeout?: string;
};

const http: {
get: (url: string, params?: Params) => Response;
request: (
method: string,
url: string,
body?: string | null,
params?: Params,
) => Response;
};

export default http;
}

declare module "k6/execution" {
const exec: {
vu: { idInTest: number };
scenario: { iterationInTest: number };
};
export default exec;
}

declare module "k6" {
import type { Response } from "k6/http";

export function check<T = Response>(
value: T,
sets: Record<string, (value: T) => boolean>,
): boolean;
export function fail(message: string): never;
export function sleep(seconds: number): void;
}

declare module "k6/metrics" {
export class Counter {
constructor(name: string);
add(value: number, tags?: Record<string, string>): void;
}

export class Rate {
constructor(name: string);
add(value: boolean, tags?: Record<string, string>): void;
}

export class Trend {
constructor(name: string, isTime?: boolean);
add(value: number, tags?: Record<string, string>): void;
}
}
Loading
Loading