feat: port pneumatube to Rust on fumi-core
This commit is contained in:
parent
5dda6567af
commit
65d52bd0e9
37 changed files with 4232 additions and 1912 deletions
|
|
@ -1,38 +1,51 @@
|
|||
#!/usr/bin/env python3
|
||||
# /// script
|
||||
# requires-python = ">=3.11"
|
||||
# dependencies = ["aiosmtpd>=1.4", "pynacl>=1.5", "noiseprotocol>=0.3.1"]
|
||||
# ///
|
||||
"""Concurrent load test for pneumatube.
|
||||
|
||||
Two targets:
|
||||
|
||||
direct Talks to a `pneumatube` subprocess directly over LMTP/SMTP,
|
||||
bypassing Postfix. Isolates pneumatube's own concurrency
|
||||
behaviour -- in particular, SmolWorker (worker.py) serializes
|
||||
every smolmail operation onto one thread, so this is where that
|
||||
design's throughput ceiling actually shows up.
|
||||
behaviour: with the Rust port there is no serialization point
|
||||
(fumi's store is Send + Sync and every dial is its own Noise
|
||||
session), so the ceiling here belongs to the smolmail server.
|
||||
postfix Drives the same load through the real docker-compose.yml rig
|
||||
(Postfix -> LMTP -> pneumatube), to confirm nothing changes with
|
||||
a real MTA in front. Needs podman (or docker) + compose.
|
||||
|
||||
Both targets spin up a throwaway smolmaild, register a gateway account plus
|
||||
N recipient/sender accounts against it, generate concurrent load, and verify
|
||||
every expected message actually arrived exactly once -- throughput numbers
|
||||
are meaningless if messages were silently dropped or duplicated.
|
||||
Both targets spin up a throwaway bunshin (the Rust smolmail server), a
|
||||
gateway account plus N recipient/sender accounts against it, generate
|
||||
concurrent load, and verify every expected message actually arrived
|
||||
exactly once -- throughput numbers are meaningless if messages were
|
||||
silently dropped or duplicated.
|
||||
|
||||
The sender and recipient accounts are driven in-process through the
|
||||
reference smolmail.py client, which doubles as an interop check: the
|
||||
Python reference client against the Rust gateway and Rust server.
|
||||
|
||||
Usage:
|
||||
uv run python scripts/stress_test.py --target direct
|
||||
uv run python scripts/stress_test.py --target postfix
|
||||
uv run python scripts/stress_test.py --target both --concurrency 40
|
||||
uv run scripts/stress_test.py --target direct
|
||||
uv run scripts/stress_test.py --target postfix
|
||||
uv run scripts/stress_test.py --target both --concurrency 40
|
||||
|
||||
Needs a `smolmail` checkout next to this repo (same default pneumatube
|
||||
itself uses: PNEUMATUBE_SMOLMAIL_REPO, default "../smolmail").
|
||||
Needs a `bunshin` and a `fumi` binary on PATH (or --server-bin/--fumi-bin),
|
||||
and a `smolmail` checkout next to this repo for the load-driving client
|
||||
(--smolmail-repo, default "../smolmail").
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import os
|
||||
import shutil
|
||||
import smtplib
|
||||
import socket
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
|
@ -44,10 +57,24 @@ from pathlib import Path
|
|||
|
||||
from aiosmtpd.controller import Controller
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
|
||||
from pneumatube import vendor # noqa: E402
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_PORT = 1961
|
||||
|
||||
|
||||
def load_smolmail(repo_path: Path):
|
||||
"""Loads the sibling reference client as a library. smolmail.py is a
|
||||
single-file `uv run --script` program, not an installed package, so it
|
||||
is loaded by path from its checkout; this script's inline dependencies
|
||||
(pynacl, noiseprotocol) are declared in the PEP 723 block above so the
|
||||
exec'd module resolves its imports."""
|
||||
module_path = repo_path / "smolmail.py"
|
||||
if not module_path.is_file():
|
||||
raise FileNotFoundError(f"no smolmail.py at {module_path}; pass --smolmail-repo")
|
||||
spec = importlib.util.spec_from_file_location("smolmail", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["smolmail"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def free_port() -> int:
|
||||
|
|
@ -111,42 +138,30 @@ class SmolServer:
|
|||
process: subprocess.Popen | None = None
|
||||
|
||||
|
||||
def parse_pubkey(text: str) -> str:
|
||||
for line in text.splitlines():
|
||||
if "public key" in line:
|
||||
return line.strip().rsplit(" ", 1)[-1]
|
||||
raise RuntimeError(f"no public key line in output:\n{text}")
|
||||
|
||||
|
||||
def start_smolmaild(smolmail_repo: Path, tmp_dir: Path, rate_connections: int,
|
||||
rate_sends: int) -> SmolServer:
|
||||
def start_bunshin(binary: str, tmp_dir: Path, rate_connections: int,
|
||||
rate_sends: int) -> SmolServer:
|
||||
port = free_port()
|
||||
key_path = tmp_dir / "server.key"
|
||||
subprocess.run(
|
||||
["uv", "run", str(smolmail_repo / "smolmaild.py"), "keygen", "--key", str(key_path)],
|
||||
cwd=smolmail_repo, check=True, capture_output=True, text=True)
|
||||
process = subprocess.Popen(
|
||||
["uv", "run", str(smolmail_repo / "smolmaild.py"), "serve",
|
||||
"--key", str(key_path), "--db", str(tmp_dir / "server.db"),
|
||||
"--host", "127.0.0.1", "--port", str(port),
|
||||
# smolmaild's connection/send rate limits are keyed by source IP
|
||||
# (SPEC.md S10); every sender in this script shares one IP, so the
|
||||
# defaults (120 conn/min, 60 sends/min) would measure the rate
|
||||
# limiter, not pneumatube, unless raised for a many-sender test.
|
||||
"--rate-connections", str(rate_connections), "--rate-sends", str(rate_sends)],
|
||||
cwd=smolmail_repo, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
keygen = subprocess.run([binary, "keygen", "--key", str(key_path)],
|
||||
capture_output=True, text=True, check=True)
|
||||
pubkey = None
|
||||
deadline = time.monotonic() + 15
|
||||
while time.monotonic() < deadline:
|
||||
line = process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
if "server public key" in line:
|
||||
pubkey = line.strip().rsplit(" ", 1)[-1]
|
||||
for line in keygen.stdout.splitlines():
|
||||
# "public key: <b32>", not the trailing "Publish the public key..."
|
||||
# sentence, which would otherwise match a looser contains-check.
|
||||
if line.startswith("public key"):
|
||||
pubkey = line.split()[-1]
|
||||
break
|
||||
if pubkey is None:
|
||||
process.kill()
|
||||
raise RuntimeError("smolmaild never printed its public key")
|
||||
raise RuntimeError(f"no public key line in bunshin keygen output:\n{keygen.stdout}")
|
||||
process = subprocess.Popen(
|
||||
[binary, "serve", "--key", str(key_path), "--db", str(tmp_dir / "server.db"),
|
||||
"--host", "127.0.0.1", "--port", str(port),
|
||||
# bunshin's connection/send rate limits are keyed by source IP;
|
||||
# every sender in this script shares one IP, so the defaults would
|
||||
# measure the rate limiter, not pneumatube, unless raised for a
|
||||
# many-sender test.
|
||||
"--rate-connections", str(rate_connections), "--rate-sends", str(rate_sends)],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
wait_for_port("127.0.0.1", port)
|
||||
return SmolServer("127.0.0.1", port, pubkey, process)
|
||||
|
||||
|
|
@ -160,14 +175,11 @@ class SmolAccount:
|
|||
|
||||
|
||||
def make_account(smol, tmp_dir: Path, name: str, server: SmolServer) -> SmolAccount:
|
||||
"""Keygen + trust + register, reusing smolmail.py's own CLI commands
|
||||
directly (the same functions the gateway itself calls) rather than
|
||||
reimplementing the protocol or shelling out per account.
|
||||
|
||||
A bare TCP connect (wait_for_port) can succeed slightly before a
|
||||
containerized smolmaild can actually complete a Noise handshake, so
|
||||
register is retried a few times rather than treating that race as fatal.
|
||||
"""
|
||||
"""Keygen + trust + register, reusing smolmail.py's own CLI functions
|
||||
directly (the reference client) rather than reimplementing the
|
||||
protocol or shelling out per account. Registered with retries, since a
|
||||
bare TCP connect can succeed slightly before the server completes
|
||||
Noise handshakes."""
|
||||
key_path, db_path = tmp_dir / f"{name}.key", tmp_dir / f"{name}.db"
|
||||
store = smol.Store(str(db_path))
|
||||
smol.cmd_keygen(argparse.Namespace(key=str(key_path), force=True), store)
|
||||
|
|
@ -187,14 +199,48 @@ def make_account(smol, tmp_dir: Path, name: str, server: SmolServer) -> SmolAcco
|
|||
return SmolAccount(name, key_path, db_path, address)
|
||||
|
||||
|
||||
def repoint_gateway_account(smol, gateway: SmolAccount, host: str, port: int,
|
||||
pubkey_b32: str) -> None:
|
||||
def make_gateway_account(fumi_bin: str, tmp_dir: Path, name: str, server: SmolServer) -> SmolAccount:
|
||||
"""The gateway's own state is a fumi store, so it is bootstrapped with
|
||||
the fumi CLI: keygen + trust + register, retried for the same
|
||||
slow-to-handshake reason as make_account."""
|
||||
key_path, db_path = tmp_dir / f"{name}.key", tmp_dir / f"{name}.db"
|
||||
run = lambda *args: subprocess.run(
|
||||
[fumi_bin, "--key", str(key_path), "--db", str(db_path), *args],
|
||||
capture_output=True, text=True)
|
||||
run("keygen")
|
||||
trust = run("trust", f"{server.host}:{server.port}", server.pubkey_b32)
|
||||
if trust.returncode != 0:
|
||||
raise RuntimeError(f"fumi trust failed: {trust.stdout}{trust.stderr}")
|
||||
address = f"{name}@{server.host}:{server.port}"
|
||||
for attempt in range(20):
|
||||
register = run("register", address)
|
||||
if register.returncode == 0:
|
||||
return SmolAccount(name, key_path, db_path, address)
|
||||
if attempt == 19:
|
||||
raise RuntimeError(f"fumi register failed: {register.stdout}{register.stderr}")
|
||||
time.sleep(1.0)
|
||||
raise RuntimeError("unreachable")
|
||||
|
||||
|
||||
def repoint_gateway_account(gateway: SmolAccount, host: str, port: int,
|
||||
pubkey_b32: str, fumi_bin: str) -> None:
|
||||
"""Point the gateway's own remembered server connection at a different
|
||||
host string (e.g. a compose service name) without a real round trip --
|
||||
`trust` only ever stores a pin locally (smolmail.py's own cmd_trust)."""
|
||||
store = smol.Store(str(gateway.db_path))
|
||||
smol.cmd_trust(argparse.Namespace(host=host, key_b32=pubkey_b32, force=True), store)
|
||||
store.set_account(smol.Address(gateway.username, host, port))
|
||||
host string (e.g. a compose service name) without a real round trip:
|
||||
`fumi trust` only ever stores a pin locally, and the account row is
|
||||
rewritten directly in the store (fumi schema v2: one state row, pins
|
||||
keyed by host[:port])."""
|
||||
trust = subprocess.run(
|
||||
[fumi_bin, "--key", str(gateway.key_path), "--db", str(gateway.db_path),
|
||||
"trust", f"{host}:{port}", pubkey_b32],
|
||||
capture_output=True, text=True)
|
||||
if trust.returncode != 0:
|
||||
raise RuntimeError(f"fumi trust failed: {trust.stdout}{trust.stderr}")
|
||||
con = sqlite3.connect(gateway.db_path)
|
||||
try:
|
||||
con.execute("UPDATE state SET host = ?, port = ? WHERE id = 1", (host, port))
|
||||
con.commit()
|
||||
finally:
|
||||
con.close()
|
||||
|
||||
|
||||
# --- outbound SMTP sink (stands in for the real relay) -------------------------
|
||||
|
|
@ -283,6 +329,49 @@ def run_concurrent(report: Report, concurrency: int, jobs: list) -> list[bool]:
|
|||
return valid_flags
|
||||
|
||||
|
||||
def gateway_env(gateway: SmolAccount, server: SmolServer, *,
|
||||
require_accepted_sender: bool = False,
|
||||
require_accepted_recipient: bool = False,
|
||||
**extra: str) -> dict[str, str]:
|
||||
env = {**os.environ,
|
||||
"PNEUMATUBE_SMOLMAIL_KEY": str(gateway.key_path),
|
||||
"PNEUMATUBE_SMOLMAIL_DB": str(gateway.db_path),
|
||||
"PNEUMATUBE_SMOLMAIL_DOMAIN": f"{server.host}:{server.port}",
|
||||
"PNEUMATUBE_EMAIL_DOMAIN": "gateway.example",
|
||||
"PNEUMATUBE_FETCH_INTERVAL": "2",
|
||||
# The accept-gate defaults live here, not in the docstring's
|
||||
# "throughput, not gates" comment: the stress test relaxes them
|
||||
# because its throwaway accounts never go through the accept
|
||||
# dance, while the security test needs each mode explicitly.
|
||||
"PNEUMATUBE_REQUIRE_ACCEPTED_SENDER": "1" if require_accepted_sender else "0",
|
||||
"PNEUMATUBE_REQUIRE_ACCEPTED_RECIPIENT": "1" if require_accepted_recipient else "0",
|
||||
"PNEUMATUBE_LISTEN_PROTOCOL": "lmtp",
|
||||
"PNEUMATUBE_LISTEN_HOST": "127.0.0.1",
|
||||
"PNEUMATUBE_SMTP_RELAY_STARTTLS": "0",
|
||||
"PNEUMATUBE_GATEWAY_FROM_ADDRESS": "gateway@gateway.example"}
|
||||
env.update(extra)
|
||||
return env
|
||||
|
||||
|
||||
def start_pneumatube(binary: str, env: dict[str, str], listen_port: int,
|
||||
lookup_port: int, sink_port: int) -> subprocess.Popen:
|
||||
env.update({"PNEUMATUBE_LISTEN_PORT": str(listen_port),
|
||||
"PNEUMATUBE_LOOKUP_HOST": "127.0.0.1",
|
||||
"PNEUMATUBE_LOOKUP_PORT": str(lookup_port),
|
||||
"PNEUMATUBE_SMTP_RELAY_HOST": "127.0.0.1",
|
||||
"PNEUMATUBE_SMTP_RELAY_PORT": str(sink_port)})
|
||||
proc = subprocess.Popen([binary], cwd=REPO_ROOT, env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
wait_for_port("127.0.0.1", listen_port)
|
||||
return proc
|
||||
|
||||
|
||||
def stop_proc(proc: subprocess.Popen) -> None:
|
||||
proc.terminate()
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=5)
|
||||
|
||||
|
||||
# --- direct target ---------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -290,12 +379,12 @@ def run_direct(args: argparse.Namespace) -> None:
|
|||
smolmail_repo = Path(args.smolmail_repo).resolve()
|
||||
with tempfile.TemporaryDirectory(prefix="pneumatube-stress-") as tmp:
|
||||
tmp_dir = Path(tmp)
|
||||
print(f"[direct] starting smolmaild in {tmp_dir}")
|
||||
server = start_smolmaild(smolmail_repo, tmp_dir, args.server_rate_connections,
|
||||
args.server_rate_sends)
|
||||
smol = vendor.load(smolmail_repo)
|
||||
print(f"[direct] starting bunshin in {tmp_dir}")
|
||||
server = start_bunshin(args.server_bin, tmp_dir, args.server_rate_connections,
|
||||
args.server_rate_sends)
|
||||
smol = load_smolmail(smolmail_repo)
|
||||
|
||||
gateway = make_account(smol, tmp_dir, "gatewayuser", server)
|
||||
gateway = make_gateway_account(args.fumi_bin, tmp_dir, "gatewayuser", server)
|
||||
recipients = [make_account(smol, tmp_dir, f"recipient{i}", server)
|
||||
for i in range(args.recipients)]
|
||||
senders = [make_account(smol, tmp_dir, f"sender{i}", server)
|
||||
|
|
@ -309,32 +398,11 @@ def run_direct(args: argparse.Namespace) -> None:
|
|||
sink_controller.start()
|
||||
|
||||
listen_port, lookup_port = free_port(), free_port()
|
||||
env = {**os.environ,
|
||||
"PNEUMATUBE_SMOLMAIL_REPO": str(smolmail_repo),
|
||||
"PNEUMATUBE_SMOLMAIL_KEY": str(gateway.key_path),
|
||||
"PNEUMATUBE_SMOLMAIL_DB": str(gateway.db_path),
|
||||
"PNEUMATUBE_SMOLMAIL_DOMAIN": f"{server.host}:{server.port}",
|
||||
"PNEUMATUBE_EMAIL_DOMAIN": "gateway.example",
|
||||
"PNEUMATUBE_FETCH_INTERVAL": "2",
|
||||
# Throughput, not the accept-gate features, is this script's
|
||||
# concern -- the throwaway recipient/sender accounts here
|
||||
# never go through the accept dance with the gateway.
|
||||
"PNEUMATUBE_REQUIRE_ACCEPTED_SENDER": "0",
|
||||
"PNEUMATUBE_REQUIRE_ACCEPTED_RECIPIENT": "0",
|
||||
"PNEUMATUBE_LISTEN_PROTOCOL": "lmtp",
|
||||
"PNEUMATUBE_LISTEN_HOST": "127.0.0.1",
|
||||
"PNEUMATUBE_LISTEN_PORT": str(listen_port),
|
||||
"PNEUMATUBE_LOOKUP_HOST": "127.0.0.1",
|
||||
"PNEUMATUBE_LOOKUP_PORT": str(lookup_port),
|
||||
"PNEUMATUBE_SMTP_RELAY_HOST": "127.0.0.1",
|
||||
"PNEUMATUBE_SMTP_RELAY_PORT": str(sink_port),
|
||||
"PNEUMATUBE_SMTP_RELAY_STARTTLS": "0",
|
||||
"PNEUMATUBE_GATEWAY_FROM_ADDRESS": "gateway@gateway.example"}
|
||||
env = gateway_env(gateway, server, PNEUMATUBE_LOOKUP_HOST="127.0.0.1",
|
||||
PNEUMATUBE_LOOKUP_PORT=str(lookup_port))
|
||||
print("[direct] starting pneumatube")
|
||||
proc = subprocess.Popen(["uv", "run", "pneumatube"], cwd=REPO_ROOT, env=env,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
||||
proc = start_pneumatube(args.pneumatube_bin, env, listen_port, lookup_port, sink_port)
|
||||
try:
|
||||
wait_for_port("127.0.0.1", listen_port)
|
||||
wait_for_port("127.0.0.1", lookup_port)
|
||||
|
||||
# Phase A: email -> smolmail, concurrent LMTP, mixed valid/invalid.
|
||||
|
|
@ -362,20 +430,16 @@ def run_direct(args: argparse.Namespace) -> None:
|
|||
report_b = Report("smolmail -> email (SEND + poll drain)")
|
||||
run_concurrent(report_b, args.concurrency, jobs)
|
||||
|
||||
drain_deadline = time.monotonic() + max(30.0, total * 0.05)
|
||||
drain_deadline = time.monotonic() + max(60.0, total * 0.25)
|
||||
while sink.count() < report_b.succeeded and time.monotonic() < drain_deadline:
|
||||
time.sleep(0.5)
|
||||
drained = sink.count()
|
||||
print(f" drained {drained}/{report_b.succeeded} into the outbound sink "
|
||||
f"({'PASS' if drained == report_b.succeeded else 'FAIL: lost or stuck messages'})")
|
||||
finally:
|
||||
proc.terminate()
|
||||
with contextlib.suppress(Exception):
|
||||
proc.wait(timeout=5)
|
||||
stop_proc(proc)
|
||||
sink_controller.stop()
|
||||
server.process.terminate()
|
||||
with contextlib.suppress(Exception):
|
||||
server.process.wait(timeout=5)
|
||||
stop_proc(server.process)
|
||||
|
||||
|
||||
# --- postfix target ---------------------------------------------------------------
|
||||
|
|
@ -385,13 +449,18 @@ def podman(*cmd: str, **kwargs) -> subprocess.CompletedProcess:
|
|||
return subprocess.run(["podman", *cmd], text=True, capture_output=True, **kwargs)
|
||||
|
||||
|
||||
# The reference smolmaild runs in a container only for this rig; the uv
|
||||
# image ships a python that can resolve its PEP 723 inline dependencies.
|
||||
UV_IMAGE = "ghcr.io/astral-sh/uv:python3.13-bookworm-slim"
|
||||
|
||||
|
||||
def run_postfix(args: argparse.Namespace) -> None:
|
||||
if shutil.which("podman") is None:
|
||||
print("[postfix] podman not found; skipping", file=sys.stderr)
|
||||
return
|
||||
|
||||
smolmail_repo = Path(args.smolmail_repo).resolve()
|
||||
smol = vendor.load(smolmail_repo)
|
||||
smol = load_smolmail(smolmail_repo)
|
||||
network = "pneumatube-stress-net"
|
||||
env = {**os.environ,
|
||||
"PNEUMATUBE_SMOLMAIL_DOMAIN": "smolmail-dev:1961",
|
||||
|
|
@ -419,18 +488,23 @@ def run_postfix(args: argparse.Namespace) -> None:
|
|||
server_dir = tmp_dir / "server"
|
||||
server_dir.mkdir()
|
||||
keygen = podman("run", "--rm", "-v", f"{smolmail_repo}:/smolmail:ro",
|
||||
"-v", f"{server_dir}:/data",
|
||||
"localhost/pneumatube_pneumatube:latest",
|
||||
"uv", "run", "/smolmail/smolmaild.py", "keygen", "--key", "/data/server.key")
|
||||
"-v", f"{server_dir}:/data", UV_IMAGE,
|
||||
"uv", "run", "/smolmail/smolmaild.py", "keygen",
|
||||
"--key", "/data/server.key")
|
||||
keygen.check_returncode()
|
||||
pubkey = parse_pubkey(keygen.stdout)
|
||||
pubkey = None
|
||||
for line in keygen.stdout.splitlines():
|
||||
if "public key" in line and "Publish" not in line:
|
||||
pubkey = line.strip().rsplit(" ", 1)[-1]
|
||||
if pubkey is None:
|
||||
raise RuntimeError(f"no public key in smolmaild keygen output:\n{keygen.stdout}")
|
||||
|
||||
host_port = free_port()
|
||||
podman("run", "-d", "--name", "pneumatube-stress-smolmaild",
|
||||
"--network", network, "--network-alias", "smolmail-dev",
|
||||
"-p", f"{host_port}:1961",
|
||||
"-v", f"{smolmail_repo}:/smolmail:ro", "-v", f"{server_dir}:/data",
|
||||
"localhost/pneumatube_pneumatube:latest",
|
||||
UV_IMAGE,
|
||||
"uv", "run", "/smolmail/smolmaild.py", "serve",
|
||||
"--key", "/data/server.key", "--db", "/data/server.db",
|
||||
"--host", "0.0.0.0", "--port", "1961",
|
||||
|
|
@ -439,12 +513,21 @@ def run_postfix(args: argparse.Namespace) -> None:
|
|||
wait_for_port("127.0.0.1", host_port)
|
||||
|
||||
server = SmolServer("127.0.0.1", host_port, pubkey)
|
||||
gateway = make_account(smol, tmp_dir, "gatewayuser", server)
|
||||
gateway = make_gateway_account(args.fumi_bin, tmp_dir, "gatewayuser", server)
|
||||
recipients = [make_account(smol, tmp_dir, f"recipient{i}", server)
|
||||
for i in range(args.recipients)]
|
||||
repoint_gateway_account(smol, gateway, "smolmail-dev", 1961, pubkey)
|
||||
# The gateway db was created against 127.0.0.1:<host_port>, which
|
||||
# only exists on the host; the pneumatube container resolves the
|
||||
# compose-network alias instead.
|
||||
repoint_gateway_account(gateway, "smolmail-dev", 1961, pubkey, args.fumi_bin)
|
||||
(data_dir / "gateway.key").write_bytes(gateway.key_path.read_bytes())
|
||||
(data_dir / "gateway.db").write_bytes(gateway.db_path.read_bytes())
|
||||
# A leftover journal from an earlier rig run must not be applied
|
||||
# to the fresh db copy (WAL salts would not match a foreign db,
|
||||
# but the files must not leak into the image either).
|
||||
for name in ("gateway.db-wal", "gateway.db-shm"):
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
(data_dir / name).unlink()
|
||||
|
||||
print("[postfix] starting pneumatube + postfix")
|
||||
subprocess.run(["podman", "compose", "up", "-d"], cwd=REPO_ROOT, env=env, check=True)
|
||||
|
|
@ -495,9 +578,12 @@ def run_postfix(args: argparse.Namespace) -> None:
|
|||
subprocess.run(["podman", "compose", "down"], cwd=REPO_ROOT, env=env)
|
||||
podman("rm", "-f", "pneumatube-stress-smolmaild")
|
||||
podman("network", "rm", network)
|
||||
with contextlib.suppress(Exception):
|
||||
(data_dir / "gateway.key").unlink()
|
||||
(data_dir / "gateway.db").unlink()
|
||||
# The gateway db is a WAL-mode sqlite file; a stale -wal/-shm
|
||||
# left behind would poison the next run's fresh db copy.
|
||||
for name in ("gateway.key", "gateway.db", "gateway.db-wal", "gateway.db-shm"):
|
||||
with contextlib.suppress(FileNotFoundError):
|
||||
(data_dir / name).unlink()
|
||||
with contextlib.suppress(OSError):
|
||||
data_dir.rmdir()
|
||||
|
||||
|
||||
|
|
@ -512,13 +598,18 @@ def main() -> None:
|
|||
parser.add_argument("--recipients", type=int, default=10,
|
||||
help="number of pre-registered valid recipient accounts")
|
||||
parser.add_argument("--smolmail-repo", default="../smolmail")
|
||||
parser.add_argument("--server-bin", default="bunshin",
|
||||
help="bunshin binary for the throwaway test server")
|
||||
parser.add_argument("--fumi-bin", default="fumi",
|
||||
help="fumi binary used to bootstrap the gateway account")
|
||||
parser.add_argument("--pneumatube-bin", default=str(REPO_ROOT / "target/release/pneumatube"))
|
||||
parser.add_argument("--server-rate-connections", type=int, default=6000,
|
||||
help="smolmaild --rate-connections for the throwaway test server "
|
||||
"(default 120/min would rate-limit this whole test, since every "
|
||||
"sender shares one source IP; pass 120 to test against that "
|
||||
"realistic default instead)")
|
||||
help="server --rate-connections for the throwaway test server "
|
||||
"(default 120/min would rate-limit this whole test, since "
|
||||
"every sender shares one source IP; pass 120 to test against "
|
||||
"that realistic default instead)")
|
||||
parser.add_argument("--server-rate-sends", type=int, default=6000,
|
||||
help="smolmaild --rate-sends for the throwaway test server "
|
||||
help="server --rate-sends for the throwaway test server "
|
||||
"(see --server-rate-connections; realistic default is 60)")
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue