Vai al contenuto
Ridgun
Italiano

40 Il Libro di Ridgun

La lib net_agent — Supervisore di Connessioni di Rete e Ponte di Eventi Non Bloccante

40.1. Perché esiste la lib net_agent? (Il problema reale e la filosofia Ridgun)

«Perché ho bisogno di una lib net_agent scritta da zero in Ridgun puro invece di collegare una libreria C tradizionale?»

Questa è la prima domanda che si pone qualsiasi ingegnere di sistema arrivando a net_agent. E la risposta riassume l'intera filosofia di ridgun-libs:

Siamo sinceri: in un server reale come Solstice, accettare connessioni TCP è la parte facile; il difficile è limitare il numero massimo di client simultanei, disconnettere i client zombie inattivi (idle timeout) e collegare il ciclo ppoll ai propri attori senza perdite di descrittori.

La lib net_agent unisce posix, socket e agent: gestisce tabelle di connessioni supervisionate (NetSupervisor), applica limiti di concorrenza (max_conns), espelle le connessioni inattive (net evict idle) e converte gli eventi dei descrittori del kernel in eventi tipizzati (NetEvReadyRead, NetEvReadyWrite, NetEvTimeout, NetEvError), oltre a offrire uno step di echo TCP completo end-to-end (net tcp echo step).

In un ecosistema tradizionale, risolvere i compiti del dominio AGENT comporta trascinare dipendenze esterne in C, gestire puntatori grezzi, conversioni opache di tipo, eccezioni non verificate o pause imprevedibili del garbage collector. In Ridgun, la lib net_agent (src/main/ridgun/lib/net_agent.rg) affronta il problema con quattro principi non negoziabili:

  1. Dichiara import net_agent (o import net_agent as alias) all'inizio del file: ogni lib definisce il proprio spazio dei nomi module :: net agent e in modalità --strict le funzioni multi-parola si invocano sempre con spazi naturali.
  2. Concatena le tue pipeline da sinistra a destra con ->: scrivi input passo uno -> passo due -> risultato, usa le maschere (+ _ _ +) per destrutturare le sequenze e ometti _ = nelle istruzioni sequenziali (o scarta il valore con -> ()).
  3. Ricorsione di coda con TCO (Tail-Call Optimization): tutte le scansioni interne di net_agent vengono compilate in cicli nativi in spazio di stack $O(1)$.
  4. Memoria deterministica (RMM1 / RMM2): ogni struttura intermedia viene liberata nell'istante esatto in cui termina la sua ultima lettura, senza Garbage Collector.

40.2. Artefatti nativi, importazione e catalogo completo dell'API di net_agent

In ridgun-libs, net_agent è una lib di prima classe situata in src/main/ridgun/lib/net_agent.rg (dominio AGENT). Compilando con stable bundle, viene impacchettata in tre artefatti nativi:

ArtefattoPercorsoScopo
Bundle Ridguntarget/bundles/net_agent.rglibDistribuzione e importazione diretta con import net_agent
File compilatotarget/net_agent.rgcArtefatto intermedio verificato da ridgunc
Oggetto condiviso ELF64target/libnet_agent.soLibreria dinamica nativa senza dipendenze C né libc

Ogni lib dichiara il proprio spazio dei nomi con module :: net agent: basta scrivere import net_agent (o import net_agent as alias) nell'intestazione e invocare le sue funzioni con spazi naturali (mia funzione x) e pipeline -> (x mia funzione -> risultato).

40.2.1. Riepilogo delle famiglie funzionali di net_agent

Firma / Funzione / TipoCosa fa e quando usarla
net supervisor new : Int -> Int -> NetSupervisorCrea un supervisore di connessioni con limite max_conns e inattività idle_timeout_ms
net register conn / net record rx / net record tx / net close connRegistra i socket accettati, aggiorna i contatori di byte/attività e chiude i peer
net evict idle : NetSupervisor -> Int -> (NetSupervisor, [Int])Chiude ed espelle le connessioni inattive oltre idle_timeout_ms, restituendo i relativi FD
net poll fd : Int -> Bool -> Int -> NetEventPonte wait fd non bloccante che emette NetEvReadyRead, NetEvReadyWrite, NetEvTimeout o NetEvError
net tcp echo step : String -> Int -> (Int, String)Step completo end-to-end TCP loopback (listen/connect/accept/supervise/echo)

40.2.2. Catalogo esaustivo dei tipi e delle firme in src/main/ridgun/lib/net_agent.rg

Di seguito trovi l'inventario completo dei tipi algebrici (type), degli operatori (op ::) e delle firme di funzione (fn ::) definiti in src/main/ridgun/lib/net_agent.rg (2 tipi, 0 operatori e 8 funzioni):

Tipi algebrici e record (type) di net_agent:

type ConnSlot = ConnSlot Int Agent
type StepOutcome = ConnKeep ConnSlot String | ConnClosed
#Funzione / Firma (fn ::)Tipo di Ritorno
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. Esplorare net_agent a fondo: Esempi passo dopo passo di ogni funzionalità

Non basta consultare la tabella delle firme: esploreremo ciascuna delle funzionalità della lib net_agent tramite esempi progressivi ed eseguibili in Ridgun. Inizieremo con due ricette tabulari di uso quotidiano e poi percorreremo nel dettaglio tutti gli scenari funzionali, le trasformazioni e i casi limite verificati in src/test/ridgun/test net_agent.rg.

40.3.1. Esempio 1: Flusso principale in stile tabulare con net_agent

Osserva come in Ridgun i segni = e i passaggi di trasformazione con net_agent risultino allineati come una tabella pulita, senza variabili mutabili superflue né effetti collaterali nascosti:

import net_agent

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

40.3.2. Esempio 2: Composizione sicura e gestione dei casi limite

Ogni funzione della lib net_agent è progettata per comporsi con le guardie booleane | e con il resto delle 34 lib di ridgun-libs:

import net_agent

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

40.3.3. Esempio 3: Combinatori di attori (agent ask, try ask, send batch, map batch, drain ready), pool partizionati e reattore di connessioni

In questo esempio pratico esploriamo in modo esaustivo come si comporta net_agent in questo blocco funzionale, inclusi input standard, combinazioni di parametri e valori di frontiera:

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. Esempio 4: Ciclo di vita degli agenti (is agent alive, agent status, stop agent) e agente contatore con stato isolato (new counter agent)

In questo esempio pratico esploriamo in modo esaustivo come si comporta net_agent in questo blocco funzionale, inclusi input standard, combinazioni di parametri e valori di frontiera:

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. Esempio 5: Agente di registro append-only (new log agent) e store chiave-valore in memoria tramite scambio di messaggi (new kv agent)

In questo esempio pratico esploriamo in modo esaustivo come si comporta net_agent in questo blocco funzionale, inclusi input standard, combinazioni di parametri e valori di frontiera:

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. Esempio 6: Polling non bloccante dei descrittori (conn poll step), scansione della tabella connessioni (reactor sweep) e arresto pulito (reactor shutdown)

In questo esempio pratico esploriamo in modo esaustivo come si comporta net_agent in questo blocco funzionale, inclusi input standard, combinazioni di parametri e valori di frontiera:

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. Sotto il cofano: come è costruita net_agent in Ridgun puro (src/main/ridgun/lib/net_agent.rg)

Uno dei maggiori vantaggi di ridgun-libs è che non ci sono scatole nere in C. Puoi aprire src/main/ridgun/lib/net_agent.rg e leggere esattamente come è implementata ogni operazione usando pattern matching, alberi di guardie | / |. e ricorsione di coda (TCO). Vediamo i pezzi chiave della sua implementazione reale:

40.4.1. Implementazione interna: Core Implementation Part 1

Nota come net_agent.rg strutturi prima le firme fn :: ... => ... e risolva ogni caso tramite equazioni allineate e chiamate di coda:

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. Implementazione interna: Core Implementation Part 2

Nota come net_agent.rg strutturi prima le firme fn :: ... => ... e risolva ogni caso tramite equazioni allineate e chiamate di coda:

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. Garanzie algoritmiche, memoria RMM1/RMM2 e le 5 Regole d'Oro di net_agent

  • Garanzia algoritmica e di sicurezza: Protezione automatica contro l'esaurimento dei descrittori (max_conns), espulsione deterministica delle connessioni inattive e chiusura garantita dei socket in net tcp echo step.
  • Complessità dello stack $O(1)$ con TCO: Tutti i cicli ricorsivi in src/main/ridgun/lib/net_agent.rg sono in posizione di coda, perciò ridgunc li compila in salti nativi senza crescita dello stack.
  • Memoria deterministica (RMM1/RMM2): Zero pause del garbage collector; tutte le strutture intermedie vengono liberate in modo deterministico all'uscita dallo scope.
  • Verifica continua: Testata in modo esaustivo con stable check e la suite automatizzata src/test/ridgun/test net_agent.rg.

Le 5 Regole d'Oro quando programmi con net_agent

  1. Dichiara import net_agent all'inizio del file e usa le funzioni multi-parola con spazi naturali affinché il tuo codice si legga come prosa.
  2. Allinea le tue pipeline di dati con = e ->: concatena le chiamate a net_agent in passaggi con nomi chiari e lascia che stable format allinei le colonne.
  3. Sfrutta le funzioni totali di net_agent nelle tue guardie |: controlla i valori di ritorno o destruttura gli ADT con pattern matching invece di dare per scontato che l'input esterno sia sempre valido.
  4. Evita di copiare grandi strutture se devi solo consultare un campo: ricorda che in RMM2 gli argomenti di input sono prestati e non incrementano né decrementano i contatori (+0 / -0).
  5. Combina net_agent con test (import test): scrivi sempre asserzioni con assert equals o assert true per fissare gli invarianti del tuo dominio.

Con net_agent completiamo lo stack di rete e concorrenza ad attori, pronti per passare agli Agenti di IA nel Capitolo 41 e all'internazionalizzazione CLDR nel Capitolo 42.