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
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,15 +82,28 @@ The dev compose file also exposes the API instances directly on `localhost:6968`
| 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.
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. The JSON input accepts the lowercase fields used by the challenge (`valor`, `tipo`, `descricao`) and PascalCase aliases (`Valor`, `Tipo`, `Descricao`) because the DTO includes Serde aliases.

Current business-limit behavior is source-backed by `InsertTransacao`: if a debit would exceed the client's limit, PostgreSQL leaves the balance unchanged, does not insert a transaction row, and returns the current balance to the API. Therefore `422` is used for invalid payloads or SQL errors, while this over-limit path currently responds with `200` and the unchanged balance payload.

Client limits are seeded in both the Rust `CLIENTS` map and the PostgreSQL dump:

| Client ID | Limit |
|-----------|-------|
| 1 | 100000 |
| 2 | 80000 |
| 3 | 1000000 |
| 4 | 10000000 |
| 5 | 500000 |

### Run Stress Tests

```bash
cp .env.example .env
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.
The dev compose path runs k6 in `MODE=dev` with InfluxDB/Grafana export, so `.env` must provide `INFLUXDB_PASSWORD` and `INFLUXDB_TOKEN` values for the local observability stack. The production compose file used by the release workflow runs k6 in `MODE=prod` and exports an HTML report artifact.

## Project Structure

Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ Astro static site deployed to GitHub Pages.

Run from this directory (`docs/`):

This Astro 6 site is built with Bun, uses Sätteri via `@astrojs/markdown-satteri` as the Markdown processor, and should be run with Node 22.12+ available on `PATH` for local npm/preview workflows.

| Command | Action |
|---|---|
| `bun install` | Install dependencies |
Expand Down
4 changes: 2 additions & 2 deletions docs/wiki/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ NGINX listens on `:9999` and proxies all routes to an upstream named `api` with
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)`.
- `POST /clientes/{id}/transacoes` validates the client ID and payload (`tipo`, `descricao`, `valor`), accepts PascalCase JSON aliases (`Valor`, `Tipo`, `Descricao`), 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.

Expand All @@ -36,7 +36,7 @@ Business logic is implemented in PostgreSQL stored procedures over UNLOGGED tabl

- `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.
- `InsertTransacao` applies credit/debit balance updates and inserts the transaction only when the update succeeds. If a debit exceeds the available limit, the function returns the unchanged current balance without inserting a transaction row; the Rust handler serializes that as a `200` response today.
- `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:
Expand Down
31 changes: 29 additions & 2 deletions docs/wiki/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,33 @@ Use port `9999` for normal validation and load-test requests.
| `/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`.
Transaction payloads must have `tipo` equal to `c` or `d`, a non-empty `descricao` of at most 10 characters, and a positive integer `valor`. The DTO also accepts PascalCase aliases (`Valor`, `Tipo`, `Descricao`) in addition to the lowercase challenge field names (`valor`, `tipo`, `descricao`).

### Current Response Contract

`POST /clientes/{id}/transacoes` returns:

```json
{
"id": 1,
"limite": 100000,
"saldo": 1000
}
```

`GET /clientes/{id}/extrato` returns a `saldo` object with `total`, `limite`, and `data_extrato`, plus `ultimas_transacoes` entries serialized as `valor`, `tipo`, and `descricao`.

The current stored procedure handles a debit that would exceed the client limit by leaving the balance unchanged and skipping the transaction insert, then returning the current balance to the API. That path currently responds with `200`; `422` is reserved for invalid payloads or database errors.

Seeded client limits:

| ID | Limit |
|----|-------|
| 1 | 100000 |
| 2 | 80000 |
| 3 | 1000000 |
| 4 | 10000000 |
| 5 | 500000 |

## Example Requests

Expand All @@ -56,7 +82,8 @@ curl http://localhost:9999/clientes/1/extrato
## Stress Tests

```bash
cp .env.example .env
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.
In `docker-compose.yml`, k6 runs in `MODE=dev` and exports time-series data to InfluxDB/Grafana. Copy `.env.example` first so `INFLUXDB_PASSWORD` and `INFLUXDB_TOKEN` are defined for InfluxDB, Grafana, and xk6 output. The release workflow uses `prod/docker-compose.yml`, where k6 runs in `MODE=prod` and writes an HTML stress-test report artifact.
1 change: 1 addition & 0 deletions docs/wiki/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Use those generated reports and workflow artifacts for exact latency, throughput
- Client existence and limits are validated from a lazy static Rust `HashMap` for the five seeded clients.
- Balance mutation and statement aggregation are handled in PostgreSQL stored procedures.
- `Clientes` and `Transacoes` are UNLOGGED tables for challenge throughput.
- Debits that would exceed a client's limit do not insert a transaction row; the stored procedure returns the unchanged balance.
- PostgreSQL durability flags are disabled for benchmarking: `synchronous_commit=0`, `fsync=0`, and `full_page_writes=0`.

## Stress Testing
Expand Down
17 changes: 16 additions & 1 deletion scripts/check_docs_drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,22 @@ def workflow_text(name: str) -> str:
require("max_connections(5)" in main_rs, "SQLx pool size changed; update docs/checker")
require("max_connections(5)" in DOC_TEXT or "pool size is 5" in DOC_TEXT, "SQLx pool size is not documented")

# Payload validation facts mirrored from is_transacao_valid.
# Payload validation facts mirrored from is_transacao_valid and DTO aliases.
for snippet in ['tipo == "c" || tipo == "d"', 'descricao.len() <= 10', 'valor > 0']:
require(snippet in main_rs, f"transaction validation changed around {snippet}; update docs/checker")
for phrase in ["tipo", "descricao", "10", "valor"]:
require(phrase in DOC_TEXT, f"transaction validation docs missing {phrase}")
for alias in ['#[serde(alias = "Valor")]', '#[serde(alias = "Tipo")]', '#[serde(alias = "Descricao")]']:
require(alias in main_rs, f"transaction DTO alias changed around {alias}; update docs/checker")
require("PascalCase aliases" in DOC_TEXT and "Valor" in DOC_TEXT and "Descricao" in DOC_TEXT, "transaction DTO aliases are not documented")

# Seeded clients and current business-limit behavior.
for client_id, limit in [(1, 100000), (2, 80000), (3, 1000000), (4, 10000000), (5, 500000)]:
require(f"m.insert({client_id}, {limit})" in main_rs, f"Rust CLIENTS map changed for client {client_id}; update docs/checker")
require(f"{client_id}\t{limit}\t0" in schema_sql, f"seed SQL changed for client {client_id}; update docs/checker")
require(str(limit) in DOC_TEXT, f"client limit {limit} is not documented")
require("IF FOUND THEN" in schema_sql and "SELECT \"SaldoInicial\" INTO novo_saldo" in schema_sql, "over-limit stored procedure behavior changed; update docs/checker")
require("unchanged balance" in DOC_TEXT and "200" in DOC_TEXT, "current over-limit response behavior is not documented")

# Compose/runtime source-backed facts.
for resource in ['cpus: "0.4"', 'memory: "100MB"', 'cpus: "0.5"', 'memory: "330MB"', 'cpus: "0.2"', 'memory: "20MB"']:
Expand All @@ -89,6 +100,10 @@ def workflow_text(name: str) -> str:
require(flag in dev_compose and flag in prod_compose and flag in DOC_TEXT, f"PostgreSQL tuning flag {flag} drifted")
require("CREATE UNLOGGED TABLE" in schema_sql and "UNLOGGED" in DOC_TEXT, "UNLOGGED table fact drifted")
require("IX_Transacoes_ClienteId_Id_Desc" in schema_sql and "ClienteId, Id DESC" in DOC_TEXT, "transaction index fact drifted")
for env_name in ["INFLUXDB_PASSWORD", "INFLUXDB_TOKEN"]:
require(env_name in dev_compose, f"dev compose env var changed/missing: {env_name}")
require(env_name in read(".env.example"), f".env.example missing {env_name}")
require(env_name in DOC_TEXT, f"local observability env var {env_name} is not documented")

# Workflow facts.
build_check = workflow_text("build-check.yml")
Expand Down
Loading