> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Arquitectura de Fetcher

> Cómo corre Fetcher: un servicio Manager, un servicio Worker y el Engine de extracción sin dependencias que ambos hospedan en proceso.

Fetcher se distribuye como dos servicios. El **Manager** expone la API HTTP y despacha el trabajo. El **Worker** consume ese trabajo y produce los resultados.

Ambos servicios son **hosts**. Ninguno contiene la lógica de extracción. Los dos ejecutan el mismo **Engine** en proceso, a través de los mismos puertos, bajo las mismas reglas. Ese único hecho explica el resto de esta página, y explica por qué tu propia aplicación puede ejecutar extracciones de Fetcher sin ninguno de los dos servicios.

## Los dos servicios

***

| Servicio    | Rol                                                                                                                                  |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Manager** | API HTTP para conexiones y jobs. Guarda los metadatos en MongoDB. Publica los jobs de extracción en RabbitMQ.                        |
| **Worker**  | Consumidor de cola. Ejecuta la extracción, protege el resultado, lo escribe en almacenamiento de objetos y emite un evento terminal. |

### Manager

El Manager sigue un diseño hexagonal. Los adaptadores HTTP mapean las solicitudes hacia los servicios, y los servicios llegan a la infraestructura solo a través de puertos. Los caminos de lectura y los de escritura viven en paquetes de servicio separados.

Sirve doce operaciones en dos prefijos de ruta:

| Ruta                                                        | Operaciones                                                                      |
| ----------------------------------------------------------- | -------------------------------------------------------------------------------- |
| `/v1/fetcher`                                               | Crear un job de extracción. Obtener un job por identificador.                    |
| `/v1/management/connections`                                | Listar y crear conexiones. Validar un mapeo de esquema.                          |
| `/v1/management/connections/{id}`                           | Obtener, actualizar, eliminar, probar y descubrir el esquema de una conexión.    |
| `/v1/management/connections/unassigned` y `.../{id}/assign` | Dos operaciones de migración para conexiones anteriores al alcance por producto. |

Tres dependencias de infraestructura respaldan la API:

* **MongoDB** guarda los registros de conexión y los registros de job.
* **RabbitMQ** lleva un job nuevo hasta el Worker por la cola que nombra `RABBITMQ_FETCHER_WORK_QUEUE`.
* **Valkey o Redis** respalda la caché de esquemas y el limitador de tasa de la prueba de conexión. La caché de esquemas recurre a la memoria del proceso cuando Redis está inalcanzable.

Una solicitud de creación de job responde `202 Accepted`. Una repetición de la misma solicitud dentro de una ventana de cinco minutos responde `200 OK` con el job que ya existe.

### Worker

El Worker no tiene servidor HTTP principal. Consume la cola de trabajo y ejecuta `RABBITMQ_NUMBERS_OF_WORKERS` jobs en paralelo, cinco por defecto. Un pequeño servidor de salud en `HEALTH_PORT` lleva las sondas y el endpoint de métricas.

Para cada job, el Worker:

1. Resuelve las conexiones que mapea el job y ejecuta la extracción a través del Engine embebido.
2. Firma el JSON en texto plano con HMAC-SHA256, con una clave derivada de la clave maestra.
3. Cifra el payload firmado con AES-GCM y lo escribe en almacenamiento de objetos compatible con S3.
4. Publica `job.completed` o `job.failed` en el exchange que nombra `RABBITMQ_JOB_EVENTS_EXCHANGE`.

El Worker conduce el Engine en modo directo. El Engine devuelve los bytes exactos, y el Worker es dueño de la protección y del almacenamiento de esos bytes. Ambos eventos terminales son rutas obligatorias. El Worker se niega a arrancar cuando `STREAMING_ENABLED` es false o el exchange de eventos está vacío, porque un emisor silencioso que no hace nada se tragaría el contrato.

<Note>
  El Worker habla el protocolo S3. SeaweedFS funciona como destino de almacenamiento a través de su gateway compatible con S3, que es como corre el stack local de Docker Compose. Define la retención de los resultados con una política de ciclo de vida en el bucket.
</Note>

### Mensajes entre ellos

Cada mensaje que publica el Manager lleva una firma HMAC-SHA256. El payload firmado une el timestamp, la versión de firma, el ID de tenant, el ID de job, el exchange y la routing key. El cuerpo va al final. Un replay del mismo cuerpo bajo otro tenant o por otra ruta falla la verificación. El firmante rechaza cualquier clave menor a 32 bytes y compara las firmas en tiempo constante.

## El Engine por debajo

***

El Engine es dueño de lo que una extracción **significa**. Un host es dueño de cómo **se ejecuta**.

| El Engine decide                               | El host provee                                           |
| ---------------------------------------------- | -------------------------------------------------------- |
| Las reglas del ciclo de vida de las conexiones | Dónde viven las conexiones                               |
| El descubrimiento y la validación de esquemas  | El driver que lee el catálogo                            |
| La planificación y la ejecución de consultas   | Los conectores de base de datos                          |
| Los límites de recursos y los timeouts         | El transporte que los expone                             |
| El alcance de tenant en cada operación         | La identidad de tenant de la solicitud                   |
| Las categorías de error                        | El estado HTTP o el comportamiento de cola por categoría |

El Engine es un módulo Go separado: `github.com/LerianStudio/fetcher/pkg/engine`. Su `go.mod` no lleva ningún bloque `require`, y un job de CI falla la compilación si aparece uno. Una segunda prueba, forzada por la compilación, recorre cada dependencia transitiva y rechaza todo lo que no sea la biblioteca estándar de Go o local al módulo del Engine. Ningún framework HTTP, ningún cliente de cola, ningún driver de base de datos y ningún SDK de nube puede entrar.

Esa restricción es el punto. El Engine no agrega ninguna dependencia de terceros, así que una aplicación host puede embeberlo sin una sola entrada nueva en su propio árbol de dependencias.

### Puertos

Un host conecta el Engine mediante un conjunto pequeño de puertos. Uno es siempre obligatorio. Un segundo lo es solo cuando la persistencia cifrada está activada. El resto son opcionales.

| Puerto                   | Obligatorio | Sin él                                                                                                                                      |
| ------------------------ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `ConnectorRegistry`      | Sí          | El Engine se niega a construirse.                                                                                                           |
| `CredentialProtector`    | Condicional | Obligatorio cuando la persistencia cifrada está activa. El Engine se niega a construirse antes que almacenar una credencial en texto plano. |
| `ConnectionStore`        | No          | El Engine se construye, pero falla toda operación que toque una conexión. En la práctica solo puede reportar sus límites.                   |
| `SchemaCache`            | No          | Cada llamada de esquema descubre en vivo desde el datasource.                                                                               |
| `ResultSink`             | No          | El modo de almacenamiento no está disponible. La extracción devuelve las filas en línea.                                                    |
| `ExecutionStore`         | No          | El Engine no lleva estado durable de ejecución. El host es dueño del estado.                                                                |
| `ActiveExecutionChecker` | No          | El Engine no aplica ninguna barrera de conflicto. Las actualizaciones y eliminaciones de conexión siguen adelante.                          |
| `Observability`          | No          | Los hooks de span desaparecen. El comportamiento no cambia.                                                                                 |

Un puerto provisto como nil tipado cuenta como ausente. El Engine lo detecta en la construcción, así que un host mal configurado falla en el arranque en lugar de romperse en la primera llamada.

El módulo también incluye implementaciones en memoria de los puertos de almacenamiento — el registro de conectores, el connection store, la caché de esquemas, el result sink y el execution store. Eso cubre la infraestructura que un test necesita, así que puedes ejercitar el Engine sin MongoDB, sin Redis, sin RabbitMQ y sin almacenamiento de objetos. No incluye un `CredentialProtector`, así que un test que active la persistencia cifrada debe aportar el suyo.

### Clasificación de fallos

El Engine devuelve una de once categorías estables: `validation`, `not_found`, `unauthorized`, `forbidden`, `limit_exceeded`, `conflict`, `unavailable`, `connect`, `timeout`, `canceled` e `internal`. Un host mapea cada categoría a su propio transporte. El Manager mapea `conflict` a HTTP 409, por ejemplo.

Los errores que cruzan la frontera del Engine llevan texto fijo. El Engine descarta el error del driver que hay detrás de un fallo, porque ese texto puede incluir un DSN, una credencial o detalles internos del driver.

## Salud y readiness

***

Ambos servicios montan `/health`, `/readyz`, `/readyz/tenant/{id}` y `/metrics` antes de la autenticación, para que Kubernetes y los balanceadores de carga los alcancen sin un token.

* `/health` responde 503 hasta que la autosonda de arranque tiene éxito. El kubelet reinicia entonces un pod que no pudo alcanzar sus dependencias al arrancar.
* `/readyz` ejecuta cada sonda de dependencia en paralelo en cada solicitud, sin caché y con un timeout por dependencia. El Manager sondea MongoDB, RabbitMQ y Redis. El Worker sondea MongoDB, RabbitMQ y el bucket de almacenamiento. En modo multi-tenant, ambos servicios agregan el Tenant Manager y el Redis multi-tenant.
* Con `SIGTERM`, ambos servicios responden `/readyz` con 503 durante `READYZ_DRAIN_DELAY_SEC` segundos, 12 por defecto, antes de cerrar las conexiones. Kubernetes saca el pod del Service mientras este todavía atiende tráfico.

## Embeber en lugar de desplegar

***

El Manager y el Worker son hosts, no capas privilegiadas. Provee un registro de conectores y un almacén de conexiones, y tu aplicación obtiene la misma planificación de consultas, los mismos límites, las mismas verificaciones de tenant y la misma forma de resultado en proceso. No necesita cola, ni almacenamiento de objetos, ni un despliegue aparte.

Los productos de Lerian embeben el Engine exactamente así. Lee [Conceptos centrales](/es/fetcher/fetcher-core-concepts) para conocer el modelo que implementa el Engine.

## Próximos pasos

***

<CardGroup cols={2}>
  <Card title="Conexiones" icon="plug" href="/es/fetcher/fetcher-connections">
    Registra, prueba, actualiza y elimina una conexión a un datasource.
  </Card>

  <Card title="Configuración" icon="gear" href="/es/fetcher/fetcher-configuration">
    Las variables de entorno que dan forma a un despliegue de Fetcher.
  </Card>
</CardGroup>
