Self-contained Go load tester for the Open Bank Project API. Creates its own user(s), bank, accounts, views, and consents, then exercises a broad mix of endpoints across API versions — including a dedicated v7.0.0 sweep designed to stress the http4s request-scoped-connection design and surface pool starvation or deadlock-like behaviour.
# 1. Set OBP_CONSUMER_KEY in .env (copy .env.example first)
cp .env.example .env
$EDITOR .env
# 2. Basic smoke run — auto-creates a user, runs everything with defaults
./run.sh
# 3. v7.0.0 only, fast — skip accounts/consents, one worker, 5 loops
./run.sh --loop-accounts 0 --loop-consents 0 --loop-v7 5
# 4. Concurrent v7 sweep — 40 workers, 10 distinct users, 20 loops each
./run.sh --users-v7 10 --workers-v7 40 --loop-v7 20 \
--loop-accounts 0 --loop-consents 0
# 5. Point at a non-default host
./run.sh --host https://api.example.com --consumer MYKEY
# 6. See all flags
./run.sh --helpFull stdout is tee'd to go-app/last_run.log. Concurrent worker lines are
prefixed [wN]. The v7 sweep ends with a per-endpoint latency table and a
pass/fail verdict flagging pool-starvation signatures.
- Copy
.env.exampleto.envand setOBP_CONSUMER_KEY(and optionallyOBP_API_HOST,OBP_USERNAME,OBP_PASSWORD). - Run
./run.sh— it auto-creates a throwaway user if no username/password is supplied.
All flags are long-form. Values can also come from environment variables
(.env is auto-sourced if present). Flags override env.
| Flag | Env var | Default | Description |
|---|---|---|---|
--consumer KEY |
OBP_CONSUMER_KEY |
— (required) | Consumer key for DirectLogin |
--host URL |
OBP_API_HOST |
http://127.0.0.1:8080 |
OBP API host including protocol and port |
--username NAME |
OBP_USERNAME |
auto-created | DirectLogin username |
--password PWD |
OBP_PASSWORD |
auto-created | DirectLogin password |
--loop-accounts N |
OBP_LOOP_ACCOUNTS |
3 |
Number of account-creation loops |
--loop-consents N |
OBP_LOOP_CONSENTS |
3 |
Number of consent create/use/revoke loops |
--loop-v7 N |
OBP_LOOP_V7 |
3 |
Number of times each worker sweeps v7.0.0 endpoints (0 to skip) |
--workers-v7 N |
OBP_WORKERS_V7 |
1 |
Concurrent goroutines for the v7.0.0 sweep |
--users-v7 N |
OBP_USERS_V7 |
1 |
Distinct users to spread v7 workers across |
--help |
Show usage |
Setup phase (serial, once per run):
- Auto-creates user if needed
- DirectLogin → token
- Grants system roles (
CanCreateEntitlementAtAnyBank,CanCreateBank) - Creates a throwaway bank (
ldtst-<random>) - Grants bank-level
CanCreateAccount - If
--loop-v7 > 0: grants the system/bank roles v7 endpoints require (CanGetAnyUser,CanGetCacheConfig,CanGetCacheInfo,CanGetCacheNamespaces,CanGetDatabasePoolInfo,CanGetMigrations,CanGetConnectorHealth,CanDeleteEntitlementAtAnyBank,CanGetCustomersAtOneBank,CanGetCardsForBank)
Accounts + views loop (--loop-accounts iterations):
- Creates an account (v4.0.0)
- Lists views (v5.0.0), creates a custom view (v3.0.0), lists views again
- Fetches account details (v6.0.0), balances (v5.1.0)
- Lists
GET /my/accounts(v3.0.0)
Consent loop (--loop-consents iterations):
- Creates an IMPLICIT consent (v5.1.0)
- Answers challenge (v3.1.0) with sandbox default
"123456" - Lists consents, uses the Consent-JWT to call
/users/current/user_id - Revokes the consent
v7.0.0 sweep (--loop-v7 iterations × --workers-v7 goroutines):
Before the concurrent phase runs, a single v7 entitlement POST + DELETE round-trip is fired using the bootstrap user, to exercise a v7 write path.
Then each worker runs the following in its inner loop:
- No auth:
/root,/banks,/banks/{BANK_ID},/features,/api/versions,/system/connectors,/resource-docs/v7.0.0/obp - User auth:
/users/current,/providers,/banks/{BANK_ID}/accounts,/my/banks/{BANK_ID}/accounts/{ACCOUNT_ID}/account,/banks/{BANK_ID}/accounts/{ACCOUNT_ID}/owner/account,/cards - Role-gated:
/banks/{BANK_ID}/cards,/banks/{BANK_ID}/customers,/users,/users/user-id/{USER_ID},/system/cache/config,/system/cache/info,/system/cache/namespaces,/system/database/pool,/system/migrations - Backend-stall simulation:
/system/connectors/stored_procedure_vDec2019/health(dials the stored-procedure connector; stalls until the client's 30s timeout when that connector isn't configured)
The counterparty endpoint is skipped — no counterparty is created in the setup flow.
./run.sh --users-v7 10 --workers-v7 40 --loop-v7 20
--workers-v7 40→ 40 concurrent goroutines in one process.--users-v7 10→ 10 distinct logged-in users; workers pick tokens round-robin (4 workers per user).- Each worker runs
--loop-v7sweeps sequentially.
All workers share the bootstrap user's bank and first account. Endpoints that
check account ownership (/my/banks/.../accounts/...,
/banks/.../accounts/.../owner/account) will 4xx for non-bootstrap users —
expected, and visible in the per-endpoint status breakdown.
Alternative: run multiple ./run.sh terminals in parallel. Each spins up its
own user/bank/accounts, so they don't collide — but each also pays the full
setup cost, and last_run.log gets overwritten by the last one to finish.
Every v7 request logs its duration inline. Any call > 5s is flagged live with
[SLOW — suspect stall]. At the end of the v7 sweep a per-endpoint table
prints p50 / p95 / p99 / max latency, a status-code breakdown, and counts of
slow/timed-out calls.
A verdict line summarises:
- No stalls: clean — no request exceeded 5s.
- Slow requests but no hard timeouts: contention present; increase
--workers-v7to find the breaking point. - Timeouts observed (cap is the HTTP client's 30s timeout): suspect
deadlock or pool starvation. Under the v7 request-scoped-connection design,
every v7 request pins one HikariCP connection for the whole handler
lifetime; if concurrent workers exceed the pool size, later requests queue
on pool acquire for up to the server's acquire timeout. Mitigations: raise
the pool size, or lower
--workers-v7.
How to tell the failure modes apart:
| Observation | Likely cause |
|---|---|
| No slow, no timeouts, all 2xx | Clean. Pool sized adequately for the concurrency. |
| p95 spikes but no timeouts | Contention, headroom shrinking — step --workers-v7 up to find the breaking point |
| Timeouts cluster at ~30s exactly | Pool starvation (HikariCP acquire timeout). Raise pool, not concurrency |
| Timeouts before 30s, or hangs that never return | Real deadlock at the DB level |
| Only one endpoint slows down | That endpoint holds the connection too long (slow auth lookup, external dial, etc.) |
The stored_procedure_vDec2019/health endpoint is the fastest way to
reproduce pool starvation: it dials an external connector that won't answer
on a dev instance, so every call holds its connection for 30s until the
client times out.
- All stdout is tee'd to
go-app/last_run.log. - Each line from a concurrent worker is prefixed
[wN](1-indexed). - The final block is the per-endpoint stats table + verdict.
- Go 1.24+ (see
go-app/go.mod) - A reachable OBP API instance
- A consumer key valid on that instance
- The test user must be super-admin or have
CanCreateEntitlementAtAnyBankCanCreateBank(granted automatically if the auto-created user has super-admin rights)
run.sh— launcher, flag parsing, forwards togo rungo-app/main.go— setup + accounts + consents phases, flag definitionsgo-app/v7_0_0.go— v7.0.0 sweep, user pool, entitlement round-tripgo-app/stats.go— per-endpoint latency recording, verdictgo-app/last_run.log— full stdout of the last run