Snapshot of zapier-decisions (optimal NATS config study)
This commit is contained in:
commit
13f1663238
42 changed files with 6600 additions and 0 deletions
155
LOG.md
Normal file
155
LOG.md
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
# Action log
|
||||
|
||||
## 2026-09-12 — exhaustive optimal-config study
|
||||
|
||||
- 8c/16G ZFS factorial: file/memory × r=1/r=3, 4 KiB, reconnect, UDP echo, MQTT QoS0 on nats-a :1883 (then removed).
|
||||
- JS memory r=1 4p **65k** pubs/s; file r=3 1p **14.3k**; MQTT QoS0 **45k**; UDP p99 **0.36 ms**.
|
||||
- Combined report + DL360 Gen10 projection: `reports/optimal-config/REPORT.{md,html,pdf}`.
|
||||
- Verdict: keep NATS+JetStream; no MQTT/UDP fabric; metal = 3 NVMe + 10GbE, expect JS r=3 ~40–80k.
|
||||
|
||||
## 2026-09-12 — maximized NS1 NATS study
|
||||
|
||||
- `pct set` 510–513 to 8 cores / 16 GiB; `max_mem: 8G`; tmpfs 8G on JetStream for the ladder only.
|
||||
- Run `20260912T053120Z`. JS file 1p 128 B **7.4k → 17.4k (2.35×)**; JS memory 4p **36.4k**; ping p99 **1.38 → 0.68 ms**.
|
||||
- tmpfs unmounted after; product streams on ZFS again. Resources and max_mem **left raised**.
|
||||
- Report: `reports/ns1-maximize/`.
|
||||
|
||||
## 2026-09-12 — NS1-host NATS study (second run)
|
||||
|
||||
- All orchestration, nats bench, matplotlib, pandoc, and weasyprint ran on **NS1.GEORGELAMBERT.ORG** (`70.88.205.138`). Laptop not in the path.
|
||||
- Script: `verae-nats-cluster/scripts/study-on-ns1.sh` (refuses unless hostname is NS1).
|
||||
- Run `20260912T051237Z`. Report: `reports/ns1-host/nats-cluster-bench-ns1.{md,html,pdf}`.
|
||||
- Host load ~8.5. JS 1p 128 B ~7.4k durable pubs/s; ping p99 1.38 ms. Methodology + fine-tuning in that report.
|
||||
|
||||
## 2026-09-12 — NATS cluster speed report
|
||||
|
||||
- Bench from LXC 510 vs nats-a/b/c (`20260912T045131Z`).
|
||||
- Full write-up with charts in `reports/nats-cluster-bench.{md,html,pdf}`.
|
||||
- JetStream r=3 file ~16k durable 128 B pubs/s; ping RTT ~0.3 ms; flood queueing 150–500 ms.
|
||||
|
||||
## 2026-09-12 — test env on the 3-node cluster
|
||||
|
||||
- Fleet, keep, middleware, billing, and access planes `NATS_URL` → `10.10.10.21–23:4222`.
|
||||
- Streams ZAPIER_JOBS/EVENTS/WEBHOOKS/USAGE + VERAE_ARCHIVE replicas=3.
|
||||
- px-worker keep worm/tree 200 on the cluster. Backup timer enabled. Uptime loop on px-worker.
|
||||
- MOCK_VERAE still true. Zapier push still waiting on login. nkeys/mTLS not flipped (would require client creds).
|
||||
|
||||
## 2026-09-12 — NATS cluster on three Proxmox LXC
|
||||
|
||||
- New repo **verae-nats-cluster**: LXC 511/512/513 (`nats-a/b/c`, `10.10.10.21–23`) JetStream cluster on `vmbr1`.
|
||||
- Lab loopback `127.0.0.1:4222` unchanged until cut-over.
|
||||
- Test: stream replicas=3; same configs later move to distinct hardware.
|
||||
|
||||
## 2026-09-11 — proxmox worker + deploy/monitor/backup
|
||||
|
||||
- NS1 **is** Proxmox (`vmbr0` public, `vmbr1` `10.10.10.0/24`). New repo **verae-proxmox** creates LXC **510** `verae-px-worker` at `10.10.10.20`.
|
||||
- NATS for guests: socat `10.10.10.1:4222` → `127.0.0.1:4222` (never vmbr0).
|
||||
- **verae-uptime** off-box HTTPS GET; **verae-backup** tar snapshots; **verae-deploy** tagged npm ci.
|
||||
- Fleet `overlays/ns1/` checked in; `start.sh` no longer rewrites fleet/machines JSON.
|
||||
- User systemd + linger for keep and fleet.
|
||||
|
||||
## 2026-09-11 — bootstrap
|
||||
|
||||
- Created this repo to record decisions while executing cleanup items 1–5 (and a second pass on 2–5).
|
||||
- Assumption: single developer, lab only; breaking changes allowed.
|
||||
|
||||
## Step 1 — account-balance is the prepaid source of truth
|
||||
|
||||
- Persist books to `BOOKS_PATH` (atomic JSON).
|
||||
- Mutations go through `balance.adjust` / `usage.recorded`; edge cache `balanceCents` is updated from the books reply only.
|
||||
- Local cache mutate only when `NATS_URL` and `ACCOUNT_BALANCE_URL` are both unset (tests).
|
||||
|
||||
## Step 2 — identity join
|
||||
|
||||
- `zappier-identity` mailbox: `verae.identity.bind` / `verae.identity.lookup`.
|
||||
- Portal + middleware tenants store `veraeUserId`; JWT never public.
|
||||
|
||||
## Step 3 — access planes are the public doors
|
||||
|
||||
- zappier-edge binds loopback (`127.0.0.1:3000`).
|
||||
- Public: access-web `:3021`, access-api `:3022`, access-leaf `:3023`, access-zapier `:3024`, access-staff `:3025`.
|
||||
|
||||
## Step 4 — leaf hardening
|
||||
|
||||
- `verae-nats-accounts` holds nats.conf + subject policy.
|
||||
- Leaf token required; billing subjects denied in policy file and access-leaf.
|
||||
|
||||
## Step 5 — fleet spawn
|
||||
|
||||
- Prefer local machines for HTTP `spawn.command` services.
|
||||
- Disable lan-134 unless `FLEET_ENABLE_LAN134=1`.
|
||||
- SSH spawn timeout 8s; failed hosts skipped.
|
||||
|
||||
## 2026-09-11 — catalog live doors
|
||||
|
||||
- Bottom of https://zapier.georgelambert.org/ lists each HTTPS door with description, screenshot, and Open link.
|
||||
- UI-REVIEW.pdf includes the live-door atlas. Walkthrough §13.
|
||||
|
||||
## 2026-09-11 — Caddy host names live
|
||||
|
||||
- Installed `/etc/caddy/sites/zapier-services.georgelambert.org.caddy` on NS1. HTTPS: portal, api, leaf, zap, staff, iam, login, cs, sales, acct, mw, fleet.
|
||||
- Not proxied: NATS, keep, workers, loopback edge.
|
||||
|
||||
## 2026-09-11 — host names
|
||||
|
||||
- Mapped every running service to **NS1.GEORGELAMBERT.ORG** (`70.88.205.138`).
|
||||
- Suggested `*.zapier.georgelambert.org` names for public doors; only `zapier.georgelambert.org` (docs) and `git.georgelambert.org` exist in Caddy today.
|
||||
|
||||
## 2026-09-11 — docs sync, bootstrap, NS1 all-in-one
|
||||
|
||||
- Catalog and ops docs: public portal is access-web `:3021/portal/`; edge is loopback; IAM + keep listed.
|
||||
- New `verae-bootstrap` repo: clone + npm for `ns1-all-in-one`, `control-plane`, `ns1-archive`, `lan-worker`.
|
||||
- Fleet starts `staff-iam` and `staff-session`. NS1 all-in-one uses edge `:13000` because host `:3000` is taken; keep still owns archive workers. `better-sqlite3` must be rebuilt on Linux (`npm rebuild`).
|
||||
|
||||
## 2026-09-11 — verae-keep on NS1
|
||||
|
||||
- New repo `verae-keep`: keep `:3860` restarts crashed units; watch `:3861` restarts keep; `guard.sh` restarts watch.
|
||||
- Honors admin pause/stop via intent files + `POST /intent`. Fleet writes `data/<instance>/intent.json` (local and SSH).
|
||||
- Tested on 70.88.205.138: crash-restart, pause-hold, watch respawn. webhook-deliver brought back. NATS observed only.
|
||||
|
||||
## 2026-09-11 — IAM JSON doors closed
|
||||
|
||||
- CS/sales/accounting/staff JSON list and sales PUT pricing now go through IAM (401 JSON, 302 HTML).
|
||||
- Fleet service env sets `STAFF_AUTH=1` with `STAFF_IAM_URL`.
|
||||
- Re-walk: cs credit 200 agent=`cs`; cs export 403; operator Fleet POST 200; admin `/me` `permissions:['*']`.
|
||||
|
||||
## 2026-09-11 — IAM on lab, portal door, hardening
|
||||
|
||||
- Restarted fleet with `STAFF_IAM_URL`. Walk: cs credits (agent=`cs`) and is 403 on accounting export; operator fleet POST 200, cs 403; admin `/me` permissions `*`.
|
||||
- Portal public door `verae-access-web` `:3021/portal/` (static + `/portal/api` → loopback edge).
|
||||
- IAM sessions persist in JSON; login rate-limit per IP+username (8/10min); credits stamp IAM username as authz principal.
|
||||
- lan-134 remains disabled unless `FLEET_ENABLE_LAN134=1`. Receipt PDF branded (indigo header + legal footer).
|
||||
|
||||
## 2026-09-11 — staff IAM
|
||||
|
||||
- New `verae-staff-iam` :3028 — users, roles, permissions, sessions, audit UI.
|
||||
- Admin login uses IAM when `STAFF_IAM_URL` is set; tabs hide without permission.
|
||||
- CS/sales/accounting/access-staff/fleet mutating APIs check `cs.credit`, `sales.quote`, `accounting.export`, `fleet.operate`, etc.
|
||||
- Seed: admin/cs/sales/accounting/operator. Shared staff key is fallback only.
|
||||
|
||||
## 2026-09-11 — last three UI leftovers
|
||||
|
||||
- Swagger `/docs` stays stock; banner names it OpenAPI explorer.
|
||||
- Shared template `verae-staff-ui` for CS + access-staff.
|
||||
- `STAFF_COOKIE_DOMAIN` + nginx example for multi-host.
|
||||
|
||||
## 2026-09-11 — names, staff session, exclusive jobs.events
|
||||
|
||||
- Account-balance stores display names (`customer.put` + lookup by name). Edge writes names on customer create/edit; CS/sales/accounting/staff join from edge if the ledger has no name.
|
||||
- New repo `verae-staff-session` (:3027). Department HTML redirects when `STAFF_AUTH=1`.
|
||||
- `JOBS_EVENTS_EXCLUSIVE=1` makes `verae-jobs-events` the durable consumer; middleware webhook router backs off.
|
||||
- Catalog index is cards. Disabled fleet machines are grey, not degraded yellow.
|
||||
|
||||
## 2026-09-11 — UI design-system pass
|
||||
|
||||
- Restyled CS/sales/accounting/access-staff to portal indigo; dollars + names.
|
||||
- Fleet ⋯ menus, role chips, Docs cards, 0.0.0.0 copy, simulator indigo (orange = faults).
|
||||
- Portal masked API key; admin customer drawer; empty invoice illustrations.
|
||||
- Catalog system-ui. New independent repo **UI-Docs** (walkthrough, screenshots, UI-REVIEW.pdf).
|
||||
|
||||
## Second pass (2–5)
|
||||
|
||||
- Identity used from portal when `IDENTITY_URL` set.
|
||||
- Leaf loads allow-list from nats-accounts `policy.json`.
|
||||
- Staff plane is a first-class mailbox (`verae-access-staff`).
|
||||
- jobs.events mailbox process `verae-jobs-events` (passthrough + count).
|
||||
3
NATS.md
Normal file
3
NATS.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# NATS — zapier-decisions
|
||||
|
||||
None. Documentation repo.
|
||||
18
README.md
Normal file
18
README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# zapier-decisions
|
||||
|
||||
Lab log of architecture decisions, actions taken, and open todos while cleaning the Verae × Zapier stack.
|
||||
|
||||
**Forgejo:** https://git.georgelambert.org/marchon/zapier-decisions
|
||||
|
||||
| File | What |
|
||||
|------|------|
|
||||
| [STATUS.md](STATUS.md) | **Current status** (2026-09-12) |
|
||||
| [reports/nats-cluster-bench.md](reports/nats-cluster-bench.md) | NATS cluster speed report (charts, HTML, PDF) |
|
||||
| [reports/ns1-host/nats-cluster-bench-ns1.md](reports/ns1-host/nats-cluster-bench-ns1.md) | Second study: all code on NS1 (methodology + tuning) |
|
||||
| [reports/ns1-maximize/nats-cluster-bench-ns1.md](reports/ns1-maximize/nats-cluster-bench-ns1.md) | Maximized: 8c/16G/tmpfs vs baseline |
|
||||
| [reports/optimal-config/REPORT.md](reports/optimal-config/REPORT.md) | All studies + MQTT/UDP + DL360 projection |
|
||||
| [LOG.md](LOG.md) | Chronological actions |
|
||||
| [TODO.md](TODO.md) | Open items |
|
||||
| [decisions/](decisions/) | One file per decision |
|
||||
|
||||
No runtime. Not a NATS mailbox.
|
||||
158
STATUS.md
Normal file
158
STATUS.md
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# Current status — 2026-09-12
|
||||
|
||||
Verae Time × Zapier is a **working lab** on one Proxmox host (**NS1.GEORGELAMBERT.ORG**, `70.88.205.138`). Public HTTPS doors, catalog, keep/fleet, and a **test 3-node NATS cluster** on separate LXC guests are up. The test environment **points at that cluster**. Zapier Platform `push` and live `api.veraetime.net` are still waiting on operator login / bind credentials.
|
||||
|
||||
**Progress repo:** this file in [zapier-decisions](https://git.georgelambert.org/marchon/zapier-decisions).
|
||||
**Catalog:** https://zapier.georgelambert.org/packages/zapier-decisions/STATUS.pdf
|
||||
**Monorepo:** `master-zapier-plan-draft` (`main` / `master`).
|
||||
|
||||
---
|
||||
|
||||
## Live right now
|
||||
|
||||
| Layer | State |
|
||||
|-------|--------|
|
||||
| Catalog | https://zapier.georgelambert.org/ (PDF links path-relative; module/model books work) |
|
||||
| Public doors | portal, api, zap, iam, mw, fleet, git — last off-box GET 8/8 OK |
|
||||
| Keep + fleet | Running. systemd + linger **enabled** for next reboot (current processes were not double-started as systemd) |
|
||||
| Test NATS | **3-node JetStream on `vmbr1`:** LXC 511/512/513 `nats-a/b/c` at `10.10.10.21–23`. Cluster name `verae`, 2 routes each. |
|
||||
| Product streams | `ZAPIER_JOBS`, `ZAPIER_EVENTS`, `ZAPIER_WEBHOOKS`, `ZAPIER_USAGE`, `VERAE_ARCHIVE` — **replicas=3** |
|
||||
| Modules on that cluster | Fleet, keep, middleware (`natsConnected: true`), billing, access planes. `NATS_URL` lists all three URLs. |
|
||||
| Host loopback `:4222` | Still listening; **clients no longer use it** |
|
||||
| Worker CT **510** `verae-px-worker` | `10.10.10.20` on `vmbr1`. Keep worm+tree **200** on the cluster |
|
||||
| Backup | Timer `verae-backup.timer` enabled; snapshots under `/SSD2/backups/verae/` |
|
||||
| Uptime | Loop on px-worker (`:3870/status`), last tick `failed: 0` |
|
||||
|
||||
Client URL (private `vmbr1` only):
|
||||
|
||||
```text
|
||||
nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222
|
||||
```
|
||||
|
||||
Repo: https://git.georgelambert.org/marchon/verae-nats-cluster
|
||||
|
||||
### Cluster message speed (`20260912T045131Z`)
|
||||
|
||||
Measured from LXC **510** (not a nats-* server) against `10.10.10.21–23`.
|
||||
|
||||
**Full report (charts + explanations):** [HTML](https://zapier.georgelambert.org/packages/zapier-decisions/reports/nats-cluster-bench.html) · [PDF](https://zapier.georgelambert.org/packages/zapier-decisions/reports/nats-cluster-bench.pdf) · [markdown](reports/nats-cluster-bench.md)
|
||||
|
||||
**Second study (all code on NS1.GEORGELAMBERT.ORG / 70.88.205.138, run `20260912T051237Z`):** [HTML](https://zapier.georgelambert.org/packages/zapier-decisions/reports/ns1-host/nats-cluster-bench-ns1.html) · [PDF](https://zapier.georgelambert.org/packages/zapier-decisions/reports/ns1-host/nats-cluster-bench-ns1.pdf) · [markdown](reports/ns1-host/nats-cluster-bench-ns1.md). Orchestrator, nats bench, charts, HTML, and PDF ran on 138; traffic was LXC 510 → 511–513 on `vmbr1`. Includes full methodology and fine-tuning notes. Host load ~8.5/40 during the run; JetStream 1p 128 B was **7.4k** durable pubs/s (vs ~16k on the quieter first ladder); ping p99 **1.38 ms**.
|
||||
|
||||
**Maximized study (`20260912T053120Z`):** 8 cores + 16 GiB on 510–513, `max_mem: 8G`, JetStream **tmpfs** for the ladder (ZFS restored after). veth/10G unchanged. [HTML](https://zapier.georgelambert.org/packages/zapier-decisions/reports/ns1-maximize/nats-cluster-bench-ns1.html) · [PDF](https://zapier.georgelambert.org/packages/zapier-decisions/reports/ns1-maximize/nats-cluster-bench-ns1.pdf).
|
||||
|
||||
| Metric | Baseline 051237Z | Maximized | Ratio |
|
||||
|--------|------------------|-----------|-------|
|
||||
| JS file r=3 1p 128 B | 7,393 pubs/s | **17,388** | **2.35×** |
|
||||
| JS memory r=3 4p 128 B | — | **36,355** | ~4.9× vs baseline file 1p |
|
||||
| Core 1p 128 B pub | 502,502 | 599,004 | 1.19× |
|
||||
| Ping p99 | 1.377 ms | **0.684 ms** | **2.01× faster** |
|
||||
|
||||
Live cluster **keeps 8 cores / 16 GiB / max_mem 8G**. Product streams are back on ZFS (tmpfs was study-only).
|
||||
|
||||
**Optimal-config study (`20260912T055851Z`):** same 8c/16G cluster on **ZFS**, plus r=1 vs r=3, memory vs file, reconnect tax, UDP echo, MQTT QoS0. Combined report with DL360 Gen10 projection: [HTML](https://zapier.georgelambert.org/packages/zapier-decisions/reports/optimal-config/REPORT.html) · [PDF](https://zapier.georgelambert.org/packages/zapier-decisions/reports/optimal-config/REPORT.pdf).
|
||||
|
||||
| Finding | Number |
|
||||
|---------|--------|
|
||||
| JS file r=3 1p 128 B (8c ZFS) | 14,330 pubs/s |
|
||||
| JS file r=1 1p | 18,888 (r=3 costs ~24% on same host) |
|
||||
| JS memory r=1 4p | **64,923** (lab JS ceiling) |
|
||||
| MQTT QoS0 | 44,862 pubs/s (not durable; ~15× slower than core) |
|
||||
| UDP p99 510→511 | 0.363 ms vs NATS ping p99 1.14 ms |
|
||||
| Reconnect-per-msg p99 | 1.75 ms vs persistent ping 1.14 ms |
|
||||
|
||||
**Do not switch the fabric to MQTT or UDP.** Optimal lab: file r=3 for jobs/archive, memory r=3 optional for events, persistent NATS connections. Metal: 3× DL360 Gen10, local NVMe, 10GbE — projected JS file r=3 **~40–80k** 128 B pubs/s (not measured).
|
||||
|
||||
Short tables: [BENCH.md](https://git.georgelambert.org/marchon/verae-nats-cluster/src/branch/main/BENCH.md). Re-run: `bash scripts/bench.sh` on NS1.
|
||||
|
||||
| Kind | Load | Result |
|
||||
|------|------|--------|
|
||||
| Core NATS | 1p1s 50k×128 B | ~1.20M msgs/s aggregate |
|
||||
| Core NATS | 8p8s 200k×128 B | ~2.01M msgs/s aggregate |
|
||||
| Core NATS | 4p4s 50k×1 KiB | ~630k msgs/s (~616 MB/s) |
|
||||
| JetStream file r=3 | 1–4 pubs, 128 B | ~16k durable pubs/s |
|
||||
| JetStream file r=3 | 4 pubs, 1 KiB | ~13.5k durable pubs/s |
|
||||
| Ping (sequential RTT) | 1k×128 B | avg 0.307 ms, p99 0.734 ms |
|
||||
| Flood (burst queueing) | 8p 20k×128 B | avg 454 ms |
|
||||
|
||||
Product job/event/archive traffic is the JetStream row. Ping is one-message delay; flood is the mailbox catching up after a burst.
|
||||
|
||||
---
|
||||
|
||||
## Built and checked in (Forgejo, each its own repo)
|
||||
|
||||
IAM, portal on access-web, keep/watch/guard, fleet floors, host-deps, catalog host-move docs, plus:
|
||||
|
||||
| Repo | Job |
|
||||
|------|-----|
|
||||
| verae-uptime | Off-box HTTPS GET of public doors |
|
||||
| verae-backup | SQLite / keep / worm-tree tar; restore dry-run default |
|
||||
| verae-deploy | Tagged checkout + npm ci on the target OS |
|
||||
| verae-proxmox | LXC 510 worker provision |
|
||||
| verae-nats-cluster | LXC 511–513 JetStream cluster |
|
||||
|
||||
---
|
||||
|
||||
## Still open
|
||||
|
||||
| Item | Notes |
|
||||
|------|--------|
|
||||
| Zapier Platform `push` | Needs Zapier login (see below) |
|
||||
| `MOCK_VERAE=false` | Live `api.veraetime.net` + admin bind credentials (see below) |
|
||||
| NATS nkeys/mTLS | Cluster is up **without** accounts so existing `NATS_URL`s work. `verae-nats-accounts` remains the sketch. Flip only after every client has credentials. Isolation today is `vmbr1`, not nkeys. |
|
||||
| lan-134 | Still disabled (SSH not working) |
|
||||
| Reboot test | systemd should own keep/fleet after reboot; do not also `nohup guard.sh` |
|
||||
| Hardware NATS | Same three configs, three boxes, private NIC — change IPs in `cluster.env` |
|
||||
|
||||
---
|
||||
|
||||
## Once we have a Zapier login
|
||||
|
||||
Zapier cloud only talks **HTTPS** to our Zap plane (`https://zap.zapier.georgelambert.org`). No NATS, no `api.veraetime.net`.
|
||||
|
||||
1. `zapier login` (or `zapier-platform login`).
|
||||
2. In `packages/verae-zapier` (and `verae-activate` if the tiny app is still wanted):
|
||||
- `zapier-platform register` (or attach an existing app id).
|
||||
- `MIDDLEWARE_BASE_URL=https://zap.zapier.georgelambert.org` (or `https://mw.zapier.georgelambert.org` if skipping the Zap plane).
|
||||
- `zapier-platform build` and `validate`.
|
||||
3. `zapier-platform push` as a **private** app.
|
||||
4. Invite the workspace; create a Zap:
|
||||
- Auth = portal **API key** (`zmw_…`), not a Verae JWT.
|
||||
- Test **Create Timestamp and Wait**, **Create Timestamp** (async `jobId`), **Find Job Status**, **Timestamp Completed** REST Hook.
|
||||
5. Fix anything `validate` / `push` flags (auth labels, samples, 402/403 mapping).
|
||||
6. When the contract is stable, bump the platform version and invite a real user.
|
||||
|
||||
Do **not** put NATS URLs or Verae passwords into Zapier.
|
||||
|
||||
---
|
||||
|
||||
## Once `MOCK_VERAE=false` and live `api.veraetime.net`
|
||||
|
||||
Middleware already has the live HTTP client. Still needed:
|
||||
|
||||
1. `VERAE_API_BASE_URL=https://api.veraetime.net` and working **login** (username/password → JWT). JWT stays in middleware (`tokenRef`); Zapier never sees it.
|
||||
2. Confirm OpenAPI vs our client:
|
||||
- `POST /api/timestamp` → **202** `{ jobId }`
|
||||
- `GET /api/status/{jobId}` until `completed` / `failed`
|
||||
- `POST /api/verify`
|
||||
- Batch create/status if used
|
||||
3. **Hash lookup is not on the live OpenAPI** — `veraeClient.lookupHash` returns **501** when not mocking. Central-chain “find by SHA-256” needs a Verae API gap fill or our own store.
|
||||
4. Map live status JSON into the receipt shape the Zap app expects (`certificate`, `timestamp`, `blockIndex`, `sha256`).
|
||||
5. Token refresh, 401 retry, and 402/403 mapping against real errors.
|
||||
6. One live seal in the lab, then REST Hook delivery to `hooks.zapier.com`.
|
||||
|
||||
---
|
||||
|
||||
## Submit → jobId → wait → receipt
|
||||
|
||||
**Yes — that is the designed path**, and it already runs in **mock**.
|
||||
|
||||
1. Zap (or curl) `POST /zapier/v1/timestamp` or `/timestamp/wait`.
|
||||
2. Middleware `createTimestamp` calls Verae `POST /api/timestamp` (or the mock) and gets **`jobId`**.
|
||||
3. It publishes **`verae.zapier.jobs.watch`** on NATS (the 3-node cluster).
|
||||
4. **Wait:** subscribe to **`verae.zapier.jobs.events`** for that `jobId`, or poll `GET /api/status/{jobId}`.
|
||||
5. Job-poller hits Verae status until `completed`.
|
||||
6. Event `timestamp.completed` carries status; webhook-deliver POSTs the Zapier REST Hook.
|
||||
7. Response includes **certificate / timestamp / blockIndex** (mock uses `mock-cert-{jobId}`, `blockIndex: 42`). Async create returns `{ jobId, sha256, existing }` immediately; wait returns the finished status. Same SHA-256 is idempotent (`existing: true`).
|
||||
|
||||
If wait times out, middleware returns **`pending` + `jobId`** so the hook can finish the Zap. Live Verae must match that HTTP contract; we have **not** proven a real chain seal yet (`MOCK_VERAE` is still true).
|
||||
15
TODO.md
Normal file
15
TODO.md
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Open todos
|
||||
|
||||
- [ ] Live `api.veraetime.net` with `MOCK_VERAE=false` and admin bind credentials.
|
||||
- [x] Three-node NATS JetStream cluster on distinct Proxmox LXC (`verae-nats-cluster`; test env `NATS_URL` cut over; streams replicas=3).
|
||||
- [ ] NATS nkeys/mTLS on that cluster (accounts file is still the lab stand-in; not flipped so clients keep working on `vmbr1`).
|
||||
- [x] Exclusive JetStream consumer for `verae.zapier.jobs.events` on `verae-jobs-events` (`JOBS_EVENTS_EXCLUSIVE=1`; middleware skips the router).
|
||||
- [x] Auth on CS/sales/accounting HTML via `verae-staff-session` (`STAFF_AUTH=1`).
|
||||
- [x] Staff IAM: named users, roles, permissions (`verae-staff-iam` :3028).
|
||||
- [ ] Zapier Platform `push` of a private app.
|
||||
- [x] Move portal static files fully into `verae-access-web` (`/portal/` public door; API proxied to loopback edge).
|
||||
- [x] Turn IAM on for the running lab (fleet `STAFF_IAM_URL` + restart).
|
||||
- [x] IAM hardening: JSON sessions, login rate-limit, JSON 401 as well as HTML 302, credit principal = IAM username.
|
||||
- [x] Host keep-alive (`verae-keep`) on NS1 with watch+guard; honors admin pause/stop.
|
||||
- [x] Docs synced to IAM, public portal `:3021`, keep, bootstrap server types.
|
||||
- [x] `verae-bootstrap` clones all module repos and installs deps per server type.
|
||||
6
decisions/D001-prepaid-sot.md
Normal file
6
decisions/D001-prepaid-sot.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# D001 — Account-balance is the only prepaid writer
|
||||
|
||||
**Status:** accepted
|
||||
**Step:** 1
|
||||
|
||||
Prepaid cents, credits, usage, and payments persist in `zappier-account-balance` (`books.json`). zappier-edge `balanceCents` is a cache filled from the books reply. Tests without NATS/HTTP books still mutate the cache so unit tests stay hermetic.
|
||||
6
decisions/D002-identity.md
Normal file
6
decisions/D002-identity.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# D002 — veraeUserId is the join key, not the JWT
|
||||
|
||||
**Status:** accepted
|
||||
**Step:** 2
|
||||
|
||||
Public token remains the zappier API key. Chain JWT stays server-side (`tokenRef`). `zappier-identity` is the mailbox for bind/lookup so portal and middleware share one map.
|
||||
6
decisions/D003-public-doors.md
Normal file
6
decisions/D003-public-doors.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# D003 — Access planes are the only public HTTP doors
|
||||
|
||||
**Status:** accepted
|
||||
**Step:** 3
|
||||
|
||||
Zapier → `:3024`, browser → `:3021`, customer API → `:3022`, leaf → `:3023`, staff → `:3025`. zappier-edge listens on `127.0.0.1:3000` for identity, Stripe, admin, and as upstream for those planes.
|
||||
6
decisions/D004-leaf-accounts.md
Normal file
6
decisions/D004-leaf-accounts.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# D004 — Leaf cannot touch billing, even if authz is skipped
|
||||
|
||||
**Status:** accepted
|
||||
**Step:** 4
|
||||
|
||||
`verae-nats-accounts/policy.json` is the allow-list. access-leaf refuses any target not in the leaf set. nats.conf documents INTERNAL vs LEAF accounts for when the lab cluster is replaced with a real accounts-enabled nats-server.
|
||||
6
decisions/D005-fleet-local-first.md
Normal file
6
decisions/D005-fleet-local-first.md
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# D005 — HTTP services spawn locally; lan-134 stays off
|
||||
|
||||
**Status:** accepted
|
||||
**Step:** 5
|
||||
|
||||
SSH to `70.88.205.134` blocked keepFloor. HTTP apps (`spawn.command`) prefer `kind=local`. `lan-134` is disabled unless `FLEET_ENABLE_LAN134=1`. Failed SSH hosts are skipped after 8s.
|
||||
6
package.json
Normal file
6
package.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "zapier-decisions",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Architecture decisions and action log for the Verae × Zapier stack"
|
||||
}
|
||||
BIN
reports/charts/core-throughput.png
Normal file
BIN
reports/charts/core-throughput.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
reports/charts/core-vs-js.png
Normal file
BIN
reports/charts/core-vs-js.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
BIN
reports/charts/delay-percentiles.png
Normal file
BIN
reports/charts/delay-percentiles.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
BIN
reports/charts/js-throughput.png
Normal file
BIN
reports/charts/js-throughput.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
reports/charts/payload-size.png
Normal file
BIN
reports/charts/payload-size.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
1278
reports/nats-cluster-bench.html
Normal file
1278
reports/nats-cluster-bench.html
Normal file
File diff suppressed because it is too large
Load diff
312
reports/nats-cluster-bench.md
Normal file
312
reports/nats-cluster-bench.md
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
**Progress report** · 2026-09-12 · run `20260912T045131Z` (UTC)
|
||||
|
||||
This is the full write-up of the test-environment NATS cluster bench: what was measured, how, the numbers, the charts, and what they mean for Verae Time × Zapier. Short tables also live in [`verae-nats-cluster/BENCH.md`](https://git.georgelambert.org/marchon/verae-nats-cluster/src/branch/main/BENCH.md). Raw logs and CSVs are in that repo under `results/20260912T045131Z/`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Executive summary
|
||||
|
||||
The test cluster is three JetStream nodes on private `vmbr1` (LXC 511–513). The bench client is a **fourth** guest (LXC 510), so the numbers are cluster-plus-network, not a process talking to itself on loopback.
|
||||
|
||||
Two different systems were measured, on purpose:
|
||||
|
||||
| System | What it is | What we got |
|
||||
|--------|------------|-------------|
|
||||
| **Core NATS** | Fire-and-forget pub/sub. No disk, no replica ack. | About **0.75–2.0 million msgs/s** at 128 B, depending on fan-out. At 1 KiB, about **630k msgs/s** and **~616 MB/s** aggregate. |
|
||||
| **JetStream file, replicas=3** | Durable, replicated — **this is what product streams use**. | About **16k durable 128 B pubs/s**, about **13.5k** at 1 KiB. Pull consume keeps up with publish at ~11k msgs/s each side. |
|
||||
| **Ping delay** | One message at a time, publish then wait. | **avg 0.307 ms**, **p99 0.734 ms**, max 2.76 ms (1k × 128 B). |
|
||||
| **Flood delay** | Publishers dump a batch; subscriber drains. | **150–505 ms**. That is **queueing under burst**, not wire time. |
|
||||
|
||||
**For this product:** timestamp jobs, job events, webhooks, and archive puts go through JetStream r=3. Plan capacity against **~16k durable msgs/s** on this stand, not the million-msg core numbers. A quiet job-event hop is a fraction of a millisecond. If a mailbox falls behind, delay jumps into hundreds of milliseconds — that is the flood column.
|
||||
|
||||
Core NATS is still useful: it is the ceiling for non-durable fan-out on this host, and it shows `vmbr1` and the nats-server processes are not the JetStream bottleneck. JetStream is.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why this test exists
|
||||
|
||||
The lab cut the test environment over to the three-node cluster. Before treating that cluster as the message fabric for keep, fleet, middleware, billing, and archive workers, we needed:
|
||||
|
||||
1. **Throughput at several loads** — one publisher vs many, 128 B vs 1 KiB, core vs durable.
|
||||
2. **Delay characteristics** — both the quiet path (one message RTT) and the overloaded path (burst into a mailbox).
|
||||
3. **A client that is not a nats-* server** — otherwise we would be measuring loopback on the broker.
|
||||
|
||||
This is a **lab stand on one Proxmox host**, not three metal boxes. It answers “is this cluster in the right order of magnitude for our traffic?” It does not replace a soak test on dedicated disks.
|
||||
|
||||
---
|
||||
|
||||
## 3. Topology
|
||||
|
||||
```text
|
||||
vmbr1 10.10.10.0/24 (not on vmbr0, not public)
|
||||
-----------------------------------------------
|
||||
LXC 510 LXC 511 LXC 512 LXC 513
|
||||
verae-px-worker nats-a nats-b nats-c
|
||||
10.10.10.20 10.10.10.21 10.10.10.22 10.10.10.23
|
||||
bench client :4222 client :4222 :4222
|
||||
:6222 routes :6222 :6222
|
||||
:8222 loopback :8222 :8222
|
||||
```
|
||||
|
||||
- Cluster name: `verae`. Each server has two routes to the other two.
|
||||
- Client URL: `nats://10.10.10.21:4222,nats://10.10.10.22:4222,nats://10.10.10.23:4222`
|
||||
- HTTP monitor is **loopback :8222** inside each guest. Zapier cloud never talks to NATS.
|
||||
- Product streams already on this cluster (`ZAPIER_JOBS`, `ZAPIER_EVENTS`, `ZAPIER_WEBHOOKS`, `ZAPIER_USAGE`, `VERAE_ARCHIVE`) use **file** storage and **replicas=3**. The JetStream bench used the same settings on a throwaway stream `benchstream`.
|
||||
- Host `127.0.0.1:4222` is still listening on NS1; **clients no longer use it**.
|
||||
|
||||
Credits for the stack: Scott Lindsey, George Lambert, NATS.IO, Grok-Code.
|
||||
|
||||
---
|
||||
|
||||
## 4. Method
|
||||
|
||||
### 4.1 Tools
|
||||
|
||||
| Piece | Role |
|
||||
|-------|------|
|
||||
| `nats` CLI **0.1.6** | Throughput (`nats bench --no-progress --csv`). Its min/avg/max are **publisher rate spread**, not delay. |
|
||||
| `scripts/latency.mjs` | Two connections, header timestamp `t`, delay = receive time − send time. |
|
||||
| `scripts/bench.sh` | Runs the ladder from NS1 via `pct exec` on VMID 510. |
|
||||
| `scripts/bench-report.py` | Turns logs into the short `BENCH.md` table. |
|
||||
|
||||
Re-run on NS1, from `verae-nats-cluster`:
|
||||
|
||||
```bash
|
||||
bash scripts/bench.sh
|
||||
```
|
||||
|
||||
### 4.2 Load ladder
|
||||
|
||||
**Core NATS** (subject `bench.core.*`):
|
||||
|
||||
| Run | Publishers | Subscribers | Messages | Payload |
|
||||
|-----|------------|-------------|----------|---------|
|
||||
| `core-1p1s-50k-128` | 1 | 1 | 50,000 | 128 B |
|
||||
| `core-4p4s-100k-128` | 4 | 4 | 100,000 | 128 B |
|
||||
| `core-8p8s-200k-128` | 8 | 8 | 200,000 | 128 B |
|
||||
| `core-4p4s-50k-1k` | 4 | 4 | 50,000 | 1024 B |
|
||||
|
||||
**JetStream** (`--js --storage file --replicas 3 --stream benchstream`). The stream is deleted between loads so the name never collides:
|
||||
|
||||
| Run | Shape | Messages | Payload |
|
||||
|-----|-------|----------|---------|
|
||||
| `js-1p-20k-128-r3` | 1 publisher | 20,000 | 128 B |
|
||||
| `js-4p-50k-128-r3` | 4 publishers | 50,000 | 128 B |
|
||||
| `js-4p-20k-1k-r3` | 4 publishers | 20,000 | 1024 B |
|
||||
| `js-2p2s-20k-128-r3` | 2 pub + 2 pull sub | 20,000 | 128 B |
|
||||
|
||||
**Delay** (core subjects, two connections):
|
||||
|
||||
| Run | Mode | Count | Pubs | Payload |
|
||||
|-----|------|-------|------|---------|
|
||||
| `lat-ping-1k-128` | **ping** — publish, wait for that message, repeat | 1,000 | 1 | 128 B |
|
||||
| `lat-1p-5k-128` | **flood** — publish all, then drain | 5,000 | 1 | 128 B |
|
||||
| `lat-4p-10k-128` | flood | 10,000 | 4 | 128 B |
|
||||
| `lat-8p-20k-128` | flood | 20,000 | 8 | 128 B |
|
||||
| `lat-4p-5k-1k` | flood | 5,000 | 4 | 1024 B |
|
||||
|
||||
Ping answers “how long does one quiet hop take?” Flood answers “what happens to the last message if we burst N messages into a mailbox?” Those are different questions. Mixing them is how 0.3 ms and 400 ms get confused.
|
||||
|
||||
### 4.3 How to read nats bench columns
|
||||
|
||||
- **Pub msgs/s** — rate at which publishers finished their share.
|
||||
- **Sub msgs/s** — rate at which subscribers finished. With several subscribers on the same subject, core NATS **fans out**, so sub rate can exceed pub rate.
|
||||
- **Aggregate msgs/s** — nats CLI `NATS Pub/Sub stats` line (pub+sub work in one number). Useful as a headline; do not treat it as “the network carried this many unique messages.”
|
||||
- Empty JetStream sub cells mean that run was publish-only (durable write, no consumer in the same process).
|
||||
|
||||
---
|
||||
|
||||
## 5. Throughput results
|
||||
|
||||
### 5.1 Core NATS
|
||||
|
||||

|
||||
|
||||
| Run | Aggregate msgs/s | Pub msgs/s | Pub MB/s | Sub msgs/s | Sub MB/s |
|
||||
|-----|------------------|------------|----------|------------|----------|
|
||||
| `core-1p1s-50k-128` | 1,200,836 | 791,094 | 96.57 | 747,461 | 91.24 |
|
||||
| `core-4p4s-100k-128` | 1,521,256 | 316,312 | 38.61 | 1,299,634 | 158.65 |
|
||||
| `core-8p8s-200k-128` | 2,007,937 | 333,957 | 40.77 | 1,790,736 | 218.60 |
|
||||
| `core-4p4s-50k-1k` | 630,460 | 247,747 | 241.94 | 510,216 | 498.26 |
|
||||
|
||||
**What this chart is saying.** Adding subscribers raises **aggregate** and **sub** rates because each published message is delivered to every subscriber. Publish rate does **not** climb the same way: 1 publisher at 128 B already pushes ~791k msgs/s; 4 and 8 publishers sit around 310–335k msgs/s **each process slower**, while fan-out on the sub side goes to 1.3M then 1.8M.
|
||||
|
||||
That publisher slowdown is expected on this stand. The four/eight publisher processes and the four/eight subscribers all run **inside one LXC** (510) against three broker LXCs on the **same Proxmox CPU and `vmbr1`**. Per-publisher logs show a wide spread (example, 4p core 128 B: 79k–524k msgs/s among the four pubs). That is CPU scheduling and client-side contention, not a NATS cluster that only has one fast node.
|
||||
|
||||
**1:1 at 128 B** is the cleanest core number: **~791k pub, ~747k sub, ~1.20M aggregate**. The cluster and the bridge can move three-quarter-million small messages per second fire-and-forget from a single client pair.
|
||||
|
||||
### 5.2 Payload size (core)
|
||||
|
||||

|
||||
|
||||
Same 4p4s shape, two sizes:
|
||||
|
||||
| Payload | Aggregate msgs/s | Aggregate MB/s | Pub msgs/s | Sub msgs/s |
|
||||
|---------|------------------|----------------|------------|------------|
|
||||
| 128 B | 1,521,256 | 185.70 | 316,312 | 1,299,634 |
|
||||
| 1 KiB | 630,460 | 615.68 | 247,747 | 510,216 |
|
||||
|
||||
Message rate falls; **byte rate rises** (186 MB/s → 616 MB/s aggregate). We are leaving the “tiny message, CPU/syscall bound” region and entering “copying bytes across `vmbr1`.” Job JSON and archive metadata sit nearer 128 B–1 KiB than megabyte blobs (blobs are HTTP/WORM, not NATS payloads).
|
||||
|
||||
### 5.3 JetStream r=3 file
|
||||
|
||||

|
||||
|
||||
| Run | Pub msgs/s | Pub MB/s | Sub msgs/s | Notes |
|
||||
|-----|------------|----------|------------|-------|
|
||||
| `js-1p-20k-128-r3` | **16,155** | 1.97 | — | publish-only |
|
||||
| `js-4p-50k-128-r3` | **16,607** | 2.03 | — | four pubs, same ceiling |
|
||||
| `js-4p-20k-1k-r3` | **13,493** | 13.18 | — | 1 KiB still disk/replica bound |
|
||||
| `js-2p2s-20k-128-r3` | 10,965 | 1.34 | 10,942 | pull consumers keep up |
|
||||
|
||||
**Four publishers do not make JetStream four times faster.** 1p and 4p at 128 B are both ~16k msgs/s. The limiter is **synchronous replication to three file-backed replicas**, not client parallelism. That is the result we wanted to see: the bench stream is behaving like a replicated log, not like core fan-out.
|
||||
|
||||
Pull consume (`js-2p2s`) is slightly slower on publish (~11k) because the same run is also reading. Pub and sub stay matched (10,965 vs 10,942): the consumer is not the straggler.
|
||||
|
||||
1 KiB durable write is ~13.5k msgs/s (~13.2 MB/s). Bytes go up; message rate dips only a little. JetStream here is **ack/fdatasync/replica** bound, not payload-copy bound, in this size range.
|
||||
|
||||
### 5.4 Core vs JetStream (same client, same cluster)
|
||||
|
||||

|
||||
|
||||
The log scale is required: core publish is **~15–50×** JetStream publish on this stand.
|
||||
|
||||
| Shape | Core pub msgs/s | JS r=3 file pub msgs/s | Ratio |
|
||||
|-------|-----------------|------------------------|-------|
|
||||
| 1 publisher, 128 B | 791,094 | 16,155 | ~49× |
|
||||
| 4 publishers, 128 B | 316,312 | 16,607 | ~19× |
|
||||
| 4 publishers, 1 KiB | 247,747 | 13,493 | ~18× |
|
||||
|
||||
This is not JetStream “losing.” Core is allowed to forget a message the instant the server accepts it. JetStream on file with replicas=3 must **record it on a majority** before the publish acks. Our product streams (`ZAPIER_*`, `VERAE_ARCHIVE`) chose that trade on purpose: a job event that survives one LXC dying is worth ~16k msgs/s instead of ~800k.
|
||||
|
||||
If we ever need core-like rates for a signal that may drop, that signal should not be on a replicated file stream.
|
||||
|
||||
---
|
||||
|
||||
## 6. Delay results
|
||||
|
||||

|
||||
|
||||
| Run | Kind | Count | min | avg | p50 | p90 | p99 | max |
|
||||
|-----|------|-------|-----|-----|-----|-----|-----|-----|
|
||||
| `lat-ping-1k-128` | ping (sequential RTT) | 1000 | 0.254 ms | **0.307 ms** | 0.286 ms | 0.332 ms | **0.734 ms** | 2.763 ms |
|
||||
| `lat-1p-5k-128` | flood | 5000 | 149.3 ms | 238.6 ms | 248.8 ms | 274.3 ms | 279.4 ms | 279.7 ms |
|
||||
| `lat-4p-5k-1k` | flood | 5000 | 155.1 ms | 211.7 ms | 217.6 ms | 223.3 ms | 227.8 ms | 228.4 ms |
|
||||
| `lat-4p-10k-128` | flood | 10000 | 174.2 ms | 263.2 ms | 266.7 ms | 299.1 ms | 304.2 ms | 304.5 ms |
|
||||
| `lat-8p-20k-128` | flood | 20000 | 304.6 ms | 453.7 ms | 466.3 ms | 499.9 ms | 505.1 ms | 505.6 ms |
|
||||
|
||||
The dashed line on the chart is 1 ms. Only **ping** lives there.
|
||||
|
||||
### 6.1 Ping — the quiet hop
|
||||
|
||||
One publisher, one subscriber, two connections, wait for each message before sending the next.
|
||||
|
||||
- **min 0.254 ms** — guest → `vmbr1` → a nats-server → `vmbr1` → guest.
|
||||
- **p50 0.286 ms / avg 0.307 ms** — typical.
|
||||
- **p99 0.734 ms** — still under a millisecond.
|
||||
- **max 2.763 ms** — one outlier in 1,000 samples (GC, scheduler, or a slow route). Not the tail we design for.
|
||||
|
||||
A middleware `jobs.watch` publish followed by a waiter on `jobs.events` is this shape when the poller is keeping up. Compared with HTTPS to Zapier (tens to hundreds of milliseconds) or a live Verae `GET /api/status/{jobId}`, NATS RTT is noise.
|
||||
|
||||
### 6.2 Flood — queueing under burst
|
||||
|
||||
Publishers write the whole batch as fast as they can, then the subscriber drains. Each message’s delay is “how long was I in the buffer before the subscriber got to me?”
|
||||
|
||||
That is why:
|
||||
|
||||
- **min is already ~150–300 ms** — even the first messages wait behind a burst that filled the socket/client queue.
|
||||
- **p50 ≈ p99 ≈ max** — a queue drain has a tight distribution: everyone waits for roughly the same backlog.
|
||||
- **8p × 20k is ~450 ms avg** — twice the messages of 4p × 10k, roughly twice the wait. Linear in backlog, not in cluster diameter.
|
||||
|
||||
Flood is **not** a measurement of NATS being slow. The ping column proves the hop is ~0.3 ms. Flood is a measurement of **what operators will see if a consumer stalls** (job-events mailbox, webhook deliver, archive reply). Backlog time ≈ `queued_messages / consume_rate`.
|
||||
|
||||
### 6.3 1 KiB flood vs 128 B flood
|
||||
|
||||
4 publishers, 5k messages at 1 KiB: avg **212 ms**, slightly **faster** than 4p 10k × 128 B (263 ms) because the **count is half**, even though each message is 8× larger. Again: delay here tracks **how many messages are queued**, not payload size, in this range.
|
||||
|
||||
---
|
||||
|
||||
## 7. What this means for Verae × Zapier
|
||||
|
||||
Product subjects on this cluster:
|
||||
|
||||
| Address | Kind | Bench analogue |
|
||||
|---------|------|----------------|
|
||||
| `verae.zapier.jobs.watch` | work queue (JetStream) | JS durable pub ~16k/s |
|
||||
| `verae.zapier.jobs.events` | events | JS + ping if waiters keep up; flood if they do not |
|
||||
| `verae.zapier.webhooks.deliver` | work queue | JS durable |
|
||||
| `verae.zapier.usage` | optional | JS durable |
|
||||
| `verae.billing.*` | request-reply | ping (quiet RTT) |
|
||||
| `verae.archive.put` / `query` / `reply.*` | JetStream + broadcast query | JS durable; query fan-out is closer to core but still JS-backed puts |
|
||||
|
||||
**Capacity.** 16k durable 128 B pubs/s is **~1.4×10⁹ messages/day** if you could fill the pipe. We will not. Zapier HTTPS, live `api.veraetime.net`, WORM bloom checks, and human Zap runs sit far below that. This cluster is not the product bottleneck on NS1.
|
||||
|
||||
**Latency budget.** A timestamp wait is: HTTP in → NATS watch → poll Verae → NATS event → HTTP out (or REST Hook). The NATS pieces are **sub-millisecond** when caught up. Do not spend time “optimizing NATS RTT” until Zapier/Verae HTTP is in the same band.
|
||||
|
||||
**Backlogs.** The failure mode that *does* show up in these numbers is flood delay. If webhook-deliver or job-events consumers pause (keep stopped, replica floor, a blocked HTTPS post to `hooks.zapier.com`), waiters will see **hundreds of milliseconds to seconds** of queue time. Fleet replica floors and keep exist to prevent that, not because 0.3 ms is too slow.
|
||||
|
||||
**Hardware move.** Same three configs, three boxes, private NIC. Expect:
|
||||
|
||||
- Core numbers to change with NIC and CPU (maybe up, maybe down).
|
||||
- JetStream numbers to change **more**, because they are disk + fsync + replica RTT. Distinct SSDs should help; a slow shared datastore would hurt.
|
||||
- Ping RTT to grow by whatever the real NIC and switch add (still likely low milliseconds on a LAN).
|
||||
|
||||
---
|
||||
|
||||
## 8. Limits of this measurement
|
||||
|
||||
1. **One Proxmox host.** LXC 510–513 share cores, memory, and the host’s disk. Replica=3 on file is **three files on the same underlying storage**, not three failure domains. HA of “one disk dies” is **not** proven. HA of “one LXC process dies” is the actual claim.
|
||||
2. **Short runs.** Tens of thousands of messages, seconds of wall time. No compaction, no multi-hour page-cache eviction, no snapshot/restore during load.
|
||||
3. **No TLS, no nkeys.** `verae-nats-accounts` is still a sketch. Auth would add CPU; it would not turn 16k into 800k.
|
||||
4. **One bench client.** All publishers live in 510. A fleet of workers on several CTs might publish more into JetStream until disk/replicas saturate — the 1p vs 4p JS result says that saturation is already ~16k from one CT.
|
||||
5. **nats 0.1.6** does not report delay. Anyone reading `min | avg | max msgs` on a bench log as microseconds will get the wrong story. Delay is only `latency.mjs`.
|
||||
6. **Core aggregate ≠ unique messages.** Fan-out double-counts. Use pub or sub columns when comparing to JetStream.
|
||||
7. **Not a Zapier or Verae API bench.** Those are still blocked on operator login / live credentials.
|
||||
|
||||
---
|
||||
|
||||
## 9. How to reproduce
|
||||
|
||||
On NS1 (Proxmox), from the `verae-nats-cluster` checkout:
|
||||
|
||||
```bash
|
||||
bash scripts/status.sh # 3/3 JetStream
|
||||
bash scripts/bench.sh # writes results/<utc>/ and BENCH.md
|
||||
```
|
||||
|
||||
The client VMID defaults to **510**. Override with `CLIENT_VMID=…`. `NATS_URL` comes from `client.env`.
|
||||
|
||||
Rebuild this progress report (charts + HTML + PDF) from the monorepo:
|
||||
|
||||
```bash
|
||||
python3 packages/zapier-decisions/scripts/build-nats-bench-report.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Appendix — environment and files
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| Run stamp | `20260912T045131Z` |
|
||||
| Client | LXC 510 `verae-px-worker` `10.10.10.20` |
|
||||
| Servers | 511/512/513 `nats-a/b/c` `10.10.10.21–23` |
|
||||
| nats CLI | 0.1.6 linux-amd64 |
|
||||
| JS storage | file, replicas=3, stream `benchstream` (deleted between loads) |
|
||||
| Isolation | `vmbr1` only; no `0.0.0.0` client bind |
|
||||
| Short tables | [BENCH.md](https://git.georgelambert.org/marchon/verae-nats-cluster/src/branch/main/BENCH.md) |
|
||||
| Raw logs | `packages/verae-nats-cluster/results/20260912T045131Z/` |
|
||||
| This report | `packages/zapier-decisions/reports/nats-cluster-bench.{md,html,pdf}` |
|
||||
|
||||
Publisher rate spread (nats CLI, msgs/s, **not** delay):
|
||||
|
||||
| Run | min | avg | max |
|
||||
|-----|-----|-----|-----|
|
||||
| core-4p4s-100k-128 pub | 79,260 | 257,805 | 524,453 |
|
||||
| core-8p8s-200k-128 pub | 41,744 | 71,488 | 152,536 |
|
||||
| core-4p4s-50k-1k pub | 61,936 | 110,271 | 176,262 |
|
||||
| js-4p-50k-128-r3 pub | 4,154 | 5,176 | 6,628 |
|
||||
| js-4p-20k-1k-r3 pub | 3,373 | 4,063 | 5,121 |
|
||||
| js-2p2s-20k-128-r3 pub | 5,485 | 7,081 | 8,678 |
|
||||
|
||||
Wide core spreads are the single-client-CT effect described in §5.1. JetStream spreads are narrow and low — every publisher is waiting on the same replicated write path.
|
||||
BIN
reports/nats-cluster-bench.pdf
Normal file
BIN
reports/nats-cluster-bench.pdf
Normal file
Binary file not shown.
BIN
reports/ns1-host/charts/core-throughput.png
Normal file
BIN
reports/ns1-host/charts/core-throughput.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue