jfastframework
Documentación English PyPI GitHub

Arquitectura

Cada decisión aquí tiene un costo. Este documento nombra las dos caras.


1. Una librería versionada, no un generador de código

Decisión. El runtime vive en jfastframework y se importa. El generador emite solo el código que es genuinamente de cada servicio.

Por qué. La generación anterior escribía su runtime como literales de string dentro de funciones generadoras. Cada servicio generado era un fork congelado en el momento de la generación; un arreglo en el cliente HTTP compartido significaba parchear N repositorios a mano, y las plantillas que viven dentro de literales de string no pasan por un linter, no se ven en un diff y no se pueden testear.

Costo. Los servicios ya no pueden editar la capa compartida libremente. Eso era una funcionalidad accidental: cuando algo no encajaba, los equipos parcheaban local. Si los puntos de extensión no son suficientemente buenos, los forks vuelven, solo que ahora escondidos. Ese es el riesgo del que el contrato de plugins se tiene que ganar la salida.


2. Todo lo que está arriba del kernel es un plugin

Decisión. Monitoreo, base de datos, cache, RAG y error tracking son plugins. El kernel los resuelve, los ordena, los arranca y los detiene. No sabe nada más.

Por qué. "Monitoreo por defecto, removible a pedido" solo es cierto si el monitoreo no es un caso especial. Los built-in se registran por el mismo grupo de entry points jfastframework.plugins que usan los terceros, así que un plugin de terceros puede reemplazar a uno built-in reclamando la misma provider key.

Costo. Indirección. Leer create_app no te dice qué hace la app: para eso corres jfast describe. Ese es el trade que el diseño acepta, y la razón por la que las herramientas de introspección son parte del kernel y no un add-on.


3. Los plugins se comunican por keys de string

Decisión. ctx.provide("db.engine", engine) / ctx.require("db.engine"). Los plugins nunca se importan entre sí.

Por qué. Los imports directos harían rígido el grafo: cambiar el cache de Redis por uno en memoria tocaría a todos los consumidores.

Costo. No hay chequeo estático de tipos cruzando la frontera. Mitigaciones: require(key, expected=Type) chequea en runtime, meta.provides se declara por adelantado, los reclamos duplicados fallan en build time, y un provider que falta levanta un error que nombra lo que sí está disponible.


4. Los plugins declaran su infraestructura

Decisión. Plugin.infra() devuelve los contenedores que el plugin necesita. jfast deploy compose los compone en un archivo de compose.

Por qué. Los archivos de compose mantenidos a mano se desvían de la app. Si apagas el plugin de cache, el contenedor de Redis debería desaparecer, no quedarse seis meses hasta que alguien lo note.

Costo. Ahora los plugins saben algo del deploy, que estrictamente no es asunto suyo. La alternativa —un manifiesto de infra aparte— se desvía, que es exactamente la falla que este diseño quiere eliminar. Aceptado a propósito.

Convención. Un servicio es dueño de un bloque de diez puertos que empieza en su puerto base; cada plugin declara un offset dentro del bloque. Heredado del esquema de CometaX, menos el IAM central como dependencia dura.


5. Dos endpoints de health, no uno

Decisión. /health es liveness y nunca sondea dependencias. /ready agrega el health check de cada plugin y devuelve 503 cuando falla uno crítico.

Por qué. Mezclarlos causa reinicios en cascada: la base de datos parpadea, la probe de liveness falla, el orquestador mata pods sanos, y la estampida termina de voltear a la base de datos.

Detalle. HealthReport.critical=False significa degradado, no caído. Un cache frío no debería sacar al servicio de rotación.


6. RFC 7807 para todos los errores

Decisión. Todas las fallas se serializan a application/problem+json.

Por qué. Una sola forma de error en todos los servicios. Los clientes y los servicios hermanos parsean una vez.

Detalle. El handler catch-all devuelve el mensaje de la excepción solo cuando debug está prendido. Filtrar stack traces y rutas internas a clientes de producción es un hallazgo de information disclosure, y el default tiene que ser el seguro.


7. Convención de nombres de constraints fijada en Base

Decisión. MetaData(naming_convention=...) en jfastframework.db.base.

Por qué. Sin ella, PostgreSQL inventa los nombres de los constraints y el --autogenerate de Alembic produce diffs distintos en máquinas distintas. Con decenas de servicios corriendo autogenerate, eso es un desorden de migraciones permanente y de bajo grado.

Costo. Las bases de datos existentes con constraints auto-nombrados necesitan una migración única para adoptarla. Hazlo antes de que la flota crezca, no después.


8. Las plantillas son archivos, no strings

Decisión. Plantillas Jinja2 bajo templates/, renderizadas por Scaffolder.

Por qué. Las plantillas que son archivos pasan por un linter, se ven en un diff durante el review, y se testean renderizándolas.

Detalle. Cada árbol generado lleva un sello .jfast-template que registra la versión del framework y el contexto de render. Ese sello es lo que le va a permitir a un futuro jfast upgrade mostrar un diff en vez de una reescritura (fase 3).


9. Precedencia de configuración

CLI overrides  >  environment  >  jfast.toml  >  field defaults

Los secretos van en el entorno como SecretStr, nunca en jfast.toml: ese archivo se commitea. SecretStr además mantiene los DSN fuera de la salida de jfast describe y del endpoint /info.


10. Las dependencias opcionales fallan tarde, no en el discovery

Decisión. Un plugin cuyo extra no está instalado se salta durante el discovery y queda registrado en discover.broken. Solo levanta error si el servicio realmente lo habilita, y ahí el error nombra la falla del import.

Por qué. Instalar jfastframework sin [rag] no debe romper jfast --help. Pero habilitar rag sin [rag] debe fallar fuerte, con la razón.


Ciclo de vida de una request

request
  → RequestContextMiddleware      assign/propagate X-Request-ID, bind contextvars
  → PrometheusMiddleware          RED metrics, labelled by route template
  → route handler
      → DbSession                     sesión; confirma antes de la respuesta
      → Service                       domain rules
      → Repository                    data access, tenant-filtered
  → response                      X-Request-ID echoed back
  ← exception                     → problem+json, request_id attached

Ciclo de vida del arranque

create_app()
  1. load jfast.toml + env            → JFastConfig
  2. discover plugins                 → entry points + dotted paths
  3. select and order                 → allow/deny, requires, cycles, conflicts
  4. instantiate                      → each plugin gets its [plugin.<name>] block
  5. install error handlers
  6. plugin.register(ctx)             → routers, middleware, providers. No I/O.
  7. mount system + app routers

lifespan startup
  8. plugin.startup(ctx)              → in order. Pools, connections, DDL.
lifespan shutdown
  9. plugin.shutdown(ctx)             → reverse order. One failure does not
                                         block the rest.