bcgov / bcgov/reserve-rec-api

End-to-end load test

Open
#469 1 comment 0 reactions 1 assignee View on GitHub

@marklise is already working on this.

Since Aug 28, 2026.

DUP Reservations
Dominant language
JavaScript
Stars
2
Forks
7
Avg merge
15h 49m
Merged PRs (30d)
57

Description

#### Description:
Simulate realistic peak (e.g. an opening-day release) through search -> hold -> book -> confirm, against a prod-like environment, with the waiting room release rate as the input.

This will gives us the number we actually need to configure the waiting room correctly - the release rate should be set from what the backend can sustain.

**Two load profiles, not one.** The Mode 2 gate covers only checkout-side routes (`/checkout`, `/reservation-flow`, `/cart`, `/facility`, `/booking-confirmation`, `/payment-retry`). `/search` and `/results` are **ungated**. So:

- **Ungated profile** — availability search takes the *full* 14,000-user burst, unthrottled by the waiting room. Search capacity must be sized against the whole crowd.
- **Gated profile** — hold → pay → confirm sees only the admitted release rate.

Running one blended profile at the release rate would under-test search by roughly the ratio of the burst to the release rate, so the two must run concurrently.

The path under test (front-door URLs, so the `/dayuse/api/*` strip function and its hop are in the measured path):

1. `POST /search` — availability search, OpenSearch-backed — **ungated**
2. `GET /product-dates/{collectionId}/{activityType}/{activityId}/{productId}` — availability detail
3. `POST /bookings` — creates the hold, decrements inventory, enforces waiting-room admission
4. `POST /transactions/{bookingId}` — payment handoff (Worldline; stubbed by default, see Notes)
5. Worldline notification webhook — inbound burst
6. `POST /bookings/{bookingId}/complete` — durable write, confirmation email, SMS enqueue
7. Async tail — DynamoDB Streams → OpenSearch reindex (`transDataStream` / `refDataStream`), SES, SNS

Scenarios to cover:

1. **Ramped arrival (gated path)** — step the rate (e.g. 10 → 25 → 50 → 100 completed bookings/min), holding each step to steady state. The last step within SLO is the number this ticket exists to produce.
2. **Full-burst search (ungated path)** — 14,000 users searching, run *concurrently* with scenario 1 so both contend for the same API and OpenSearch domain.
3. **Realistic peak** — an opening-day arrival pattern, fed at the release rate #680 can deliver, with the ungated search profile alongside.
4. **Contention** — a large share of load on a single facility/date/product, so inventory decrements land on one DynamoDB item.
5. **Abandonment** — 30–50% hold and never complete; held inventory releases on expiry and becomes bookable again under load.
6. **Cold start** — the burst against fully cold Lambdas, matching a scheduled opening.

#### Acceptance Criteria:

- **Given** a stepped arrival rate on the gated path, **when** the system reaches steady state at each step, **then** we can state the maximum sustained end-to-end bookings/min at which p95 latency stays within SLO and error rate stays under target. *(SLO and error target to be agreed with Product — proposal: p95 < 3s per step, < 0.5% 5xx.)*
- **Given** the ungated search profile at full 14,000-user burst, **when** run concurrently with the gated profile, **then** search stays within SLO, or the load at which it stops doing so is reported as a separate, explicit number.
- **Given** both numbers, **when** results are written up, **then** they are translated into concrete recommended `MAX_ACTIVE_SESSIONS` / `RELEASE_BATCH_SIZE` / `releaseIntervalSeconds` values for bcgov/reserve-rec-public#680.
- **Given** heavy contention on a single product/date, **when** many users book simultaneously, **then** inventory is never oversold, and conditional-write failure and retry rates are measured and reported.
- **Given** the burst, **when** DynamoDB Streams propagate to OpenSearch, **then** index lag is measured — users searching during the burst see stale availability and will attempt to book sold-out inventory. Report the lag and the resulting failed-booking rate.
- **Given** the peak, **when** confirmations fire, **then** SES send rate and SNS SMS throughput stay within account limits, or the required limit increases are identified.
- **Given** the peak completes, **then** no component degrades silently: Lambda throttles, DynamoDB throttles, OpenSearch queue rejections and API Gateway 429s are each reported at zero or explained.
- **Given** the measured worst-case checkout duration, **when** compared against `ADMISSION_TTL_MINUTES` and `product.holdDuration.minutes`, **then** confirm an admission token cannot expire mid-checkout.
- **Given** the run completes, **then** the bottleneck is named — which resource saturates first, and what raising it would take — and the result is explicitly recorded as a **shared** front-door/API ceiling, not per-product headroom.

#### Development Checklist:

- [ ] Choose and stand up the harness (recommend **k6** — ramping, thresholds, CI-friendly output). Nothing exists in either repo today
- [ ] Drive the front-door hostname with the `/dayuse` prefix so the strip function and extra hop are measured
- [ ] Build two concurrent profiles — ungated search at full burst, gated booking at the release rate — plus realistic browse ratio, abandonment and repeat searches
- [ ] Pre-mint a pool of Cognito test users / JWTs — booking requires authentication, and live token minting is itself rate-limited
- [ ] Seed a prod-like data set: enough collections / activities / products / inventory that search isn't hitting one hot document
- [ ] Stub the Worldline payment step by default; agree a separate small-scale burst against their sandbox to establish their ceiling
- [ ] Confirm WAF / rate-limit allowlisting for load-generator IPs
- [ ] Instrument: per-endpoint p50/p95/p99 and error rate; Lambda duration/errors/throttles/init duration; DynamoDB consumed capacity, throttles, conditional-check failures per table; OpenSearch CPU, JVM pressure, indexing lag; API Gateway 4xx/5xx/latency; CloudFront per-behaviour metrics; SES and SNS send rates
- [ ] Run scenarios 1–6
- [ ] Write up: sustained capacity number (gated), search ceiling (ungated), bottleneck, recommended waiting-room config, and any changes needed before release
- [ ] Establish a teardown/reset path so the run can be repeated after fixes

#### Dependencies

- Blocked by: prod-like test environment (matching Lambda memory and concurrency, OpenSearch instance sizing, DynamoDB capacity mode); Worldline decision (stub vs sandbox); WAF allowlist; seeded test data
- Blocking: waiting-room production configuration; release readiness sign-off
- Related to: bcgov/reserve-rec-public#680 — consumes this ticket's throughput number as its release-rate input
- Related to: #207 — unified front door; the capacity measured here is shared across every tenant mounted on it

**Relevant documentation as reference**

- `src/handlers/bookings/` (`POST/public.js`, `_bookingId/complete/POST/public.js`), `src/handlers/transactions/`, `src/handlers/search/`, `src/handlers/productDates/`, `src/handlers/worldlineNotification/`, `src/handlers/dynamoStream/`
- Front door routing and the `/dayuse/api/*` strip function: `lib/front-door-stack/front-door-stack.js` in reserve-rec-public

**Definition of Ready**

- [ ] Acceptance criteria are included
- [ ] Wireframes are included (if applicable)
- [ ] Design / Solution is accepted by Product Owner (if applicable)
- [ ] Dependencies are identified (technical, business, regulatory/policy)
- [ ] Story has been estimated (under 13 pts)

**Definition of Done**
- In progress:
- [ ] Acceptance criteria are tested (Functionality meets the acceptance criteria defined in the ticket)
- [ ] UI meets accessibility requirements
- [ ] Unit tests are written
- [ ] Work is traceable in GitHub
- [ ] PR linked to ticket number
- [ ] If needed/required - Dev adds flag/label to highlight any migration steps necessary prior to PROD deployment
- Code review:
- [ ] Code is peer reviewed and has passed CI/CD tests
- QA:
- [ ] Acceptance criteria are tested (Functionality meets the acceptance criteria defined in the ticket)
- [ ] Code is potentially shippable to the production environment
- [ ] Functional features have been tested and passed by QA
- [ ] UI components tested by designer
- [ ] Code is deployed to PROD when moved to 'done' column (unless requested otherwise by PO)
- PO Review:
- [ ] Acceptance criteria are tested (Functionality meets the acceptance criteria defined in the ticket)
- [ ] Reviewed and approved by Product Owner

#### Notes:

- **Environment fidelity is the whole ballgame.** A number measured against a scaled-down dev environment is not usable for configuring production. If a prod-like environment isn't available, that needs saying before this is estimated.
- **This is a shared ceiling.** The front door is a multi-product entry point — day-use is tenant #1 at `/dayuse`, and a future tenant (e.g. `/backcountry`) would strip to the same `/api/*` on the same API Gateway origin. The number produced here is total capacity across all tenants, not day-use headroom. It should be re-measured when a second product mounts.
- **Worldline is a third party** — we can't load-test their production endpoint without agreement. Default plan: stub the payment call to measure our own throughput, then a small separate burst against their sandbox to confirm their limits and the notification webhook's inbound capacity.
- **Fail-open risk:** the waiting-room check in `POST /bookings` catches lookup errors and falls through to `waitingRoomActive = false`. Under load, a throttle on the waiting-room table would silently disable queue enforcement. Worth measuring rather than assuming — and note there is no Lambda@Edge backstop on the front door's `/dayuse/api/*` behaviour, so origin enforcement is the only layer.
- **Cold-start amplification:** no reserved or provisioned concurrency is configured. At a scheduled opening every container is cold, and each fetches the HMAC key from Secrets Manager on first invocation (cached per container, not globally). Scenario 6 exists to measure this.
- **Single-item contention** is the likeliest hard failure: an opening-day release concentrates thousands of users on one facility/date, so inventory decrements and the queue's `admittedCount` counter all land on individual DynamoDB items.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.