feat: Add migration documentation for microservices architecture and update README to include new documentation link. Introduce detailed reference for the migration process, infrastructure, and service interactions in the new microservices setup.
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
# Migración a microservicios y arquitectura IPSExtractor
|
||||
|
||||
Documento de referencia para la rama `migration-services` respecto a `main`.
|
||||
Describe qué hay de nuevo, cómo funciona la infraestructura, los patrones de diseño, cómo escalar el sistema y cómo incorporar servicios futuros.
|
||||
|
||||
> **Nota:** el repositorio no tiene rama `master`; la línea base de comparación es **`main`**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumen ejecutivo
|
||||
|
||||
IPSExtractor evolucionó de un **monolito Python** (interfaz gráfica de escritorio + API embebida) a una **arquitectura de microservicios** orquestada con Docker Compose, con una **aplicación de escritorio Electron + React** como cliente principal.
|
||||
|
||||
| Aspecto | Estado actual |
|
||||
|---------|---------------|
|
||||
| Despliegue | `docker compose up` — stack completo en contenedores |
|
||||
| Puerta de entrada | **API Gateway** en `0.0.0.0:8189` (único puerto expuesto al host) |
|
||||
| Persistencia | PostgreSQL 16 + migraciones Alembic |
|
||||
| Cola async | Redis 7 + workers ARQ para extracción LLM |
|
||||
| Conteo | `count-api` (Go, Tesseract + ONNX), stateless |
|
||||
| Cliente | IPSExtractor Desktop **v1.0.0** (2026-07-06), instalador NSIS Windows |
|
||||
|
||||
**Alcance de la migración (rama `migration-services`):** ~170 archivos modificados, +27 000 / −8 500 líneas, 5 commits sobre `main`.
|
||||
|
||||
---
|
||||
|
||||
## 2. Qué cambió respecto a `main`
|
||||
|
||||
### Antes y después
|
||||
|
||||
| Antes (`main`) | Ahora (`migration-services`) |
|
||||
|----------------|------------------------------|
|
||||
| `gui/` + `gui_main.py` — UI desktop en Python | `services/desktop_app/` — Electron + React + TypeScript |
|
||||
| `server/app/` — API monolítica | `api_gateway` + `extraction` + `cleaning` + `count-api` |
|
||||
| `application/` — lógica de dominio acoplada al monolito | Dominio repartido bajo `services/<nombre>/` |
|
||||
| `installer.iss` / `build_installer.bat` — Inno Setup | `electron-builder` → `IPSExtractor-Desktop-1.0.0-Setup.exe` |
|
||||
| Sin orquestación unificada | [`docker-compose.yml`](../docker-compose.yml) — stack completo |
|
||||
| Sin modelo de trabajos en BD | PostgreSQL: `jobs`, `tasks`, conteo, extracción, limpieza |
|
||||
| Procesamiento síncrono en el mismo proceso | Redis + workers ARQ (`WORKER_REPLICAS`) |
|
||||
|
||||
### Eliminado
|
||||
|
||||
- `gui/` — pestañas de extracción, limpieza, métricas, preclasificación, etc.
|
||||
- `application/` en la raíz del repo
|
||||
- `server/` — API FastAPI monolítica
|
||||
- `gui_main.py`, `requirements.txt` raíz, `installer.iss`
|
||||
- Tests legacy en `tests/` (sustituidos por tests por servicio / desktop)
|
||||
|
||||
### Añadido (highlights)
|
||||
|
||||
| Ruta | Rol |
|
||||
|------|-----|
|
||||
| `services/api_gateway/` | Proxy HTTP único hacia el resto de servicios |
|
||||
| `services/extraction/` | Jobs, tasks, conteo en BD, extracción LLM, API REST |
|
||||
| `services/extraction/worker/` | Workers ARQ para procesamiento asíncrono |
|
||||
| `services/cleaning/` | Limpieza y transformación de datos CSV |
|
||||
| `services/services-count-facturas-medicas-serving/` | `count-api` — OCR + clasificación ONNX (Go) |
|
||||
| `services/db/` | Migraciones Alembic (PostgreSQL) |
|
||||
| `services/desktop_app/` | App Electron + React |
|
||||
| `docs/database.dbml` | Esquema completo de la base de datos |
|
||||
| `.env.example` | Variables de entorno de referencia |
|
||||
|
||||
### Commits en `migration-services` (sobre `main`)
|
||||
|
||||
1. Eliminación del monolito legacy + introducción de Docker Compose microservicios
|
||||
2. App desktop Electron + wizards de extracción
|
||||
3. Redis, `.env.example`, indexación de carpetas
|
||||
4. Servicio `db-migrations`, jobs y conteo en PostgreSQL
|
||||
5. Healthchecks, puerto 8189, OCR tuning, instalador desktop v1.0.0
|
||||
|
||||
---
|
||||
|
||||
## 3. Infraestructura nueva
|
||||
|
||||
### 3.1 Despliegue Docker
|
||||
|
||||
Solo el **API Gateway** publica un puerto al host. El resto de servicios se comunican por la red interna de Docker.
|
||||
|
||||
| Servicio | Puerto host | Puerto interno | Tecnología |
|
||||
|----------|-------------|----------------|------------|
|
||||
| `api_gateway` | **8189** | 8000 | Python / FastAPI |
|
||||
| `extraction` | — | 8001 | Python / FastAPI |
|
||||
| `cleaning` | — | 8002 | Python / FastAPI |
|
||||
| `count-api` | — | 8080 | Go |
|
||||
| `database` | — | 5432 | PostgreSQL 16 |
|
||||
| `redis` | — | 6379 | Redis 7 |
|
||||
| `worker` | — | — | Python ARQ (N réplicas) |
|
||||
| `db-migrations` | — | — | Alembic (one-shot) |
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph host [Host]
|
||||
Desktop[DesktopApp Electron]
|
||||
Port8189["0.0.0.0:8189"]
|
||||
end
|
||||
subgraph docker [Red Docker interna]
|
||||
GW[api_gateway :8000]
|
||||
EXT[extraction :8001]
|
||||
CLN[cleaning :8002]
|
||||
CNT[count-api :8080]
|
||||
WRK[worker ARQ x N]
|
||||
PG[(PostgreSQL)]
|
||||
RD[(Redis)]
|
||||
end
|
||||
Desktop -->|"HTTPS prod o 127.0.0.1:8189 dev"| Port8189
|
||||
Port8189 --> GW
|
||||
GW --> EXT
|
||||
GW --> CLN
|
||||
GW --> CNT
|
||||
EXT --> PG
|
||||
EXT --> RD
|
||||
WRK --> PG
|
||||
WRK --> RD
|
||||
CLN --> PG
|
||||
```
|
||||
|
||||
### 3.2 Arranque y dependencias
|
||||
|
||||
El orden de arranque está controlado por healthchecks y `depends_on`:
|
||||
|
||||
1. `database` y `redis` deben estar **healthy**
|
||||
2. `db-migrations` ejecuta Alembic y termina con éxito
|
||||
3. `extraction`, `cleaning`, `count-api` y `worker` arrancan
|
||||
4. `api_gateway` espera a `extraction` healthy y `count-api` healthy
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up --build
|
||||
curl http://127.0.0.1:8189/health/live
|
||||
```
|
||||
|
||||
> En Windows use `127.0.0.1` en lugar de `localhost` para evitar timeouts por resolución IPv6 (`::1`).
|
||||
|
||||
### 3.3 Flujo de negocio (centrado en jobs)
|
||||
|
||||
Un **job** agrupa el trabajo del usuario (carpeta de documentos). Dentro del job se ejecutan **tasks** por módulo (`counting`, `extraction`, `cleaning`).
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant App as DesktopApp
|
||||
participant GW as ApiGateway
|
||||
participant CNT as CountApi
|
||||
participant EXT as Extraction
|
||||
participant DB as PostgreSQL
|
||||
|
||||
App->>GW: POST /count/v1/count
|
||||
GW->>CNT: proxy OCR+ONNX
|
||||
CNT-->>App: resultados por PDF
|
||||
App->>GW: POST /count-jobs
|
||||
GW->>EXT: persistir conteo
|
||||
EXT->>DB: count_job_files
|
||||
App->>GW: POST /tasks + ingest-many
|
||||
GW->>EXT: encolar extracción
|
||||
EXT->>DB: extraction_rows
|
||||
```
|
||||
|
||||
### 3.4 Rutas del API Gateway
|
||||
|
||||
El gateway en [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py) actúa como reverse proxy:
|
||||
|
||||
| Prefijo público | Servicio upstream |
|
||||
|-----------------|-------------------|
|
||||
| `/jobs`, `/tasks`, `/count-jobs` | `extraction:8001` |
|
||||
| `/extraction/*` | `extraction:8001` |
|
||||
| `/cleaning/*` | `cleaning:8002` |
|
||||
| `/count/*` | `count-api:8080` |
|
||||
| `/health`, `/health/live`, `/config`, `/models` | Agregación / proxy |
|
||||
|
||||
Timeouts diferenciados: peticiones JSON (~30 s) vs uploads OCR/conteo (hasta 600 s).
|
||||
|
||||
---
|
||||
|
||||
## 4. Patrones de diseño
|
||||
|
||||
| Patrón | Ubicación | Propósito |
|
||||
|--------|-----------|-----------|
|
||||
| **API Gateway / Reverse Proxy** | [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py) | Un solo endpoint público; enrutamiento por prefijo |
|
||||
| **Microservicios sin paquetes compartidos** | `services/*` | Cada servicio con Dockerfile, dominio y dependencias propias |
|
||||
| **Cola de trabajos async** | [`services/extraction/worker/`](../services/extraction/worker/), Redis | Extracción LLM fuera del ciclo request/response HTTP |
|
||||
| **Registry / Plugin (desktop)** | [`services/desktop_app/src/renderer/modules/registry.ts`](../services/desktop_app/src/renderer/modules/registry.ts) | Módulos `counting` / `extraction` se registran sin acoplar `App.tsx` |
|
||||
| **Job + Task (agregado)** | Tablas `jobs` → `tasks` en PostgreSQL | Un trabajo del usuario agrupa módulos ejecutados |
|
||||
| **Healthchecks en cadena** | [`docker-compose.yml`](../docker-compose.yml) | `depends_on: service_healthy` evita arranques prematuros |
|
||||
| **BFF con timeouts por ruta** | Gateway `_is_upload_path()` | JSON ligero vs uploads pesados |
|
||||
| **Stateless vs stateful** | `count-api` (stateless) vs `extraction` (stateful + BD) | Conteo horizontalmente escalable; extracción con persistencia |
|
||||
|
||||
### Registry de módulos (desktop)
|
||||
|
||||
Cada módulo implementa `ModuleDescriptor` y se auto-registra al importarse:
|
||||
|
||||
```typescript
|
||||
// services/desktop_app/src/renderer/modules/counting/CountingModule.ts
|
||||
registerModule({
|
||||
type: "counting",
|
||||
label: "Conteo",
|
||||
Wizard: CountingWizardModule,
|
||||
Results: CountingResults,
|
||||
hasTask: (tasks) => tasks.some((t) => t.type === "counting"),
|
||||
});
|
||||
```
|
||||
|
||||
`App.tsx` solo importa los módulos; no necesita conocer su implementación interna.
|
||||
|
||||
---
|
||||
|
||||
## 5. Componentes principales
|
||||
|
||||
### 5.1 Backend
|
||||
|
||||
| Servicio | Responsabilidad |
|
||||
|----------|-----------------|
|
||||
| **api_gateway** | Proxy HTTP, `/health/live`, CORS, logging, errores 503/504 hacia upstream caído |
|
||||
| **extraction** | CRUD de jobs/tasks, count-jobs, ingest de PDFs, estimación de costos, API LLM |
|
||||
| **worker** | Procesamiento ARQ en background; escala con `WORKER_REPLICAS` |
|
||||
| **count-api** | OCR (Tesseract) + inferencia ONNX por lote; sin estado ni BD |
|
||||
| **cleaning** | Reglas de limpieza CSV; expuesto vía `/cleaning/*` |
|
||||
| **db-migrations** | Alembic one-shot antes de que arranquen los servicios de aplicación |
|
||||
|
||||
### 5.2 Base de datos (resumen)
|
||||
|
||||
```
|
||||
jobs — contenedor de un trabajo del usuario
|
||||
tasks — módulo ejecutado (counting / extraction / cleaning)
|
||||
count_job_files — resultado de conteo por PDF
|
||||
extraction_batches — lote de extracción
|
||||
extraction_rows — fila extraída por documento
|
||||
cleaning_jobs — job de limpieza
|
||||
cleaned_rows — fila transformada
|
||||
```
|
||||
|
||||
Esquema completo: [`database.dbml`](database.dbml)
|
||||
|
||||
### 5.3 Desktop (`services/desktop_app/`)
|
||||
|
||||
| Componente | Función |
|
||||
|------------|---------|
|
||||
| `App.tsx` | Navegación Home / Job / Settings; sidebar con submenú por job |
|
||||
| `HomeView.tsx` | Listado y creación de jobs |
|
||||
| `JobView.tsx` | Paneles de conteo y extracción (progreso en segundo plano) |
|
||||
| `JobResultsView.tsx` | Resultados unificados, export CSV, lanzar extracción desde válidos |
|
||||
| `CountingWizard.tsx` | Lotes paralelos hacia `POST /count/v1/count` |
|
||||
| `JobExtractionWizard.tsx` | Extracción con upload de PDFs desde el escritorio |
|
||||
| `apiClient.ts` | Cliente HTTP único hacia el gateway (timeouts, mensajes de error) |
|
||||
| `electron/folderIndex.cjs` | Indexación local: modo `general` o `radicados` |
|
||||
| `electron/preload.cjs` | Bridge seguro main ↔ renderer (`contextBridge`) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Cómo escalar para mayor velocidad
|
||||
|
||||
### 6.1 Conteo (`count-api`)
|
||||
|
||||
El conteo es el cuello de botella más frecuente en lotes grandes de PDFs.
|
||||
|
||||
| Variable / ajuste | Efecto | Dónde |
|
||||
|-------------------|--------|-------|
|
||||
| `OCR_DPI=150` | ~4× menos RAM que 300 DPI | `docker-compose.yml` |
|
||||
| `OCR_POOL_SIZE` | Goroutines OCR por request | `docker-compose.yml` |
|
||||
| `SYNC_PROCESS_CONCURRENCY` | Paralelismo interno por request | `docker-compose.yml` |
|
||||
| `COUNT_API_MEMORY` | Límite RAM del contenedor (evitar OOM) | `docker-compose.yml` |
|
||||
| `restart: unless-stopped` | Recuperación automática tras OOM | `docker-compose.yml` |
|
||||
| `BATCH_SIZE=10` | PDFs por petición HTTP | `CountingWizard.tsx` |
|
||||
| `BATCH_CONCURRENCY=2` | Lotes HTTP simultáneos desde desktop | `CountingWizard.tsx` |
|
||||
|
||||
**Futuro:** réplicas de `count-api` detrás del gateway con balanceo round-robin en el proxy.
|
||||
|
||||
### 6.2 Extracción LLM
|
||||
|
||||
| Variable / comando | Efecto |
|
||||
|--------------------|--------|
|
||||
| `WORKER_REPLICAS=4` | Más workers ARQ procesando en paralelo |
|
||||
| `docker compose up --scale worker=4` | Escala horizontal de workers |
|
||||
| `WORKER_CONCURRENCY` | Tareas concurrentes por worker |
|
||||
| `CONCURRENCY_LIMIT` | Límite global en el servicio extraction |
|
||||
| `PDF_THREAD_COUNT` | Hilos para renderizado de PDF |
|
||||
| `PDF_DPI` | Resolución de páginas enviadas al LLM |
|
||||
|
||||
### 6.3 Infraestructura y build
|
||||
|
||||
| Problema | Solución |
|
||||
|----------|----------|
|
||||
| BuildKit satura al construir `count-api` | `docker compose build count-api` antes del stack completo |
|
||||
| Build paralelo falla en Docker Desktop | `COMPOSE_BAKE=false COMPOSE_PARALLEL_LIMIT=2` (ver `.env.example`) |
|
||||
| Conexiones BD colgadas | Pool SQLAlchemy + `connect_timeout` en [`database.py`](../services/extraction/infrastructure/database.py) |
|
||||
| Gateway sin respuesta en Windows | Usar `127.0.0.1:8189`, no `localhost` |
|
||||
|
||||
---
|
||||
|
||||
## 7. Cómo añadir un servicio futuro
|
||||
|
||||
Checklist para incorporar un nuevo microservicio sin romper la arquitectura:
|
||||
|
||||
### Paso 1 — Crear el servicio
|
||||
|
||||
```
|
||||
services/mi-servicio/
|
||||
Dockerfile
|
||||
api/main.py # API propia (FastAPI, Go, etc.)
|
||||
domain/ # lógica de negocio
|
||||
requirements.txt
|
||||
README.md
|
||||
```
|
||||
|
||||
### Paso 2 — Registrar en Docker Compose
|
||||
|
||||
En [`docker-compose.yml`](../docker-compose.yml):
|
||||
|
||||
- Añadir el servicio **sin** `ports` al host (solo red interna)
|
||||
- Definir `healthcheck` si expone `/health`
|
||||
- Configurar `depends_on` según dependencias (BD, migraciones, etc.)
|
||||
|
||||
### Paso 3 — Exponer vía API Gateway
|
||||
|
||||
En [`services/api_gateway/api/main.py`](../services/api_gateway/api/main.py):
|
||||
|
||||
```python
|
||||
MI_SERVICIO_URL = os.getenv("MI_SERVICIO_URL", "http://mi-servicio:8003").rstrip("/")
|
||||
|
||||
@app.api_route("/mi-servicio/{path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE"])
|
||||
async def mi_servicio_proxy(path: str, request: Request) -> Response:
|
||||
return await _proxy_request(request, f"{MI_SERVICIO_URL}/{path}")
|
||||
```
|
||||
|
||||
Añadir `MI_SERVICIO_URL` en el bloque `environment` de `api_gateway` en compose.
|
||||
|
||||
### Paso 4 — Base de datos (si aplica)
|
||||
|
||||
1. Crear migración Alembic en `services/db/alembic/versions/`
|
||||
2. El servicio `db-migrations` la aplicará en el próximo `docker compose up`
|
||||
|
||||
### Paso 5 — Módulo en la app desktop (si aplica)
|
||||
|
||||
1. Crear `services/desktop_app/src/renderer/modules/mi-modulo/`
|
||||
2. Implementar `ModuleDescriptor` y llamar `registerModule()`
|
||||
3. Importar el módulo en `App.tsx` (una línea, igual que counting/extraction)
|
||||
4. Añadir entrada en el sidebar de `JobView` si requiere sección propia
|
||||
|
||||
### Paso 6 — Documentación
|
||||
|
||||
- README del servicio
|
||||
- Actualizar tabla de servicios en [`README.md`](../README.md)
|
||||
- Actualizar este documento si el patrón de integración cambia
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
NewSvc[nuevo_servicio]
|
||||
Compose[docker-compose]
|
||||
GW[api_gateway nueva ruta]
|
||||
Reg[registerModule desktop]
|
||||
NewSvc --> Compose
|
||||
Compose --> GW
|
||||
GW --> Desktop
|
||||
Reg --> Desktop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. App de escritorio — versión y funcionalidades
|
||||
|
||||
### 8.1 Versión e instalador
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **Versión** | 1.0.0 |
|
||||
| **Fecha de release** | 2026-07-06 |
|
||||
| **Instalador** | `services/desktop_app/release/IPSExtractor-Desktop-1.0.0-Setup.exe` |
|
||||
| **Formato** | NSIS, Windows x64 |
|
||||
| **Fuente de versión** | `services/desktop_app/package.json` + `src/shared/version.json` |
|
||||
|
||||
Generar instalador:
|
||||
|
||||
```bash
|
||||
cd services/desktop_app
|
||||
npm install
|
||||
npm run dist
|
||||
```
|
||||
|
||||
### 8.2 URL del API Gateway
|
||||
|
||||
| Entorno | URL por defecto |
|
||||
|---------|-----------------|
|
||||
| Desarrollo (`npm run dev`) | `http://127.0.0.1:8189` |
|
||||
| Instalador (build producción) | `https://pys1.builder.xyz` |
|
||||
|
||||
La URL es configurable en **Configuración** de la app y se persiste en `localStorage`.
|
||||
|
||||
### 8.3 Funcionalidades incluidas
|
||||
|
||||
- **Gestión de trabajos (jobs):** crear, abrir y organizar trabajos con carpeta local de documentos
|
||||
- **Conteo:** OCR + clasificación ML en lotes con barra de progreso en segundo plano
|
||||
- **Validación humana:** marcar archivos del conteo como válidos/inválidos
|
||||
- **Extracción LLM:** wizard con descubrimiento de campos, estimación de costo y ejecución
|
||||
- **Extracción desde conteo:** lanzar extracción solo sobre PDFs válidos del conteo
|
||||
- **Resultados unificados:** vista de conteo y extracción con exportación CSV
|
||||
- **Configuración:** URL del gateway, verificación de conexión, vista de config/modelos del backend
|
||||
- **Versión visible:** sidebar (menú) y esquina superior derecha (`v1.0.0 · 6 jul 2026`)
|
||||
|
||||
### 8.4 Arquitectura de la UI
|
||||
|
||||
```
|
||||
Home (lista de jobs)
|
||||
└── Job activo
|
||||
├── Conteo → CountingWizardModule
|
||||
├── Extracción → JobExtractionWizard
|
||||
└── Resultados → JobResultsView
|
||||
Configuración (global)
|
||||
```
|
||||
|
||||
La navegación usa sidebar lateral con submenú contextual por job (sin tabs en el área principal).
|
||||
|
||||
---
|
||||
|
||||
## 9. Enlaces relacionados
|
||||
|
||||
| Documento | Descripción |
|
||||
|-----------|-------------|
|
||||
| [`README.md`](../README.md) | Guía rápida del stack completo |
|
||||
| [`services/api_gateway/README.md`](../services/api_gateway/README.md) | API Gateway |
|
||||
| [`services/extraction/README.md`](../services/extraction/README.md) | Extracción LLM y jobs |
|
||||
| [`services/cleaning/README.md`](../services/cleaning/README.md) | Limpieza de datos |
|
||||
| [`services/services-count-facturas-medicas-serving/README.md`](../services/services-count-facturas-medicas-serving/README.md) | Count API (Go) |
|
||||
| [`services/db/README.md`](../services/db/README.md) | Migraciones Alembic |
|
||||
| [`services/desktop_app/README.md`](../services/desktop_app/README.md) | App Electron |
|
||||
| [`database.dbml`](database.dbml) | Esquema completo de PostgreSQL |
|
||||
| [`.env.example`](../.env.example) | Variables de entorno |
|
||||
|
||||
---
|
||||
|
||||
*Última actualización: 2026-07-06 — rama `migration-services`.*
|
||||
Reference in New Issue
Block a user