Skip to content
Ridgun
English

40 The Ridgun Book

The net_agent Lib — Socket Connection Actor Supervisor and Non-Blocking Event Bridge

40.1. Why Does the net_agent Lib Exist? (The Real-World Problem & Ridgun Philosophy)

"Why do I need a net_agent lib written from scratch in pure Ridgun instead of just linking a legacy C library?"

That is the first question every systems engineer asks when meeting net_agent. And the answer captures the entire philosophy of ridgun-libs:

Let's be honest: in a real server like Solstice, accepting TCP connections is the easy part; the hard part is enforcing max concurrent clients, reaping idle zombie connections, and bridging ppoll readiness into actor messages without leaking file descriptors.

The net_agent lib closes the loop of ridgun-libs by uniting posix, socket, and agent: it manages supervised connection tables (NetSupervisor), enforces concurrency limits (max_conns), reaps idle peers (net evict idle), bridges kernel descriptor readiness into typed actor events (NetEvReadyRead, NetEvReadyWrite, NetEvTimeout, NetEvError), and provides an end-to-end supervised TCP echo step (net tcp echo step).

In a traditional ecosystem, solving AGENT tasks means pulling in external C dependencies, wrestling with raw pointers, opaque type conversions, unchecked exceptions, or unpredictable garbage-collector pauses. In Ridgun, the net_agent lib (src/main/ridgun/lib/net_agent.rg) tackles the problem with four non-negotiable principles:

  1. Declare import net_agent (or import net_agent as alias) at the top of your file: every lib defines its module :: net agent namespace, and under --strict multi-word functions are always called with natural spaces.
  2. Chain your data pipelines left-to-right with ->: write input step one -> step two -> result, use (+ _ _ +) sequence masks to destructure tuples/lists, and omit _ = on sequential statements (or discard with -> ()).
  3. Tail-Call Optimization (TCO): all internal loops in net_agent are tail-recursive and compile into native hardware loops using $O(1)$ stack space.
  4. Deterministic Memory (RMM1 / RMM2): every intermediate allocation is reclaimed at the exact instruction where its last read finishes, with no Garbage Collector.

40.2. Native Artifacts, Imports, and Complete net_agent API Catalog

In ridgun-libs, net_agent is a first-class lib located at src/main/ridgun/lib/net_agent.rg (domain AGENT). When built with stable bundle, it compiles into three native artifacts:

ArtifactPathPurpose
Ridgun Bundletarget/bundles/net_agent.rglibPortable distribution and direct import net_agent resolution
Compiled Archivetarget/net_agent.rgcType-checked intermediate artifact verified by ridgunc
ELF64 Shared Objecttarget/libnet_agent.soNative dynamic shared library with zero C or libc dependencies

Every lib declares its own namespace via module :: net agent: simply write import net_agent (or import net_agent as alias) at the top and call its functions with natural spaces (my fn x) and left-to-right -> pipelines (x my fn -> result).

40.2.1. Summary of net_agent Functional Families

Signature / Function / TypePurpose & Usage
net supervisor new : Int -> Int -> NetSupervisorCreate a connection supervisor with max_conns and idle_timeout_ms
net register conn / net record rx / net record tx / net close connRegister accepted sockets, track byte counters/activity timestamps, and close peers
net evict idle : NetSupervisor -> Int -> (NetSupervisor, [Int])Close and evict connections idle longer than idle_timeout_ms, returning evicted FDs
net poll fd : Int -> Bool -> Int -> NetEventNon-blocking wait fd bridge emitting NetEvReadyRead, NetEvReadyWrite, NetEvTimeout, or NetEvError
net tcp echo step : String -> Int -> (Int, String)End-to-end loopback TCP listen/connect/accept/supervise/echo verification step

40.2.2. Exhaustive Catalog of Types and Signatures in src/main/ridgun/lib/net_agent.rg

Below is the complete inventory of algebraic data types (type), mixfix operators (op ::), and function signatures (fn ::) defined in src/main/ridgun/lib/net_agent.rg (2 types, 0 operators, and 8 functions):

Algebraic Data Types & Records (type) in net_agent:

type ConnSlot = ConnSlot Int Agent
type StepOutcome = ConnKeep ConnSlot String | ConnClosed
#Function / Signature (fn ::)Return Type
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. Deep Dive into net_agent: Step-by-Step Examples for Every Feature

A signature table alone is never enough: let's explore every capability of the net_agent lib through progressive, runnable Ridgun examples. We will start with two everyday tabular recipes and then walk through every functional scenario, transformation, and boundary condition verified in src/test/ridgun/test net_agent.rg.

40.3.1. Example 1: Core Tabular Workflow with net_agent

Notice how in Ridgun the = signs and transformation steps using net_agent align into a clean, scannable table without unnecessary mutable variables or hidden side effects:

import net_agent

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

40.3.2. Example 2: Safe Composition and Boundary Handling

Every function in the net_agent lib is designed to compose cleanly with boolean guards | and with the rest of the 32 libs in ridgun-libs:

import net_agent

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

40.3.3. Example 3: Actor Combinators (agent ask, try ask, send batch, map batch, drain ready), Sharded Pools, and Connection Reactor

In this hands-on walkthrough we thoroughly exercise how net_agent behaves across this functional area, covering standard inputs, parameter combinations, and corner cases:

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. Example 4: Agent Lifecycle (is agent alive, agent status, stop agent) and Isolated Stateful Counter Agent (new counter agent)

In this hands-on walkthrough we thoroughly exercise how net_agent behaves across this functional area, covering standard inputs, parameter combinations, and corner cases:

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. Example 5: Append-Only Log Agent (new log agent) and Message-Passing In-Memory Key-Value Store (new kv agent)

In this hands-on walkthrough we thoroughly exercise how net_agent behaves across this functional area, covering standard inputs, parameter combinations, and corner cases:

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. Example 6: Non-Blocking Descriptor Polling (conn poll step), Connection Table Sweep (reactor sweep), and Graceful Shutdown (reactor shutdown)

In this hands-on walkthrough we thoroughly exercise how net_agent behaves across this functional area, covering standard inputs, parameter combinations, and corner cases:

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. Under the Hood: How net_agent Is Built in Pure Ridgun (src/main/ridgun/lib/net_agent.rg)

One of the biggest advantages of ridgun-libs is that there are no C black boxes. You can open src/main/ridgun/lib/net_agent.rg and read exactly how every operation is implemented using pattern matching, | / |. guard trees, and tail-call optimization (TCO). Let's inspect the key building blocks of its real implementation:

40.4.1. Internal Implementation: Core Implementation Part 1

Observe how net_agent.rg declares explicit fn :: ... => ... contracts first and resolves each case through aligned equations and tail calls:

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

Observe how net_agent.rg declares explicit fn :: ... => ... contracts first and resolves each case through aligned equations and tail calls:

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. Algorithmic Guarantees, RMM1/RMM2 Memory, and the 5 Golden Rules of net_agent

  • Algorithmic & Safety Guarantee: Automatic protection against descriptor exhaustion (max_conns), deterministic eviction of idle peers, and guaranteed socket cleanup in net tcp echo step.
  • $O(1)$ Stack Complexity via TCO: Every recursive loop in src/main/ridgun/lib/net_agent.rg is in tail position, so ridgunc compiles them into native jumps with zero stack growth.
  • Deterministic Memory (RMM1/RMM2): Zero garbage-collector pauses; all intermediate allocations are reclaimed deterministically on scope exit.
  • Continuous Verification: Statically checked via stable check and tested by the automated suite in src/test/ridgun/test net_agent.rg.

The 5 Golden Rules When Programming with net_agent

  1. Declare import net_agent at the top of your file and take advantage of space/underscore equivalence so your code reads like natural prose.
  2. Align your data pipelines with =: chain net_agent calls into named intermediate steps and let stable format align the columns.
  3. Leverage net_agent's total functions inside | guards: inspect return values or unwrap ADTs via pattern matching rather than assuming external input is valid.
  4. Avoid cloning large structures when only reading: remember that under RMM2, function input arguments incur zero reference-count overhead (+0 / -0).
  5. Pair net_agent with test (import test): always lock in your domain invariants with assert equals or assert true.

You have completed the comprehensive tour of all 32 standard libs in ridgun-libs! You now have every building block needed to craft complete, zero-libc applications and systems tools in pure Ridgun.