Fecha: 2026-06-15 Estado: Aceptado Bloque: M13 (server HTTP — sesiones tx) Origen: docs/TAREAS_PENDIENTES.md §6.5 — declarado como “depende de M12”. Refina: ADR-0089 M12 (savepoints — habilita el caso N-puntos), Bloque T (BEGIN/COMMIT/ROLLBACK).

Contexto

Antes de M13, el servidor HTTP de gabysql era estrictamente auto-commit por request:

POST /exec  { "sql": "BEGIN; INSERT (1); ..." }   ← request 1 (commiteado)
POST /exec  { "sql": "INSERT (2); COMMIT;" }      ← request 2 (NO ve el (1))

Cada request abría su propio Pager, hacía begin/exec/commit/close. Imposible que dos requests compartieran tx. Cualquier cliente serio (ORMs, batch loaders, herramientas de migración) que quiere preparar una tx larga, validar resultados intermedios, y decidir COMMIT o ROLLBACK al final — no podía usar gabysql vía HTTP.

M12 (ADR-0089) había agregado savepoints al motor; el server seguía sin exponerlos porque la tx misma no sobrevivía al request.

Decisión

Agregar sesiones cross-request con un único slot global.

Endpoints nuevos

POST /tx/begin                 → crea sesión, devuelve {"session":"<hex16>"}
POST /tx/commit?session=<id>   → COMMIT + cierra sesión
POST /tx/rollback?session=<id> → ROLLBACK + cierra sesión

/exec extendido

Acepta session ID via header X-Gabysql-Session: <hex16> o query param ?session=<id>. Cuando está presente:

Cuando NO hay session ID: comportamiento clásico (auto-commit por request).

Single-slot global

SessionStore::current: Option<Session>. Máximo UNA sesión activa a la vez en todo el servidor.

¿Por qué single-slot?

Si llega un /tx/begin con sesión ya activa → 409. Si el cliente quiere reemplazar, debe /tx/rollback primero.

Idle timeout

SESSION_IDLE_TIMEOUT_SECS = 300 (5 min). El GC es pasivo: cada request a /tx/* o /exec con session ID checkea last_used.elapsed() ≥ 300s y si sí, hace rollback + drop. Sin thread sweeper aparte.

Razón de elegir pasivo: el server es single-writer, así que cualquier operación nueva pasa por el lock → ahí mismo se puede GC. Un sweeper thread agregaría complejidad de scheduling sin ganancia funcional.

Session ID generator

fresh_session_id() deriva 16 hex chars del clock nanosegundo + splitmix64 mixer. No es un token de seguridad — solo identifica la sesión vigente. La autenticación al server sigue via Authorization: Bearer <token> o X-Gabysql-Token (Sec2).

Consecuencias

Positivas

Negativas / deuda

Tests añadidos

Cuatro tests E2E en nuevo binario tests/m13_server.rs. Cada uno arranca el server real en un thread con puerto efímero (TcpListener::bind("127.0.0.1:0")) y hace requests reales via TcpStream:

Cero deps externas para los tests (TcpStream + parsing JSON ad-hoc).

Suite total: 824 → 828 (+4).

Ejemplo de uso (curl)

# 1) Iniciar sesión.
SESSION=$(curl -sX POST http://127.0.0.1:8080/tx/begin -d '{}' \
    | jq -r '.session')

# 2) Operar varios requests dentro de la misma tx.
curl -sX POST http://127.0.0.1:8080/exec \
    -H "X-Gabysql-Session: $SESSION" \
    -d '{"sql":"INSERT INTO users (id, name) VALUES (1, \"Ana\")"}'

curl -sX POST http://127.0.0.1:8080/exec \
    -H "X-Gabysql-Session: $SESSION" \
    -d '{"sql":"INSERT INTO users (id, name) VALUES (2, \"Beto\")"}'

# 3) Decidir commit o rollback al final.
curl -sX POST "http://127.0.0.1:8080/tx/commit?session=$SESSION"
# o
# curl -sX POST "http://127.0.0.1:8080/tx/rollback?session=$SESSION"

Alternativas consideradas

  1. Multi-session con file lock por DB. Requiere repensar el lock del Pager (ADR-0013 lo asume exclusive). Diferido a Fase 6.
  2. Auto-creación de sesión cuando /exec empieza con BEGIN sin session header. Más mágico pero menos predecible — un typo del cliente podía crear sesiones huérfanas. Rechazado.
  3. Sweeper thread para idle timeout. Más correcto bajo carga baja, pero agrega complejidad de scheduling. Pasivo basta para v1 — re-evaluar si se ve memoria acumulada en producción.
  4. REST estricto: POST /sessions, DELETE /sessions/<id>. Más RESTful pero menos descubrible vía curl. Elegimos /tx/{begin,commit,rollback} por simetría con BEGIN/COMMIT/ROLLBACK SQL.

Próximo trabajo

Referencias