Conceptos
Cinco piezas explican casi todo el comportamiento de ExyonQ: listener, server, route, upstream y módulos. A ellas se suman dos reglas que conviene tener presentes: cómo se elige una ruta cuando varias encajan, y qué parte de la configuración cambia en caliente.
El modelo
Listener
Es la dirección que el proceso abre: [[server]].listen. Cada dirección declarada se abre y se sirve (multilistener). Sobre ese socket se cuelgan las opciones que no pueden ser por host:
[server.tls]: certificado y clave, y la política criptográfica del socket.http3_listen: dirección QUIC adicional, solo en el primer[[server]].proxy_protocol: si se espera cabecera PROXY antes de TLS/HTTP.
Los bloques [[server]] que comparten un listen se colapsan en un solo socket. Por eso existen restricciones: todos deben habilitar TLS o ninguno, deben declarar el mismo proxy_protocol, y solo el primero puede omitir server_name, llevar acme, strict_sni, protocols, ciphers o curves.
Server
[[server]] agrupa tres cosas: server_name (nombres que identifican el bloque, usados para elegir certificado por SNI), la lista routes de rutas declaradas y el material TLS. No es un «virtual host» cerrado: la coincidencia de una petición se decide con el match de cada ruta, y esa identidad es global.
Route
[[route]] es la unidad de decisión. Tiene un name único, un match y exactamente una acción:
| Acción | Campo |
|---|---|
| Servir ficheros | root (+ index) |
| Proxy HTTP | upstream |
| FastCGI | fastcgi (nombre de pool) |
| Redirección | redirect |
| Reescritura interna | rewrite |
Declarar dos acciones es un error de configuración (route ... defines conflicting actions), igual que no declarar ninguna (route ... must define upstream, root, redirect, rewrite, or fastcgi).
Upstream
[[upstream]] es un backend HTTP con nombre. O bien un target único (http://127.0.0.1:9000), o bien un conjunto endpoints con pesos y prioridades. Añade timeouts, política de selección, failover y health checks activos. Las rutas lo referencian por nombre; un nombre inexistente es error de configuración.
Módulos
Todo lo que no es enrutado vive en tablas propias: [modules.compression], [modules.metrics], [modules.ratelimit], [modules.real_ip], [static.preload], [static.encoding_cache], [full_page_cache], [http3], [waf] y [logging]. Están apagados por defecto salvo donde se indique lo contrario, y cada uno se documenta en Configuración.
Match y prioridad de rutas
Un match combina un patrón de ruta (exactamente uno) con filtros opcionales (AND):
| Campo | Función |
|---|---|
path | Prefijo de directorio; / es el catch-all |
suffix | Sufijo literal (path.ends_with) |
regex | Expresión regular de Rust, sensible a mayúsculas, máximo 256 bytes |
host | Host exacto o *.sufijo |
header + header_value | Cabecera presente y no vacía, o con valor exacto |
path, suffix y regex son mutuamente excluyentes: ninguno o varios a la vez produce route match must set exactly one of path, suffix, or regex.
Cuando varias rutas encajan, se elige la mayor según esta tupla, en orden:
- Rango de host: host exacto (2) >
*.sufijo(1) > sin host (0). - Rango de tipo:
regexysuffix(3) > prefijo normal (1) > catch-all/(0). - Longitud del patrón: dentro del mismo tipo, gana el patrón más largo (
/assets/imgantes que/assets). - Índice de declaración: a igualdad de todo lo anterior, gana la ruta declarada antes.
Es el criterio de «prefijo más largo» con dos correcciones: una regex o un sufijo siempre baten a un prefijo, y un host concreto siempre bate a un comodín.
Identidad de ruta
La identidad de una ruta es la tupla (host, tipo, patrón), con el host normalizado y el patrón sin barra final en el caso de prefijo. Es global para toda la configuración: dos rutas con la misma identidad hacen fallar la compilación del plan con duplicate route match identity host=… pattern=…. Esto es deliberado: evita que el orden de declaración decida en silencio qué ruta gana, y es la razón por la que los puertos 80 y 443 se reparten en dos procesos (ver Instalación).
Consecuencia práctica: para publicar el mismo patrón en cinco dominios hacen falta cinco rutas, una por host. Por eso las configuraciones reales son repetitivas: exyonq, exyonq_www, eqproxy, eqproxy_www… son la misma idea con distinto host.
Recarga frente a reinicio
Cada configuración válida compilada se publica como una generación numerada, con una huella (fingerprint) que la identifica. El proceso vivo conserva la generación actual y las peticiones en vuelo terminan con la que empezaron.
Recargar no es releer un fichero: es compilar un plan candidato, validarlo y publicarlo por el socket de control.
exyonqctl config reload /etc/exyonq/https.toml --diff # clasifica sin publicar
exyonqctl config reload /etc/exyonq/https.toml --check # preflight offline
exyonqctl reload -s /run/exyonq/https.sock # publica
--diff imprime current_fingerprint, candidate_fingerprint, las rutas añadidas y quitadas, y clasifica cada cambio. Un candidato idéntico se marca IDENTICAL_RELOAD = NO_OP. Si la compilación del candidato falla, la generación anterior sigue viva: la recarga no deja el servicio a medias.
Qué cambia en caliente
| Elemento | Recarga | Notas |
|---|---|---|
| Rutas, hosts, rewrites, redirecciones | Sí | Generación nueva; las peticiones en vuelo siguen en la anterior |
| Upstreams, pesos, timeouts, health checks | Sí | |
Material TLS (cert, key) | Sí | El aceptador se reconstruye con las rutas válidas |
Fichero OCSP (ocsp_stapling) | Sí | Se relee al construir el aceptador; un reload basta aunque el TOML no cambie |
| WAF | Sí | El snapshot se compila en la fase de preparación y se intercambia al publicar |
[static.preload] | Sí | La precarga se reconstruye con la generación; si falla, se conserva el snapshot anterior |
| Caché de página completa | Parcial | Las entradas van ligadas a la generación; se pueden purgar por generación |
[logging] | Sí, con matices | Cambiar sinks es configuración nueva |
Dirección de escucha, http3_listen, habilitar un protocolo | No | Requiere reinicio (EXY-RELOAD-0005) |
La regla corta: si el cambio toca qué socket se abre, reinicia. Si toca cómo se atiende una petición en un socket ya abierto, recarga.
exyonqctl draindeja de aceptar trabajo nuevo y espera a que termine lo que está en vuelo. Los túneles WebSocket vivos sobreviven a una recarga y al drenaje; las peticiones nuevas durante el drenaje se rechazan con 503.