0f7d8696de
Implements geode/plans/2026-05-07-connection-scaling.md to push the
single-relay connection ceiling past the ~2k floor measured by
LoadBenchmark.connectionsHeldOpen.
- WebSocketSessionPump: switch outQueue to Channel.UNLIMITED bounded
by an AtomicInteger backlog cap. Idle connections no longer reserve
an 8 192-slot fixed buffer; lazy-allocated head segments cost only
a few hundred bytes per idle session. Slow-client policy preserved:
once outstanding > MAX_OUTGOING_BUFFER, the queue is closed and
the connection drops, so NIP-01 ordering is never silently
violated.
- LocalRelayServer / RelayConfig.NetworkSection: expose CIO
connectionGroupSize / workerGroupSize / callGroupSize so big-VM
operators can tune Ktor's event-loop pools without forking. Switch
to the serverConfig {} + configure {} embeddedServer overload so
CIO tunables can be set.
- LoadBenchmark: add connectionsHeldOpen10k (asserts 10 000 idle
WebSockets settle under a 1 GiB heap ceiling) and
connectionsHeldOpenWithFanout (5 000 subscribers, 10 EPS fan-out
for 10 s, reports p50/p99 last-fanout latency).
- config.example.toml: document the three CIO tunables and the
ulimit -n requirement for operators targeting >1k connections.
97 lines
3.8 KiB
TOML
97 lines
3.8 KiB
TOML
# Example config for geode. Section layout mirrors
|
|
# nostr-rs-relay's config.toml so existing operators can port across.
|
|
#
|
|
# Run with:
|
|
# ./gradlew :geode:run --args="--config /etc/geode.toml"
|
|
#
|
|
# CLI flags override individual values: e.g. `--port 8888` wins over
|
|
# `[network].port`.
|
|
|
|
[info]
|
|
# The wss:// URL clients use to reach this relay (mandatory for NIP-42
|
|
# AUTH challenges). If not set, the relay synthesises one from the
|
|
# [network] section.
|
|
relay_url = "wss://relay.example.com/"
|
|
name = "Example Geode"
|
|
description = "A geode deployment."
|
|
contact = "admin@example.com"
|
|
# Operator pubkey (NIP-11). Optional.
|
|
# pubkey = "..."
|
|
# Override the supported NIPs advertised on the NIP-11 endpoint. If
|
|
# omitted, the relay advertises the NIPs it actually implements.
|
|
# supported_nips = [1, 9, 11, 40, 42, 45, 50, 62]
|
|
|
|
[network]
|
|
host = "0.0.0.0"
|
|
port = 7447
|
|
path = "/"
|
|
# Ktor CIO event-loop pool sizing. Leave commented-out for sensible
|
|
# per-CPU defaults (typical for <2k concurrent connections). Lift on
|
|
# big-VM deployments targeting 10k+ connections — over-threading at
|
|
# low connection counts hurts L1/L2 cache locality, so always
|
|
# benchmark before/after when tuning these.
|
|
#
|
|
# Operators targeting >1k concurrent WebSockets should also raise the
|
|
# OS file-descriptor limit: `ulimit -n 65536` (or higher) before
|
|
# launching, plus a matching `LimitNOFILE=` in any systemd unit. The
|
|
# default of 1024 on most distros caps the relay well below 1k FDs
|
|
# (one per WS plus DB and listening sockets).
|
|
# connection_group_size = 4
|
|
# worker_group_size = 16
|
|
# call_group_size = 64
|
|
|
|
[database]
|
|
# True keeps an in-memory SQLite db (events vanish on restart). Useful
|
|
# for tests; set false + `file = "..."` for persistent storage.
|
|
in_memory = false
|
|
file = "/var/lib/geode/events.db"
|
|
|
|
[options]
|
|
# Drop events whose Schnorr signature does not verify. Strongly
|
|
# recommended for any relay accepting traffic from real clients.
|
|
# Verify Schnorr signatures on every EVENT. Default: true. Disable
|
|
# only for trusted-input scenarios (test fixtures, mirror replays).
|
|
# verify_signatures = true
|
|
|
|
# Require clients to NIP-42 AUTH before REQ/EVENT/COUNT.
|
|
require_auth = false
|
|
|
|
# Reject events whose `created_at` is more than this many seconds in
|
|
# the future. Enforced by RejectFutureEventsPolicy.
|
|
# reject_future_seconds = 1800
|
|
|
|
[limits]
|
|
# Maximum WebSocket frame size. Frames larger than this are dropped at
|
|
# the WS layer. (max_ws_message_bytes maps to the same setting since
|
|
# Ktor's WebSockets plugin only exposes per-frame caps.)
|
|
# max_ws_message_bytes = 1048576
|
|
# max_ws_frame_bytes = 1048576
|
|
|
|
[authorization]
|
|
# Allow / deny lists. Allow is a permissive ceiling; deny still
|
|
# removes specific entries inside it. Enforced by Pubkey/KindAllowDenyPolicy.
|
|
# pubkey_whitelist = ["abcdef...64hex..."]
|
|
# pubkey_blacklist = []
|
|
# kind_whitelist = [0, 1, 3, 7, 1059, 30023]
|
|
# kind_blacklist = [4]
|
|
|
|
[admin]
|
|
# NIP-86 relay management API. When `pubkeys` is non-empty, the relay
|
|
# accepts HTTP POST application/nostr+json+rpc on the same URL,
|
|
# authenticated with NIP-98 HTTP-Auth. Only events signed by one of
|
|
# the listed pubkeys can run admin RPCs (banpubkey / banevent /
|
|
# changerelayname / …). Empty (the default) disables the endpoint.
|
|
# pubkeys = ["abcdef...64hex..."]
|
|
#
|
|
# Canonical URL the relay is reachable at, e.g. behind a reverse proxy.
|
|
# NIP-98 binds requests to this URL via the `u` tag. **Required** in
|
|
# any production deployment — without it, an attacker can spoof the
|
|
# Host header to bypass URL binding.
|
|
# public_url = "https://relay.example.com/"
|
|
|
|
# Path for the JSON snapshot that persists NIP-86 admin state (ban
|
|
# lists + the live NIP-11 doc) across restarts. When unset, admin
|
|
# state is in-memory only and forgotten on every restart. Convention
|
|
# is to place this next to the SQLite event-store file.
|
|
# state_file = "/var/lib/geode/events.db.admin.json"
|