From e74f122e1b4072021a38b1ab800dda960e620d0a Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Fri, 29 May 2026 22:53:21 +0000 Subject: [PATCH] docs: close code and docs audit gaps --- README.md | 8 +- docs/README.md | 49 +++++++++++- docs/src/pages/docs/[...slug].astro | 2 + docs/src/pages/reports/index.astro | 9 ++- docs/src/sidebar.config.ts | 2 +- docs/wiki/api-reference.md | 115 ++++++++++++++++++++++++++++ docs/wiki/architecture.md | 18 ++++- docs/wiki/challenge.md | 19 ++++- docs/wiki/ci-cd-pipeline.md | 16 +++- docs/wiki/getting-started.md | 35 ++++++++- docs/wiki/home.md | 1 + docs/wiki/performance.md | 15 ++++ nginx.conf | 2 +- prod/README.md | 2 +- prod/conf/nginx.conf | 2 +- 15 files changed, 276 insertions(+), 19 deletions(-) create mode 100644 docs/wiki/api-reference.md diff --git a/README.md b/README.md index 11e1a70..5c4bfdb 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Build Check](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/build-check.yml/badge.svg)](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/build-check.yml) [![Main Release](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/main-release.yml/badge.svg)](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/main-release.yml) [![CodeQL](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/codeql.yml/badge.svg)](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/codeql.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -**[Live demo →](https://jonathanperis.github.io/rinha2-back-end-dotnet/)** | **[Documentation →](https://github.com/jonathanperis/rinha2-back-end-dotnet/wiki)** +**[Live demo →](https://jonathanperis.github.io/rinha2-back-end-dotnet/)** | **[Documentation →](https://jonathanperis.github.io/rinha2-back-end-dotnet/docs/)** --- @@ -54,6 +54,8 @@ API available at `http://localhost:9999` | `/clientes/{id}/extrato` | GET | Get account balance statement | | `/healthz` | GET | Health check | +For request/response payloads, validation behavior, and known implementation notes, see the [API Reference](https://jonathanperis.github.io/rinha2-back-end-dotnet/docs/api-reference/). + ## Project Structure ``` @@ -69,8 +71,8 @@ rinha2-back-end-dotnet/ ├── prod/docker-compose.yml — Prod stack with GHCR images ├── nginx.conf — Load balancer (least_conn) ├── grafana/ — Legacy Grafana dashboard provisioning -├── docs/wiki/ — Markdown source for the documentation/wiki pages -├── docs/ — Astro GitHub Pages site + published k6 reports +├── docs/wiki/ — Markdown source rendered to the Pages docs routes +├── docs/ — Astro 6.4 GitHub Pages site using Sätteri + published k6 reports └── .github/workflows/ — CI/CD pipelines ``` diff --git a/docs/README.md b/docs/README.md index 5742746..0bb39ef 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,30 @@ # Docs -Astro static site deployed to GitHub Pages. The Markdown pages under `docs/wiki/` are the source for the public documentation/wiki-style routes. +Astro static site deployed to GitHub Pages. The Markdown pages under `docs/wiki/` are the source for the public documentation routes at `/docs/`. + +## Architecture + +| Area | Source of truth | +|---|---| +| Site framework | Astro `^6.4.2` | +| Markdown renderer | Astro 6.4 `markdown.processor` using `@astrojs/markdown-satteri` | +| Markdown content | `docs/wiki/*.md` | +| Docs route renderer | `docs/src/pages/docs/[...slug].astro` | +| Sidebar/category order | `docs/src/sidebar.config.ts` | +| Reports archive | Static files in `docs/public/reports/*.html` rendered by `docs/src/pages/reports/index.astro` | +| Production base path | `/rinha2-back-end-dotnet` when `NODE_ENV=production` | +| Build output | `docs/out/` | + +The docs are static. There is no server runtime after GitHub Pages publishes the generated HTML. + +## Adding or editing a docs page + +1. Add or edit Markdown in `docs/wiki/`. +2. If the page is new, add the slug to `SECTION_CATEGORIES` in `docs/src/sidebar.config.ts`. +3. Add a label and summary for the slug in `docs/src/pages/docs/[...slug].astro`. +4. Build locally before opening a PR. + +The docs build has a build-time assertion that every sidebar slug maps to a real `docs/wiki/{slug}.md` file. ## Commands @@ -10,8 +34,27 @@ Run from this directory (`docs/`): |---|---| | `bun install` | Install dependencies | | `bun run dev` | Start dev server | -| `bun run build` | Build the Astro site to `./out/` | -| `bun run preview` | Preview production build locally | +| `NODE_ENV=production bun run build` | Build the GitHub Pages-shaped site to `./out/` with the repository base path | +| `bun run preview` | Preview the production build locally | +| `bun run lint` | Run ESLint | + +Use Bun for this package so `bun.lock` stays authoritative. + +## Markdown processor notes + +The site uses Sätteri through Astro's `markdown.processor` API: + +```js +import { satteri } from '@astrojs/markdown-satteri'; + +export default defineConfig({ + markdown: { + processor: satteri(), + }, +}); +``` + +Do not assume arbitrary Remark/Rehype plugin behavior unless it has been tested with Sätteri. If a future docs feature needs Markdown plugins, validate the build and the rendered Pages output before merging. ## Environment diff --git a/docs/src/pages/docs/[...slug].astro b/docs/src/pages/docs/[...slug].astro index b8f11dc..f2d1e13 100644 --- a/docs/src/pages/docs/[...slug].astro +++ b/docs/src/pages/docs/[...slug].astro @@ -9,6 +9,7 @@ const SLUG_LABEL: Record = { challenge: 'Challenge', 'ci-cd-pipeline': 'CI/CD Pipeline', 'getting-started': 'Getting Started', + 'api-reference': 'API Reference', performance: 'Performance', }; @@ -17,6 +18,7 @@ const SECTION_SUMMARIES: Record

Stress Test Reports

-

{reports.length} runs archived — k6 load tests executed on every push to main

+

{reports.length} runs archived — k6 load-test HTML reports preserved from release validation runs and benchmark experiments.

+

Each file is an immutable report artifact. Use the docs performance page for methodology and only update homepage benchmark numbers when they can be traced to a specific archived report.

@@ -101,6 +102,12 @@ function parseReportMeta(filename: string) { font-size: 0.9rem; } + .reports-note { + margin-top: 0.75rem; + max-width: 720px; + line-height: 1.6; + } + .reports-header code { background: rgba(10, 22, 40, 0.8); border: 1px solid var(--rust-dark); diff --git a/docs/src/sidebar.config.ts b/docs/src/sidebar.config.ts index c30b28b..76b8f6f 100644 --- a/docs/src/sidebar.config.ts +++ b/docs/src/sidebar.config.ts @@ -1,7 +1,7 @@ export const SECTION_CATEGORIES = [ { label: "", ids: ["home"] }, { label: "Overview", ids: ["challenge", "architecture"] }, - { label: "Develop", ids: ["getting-started", "performance", "ci-cd-pipeline"] }, + { label: "Develop", ids: ["getting-started", "api-reference", "performance", "ci-cd-pipeline"] }, ] as const; export const SECTION_ORDER = SECTION_CATEGORIES.flatMap(({ ids }) => ids); diff --git a/docs/wiki/api-reference.md b/docs/wiki/api-reference.md new file mode 100644 index 0000000..2a5cc2a --- /dev/null +++ b/docs/wiki/api-reference.md @@ -0,0 +1,115 @@ +# API Reference + +## Base URL + +Local and CI smoke tests reach the API through NGINX: + +```text +http://localhost:9999 +``` + +The production compose file exposes the same public entrypoint on port `9999`. + +## Client IDs and limits + +The implementation has five predefined clients. The API keeps this small map in memory for fast invalid-client rejection, and PostgreSQL seeds the same limits in `docker-entrypoint-initdb.d/rinha.dump.sql`. + +| Client ID | Limit, in cents | +|---:|---:| +| `1` | `100000` | +| `2` | `80000` | +| `3` | `1000000` | +| `4` | `10000000` | +| `5` | `500000` | + +## `POST /clientes/{id}/transacoes` + +Submits a credit or debit transaction. + +### Request body + +```json +{ + "valor": 1000, + "tipo": "c", + "descricao": "deposito" +} +``` + +| Field | Type | Rule | +|---|---|---| +| `valor` | integer | Required positive integer amount in cents (`> 0`) | +| `tipo` | string | Required; `"c"` for credit or `"d"` for debit | +| `descricao` | string | Required, non-empty, maximum 10 characters | + +### Successful response + +The response uses `snake_case` JSON output generated by `System.Text.Json` source generation. + +```json +{ + "id": 1, + "limite": 100000, + "saldo": 1000 +} +``` + +### Status codes + +| Status | When | +|---:|---| +| `200` | Transaction accepted and a balance is returned | +| `404` | Client ID is not one of `1` through `5` | +| `422` | Payload validation fails (`valor <= 0`, invalid `tipo`, missing/empty/long `descricao`) | + +### Current implementation note + +The intended Rinha contract treats a debit that would exceed the client's limit as an unprocessable transaction. The current PostgreSQL function keeps the balance unchanged and returns the current balance when the limit update fails, so the API can return `200` with an unchanged balance for this case. If strict challenge behavior is required, adjust `InsertTransacao`/the route handler before documenting over-limit debits as guaranteed `422` responses. + +## `GET /clientes/{id}/extrato` + +Returns the current account statement. + +### Successful response + +```json +{ + "saldo": { + "total": 1000, + "limite": 100000, + "data_extrato": "2026-04-01T19:20:20.000000" + }, + "ultimas_transacoes": [ + { + "valor": 1000, + "tipo": "c", + "descricao": "deposito" + } + ] +} +``` + +The statement returns the latest 10 transactions ordered from newest to oldest. + +### Status codes + +| Status | When | +|---:|---| +| `200` | Client exists and statement data was returned | +| `404` | Client ID is not one of `1` through `5` | + +## `GET /healthz` + +Health check used by local smoke tests and GitHub Actions. + +| Status | When | +|---:|---| +| `200` | ASP.NET Core health checks pass | + +## Responsibility split + +| Layer | Responsibility | +|---|---| +| API (`Program.cs`) | Route mapping, JSON serialization, payload validation, fast invalid-client checks, database calls | +| PostgreSQL (`rinha.dump.sql`) | Seed clients, keep balances, apply atomic balance updates, insert transaction rows, return recent statement data | +| NGINX (`nginx.conf`) | Public port `9999` and load balancing across the two API instances | diff --git a/docs/wiki/architecture.md b/docs/wiki/architecture.md index d50dd30..bd3337a 100644 --- a/docs/wiki/architecture.md +++ b/docs/wiki/architecture.md @@ -42,11 +42,11 @@ The API project targets `net9.0`; its Dockerfile currently builds and runs it wi ## Database boundary -PostgreSQL owns the race-sensitive work. The API calls stored procedures for balance reads and transaction inserts instead of doing multi-step application-side coordination. +PostgreSQL owns the race-sensitive work. The API calls stored procedures for balance reads and transaction inserts instead of doing multi-step application-side coordination. Payload-shape validation remains API-side; the stored procedure should be read as the atomic consistency boundary, not as the only validation layer. | Procedure | Responsibility | |-----------|----------------| -| `InsertTransacao` | Validate client, type, value, description, and credit limit, then insert atomically | +| `InsertTransacao` | Check client existence, apply the atomic balance update, enforce the database-side credit-limit guard, and insert the transaction row when the update succeeds | | `GetSaldoClienteById` | Return balance metadata and recent transactions as JSONB | The compose command also tunes write durability for the contest setting: @@ -77,3 +77,17 @@ That shape keeps database concurrency explicit, avoids unbounded connection grow | `TRIM` | `false` | Leaves trimming separate from AOT path | | `EXTRA_OPTIMIZE` | `true` | Removes observability/runtime support guarded by `EXTRAOPTIMIZE` | | `BUILD_CONFIGURATION` | `Release` | Uses optimized .NET build configuration | + + +## API and database contract + +| Rule | Enforced by | Source | +|------|-------------|--------| +| Client ID is one of `1..5` | API fast path and database existence check | `Program.cs`, `InsertTransacao` | +| `valor` is positive | API | `IsTransacaoValid` | +| `tipo` is exactly `c` or `d` | API | `IsTransacaoValid` | +| `descricao` is present and <= 10 chars | API, with SQL parameter width as a backstop | `IsTransacaoValid`, `InsertTransacao(... descricao VARCHAR(10))` | +| Balance update and transaction insert stay atomic | PostgreSQL | `InsertTransacao` | +| Statement returns newest 10 transactions | PostgreSQL | `GetSaldoClienteById` | + +The NGINX config currently uses `least_conn`. If this repository is used as a strict official challenge submission, keep the config comments and docs aligned with whatever balancing policy is allowed by the target ruleset. diff --git a/docs/wiki/challenge.md b/docs/wiki/challenge.md index f9f09bd..2160528 100644 --- a/docs/wiki/challenge.md +++ b/docs/wiki/challenge.md @@ -8,7 +8,7 @@ Rinha de Backend is a Brazilian backend challenge focused on constrained, concur | Endpoint | Method | Purpose | Expected statuses | |----------|--------|---------|-------------------| -| `/clientes/{id}/transacoes` | POST | Submit a debit (`d`) or credit (`c`) transaction for client IDs 1 through 5 | `200`, `404`, `422` | +| `/clientes/{id}/transacoes` | POST | Submit a debit (`d`) or credit (`c`) transaction for client IDs 1 through 5 | `200`, `404`, `422` for invalid payloads | | `/clientes/{id}/extrato` | GET | Return balance, credit limit, statement time, and recent transactions | `200`, `404` | | `/healthz` | GET | Local and CI health check for this implementation | `200` | @@ -16,9 +16,10 @@ Rinha de Backend is a Brazilian backend challenge focused on constrained, concur - Only client IDs `1` through `5` exist. - Transaction type must be debit (`d`) or credit (`c`). -- Transaction value must be an integer amount. -- Description must be present and at most 10 characters, matching the current API validation and database function signature. -- Debits cannot push the account beyond the configured credit limit. +- Transaction value must be a positive integer amount in cents (`valor > 0`). +- Description must be present, non-empty, and at most 10 characters. +- Transaction type must be checked by the API before the request reaches PostgreSQL. +- Debits are intended not to push the account beyond the configured credit limit. - Statement responses return the current balance plus the latest 10 transactions. ## Resource envelope @@ -42,3 +43,13 @@ The k6 runner and observability containers are test infrastructure. They are use ## Source Full specification: [github.com/zanfranceschi/rinha-de-backend-2024-q1](https://github.com/zanfranceschi/rinha-de-backend-2024-q1) + + +## Current implementation compatibility note + +Payload validation is split across the API and PostgreSQL: + +- `Program.cs` rejects invalid client IDs, non-positive values, invalid transaction types, and missing/empty/long descriptions before calling the database. +- `InsertTransacao` in `docker-entrypoint-initdb.d/rinha.dump.sql` performs the atomic balance update and transaction insert. + +One important edge case is worth calling out: when a debit would exceed the account limit, the current SQL function returns the existing balance without inserting a transaction. Because the route handler treats the returned balance as a successful result, this case can surface as `200 OK` with an unchanged balance rather than the strict Rinha `422` behavior. Treat this as a known implementation note until the SQL/API contract is changed. diff --git a/docs/wiki/ci-cd-pipeline.md b/docs/wiki/ci-cd-pipeline.md index 57be412..baef239 100644 --- a/docs/wiki/ci-cd-pipeline.md +++ b/docs/wiki/ci-cd-pipeline.md @@ -45,9 +45,23 @@ After a PR is rebased into `main`, the release workflow publishes the runtime im | Docs site | `https://jonathanperis.github.io/rinha2-back-end-dotnet/` | | Docs section | `https://jonathanperis.github.io/rinha2-back-end-dotnet/docs/` | +## Build/version matrix + +| Path | SDK/runtime | Flags | Purpose | +|------|-------------|-------|---------| +| Local Docker/dev compose | .NET `10.0` SDK/runtime images from `src/WebApi/Dockerfile` | `AOT=true`, `TRIM=false`, `EXTRA_OPTIMIZE=false` | Local stack with OpenTelemetry support | +| PR Build Check | `actions/setup-dotnet` `9.0.x` | `AOT=true`, `TRIM=false`, `EXTRA_OPTIMIZE=true` | Fast compile gate plus compose health check | +| Main Release image | .NET `10.0` Docker build/runtime images | `AOT=true`, `TRIM=false`, `EXTRA_OPTIMIZE=true` | GHCR release image and multi-arch manifest | +| CodeQL | `actions/setup-dotnet` `9.0.x` | Plain Release build | Security analysis | +| Docs deploy | Bun + Astro `^6.4.2` | `NODE_ENV=production` base path | GitHub Pages static site | + +The project targets `net9.0` even when Docker build images are `10.0`. + ## Documentation deploy -The Pages workflow delegates to Jonathan's shared GitHub Pages workflow and uses Bun as the package manager. The docs package is an Astro static site under `docs/`; Markdown content lives in `docs/wiki/`, and production routes are served under the `/rinha2-back-end-dotnet` base path. +The Pages workflow delegates to Jonathan's shared GitHub Pages workflow and uses Bun as the package manager. The docs package is an Astro 6.4 static site under `docs/`; Markdown content lives in `docs/wiki/`, and production routes are served under the `/rinha2-back-end-dotnet` base path. + +The docs site uses Astro's `markdown.processor` API with `@astrojs/markdown-satteri`. That keeps Markdown rendering explicit, but it also means future Remark/Rehype-style extensions should be tested against Sätteri before they are assumed to work. ## Operational notes diff --git a/docs/wiki/getting-started.md b/docs/wiki/getting-started.md index e8f2689..fda4701 100644 --- a/docs/wiki/getting-started.md +++ b/docs/wiki/getting-started.md @@ -38,7 +38,7 @@ A healthy stack returns `HTTP/1.1 200 OK`. ## Example transaction -Credits use `tipo: "c"`, debits use `tipo: "d"`. Values are integer cents. +Credits use `tipo: "c"`, debits use `tipo: "d"`. Values are positive integer cents. ```bash curl -X POST http://localhost:9999/clientes/1/transacoes \ @@ -46,12 +46,45 @@ curl -X POST http://localhost:9999/clientes/1/transacoes \ -d '{"valor": 1000, "tipo": "c", "descricao": "deposito"}' ``` +The successful response returns the client ID, limit, and updated balance: + +```json +{ + "id": 1, + "limite": 100000, + "saldo": 1000 +} +``` + +Invalid payloads return `422`. Unknown client IDs return `404`. + ## Example statement ```bash curl http://localhost:9999/clientes/1/extrato ``` +Example response: + +```json +{ + "saldo": { + "total": 1000, + "limite": 100000, + "data_extrato": "2026-04-01T19:20:20.000000" + }, + "ultimas_transacoes": [ + { + "valor": 1000, + "tipo": "c", + "descricao": "deposito" + } + ] +} +``` + +For the full endpoint contract and the current over-limit debit behavior, see [API Reference](https://jonathanperis.github.io/rinha2-back-end-dotnet/docs/api-reference/). + ## Run the load-test lane The compose file includes the shared k6 runner and observability services. After the API stack is up, run: diff --git a/docs/wiki/home.md b/docs/wiki/home.md index abb52db..550c21b 100644 --- a/docs/wiki/home.md +++ b/docs/wiki/home.md @@ -9,6 +9,7 @@ Operator documentation for a C#/.NET 9 Native AOT implementation of Rinha de Bac | Understand the contest rules | [Challenge](challenge) | Endpoints, validation rules, and scoring constraints | | See the runtime topology | [Architecture](architecture) | Service map, CPU/RAM split, database strategy | | Run the stack locally | [Getting Started](getting-started) | Compose commands, smoke checks, sample requests | +| Check endpoint contracts | [API Reference](api-reference) | Payloads, responses, status codes, and implementation notes | | Review benchmark evidence | [Performance](performance) | Resource margins, test shape, and report links | | Follow releases and deploys | [CI/CD Pipeline](ci-cd-pipeline) | PR gates, image publishing, Pages deploy flow | diff --git a/docs/wiki/performance.md b/docs/wiki/performance.md index a961e9c..8d24d5f 100644 --- a/docs/wiki/performance.md +++ b/docs/wiki/performance.md @@ -27,6 +27,17 @@ The challenge allows 1.5 CPU and 550MB RAM across the counted runtime containers Load tests run through the shared [rinha2-back-end-k6](https://github.com/jonathanperis/rinha2-back-end-k6) test suite. The runner drives transaction and statement requests through NGINX, so the measured path includes load balancing, both API containers, and PostgreSQL. +The homepage benchmark cards are intentionally treated as archived-run claims, not timeless guarantees. When updating numbers like `46k+ requests/second`, `<50ms p95 latency`, or `99.9% success rate`, tie the change to a concrete archived report and keep the report file in `docs/public/reports/`. + +| Claim on homepage | What to verify before changing it | +|-------------------|-----------------------------------| +| Requests/second | The k6 report's request-throughput metric for the selected run | +| p95 latency | The report's p95 request-duration metric for the same run | +| Success rate | Failed request/check rate for the same run | +| PASS report link | The corresponding `stress-test-report-YYYYMMDDHHMMSS.html` file is present in the Pages report archive | + +CI runners and local machines can vary. Prefer wording such as "archived run" or "representative run" unless the number comes from a repeatable benchmark protocol documented here. + ```bash docker compose up nginx -d --build docker compose up k6 @@ -54,6 +65,10 @@ The `Main Release` workflow validates more than a build: 5. Builds and pushes the arm64 image. 6. Merges the platform images into the `latest` manifest. +## Report archive workflow + +The release workflow uploads the HTML stress-test report as a GitHub Actions artifact. Reports that should be published on Pages are committed under `docs/public/reports/`; the Astro reports page indexes every `.html` file in that directory at build time. The reports index is an archive, not a parser, so summary metrics must be copied into docs/homepage copy deliberately. + ## Evidence links | Evidence | Link | diff --git a/nginx.conf b/nginx.conf index 4478477..dc726d1 100644 --- a/nginx.conf +++ b/nginx.conf @@ -17,7 +17,7 @@ http { access_log off; upstream api { - least_conn; # Sends new requests to the server with the least number of active connections (Não pode na rinha) + least_conn; # Sends new requests to the server with the fewest active connections. Keep this aligned with docs/challenge rules if targeting a strict official submission. server webapi1-dotnet:8080; server webapi2-dotnet:8080; } diff --git a/prod/README.md b/prod/README.md index a27d890..e12ba00 100644 --- a/prod/README.md +++ b/prod/README.md @@ -7,7 +7,7 @@ Production submission assets for Jonathan Peris' .NET implementation. - C# / ASP.NET Core Minimal API targeting `net9.0` - Native AOT release image built from `src/WebApi/Dockerfile` - Npgsql 10.0.2 with `Multiplexing=true` -- PostgreSQL with stored procedures for atomic transaction rules +- PostgreSQL with stored procedures for atomic transaction rules (`prod/docker-compose.yml` currently uses the default `postgres` image tag; pin it if reproducible engine version is required) - NGINX on port `9999` using `least_conn` - k6 runner image for production-mode HTML report generation diff --git a/prod/conf/nginx.conf b/prod/conf/nginx.conf index 4478477..dc726d1 100644 --- a/prod/conf/nginx.conf +++ b/prod/conf/nginx.conf @@ -17,7 +17,7 @@ http { access_log off; upstream api { - least_conn; # Sends new requests to the server with the least number of active connections (Não pode na rinha) + least_conn; # Sends new requests to the server with the fewest active connections. Keep this aligned with docs/challenge rules if targeting a strict official submission. server webapi1-dotnet:8080; server webapi2-dotnet:8080; }