Saltar al contenido
Ridgun
Español

40 El Libro de Ridgun

La lib net_agent — Supervisor de Conexiones de Red y Puente de Eventos No Bloqueante

40.1. ¿Por qué existe la lib net_agent? (El problema real y la filosofía Ridgun)

«¿Por qué necesito una lib net_agent escrita desde cero en Ridgun puro en lugar de enlazar una librería de C de toda la vida?»

Esa es la primera pregunta que se hace cualquier ingeniero de sistemas al llegar a net_agent. Y la respuesta resume toda la filosofía de ridgun-libs:

Seamos sinceros: en un servidor real como Solstice, aceptar conexiones TCP es la parte fácil; lo difícil es limitar el máximo de clientes simultáneos, desconectar clientes zombis inactivos (idle timeout) y conectar el bucle ppoll con tus actores sin fugas de descriptores.

La lib net_agent cierra el círculo de ridgun-libs uniendo posix, socket y agent: gestiona tablas de conexiones supervisadas (NetSupervisor), aplica límites de concurrencia (max_conns), purga conexiones inactivas (net evict idle) y convierte eventos de descriptores del kernel en eventos tipados (NetEvReadyRead, NetEvReadyWrite, NetEvTimeout, NetEvError) además de ofrecer un paso de eco TCP completo de extremo a extremo (net tcp echo step).

En un ecosistema tradicional, resolver las tareas del dominio AGENT implica arrastrar dependencias externas en C, lidiar con punteros crudos, conversiones opacas de tipos, excepciones no verificadas o pausas impredecibles del recolector de basura. En Ridgun, la lib net_agent (src/main/ridgun/lib/net_agent.rg) aborda el problema con cuatro principios innegociables:

  1. Declara import net_agent (o import net_agent as alias) al inicio del archivo: cada lib define su espacio de nombres module :: net agent y en modo --strict las funciones multi-palabra se invocan siempre con espacios naturales.
  2. Encadena tus tuberías de izquierda a derecha con ->: escribe entrada paso uno -> paso dos -> resultado, usa máscaras (+ _ _ +) para desestructurar secuencias y omite _ = en sentencias secuenciales (o descarta con -> ()).
  3. Recursión de cola con TCO (Tail-Call Optimization): todos los recorridos internos de net_agent se compilan a bucles nativos en $O(1)$ espacio de pila.
  4. Memoria determinista (RMM1 / RMM2): cada estructura intermedia se libera en el instante exacto en que termina su última lectura, sin Garbage Collector.

40.2. Artefactos nativos, importación y catálogo completo de la API de net_agent

En ridgun-libs, net_agent es una lib de primera clase ubicada en src/main/ridgun/lib/net_agent.rg (dominio AGENT). Al compilar con stable bundle, se empaqueta en tres artefactos nativos:

ArtefactoRutaPropósito
Bundle Ridguntarget/bundles/net_agent.rglibDistribución e importación directa con import net_agent
Archivo compiladotarget/net_agent.rgcArtefacto intermedio verificado por ridgunc
Objeto compartido ELF64target/libnet_agent.soBiblioteca dinámica nativa sin dependencias de C ni libc

Cada lib declara su propio espacio de nombres con module :: net agent: basta con escribir import net_agent (o import net_agent as alias) en el encabezado y llamar a sus funciones con espacios naturales (mi funcion x) y tuberías -> (x mi funcion -> resultado).

40.2.1. Resumen de familias funcionales de net_agent

Firma / Función / TipoQué hace y cuándo usarla
net supervisor new : Int -> Int -> NetSupervisorCrea un supervisor de conexiones con límite max_conns e inactividad idle_timeout_ms
net register conn / net record rx / net record tx / net close connRegistra sockets aceptados, actualiza contadores de bytes/actividad y cierra pares
net evict idle : NetSupervisor -> Int -> (NetSupervisor, [Int])Cierra y expulsa conexiones inactivas más allá de idle_timeout_ms, devolviendo sus FDs
net poll fd : Int -> Bool -> Int -> NetEventPuente wait fd no bloqueante que emite NetEvReadyRead, NetEvReadyWrite, NetEvTimeout o NetEvError
net tcp echo step : String -> Int -> (Int, String)Paso completo de extremo a extremo TCP loopback (listen/connect/accept/supervise/echo)

40.2.2. Catálogo exhaustivo de tipos y firmas en src/main/ridgun/lib/net_agent.rg

A continuación tienes el inventario completo de tipos algebraicos (type), operadores (op ::) y firmas de funciones (fn ::) definidos en src/main/ridgun/lib/net_agent.rg (2 tipos, 0 operadores y 8 funciones):

Tipos algebraicos y registros (type) de net_agent:

type ConnSlot = ConnSlot Int Agent
type StepOutcome = ConnKeep ConnSlot String | ConnClosed
#Función / Firma (fn ::)Tipo de Retorno
1fn :: conn poll step ConnSlotStepOutcome
2fn :: conn handle event Int Agent IntStepOutcome
3fn :: conn feed payload Int Agent StringStepOutcome
4fn :: conn cast str AString
5fn :: conn drain outbound Int Agent OptionStepOutcome
6fn :: reactor sweep [ConnSlot][ConnSlot]
7fn :: reactor sweep next StepOutcome [ConnSlot][ConnSlot]
8fn :: reactor shutdown [ConnSlot]()

40.3. Explorando net_agent a fondo: Ejemplos paso a paso de cada funcionalidad

No basta con ver la tabla de firmas: vamos a explorar cada una de las funcionalidades de la lib net_agent mediante ejemplos progresivos y ejecutables en Ridgun. Empezaremos por las dos recetas tabulares de uso cotidiano y después recorreremos en detalle todos los escenarios funcionales, transformaciones y casos límite verificados en src/test/ridgun/test net_agent.rg.

40.3.1. Ejemplo 1: Flujo principal en estilo tabular con net_agent

Observa cómo en Ridgun los signos = y los pasos de transformación con net_agent quedan alineados como una tabla limpia, sin variables mutables innecesarias ni efectos laterales ocultos:

import net_agent

fn :: barrer conexiones activas [ConnSlot] => [ConnSlot]
      barrer conexiones activas tabla =
        tabla reactor sweep

40.3.2. Ejemplo 2: Composición segura y manejo de casos de borde

Toda función de la lib net_agent está diseñada para componerse con guardas booleanas | y con el resto de las 32 libs de ridgun-libs:

import net_agent

fn :: apagar reactor [ConnSlot] => ()
      apagar reactor tabla =
        tabla reactor shutdown

40.3.3. Ejemplo 3: Combinadores de actores (agent ask, try ask, send batch, map batch, drain ready), pools particionados y reactor de conexiones

En este ejemplo práctico exploramos de forma exhaustiva cómo se comporta net_agent en este bloque funcional, incluyendo entradas estándar, combinaciones de parámetros y valores frontera:

fn :: test spec combinators pool and net agent => ()
      test spec combinators pool and net agent =
        w1 = agent (ag double worker)
        assert int equals (agent ask w1 21) 42 "agent ask 21 -> 42"
        mb = agent map batch w1 [3, 5, 7]
        assert int equals (ag sum ints mb) 30 "agent map batch [3,5,7] -> [6,10,14] sum 30"
        agent send batch w1 [10, 20] -> ()
        rc = agent receive count w1 2
        assert int equals (ag sum ints rc) 60 "agent send batch + agent receive count -> 60"
        c_dr = new counter agent 100
        send 5 to c_dr
        send 10 to c_dr
        dr = agent drain ready c_dr
        assert int equals (ag sum ints dr) 220 "agent drain ready [105, 115] -> 220"
        agent stop all [w1, c_dr] -> ()
        assert false (is agent alive w1) "w1 stopped by agent stop all"
        assert false (is agent alive c_dr) "c_dr stopped by agent stop all"

        s0 = new counter agent 0
        s1 = new counter agent 1000
        pl = Pool 2 [s0, s1]
        assert int equals (pool size pl) 2 "pool size 2"
        assert int equals (pool ask shard pl 0 5) 5 "pool ask shard 0 -> 5"
        assert int equals (pool ask shard pl 1 7) 1007 "pool ask shard 1 -> 1007"
        assert int equals (pool ask shard pl (0 - 3) 3) 1010 "pool ask shard negative key -> shard 1"
        pool broadcast pl 10 -> ()
        all_dr = pool drain all pl
        assert int equals (ag sum ints all_dr) 1035 "pool drain all [15, 1020] -> 1035"
        pool shutdown pl -> ()
        assert false (is agent alive s0) "pool s0 stopped"
        assert false (is agent alive s1) "pool s1 stopped"

        ca = agent (ag echo conn step) (CounterState 0)
        cb = agent (ag echo conn step) (CounterState 0)
        s_eof = conn feed payload (0 - 1) ca ""
        s_err = conn handle event (0 - 1) cb 8
        kept  = reactor sweep next s_eof (reactor sweep next s_err [])
        assert int equals (agent list len kept 0) 0 "reactor sweep next prunes closed slots"
        assert false (is agent alive ca) "ca stopped on EOF"
        assert false (is agent alive cb) "cb stopped on POLLERR"
        cc = agent (ag echo conn step) (CounterState 0)
        reactor shutdown [ConnSlot (0 - 1) cc] -> ()
        assert false (is agent alive cc) "cc stopped on reactor shutdown"

40.3.4. Ejemplo 4: Ciclo de vida de agentes (is agent alive, agent status, stop agent) y agente contador con estado aislado (new counter agent)

En este ejemplo práctico exploramos de forma exhaustiva cómo se comporta net_agent en este bloque funcional, incluyendo entradas estándar, combinaciones de parámetros y valores frontera:

fn :: test agent lifecycle and counter => ()
      test agent lifecycle and counter =
        assert str eq (format agent status AgentRunning) "running" "format AgentRunning"
        assert str eq (format agent status AgentStopped) "stopped" "format AgentStopped"
        assert str eq (format agent status (AgentTimedOut 250)) "timeout after 250 ms" "format AgentTimedOut"
        assert str eq (format agent status (AgentFailed "boom")) "failed: boom" "format AgentFailed"
        assert str eq (format agent reply (ReplyInt 42)) "42" "format ReplyInt"
        assert str eq (format agent reply (ReplyStr "ok")) "ok" "format ReplyStr"
        assert str eq (format agent reply (ReplyBool True)) "true" "format ReplyBool True"
        assert str eq (format agent reply (ReplyBool False)) "false" "format ReplyBool False"
        assert str eq (format agent reply ReplyEmpty) "agent outbox empty" "format ReplyEmpty"
        assert str eq (format agent reply ReplyStopped) "agent stopped" "format ReplyStopped"
        assert str eq (format agent reply (ReplyTimeout 50)) "agent timeout after 50 ms" "format ReplyTimeout"
        assert str eq (format agent reply (ReplyError "bad")) "agent error: bad" "format ReplyError"

        c   = new counter agent 10
        assert ok (is agent alive c)
        assert str eq (format agent status (agent status c)) "running" "counter running"
        assert str eq (format agent reply (try poll int c)) "agent outbox empty" "empty outbox before send"
        assert eq (counter get c) 10
        assert eq (counter add c 5) 15
        assert eq (counter add c 7) 22
        assert str eq (format agent reply (ask int c 8)) "30" "ask int 8 -> 30"
        assert str eq (format agent reply (ask int with timeout c 12 200)) "42" "ask int with timeout -> 42"
        assert eq (counter reset c) 0
        assert eq (counter get c) 0
        bs  = ask batch int c [1, 2, 3, 4]
        assert eq (ag sum ints bs) 20

        c2  = new counter agent 100
        rel = relay int c c2 5
        assert str eq (format agent reply rel) "115" "relay int c (10+5=15) -> c2 (100+15=115)"

        st1 = stop agent c
        assert str eq (format agent status st1) "stopped" "stop agent returns AgentStopped"
        assert eq (is agent alive c) False
        assert str eq (format agent status (agent status c)) "stopped" "agent status after stop"
        assert str eq (format agent reply (ask int c 1)) "agent stopped" "ask int after stop"
        assert str eq (format agent reply (try poll int c)) "agent stopped" "try poll int after stop"
        assert str eq (format agent reply (ask int with timeout c 1 50)) "agent stopped" "ask int with timeout after stop"
        assert str eq (format agent reply (relay int c c2 1)) "agent stopped" "relay int after stop"
        c0  = new counter agent 0
        assert int equals (counter get c0) 0 "counter 0 initial"
        assert int equals (counter add c0 3) 3 "counter 0 add 3"
        stop agent c0
        stop agent c2
        assert eq (is agent alive c2) False

40.3.5. Ejemplo 5: Agente de bitácora append-only (new log agent) y almacén clave-valor en memoria por paso de mensajes (new kv agent)

En este ejemplo práctico exploramos de forma exhaustiva cómo se comporta net_agent en este bloque funcional, incluyendo entradas estándar, combinaciones de parámetros y valores frontera:

fn :: test log and kv agents => ()
      test log and kv agents =
        lg  = new log agent "\n"
        assert ok (is agent alive lg)
        assert str eq (format agent reply (try poll str lg)) "agent outbox empty" "log empty outbox"
        assert str eq (log read lg) "" "initial log empty"
        assert str eq (log append lg "alpha") "alpha" "log append alpha"
        assert str eq (log append lg "beta") "alpha\nbeta" "log append beta"
        assert str eq (format agent reply (ask str with timeout lg "gamma" 200)) "alpha\nbeta\ngamma" "ask str with timeout gamma"
        assert str eq (log read lg) "alpha\nbeta\ngamma" "log read"
        assert str eq (log clear lg) "" "log clear"
        assert str eq (log read lg) "" "log read after clear"
        b   = ask batch str lg ["one", "two"]
        assert str eq (ag join strs b) "one|one\ntwo" "ask batch str"
        lg2 = new log agent ":"
        assert str eq (format agent reply (relay str lg lg2 "three")) "one\ntwo\nthree" "relay str"
        stop agent lg
        stop agent lg2
        assert str eq (format agent reply (ask str lg "x")) "agent stopped" "ask str after stop"

        kv  = new kv agent
        assert ok (is agent alive kv)
        assert int equals (kv count kv) 0 "kv count 0"
        assert false (kv has kv "host") "kv has host false"
        assert str eq (kv put kv "host" "127.0.0.1") "127.0.0.1" "kv put host"
        assert str eq (kv put kv "port" "8080") "8080" "kv put port"
        assert int equals (kv count kv) 2 "kv count 2"
        assert ok (kv has kv "host")
        assert ok (kv has kv "port")
        assert str eq (kv get kv "host") "127.0.0.1" "kv get host"
        assert str eq (kv get kv "port") "8080" "kv get port"
        assert str eq (kv keys kv) "host,port" "kv keys"
        assert str eq (kv put kv "port" "9090") "9090" "kv overwrite port"
        assert str eq (kv get kv "port") "9090" "kv get updated port"
        assert int equals (kv count kv) 2 "kv count after overwrite"
        assert str eq (kv delete kv "host") "127.0.0.1" "kv delete host"
        assert false (kv has kv "host") "kv has host after delete"
        assert int equals (kv count kv) 1 "kv count 1"
        assert str eq (kv clear kv) "" "kv clear"
        assert int equals (kv count kv) 0 "kv count after clear"
        stop agent kv
        assert false (is agent alive kv) "kv stopped"

40.3.6. Ejemplo 6: Sondeo no bloqueante de descriptores (conn poll step), barrido de tabla de conexiones (reactor sweep) y apagado limpio (reactor shutdown)

En este ejemplo práctico exploramos de forma exhaustiva cómo se comporta net_agent en este bloque funcional, incluyendo entradas estándar, combinaciones de parámetros y valores frontera:

import agent
import net_agent

fn :: sesion eco (n : Int) (trama : String) => (Int, String)
      sesion eco n trama =
        sig = n + 1
        (sig, "[#{sig}] {trama}")

fn :: ejemplo reactor conexiones (fd1 : Int) (fd2 : Int) => [ConnSlot]
      ejemplo reactor conexiones fd1 fd2 =
        ag1     = agent sesion eco 0
        ag2     = agent sesion eco 0
        activas = [ConnSlot fd1 ag1, ConnSlot fd2 ag2]
        -- Barrido no bloqueante (sys poll timeout=0) de todas las conexiones
        vivas   = reactor sweep activas
        -- Cierre ordenado de descriptores y parada determinista de agentes
        reactor shutdown vivas -> ()
        []

40.4. Bajo el capó: cómo está construida net_agent en Ridgun puro (src/main/ridgun/lib/net_agent.rg)

Una de las mayores ventajas de ridgun-libs es que no hay cajas negras en C. Puedes abrir src/main/ridgun/lib/net_agent.rg y leer exactamente cómo está implementada cada operación usando pattern matching, árboles de guardas | / |. y recursión de cola (TCO). Veamos las piezas clave de su implementación real:

40.4.1. Implementación interna: Core Implementation Part 1

Fíjate en cómo net_agent.rg estructura las firmas fn :: ... => ... primero y resuelve cada caso mediante ecuaciones alineadas y llamadas de cola:

module :: net agent

type ConnSlot = ConnSlot Int Agent

type StepOutcome = ConnKeep ConnSlot String | ConnClosed

fn :: conn poll step ConnSlot         => StepOutcome
      conn poll step (ConnSlot fd ag) =
        sys poll fd 1 0 -> ev
        conn handle event fd ag ev

fn :: conn handle event Int Agent Int => StepOutcome
      conn handle event fd ag ev
        | ev < 0 =
          _ = sys close fd
          stop ag
          ConnClosed
        | ev >= 8 =
          _ = sys close fd
          stop ag
          ConnClosed
        | ev == 0 =
      conn drain outbound fd ag (try receive from ag)
        | otherwise =
          payload = sys read fd fd 65536
          conn feed payload fd ag payload

fn :: conn feed payload Int Agent String => StepOutcome
      conn feed payload fd ag "" =
        sys close fd -> ()
        stop ag
        ConnClosed
      conn feed payload fd ag data =
        send data to ag
        conn drain outbound fd ag (try receive from ag)

40.4.2. Implementación interna: Core Implementation Part 2

Fíjate en cómo net_agent.rg estructura las firmas fn :: ... => ... primero y resuelve cada caso mediante ecuaciones alineadas y llamadas de cola:

fn :: conn cast str A => String
      conn cast str x =  x

fn :: conn drain outbound Int Agent Option => StepOutcome
      conn drain outbound fd ag None = ConnKeep (ConnSlot fd ag) ""
      conn drain outbound fd ag (Some resp) =
        resp conn cast str -> s
        sys write fd fd s -> ()
        ConnKeep (ConnSlot fd ag) s

fn :: reactor sweep [ConnSlot]  => [ConnSlot]
      reactor sweep []          =  []
      reactor sweep (slot:rest) =
        slot conn poll step -> outcome
        rest reactor sweep -> kept
        reactor sweep next outcome kept

fn :: reactor sweep next StepOutcome       [ConnSlot] => [ConnSlot]
      reactor sweep next ConnClosed        kept       =  kept
      reactor sweep next (ConnKeep slot _) kept       =  [slot] :: kept

fn :: reactor shutdown [ConnSlot] => ()
      reactor shutdown []         =  ()
      reactor shutdown ((ConnSlot fd ag):rest) =
        sys close fd -> ()
        stop ag
        reactor shutdown rest

40.5. Garantías algorítmicas, memoria RMM1/RMM2 y las 5 Reglas de Oro de net_agent

  • Garantía algorítmica y de seguridad: Protección automática frente a agotamiento de descriptores (max_conns), expulsión determinista de conexiones inactivas y cierre garantizado de sockets en net tcp echo step.
  • Complejidad de pila $O(1)$ con TCO: Todos los bucles recursivos en src/main/ridgun/lib/net_agent.rg están en posición de cola, por lo que ridgunc los compila a saltos nativos sin crecimiento de pila.
  • Memoria determinista (RMM1/RMM2): Cero pausas de recolector de basura; todas las estructuras intermedias se liberan de forma determinista al salir del ámbito.
  • Verificación continua: Probada exhaustivamente con stable check y la suite automatizada src/test/ridgun/test net_agent.rg.

Las 5 Reglas de Oro al programar con net_agent

  1. Declara import net_agent al inicio del archivo y aprovecha la equivalencia entre espacios y guiones bajos para que tu código se lea como prosa.
  2. Alinea tus tuberías de datos con =: encadena las llamadas a net_agent en pasos con nombre propio y deja que stable format alinee las columnas.
  3. Aprovecha las funciones totales de net_agent en tus guardas |: comprueba los valores de retorno o desenvuelve los ADTs con pattern matching en lugar de asumir que la entrada externa siempre es válida.
  4. Evita copiar grandes estructuras si solo necesitas consultar un campo: recuerda que en RMM2 los argumentos de entrada no incrementan ni decrementan contadores (+0 / -0).
  5. Combina net_agent con test (import test): escribe siempre aserciones con assert equals o assert true para fijar los invariantes de tu dominio.

¡Has completado el recorrido exhaustivo por las 32 libs estándar de ridgun-libs! Ahora tienes en tus manos todas las piezas para construir aplicaciones y herramientas de sistemas completas en Ridgun puro.