Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Build the application
run: cargo build --release --manifest-path ./src/WebApi/Cargo.toml

- name: Check README and wiki drift
run: python3 scripts/check_docs_drift.py

container-test:
runs-on: ubuntu-latest
steps:
Expand Down
92 changes: 55 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,27 +10,27 @@

## About

A Rust implementation of the Brazilian backend challenge Rinha de Backend 2024/Q1, where a fictional bank API must handle concurrent transactions under strict resource constraints (1.5 CPU, 550MB RAM total). Built as a minimal single-file API (~140 lines) using Actix-web 4 with Tokio async runtime and SQLx 0.8 for compile-time checked PostgreSQL queries.
A Rust implementation of the Brazilian backend challenge Rinha de Backend 2024/Q1, where a fictional bank API must handle concurrent transactions under strict resource constraints (1.5 CPU, 550MB RAM total). The current API is a single `src/WebApi/main.rs` entrypoint (173 total lines at this revision) using Actix-web 4 with the Tokio runtime and SQLx 0.8 for PostgreSQL access.

## Tech Stack

| Technology | Version | Purpose |
|-----------|---------|---------|
| Rust | 1.94 | API implementation |
| Technology | Version / Source | Purpose |
|-----------|------------------|---------|
| Rust | edition 2024; Docker builder `rust:1.95`; CI uses `dtolnay/rust-toolchain@stable` | API implementation |
| Actix-web | 4 | Async HTTP framework |
| SQLx | 0.8 | Compile-time checked PostgreSQL driver |
| Tokio | 1 | Async runtime |
| PostgreSQL | 16.7 | Database with stored procedures |
| NGINX | 1.27 | Reverse proxy and load balancer (least_conn) |
| Docker | - | Multi-stage build and orchestration |
| k6 | - | Stress testing |
| SQLx | 0.8 | PostgreSQL driver with offline query metadata (`.sqlx/`) |
| Tokio | 1 (`full`) | Async runtime |
| PostgreSQL | 16.7 Alpine | Database with stored procedures |
| NGINX | 1.27 Alpine | Reverse proxy and load balancer (`least_conn`) |
| Docker | Compose + Buildx | Multi-stage image build and orchestration |
| k6 | `ghcr.io/jonathanperis/rinha2-back-end-k6:latest` | Stress testing |

## Architecture

```
NGINX (:9999, least_conn)
├── webapi1-rust (:8080, 0.4 CPU, 100MB)
├── webapi2-rust (:8080, 0.4 CPU, 100MB)
├── webapi1-rust (:8080, 0.4 CPU, 100MB; host :6968 in dev)
├── webapi2-rust (:8080, 0.4 CPU, 100MB; host :6969 in dev)
└── PostgreSQL (0.5 CPU, 330MB)
├── InsertTransacao() — atomic balance update + validation
└── GetSaldoClienteById() — statement with JSONB aggregation
Expand All @@ -45,16 +45,16 @@ NGINX (:9999, least_conn)

## Features

- Minimal single-file API implementation (~140 lines of Rust)
- Zero-cost async with Tokio runtime and Actix-web
- SQLx compile-time query validation with offline cache (`.sqlx/`)
- PostgreSQL stored procedures for atomic server-side business logic
- UNLOGGED tables for maximum write throughput (no WAL)
- Lazy static HashMap for client validation (no DB round-trip)
- PostgreSQL tuned: `synchronous_commit=0`, `fsync=0`, `full_page_writes=0`
- Multi-platform Docker image (amd64/arm64) published to GHCR
- Observability stack with Prometheus, Grafana, and InfluxDB
- All requests under 800ms at 250MB RAM usage (60% below limit)
- Single Rust API entrypoint (`src/WebApi/main.rs`, 173 total lines at this revision)
- Actix-web route handlers for transactions, statements, and `/healthz`
- SQLx compile-time query validation with offline cache (`src/WebApi/.sqlx/`)
- PostgreSQL stored procedures for server-side balance updates and statement aggregation
- UNLOGGED `Clientes` and `Transacoes` tables for challenge-oriented write throughput
- Lazy static `HashMap` for the five seeded client limits (no DB round-trip for client existence checks)
- PostgreSQL tuned for benchmark throughput: `synchronous_commit=0`, `fsync=0`, `full_page_writes=0`
- Multi-platform GHCR release flow: `latest` is assembled as a multi-arch manifest from amd64 and arm64 builds; `latest-arm64` is also pushed during the arm64 leg
- Development observability stack with Prometheus, Grafana, InfluxDB, and postgres-exporter
- Committed stress-test report artifacts under `docs/public/reports/`; treat generated reports, not hard-coded README numbers, as the performance source of truth

## Getting Started

Expand All @@ -70,50 +70,68 @@ cd rinha2-back-end-rust
docker compose up nginx -d --build
```

API available at `http://localhost:9999`
API available at `http://localhost:9999`.

The dev compose file also exposes the API instances directly on `localhost:6968` and `localhost:6969`; use the NGINX port (`9999`) for challenge-compatible requests.

### API Endpoints

| Method | Path | Status Codes | Description |
|--------|------|-------------|-------------|
| POST | `/clientes/{id}/transacoes` | 200, 404, 422 | Submit debit or credit transaction |
| GET | `/clientes/{id}/extrato` | 200, 404 | Get account balance statement |
| GET | `/healthz` | 200 | Health check |
| POST | `/clientes/{id}/transacoes` | 200, 404, 422 | Submit debit or credit transaction for client IDs 1-5 |
| GET | `/clientes/{id}/extrato` | 200, 404 | Get account balance statement with the 10 most recent transactions |
| GET | `/healthz` | 200 | Health check returning `Healthy` |

Transaction payload validation is handled in Rust before the stored procedure call: `tipo` must be `c` or `d`, `descricao` must be non-empty and at most 10 characters, and `valor` must be positive.

### Run Stress Tests

```bash
docker compose up k6 --build --force-recreate
```

The dev compose path runs k6 in `MODE=dev` with InfluxDB/Grafana export. The production compose file used by the release workflow runs k6 in `MODE=prod` and exports an HTML report artifact.

## Project Structure

```
rinha2-back-end-rust/
├── src/WebApi/
│ ├── main.rs # Complete API (~140 lines)
│ ├── Cargo.toml # Dependencies
│ ├── Dockerfile # Multi-stage: rust:1.94 → debian:bookworm-slim
│ ├── main.rs # Complete API entrypoint (173 total lines at this revision)
│ ├── Cargo.toml # Rust 2024 package and dependencies
│ ├── Dockerfile # Multi-stage: rust:1.95 → debian:bookworm-slim
│ └── .sqlx/ # SQLx offline query cache
├── docker-entrypoint-initdb.d/
│ └── rinha.dump.sql # Schema + stored procedures + seed data
├── docker-compose.yml # Dev stack with observability
├── prod/docker-compose.yml # Prod stack with GHCR images
│ └── rinha.dump.sql # UNLOGGED schema + stored procedures + seed data
├── docker-compose.yml # Dev stack: APIs, PostgreSQL, NGINX, observability, k6
├── prod/docker-compose.yml # Release/load-test stack using GHCR images and prod k6 mode
├── nginx.conf # Load balancer config
└── .github/workflows/ # CI/CD (build-check, main-release, codeql)
├── docs/ # Astro/Bun GitHub Pages site and wiki markdown
├── scripts/check_docs_drift.py # Source-backed README/wiki drift guard
└── .github/workflows/ # build-check, main-release, deploy, codeql
```

## CI/CD

| Workflow | Trigger | Description |
|----------|---------|-------------|
| Build Check | Pull requests | Cargo build (release) + Docker health check |
| Main Release | Push to main | Build + Multi-platform Docker push (amd64/arm64) to GHCR + k6 load test + GitHub Pages report |
| CodeQL | Push/PR + weekly | Security and quality analysis for Rust |
| Deploy | Push to main | Deploy documentation to GitHub Pages |
| Build Check | Pull requests to `main`; manual dispatch | Rust release build, docs drift guard, and Docker Compose `/healthz` test |
| Main Release | Push to `main`; manual dispatch | Rust release build, amd64 + arm64 GHCR image pushes, multi-arch manifest merge, production compose healthcheck, k6 report artifact upload |
| CodeQL | Push to `main`, pull requests to `main`, Monday weekly schedule | Rust CodeQL analysis with `security-and-quality` queries |
| Deploy to GitHub Pages | Push to `main`; manual dispatch | Reusable shared workflow that builds `docs/` with Bun and deploys GitHub Pages |

**Docker image:** `ghcr.io/jonathanperis/rinha2-back-end-rust:latest`

## Documentation Drift Guard

README and wiki facts that mirror code, Docker, Compose, or workflow state are checked by:

```bash
python3 scripts/check_docs_drift.py
```

The guard verifies high-value source-backed facts such as API line count, Docker builder version, Rust edition, endpoint coverage, workflow triggers, wiki navigation coverage, and known-stale phrases.

## License

MIT — see [LICENSE](LICENSE)
3 changes: 2 additions & 1 deletion docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"lint": "eslint"
"lint": "eslint",
"check:drift": "python3 ../scripts/check_docs_drift.py"
},
"dependencies": {
"@astrojs/sitemap": "^3",
Expand Down
43 changes: 29 additions & 14 deletions docs/wiki/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,49 @@

## Overview

The system follows a shared architecture across all Rinha de Backend implementations: two API instances behind an Nginx reverse proxy, with a single PostgreSQL database and an observability stack.
The system follows the Rinha de Backend topology used by this repository: two Rust/Actix-web API containers behind NGINX, one PostgreSQL database, and optional observability/load-test services in the development compose file.

## Services

| Service | Role | CPU | RAM |
|---------|------|-----|-----|
| webapi1 | Rust/Actix-web 4 API instance (SQLx 0.8 + Tokio) | 0.4 | 100MB |
| webapi2 | Rust/Actix-web 4 API instance (SQLx 0.8 + Tokio) | 0.4 | 100MB |
| nginx | Reverse proxy / load balancer (least-conn) | 0.2 | 20MB |
| postgresql | Database with stored procedures | 0.5 | 330MB |
| k6 | Load testing | (not counted) | (not counted) |
| grafana + influxdb | Observability dashboards | (not counted) | (not counted) |
| `webapi1-rust` | Actix-web 4 API instance using SQLx 0.8 + Tokio | 0.4 | 100MB |
| `webapi2-rust` | Actix-web 4 API instance using SQLx 0.8 + Tokio | 0.4 | 100MB |
| `nginx` | Reverse proxy / load balancer on port 9999 (`least_conn`) | 0.2 | 20MB |
| `db` | PostgreSQL 16.7 Alpine with stored procedures | 0.5 | 330MB |
| `k6` | Shared load-test runner (`MODE=dev` in dev compose, `MODE=prod` in prod compose) | not counted | not counted |
| `prometheus`, `grafana`, `influxdb`, `postgres-exporter` | Development observability stack | not counted | not counted |

The compose CPU/RAM limits for the challenge services total 1.5 CPU and 550MB.

## Load Balancing

Nginx uses `least_conn` strategy to distribute requests across the two API instances.
NGINX listens on `:9999` and proxies all routes to an upstream named `api` with `least_conn` across `webapi1-rust:8080` and `webapi2-rust:8080`. The dev compose file also maps the two API containers to host ports `6968` and `6969`, but challenge-compatible traffic should go through NGINX.

## API Runtime

The API is a single Rust entrypoint (`src/WebApi/main.rs`, 173 total lines at this revision):

- `GET /clientes/{id}/extrato` validates the client ID against a lazy static `HashMap`, then calls `GetSaldoClienteById($1)`.
- `POST /clientes/{id}/transacoes` validates the client ID and payload (`tipo`, `descricao`, `valor`), then calls `InsertTransacao($1, $2, $3, $4)`.
- `GET /healthz` returns `Healthy` for compose and CI smoke checks.
- The SQLx pool is created from `DATABASE_URL` with `max_connections(5)` per API instance.

## Database

Business logic is implemented in PostgreSQL stored procedures. The database is tuned for maximum write performance:
Business logic is implemented in PostgreSQL stored procedures over UNLOGGED tables:

- `Clientes` stores the five seeded clients and their current balance (`SaldoInicial`).
- `Transacoes` stores accepted transactions and has an index on `(ClienteId, Id DESC)` for recent-statement reads.
- `InsertTransacao` applies credit/debit balance updates and inserts the transaction only when the update succeeds.
- `GetSaldoClienteById` returns the current balance, limit, timestamp, and up to 10 latest transactions as JSONB.

The database command is tuned for benchmark throughput, not durability:

- `synchronous_commit=0` — no wait for WAL flush
- `fsync=0` — skip fsync on writes
- `full_page_writes=0` — skip full page writes

## Implementation Details
## Container Images

- Minimal single-file Rust API (~140 lines)
- Actix-web 4 HTTP framework with Tokio async runtime
- SQLx 0.8 async PostgreSQL driver
- Multi-stage Docker build for minimal container image size
The API Dockerfile builds with `rust:1.95` and runs on `debian:bookworm-slim` as a non-root `app` user. The release workflow publishes `ghcr.io/jonathanperis/rinha2-back-end-rust:latest` as the multi-arch image used by `prod/docker-compose.yml`.
36 changes: 24 additions & 12 deletions docs/wiki/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,40 @@

## Workflows

This repository uses four GitHub Actions workflows:
This repository uses four GitHub Actions workflows plus a source-backed docs drift guard.

### build-check.yml

- **Trigger:** Pull requests to main
- **Steps:** Builds the Rust release binary, then runs Docker Compose and validates the healthcheck endpoint
- **Purpose:** Catch build failures and regressions before merging
- **Trigger:** Pull requests to `main` and manual dispatch
- **Rust build:** `cargo build --release --manifest-path ./src/WebApi/Cargo.toml`
- **Docs drift guard:** `python3 scripts/check_docs_drift.py`
- **Container smoke:** `docker compose -f ./docker-compose.yml up nginx --wait`, then `GET http://localhost:9999/healthz`
- **Purpose:** Catch Rust build failures, README/wiki drift, and compose health regressions before merging

### main-release.yml

- **Trigger:** Push to main branch
- **Steps:** Builds the release binary, builds and pushes a multi-platform Docker image (amd64/arm64) to GHCR, runs container healthcheck, then runs k6 load tests and uploads the stress test report as an artifact
- **Purpose:** Automated release of production-ready container images with load test validation
- **Trigger:** Push to `main` and manual dispatch
- **Build/release path:** build Rust release binary, push amd64 image as `latest`, push arm64 image as `latest-arm64`, merge both digests into the multi-arch `latest` manifest
- **Validation:** start `prod/docker-compose.yml`, poll `/healthz`, then run k6 in `MODE=prod`
- **Artifact:** upload `./prod/conf/stress-test/reports/stress-test-report.html` as `stress-test-report`
- **Purpose:** Publish production-ready container images and preserve the load-test report from the release run

### codeql.yml

- **Trigger:** Push to main, pull requests to main, weekly schedule (Mondays)
- **Steps:** Runs CodeQL static analysis for Rust with security-and-quality queries
- **Trigger:** Push to `main`, pull requests to `main`, and weekly schedule (`0 3 * * 1`)
- **Steps:** Set up stable Rust, initialize CodeQL for Rust with `security-and-quality` queries, build the release binary, perform analysis
- **Purpose:** Continuous security and code quality analysis

### deploy.yml

- **Trigger:** Push to main branch
- **Steps:** Deploys the `docs/` directory to GitHub Pages using the actions/deploy-pages workflow
- **Purpose:** Publish project documentation and stress test reports to GitHub Pages
- **Trigger:** Push to `main` and manual dispatch
- **Steps:** Calls the shared `jonathanperis/.github/.github/workflows/pages-docs-deploy.yml@main` reusable workflow with `package-manager: bun`
- **Purpose:** Build and publish the Astro docs site to GitHub Pages

## Local Documentation Drift Check

```bash
python3 scripts/check_docs_drift.py
```

The guard checks README/wiki claims against the current code and configuration: line count, Rust edition, Docker builder image, endpoints, workflow triggers, image tags, compose resources, and wiki navigation coverage. It also rejects known-stale phrases such as old line-count/version/performance claims.
38 changes: 30 additions & 8 deletions docs/wiki/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,27 +14,49 @@ docker compose up nginx -d --build

## Access

The API is available at `http://localhost:9999`
The challenge-facing API is available at `http://localhost:9999` through NGINX.

The dev compose file also maps the two API containers directly:

- `http://localhost:6968` → `webapi1-rust:8080`
- `http://localhost:6969` → `webapi2-rust:8080`

Use port `9999` for normal validation and load-test requests.

## Endpoints

| Endpoint | Method | Description |
|----------|--------|-------------|
| `/clientes/{id}/transacoes` | POST | Submit debit or credit transaction |
| `/clientes/{id}/extrato` | GET | Get account balance statement |
| Endpoint | Method | Status Codes | Description |
|----------|--------|--------------|-------------|
| `/clientes/{id}/transacoes` | POST | 200, 404, 422 | Submit a debit or credit transaction for client IDs 1-5 |
| `/clientes/{id}/extrato` | GET | 200, 404 | Get current balance, credit limit, timestamp, and up to 10 recent transactions |
| `/healthz` | GET | 200 | Health check returning `Healthy` |

Transaction payloads must have `tipo` equal to `c` or `d`, a non-empty `descricao` of at most 10 characters, and a positive integer `valor`.

## Example Requests

### Health Check

```bash
curl http://localhost:9999/healthz
```

### Create Transaction

```bash
curl -X POST http://localhost:9999/clientes/1/transacoes \
-H "Content-Type: application/json" \
-d '{"valor": 1000, "tipo": "c", "descricao": "deposito"}'
curl -X POST http://localhost:9999/clientes/1/transacoes -H "Content-Type: application/json" -d '{"valor": 1000, "tipo": "c", "descricao": "deposito"}'
```

### Get Statement

```bash
curl http://localhost:9999/clientes/1/extrato
```

## Stress Tests

```bash
docker compose up k6 --build --force-recreate
```

In `docker-compose.yml`, k6 runs in `MODE=dev` and exports time-series data to InfluxDB/Grafana. The release workflow uses `prod/docker-compose.yml`, where k6 runs in `MODE=prod` and writes an HTML stress-test report artifact.
20 changes: 11 additions & 9 deletions docs/wiki/home.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
# rinha2-back-end-rust

Rust/Actix-web 4 implementation for the Rinha de Backend 2024/Q1 challenge. Manages a fictional bank API with transaction processing and balance statements under strict resource constraints (1.5 CPU, 550MB RAM total across all containers).
Rust/Actix-web 4 implementation for the Rinha de Backend 2024/Q1 challenge. It manages a fictional bank API with transaction processing and balance statements under strict resource constraints (1.5 CPU, 550MB RAM total across the challenge containers).

## Wiki Pages

| Page | Description |
|------|-------------|
| [Challenge](#challenge) | What is Rinha de Backend 2024/Q1 |
| [Architecture](#architecture) | Stack, services, resource constraints |
| [Getting Started](#getting-started) | Prerequisites and how to run |
| [Performance](#performance) | Results, benchmarks, resource usage |
| [CI/CD Pipeline](#ci-cd-pipeline) | GitHub Actions workflows |
| [Challenge](#challenge) | What Rinha de Backend 2024/Q1 requires |
| [Architecture](#architecture) | Current stack, services, resource constraints, and runtime flow |
| [Getting Started](#getting-started) | Prerequisites, run commands, endpoint smoke tests |
| [Performance](#performance) | How to read benchmark/report artifacts without hard-coding stale numbers |
| [CI/CD Pipeline](#ci-cd-pipeline) | GitHub Actions workflows and release/deploy path |

## Key Features

- Minimal single-file API implementation (~140 lines of Rust)
- Single Rust API entrypoint (`src/WebApi/main.rs`, 173 total lines at this revision)
- Actix-web 4 with Tokio async runtime and SQLx 0.8
- PostgreSQL stored procedures for server-side business logic
- All requests under 800ms at 250MB RAM usage
- Rust 2024 package; Docker builder currently uses `rust:1.95`
- PostgreSQL stored procedures for balance updates and statement aggregation
- Health endpoint at `/healthz` returns `Healthy`
- Source-backed drift guard: `python3 scripts/check_docs_drift.py`

---

Expand Down
Loading
Loading