moonzero
A service framework for MoonBit — config-driven assembly of a moonapi app with middleware into a runnable AsgiApp, the way go-zero does for Go. Backend-agnostic; served by mooncat.
moon add moonbitstack/moonzero✶ The contract at a glance
let conf = ServiceConf::new(name="greet", port=8888) let server = Server::new(conf, app).use_(logging) server.describe() // "greet listening on 0.0.0.0:8888" @mooncat.serve(server.to_asgi(), port=conf.port) // run it (native)
§Service assembly
ServiceConf (typed config: name, host, port, timeout, log level) + Server tie a moonapi App and a middleware onion into a runnable AsgiApp; logging is a built-in middleware. Served by mooncat.
struct Server
A moonzero service: its config plus the assembled application (a moonapi App with any middleware already wrapped around it).
fn Server::new(conf : @conf.ServiceConf, app : @moonapi.App) -> Server
Assemble a service from config and a moonapi application.
fn Server::use_(self : Server, mw : @handler.Middleware) -> Server
Wrap the current application in another middleware layer (outermost last).
fn Server::to_asgi(self : Server) -> @moonasgi.AsgiApp
The assembled AsgiApp, ready for a server (mooncat) to run.
fn Server::describe(self : Server) -> String
A human-readable description of what this service binds to.
§Middleware set
The onion layers that wrap the app: recovery (500 instead of a panic), cors (Access-Control-* headers), and request_id (x-request-id per request).
fn recovery(inner : @moonasgi.AsgiApp) -> @moonasgi.AsgiApp
Recovery middleware (← go-zero's RecoverHandler): run the wrapped application inside a try, and if it raises, emit a 500 Internal Server Error instead of letting the failure escape to the server. A downstream that has already streamed its response start before raising will produce a second start event; recovery is a last-resort guard, so it always answers rather than trying to detect that race.
struct CorsConf
CORS configuration (← go-zero's cors.Middleware options): the values echoed back in the Access-Control-* preflight/response headers.
fn CorsConf::new( allow_origin~ : String = "*", allow_methods~ : String = "GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS", allow_headers~ : String = "Content-Type, Authorization", allow_credentials~ : Bool = false, max_age~ : Int32 = 86400) -> CorsConf
Build a permissive CORS config: any origin, the full method set, and a one-day preflight cache. Credentials are off by default, matching go-zero.
fn cors(conf : CorsConf) -> Middleware
CORS middleware (← go-zero's cors.Middleware): wrap the outbound Send so the configured Access-Control-* headers are injected onto every HttpResponseStart, leaving the body and other events untouched.
fn request_id(header~ : String = "x-request-id") -> Middleware
Request-ID middleware (← go-zero's trace/x-request-id handling): reuse an inbound x-request-id if the client sent one, otherwise mint a fresh monotonic id, and stamp it onto every response's HttpResponseStart. The counter is captured once per assembly, so ids stay unique across the requests this layer serves.
type Middleware = (@moonasgi.AsgiApp) -> @moonasgi.AsgiApp
An AsgiApp transformer — one layer of the middleware onion.
fn logging(inner : @moonasgi.AsgiApp) -> @moonasgi.AsgiApp
A request-logging middleware: writes METHOD path through the process logx at info for each HTTP request, then delegates to the wrapped application. It logs on the way in and knows nothing of the response; structured_logging is the layer that times the request and records its status.
§Route groups
Group registers a set of moonapi routes under a shared path prefix, so related endpoints are declared without repeating the prefix.
struct Group
A route group (← go-zero's RouteGroup): registers a set of routes on an underlying moonapi.App under a shared path prefix, so related endpoints (e.g. everything under /api/v1) are declared without repeating the prefix.
fn Group::new(app : @moonapi.App, prefix : String) -> Group
Open a group that prefixes every route it registers with prefix on app.
fn Group::prefix(self : Group) -> String
The prefix this group joins onto each registered route.
fn Group::route( self : Group, verb : @moonapi.Method, path : String, handler : @moonapi.ApiHandler, summary~ : String = "") -> Unit
Register a route for an explicit method under the group's prefix.
fn Group::get( self : Group, path : String, handler : @moonapi.ApiHandler, summary~ : String = "") -> Unit
Register a GET route under the group's prefix.
fn Group::post( self : Group, path : String, handler : @moonapi.ApiHandler, summary~ : String = "") -> Unit
Register a POST route under the group's prefix.
fn Group::put( self : Group, path : String, handler : @moonapi.ApiHandler, summary~ : String = "") -> Unit
Register a PUT route under the group's prefix.
fn Group::patch( self : Group, path : String, handler : @moonapi.ApiHandler, summary~ : String = "") -> Unit
Register a PATCH route under the group's prefix.
fn Group::delete( self : Group, path : String, handler : @moonapi.ApiHandler, summary~ : String = "") -> Unit
Register a DELETE route under the group's prefix.
§Typed config loading
Parse a JSON or YAML config string into a ServiceConf: a strict derived FromJson path plus lenient ServiceConf::from_json / from_yaml loaders that fill omitted fields from the new() defaults, the way go-zero's conf.Load applies ,optional/,default= tags.
struct ServiceConf
Service configuration (← go-zero's ServiceConf): the service name, its bind address, a request timeout, and the log level. timeout_ms is the per-request budget in milliseconds; 0 disables the deadline.
fn ServiceConf::new( name~ : String = "app", host~ : String = "0.0.0.0", port~ : Int32 = port, timeout_ms~ : Int32 = 3000, log_level~ : @logx.LogLevel = Info) -> ServiceConf
Build a config with sensible defaults (0.0.0.0:12000, 3s timeout, info).
extend ServiceConf with Eq::
let port : Int32 = 12000
The default port a moonzero service listens on. 12000 is the family's number, not go-zero's 8888: every example, every document and every local run in this ecosystem points at the same place, and on a development machine 8888 is somebody else's already.
let rpc_port : Int32 = 12001
The default port the zRPC server listens on. gRPC shares 12000 with HTTP wherever one listener carries both, but a moonzero process binds RestEngine and RpcServer separately — two listeners cannot hold one port — so the second takes the next number.
§JWT (HS256)
base64url plus compact-JWT signing and verification under HS256: jwt_sign / jwt_verify check the signature in constant time and enforce exp/nbf, rejecting the alg:none downgrade — go-zero's token auth core.
fn jwt_sign( claims : Map[String, Json], secret : String, typ~ : String? = Some("JWT"), extra? : Map[String, Json], wins? : @jwt.Wins, clash? : @jwt.OnClash[Map[String, Json]]) -> String
Sign a claims set as a compact HS256 JWT (← go-zero's jwt.NewWithClaims(SigningMethodHS256, ...)). claims is the payload; put exp/iat/nbf/sub in it as ordinary entries. typ is the header's type ("JWT" unless given; None writes no typ header at all), and extra merges further members into the header. Configuration has three layers, later ones winning: the library preset < the typ given here < extra. wins turns the last two around and clash decides what a collision does — by default the reserved header fields we write are the ones that stand, and overwriting one raises.
fn jwt_verify( token : String, secret : String, now_secs : Int64, policy~ : @jwt.Policy = @jwt.policy) -> Map[String, Json] raise @jwt.Refused
Verify a compact HS256 JWT and return its claims (← go-zero's handler.Authorize). now_secs is the verification time as a Unix timestamp in **seconds**, the unit JWT's NumericDate uses — not the milliseconds the [Clock] seam carries, because the claim is defined in seconds. policy carries every check: exp and nbf are verified by default and iat is not, which is what PyJWT and jjwt both do. Hand it a different Policy to require an issuer, an audience, or particular claims.
§JWT auth middleware
The auth middleware requires every HTTP request to carry a valid Authorization: Bearer <jwt>, rejecting absent/malformed/tampered/expired tokens with 401 before the app runs.
fn jwt_authorized( token : String?, secret : String, now_secs : Int64) -> Bool
Whether a request bearing token is authorised at now_secs: the token verifies against secret under HS256 and is neither expired nor not-yet-valid. A missing token is unauthorised. Exposed as a pure decision so the middleware's accept/reject is testable without driving the transport.
fn auth(secret : String, clock : @clock.Clock) -> Middleware
JWT auth middleware (← go-zero's handler.Authorize): require every HTTP request to carry a valid Authorization: Bearer <jwt> header. The token is verified against secret under HS256 at the current time read from clock (milliseconds, converted to the JWT seconds epoch); an absent, malformed, tampered, expired, or not-yet-valid token is rejected with 401 Unauthorized before the wrapped app runs. Non-HTTP scopes (lifespan, websocket) pass through untouched.
§zRPC service groups
An RpcServer (config-driven) registers moonrpc Method handlers by gRPC path and dispatches unary calls, returning Unimplemented for unknown methods; RpcGroup registers a set of methods under one package.Service.
type RpcHandler = (Bytes) -> Bytes
A unary RPC handler: it maps a request message's wire bytes to a response message's wire bytes (the application/grpc+proto payload, sans the length-prefix framing that @moonrpc.encode_message adds). Streaming handlers arrive with the h2 transport; this is the unary shape zRPC registers today.
type ServerStreamHandler = (Bytes) -> Array[Bytes]
A server-streaming handler: one request message in, an ordered sequence of response messages out (each framed as its own length-prefixed gRPC message). Mirrors go-zero's pb.XxxServer server-streaming method, which writes to a grpc.ServerStream instead of returning one reply.
type ClientStreamHandler = (Array[Bytes]) -> Bytes
A client-streaming handler: every request message the client sends is collected, and after the client half-closes the handler returns one reply.
struct BidiStreamHandler
A live bidirectional call (← go-zero's pb.XxxServer bidi method, which reads from and writes to the same grpc.ServerStream): on_message fires once per request message and returns the replies to send right then, so responses interleave with requests; on_end runs after the client half-closes and returns the final replies before the grpc-status trailer. The moonzero-local mirror of @moonrpc.BidiHandler, so callers register bidi methods without naming the transport package.
type BidiStreamFactory = () -> BidiStreamHandler
A factory that mints one BidiStreamHandler per call, so each stream gets its own handler state (← the fresh ServerStream gRPC hands every bidi invocation).
struct RpcServer
A zRPC server (← go-zero's zrpc.Server): config plus a registry mapping each method's gRPC :path (/package.Service/Method) to its handler. Handlers are registered via @moonrpc.Method descriptors — directly or through a RpcGroup — and dispatched by path, mirroring how go-zero registers service implementations on the underlying gRPC server. Unary, server-streaming, and client-streaming methods live in separate registries so one path resolves to exactly one cardinality.
fn RpcServer::new(conf : @conf.RpcServerConf) -> RpcServer
Build an empty RPC server from its config.
fn RpcServer::conf(self : RpcServer) -> @conf.RpcServerConf
The server's configuration.
fn RpcServer::register( self : RpcServer, desc : @moonrpc.Method, handler : RpcHandler) -> Unit
Register handler for method, keyed by its gRPC path. A later registration for the same path replaces the earlier one.
fn RpcServer::register_server_streaming( self : RpcServer, desc : @moonrpc.Method, handler : ServerStreamHandler) -> Unit
Register a server-streaming handler for method, keyed by its gRPC path.
fn RpcServer::register_client_streaming( self : RpcServer, desc : @moonrpc.Method, handler : ClientStreamHandler) -> Unit
Register a client-streaming handler for method, keyed by its gRPC path.
fn RpcServer::register_bidi_streaming( self : RpcServer, desc : @moonrpc.Method, factory : BidiStreamFactory) -> Unit
Register a bidirectional-streaming handler for method, keyed by its gRPC path. factory runs once per call so each stream gets fresh handler state.
fn RpcServer::group(self : RpcServer, service : String) -> RpcGroup
Open a RpcGroup that registers methods under the fully-qualified package.Service name — go-zero's per-service registration, without repeating the service name on each method.
fn RpcServer::lookup(self : RpcServer, path : String) -> RpcHandler?
Look up the handler registered for a gRPC :path, or None if unregistered.
fn RpcServer::methods(self : RpcServer) -> Array[String]
The gRPC paths of every registered method.
fn RpcServer::has_method(self : RpcServer, path : String) -> Bool
Whether a handler is registered for path.
fn RpcServer::dispatch( self : RpcServer, path : String, request : Bytes) -> Result[Bytes, @moonrpc.Status]
Dispatch a unary call to the handler registered for path, returning the response bytes. An unregistered path yields Err(Unimplemented) — exactly the grpc-status a real gRPC server returns for an unknown method — so a transport can translate the result straight onto the wire.
struct RpcGroup
A per-service registration handle (← go-zero's service registrar closure): binds a set of methods to one package.Service on a shared RpcServer.
fn RpcGroup::service(self : RpcGroup) -> String
The fully-qualified package.Service this group registers under.
fn RpcGroup::register( self : RpcGroup, name : String, handler : RpcHandler) -> Unit
Register a method name on this group's service, building the @moonrpc.Method descriptor and installing handler under its gRPC path. (register, not method — the latter is a reserved word.)
fn RpcGroup::register_server_streaming( self : RpcGroup, name : String, handler : ServerStreamHandler) -> Unit
Register a server-streaming method name on this group's service.
fn RpcGroup::register_client_streaming( self : RpcGroup, name : String, handler : ClientStreamHandler) -> Unit
Register a client-streaming method name on this group's service.
fn RpcGroup::register_bidi_streaming( self : RpcGroup, name : String, factory : BidiStreamFactory) -> Unit
Register a bidirectional-streaming method name on this group's service.
fn RpcServer::dispatch_graceful( self : RpcServer, coord : @proc.ShutdownCoordinator, path : String, request : Bytes) -> Result[Bytes, @moonrpc.Status]
Dispatch a unary call through the shutdown gate: refuse with Unavailable when the server is shutting down, otherwise run the handler and count it as in-flight for the duration so a concurrent shutdown drains behind it. The gate wraps RpcServer::dispatch, so an unregistered path still yields Unimplemented.
§zRPC over the h2c transport
RpcServer::to_h2 exposes the registered handlers as a moonrpc H2Server, and RpcChannel drives real unary, server/client-streaming, and bidirectional calls over that transport: HPACK-coded HEADERS, length-prefixed DATA frames, and the grpc-status trailer read back off the reply. A BidiCall keeps the stream open both ways — send returns the replies produced right then, close_send runs the server's on_end and reports the final grpc-status.
fn status_of_code(code : Int32) -> @moonrpc.Status
Map a numeric grpc-status code back to a @moonrpc.Status. Anything outside the canonical 0–16 range is reported as Unknown, matching how a gRPC client treats an unrecognised code.
fn RpcServer::to_h2(self : RpcServer) -> @moonrpc.H2Server
Build a @moonrpc.H2Server protocol engine from this zRPC server's registered handlers — the transport-facing view of the same registry dispatch reads. Each handler is bound to its gRPC path, so a request arriving over the h2c transport is dispatched to exactly the handler the group registered.
struct RpcChannel
An in-process gRPC channel bound to a server engine — the client half of the h2c transport. A call is carried as the real HTTP/2 frames a socket-backed client would send: an HPACK-coded HEADERS block with the gRPC pseudo-headers, a length-prefixed DATA frame closing the stream, and the grpc-status trailer read back off the engine's reply. The channel's HPACK encoder pairs with the engine's decoder and vice versa, so the dynamic-table state stays in lockstep across every call on the channel.
fn RpcChannel::connect( server : RpcServer, authority~ : String = "localhost") -> RpcChannel
Open a channel to server over an in-process h2c transport, exchanging the opening SETTINGS the way a real connection does. Client-initiated streams use odd identifiers (RFC 7540 §5.1.1), starting at 1.
fn RpcChannel::call( self : RpcChannel, path : String, request : Bytes) -> Result[Bytes, @moonrpc.Status] raise
Invoke a unary method at path with request as its message payload, driving the call through the h2c engine and returning the reply payload on grpc-status: 0, or the mapped @moonrpc.Status otherwise. request and the returned reply are bare message bytes; the length-prefix framing is applied and stripped by the transport.
fn RpcChannel::call_server_streaming( self : RpcChannel, path : String, request : Bytes) -> Result[Array[Bytes], @moonrpc.Status] raise
Invoke a server-streaming method at path: send the single request message and read back the ordered sequence of reply messages the server produced, or the mapped error @moonrpc.Status if the stream closed with a non-zero grpc-status. On Ok the array holds every message in emission order (possibly empty).
fn RpcChannel::call_client_streaming( self : RpcChannel, path : String, requests : Array[Bytes]) -> Result[Bytes, @moonrpc.Status] raise
Invoke a client-streaming method at path: send every message in requests as its own DATA frame, half-close the stream, and read back the single reply. An empty requests still opens and half-closes the stream, so the handler runs with no messages.
struct BidiCall
A live client-side bidirectional call over the h2c channel (← gRPC's ClientStream): the request stream stays open while messages flow both ways. send writes one request message and returns whatever replies the server produced right then (bidi interleaving — an echo handler answers each message as it arrives); close_send half-closes the request stream, runs the server's on_end, and reports the final grpc-status. The channel's HPACK decoder is advanced across every reply block, so its dynamic table stays in lockstep with the engine's encoder for the life of the call. pending holds DATA octets not yet split into a whole length-prefixed message (a message may straddle two DATA frames under flow control).
fn RpcChannel::open_bidi( self : RpcChannel, path : String) -> BidiCall raise
Open a bidirectional stream to path, sending the request HEADERS without half-closing so the stream stays open for interleaved sends. An unregistered path answers trailers-only UNIMPLEMENTED during this HEADERS feed, which the returned call captures as its status.
fn BidiCall::send(self : BidiCall, msg : Bytes) -> Array[Bytes] raise
Send one request message on the open stream and return the replies the server emitted in response to it (possibly empty). A no-op once the stream is half-closed.
fn BidiCall::close_send( self : BidiCall) -> Result[Array[Bytes], @moonrpc.Status] raise
Half-close the request stream: run the server's on_end, return its final reply messages, and map the grpc-status trailer to Ok/Err. Calling it a second time is an error (Cancelled).
fn RpcChannel::call_bidi_streaming( self : RpcChannel, path : String, requests : Array[Bytes]) -> Result[Array[Bytes], @moonrpc.Status] raise
Drive a whole bidirectional call at path in one shot: send every message in requests (collecting the interleaved replies in order), then half-close and append the on_end replies. The result is every reply message the server produced, in emission order, or the non-zero grpc-status the stream closed with.
§Graceful shutdown
A ShutdownCoordinator that drains in-flight zRPC calls: dispatch_graceful counts each call for its duration, initiate_shutdown makes new calls come back Unavailable while in-flight ones finish, and is_drained reports when the last one has completed.
struct ShutdownCoordinator
A graceful-shutdown coordinator for a zRPC server (← go-zero's proc.AddShutdownListener + gRPC's GracefulStop): once shutdown is initiated the server stops admitting new calls, but calls already in flight are allowed to run to completion. A call brackets its work between begin_call and end_call; begin_call returns false when the server is shutting down, which the transport surfaces as Unavailable — exactly the status a client sees once a server has stopped listening. The server is fully drained once shutdown has been initiated and no calls remain in flight. The counter is plain mutable state, which is safe under moonbitlang/async's cooperative single-threaded scheduling: begin_call/end_call never yield, so the count is only observed at await points between them.
fn ShutdownCoordinator::new() -> ShutdownCoordinator
A coordinator that is serving normally with no calls in flight.
fn ShutdownCoordinator::begin_call(self : ShutdownCoordinator) -> Bool
Admit a new call: register it as in-flight and return true, unless shutdown has been initiated, in which case the call is refused (false) and the count is left untouched.
fn ShutdownCoordinator::end_call(self : ShutdownCoordinator) -> Unit
Mark an admitted call finished, dropping it from the in-flight count. Only call it for a call that begin_call admitted; the count never goes below zero.
fn ShutdownCoordinator::initiate_shutdown( self : ShutdownCoordinator) -> Unit
Begin the graceful shutdown: from now on begin_call refuses new calls while in-flight ones keep running. Idempotent.
fn ShutdownCoordinator::is_shutting_down( self : ShutdownCoordinator) -> Bool
Whether shutdown has been initiated.
fn ShutdownCoordinator::in_flight(self : ShutdownCoordinator) -> Int32
The number of calls currently in flight.
fn ShutdownCoordinator::is_drained(self : ShutdownCoordinator) -> Bool
Whether the server is fully drained: shutdown initiated and no call in flight. A supervisor loops on this (yielding between checks) to know the last in-flight RPC has finished and the process may exit.
§Service registry & discovery
An InMemoryRegistry (etcd-shaped: service -> instance -> endpoint with a store revision) plus RoundRobin/pick_first balancers and resolve_one, the resolve-then-balance step a client runs before a call.
struct Endpoint
A service endpoint (← go-zero's discov target): the host and port an instance listens on, plus a routing weight (default 1). The weight is carried through registration and snapshots and is what WeightedRoundRobin shares traffic by; round-robin and pick-first ignore it.
fn Endpoint::new( host : String, port : Int32, weight~ : Int32 = 1) -> Endpoint
Build an endpoint; weight defaults to 1, matching an unweighted instance.
fn Endpoint::address(self : Endpoint) -> String
The host:port dial string.
struct InMemoryRegistry
An in-memory service registry (← go-zero's etcd discov store, minus the network): a two-level map of service -> instance-id -> endpoint and a monotonic revision bumped on every mutation, mirroring etcd's store revision so a watcher could detect change. Instance ids are <service>/<n>, the leaf of the etcd key an instance would lease.
fn InMemoryRegistry::new() -> InMemoryRegistry
A fresh, empty registry at revision 0.
fn InMemoryRegistry::revision(self : InMemoryRegistry) -> Int64
The store revision, incremented on each register/deregister — etcd's mod-revision, the value a watcher compares against to see new state.
fn InMemoryRegistry::register( self : InMemoryRegistry, service : String, endpoint : Endpoint) -> String
Register endpoint under service and return its instance key. Each call mints a distinct key, so two instances of one service coexist, and bumps the revision.
fn InMemoryRegistry::deregister( self : InMemoryRegistry, service : String, key : String) -> Bool
Remove the instance at key from service. Returns true if it existed (and bumps the revision), false if the service or key was unknown.
fn InMemoryRegistry::resolve( self : InMemoryRegistry, service : String) -> Array[Endpoint]
The endpoints registered for service, in registration order.
fn InMemoryRegistry::services(self : InMemoryRegistry) -> Array[String]
Every service name with at least one live instance.
fn resolve_one( registry : InMemoryRegistry, service : String, balancer : RoundRobin) -> Endpoint?
Resolve service on the registry and pick one endpoint with balancer — the resolve-then-balance step a zRPC client runs before each call. An etcd- or consul-backed registry with the same resolve shape drops in unchanged.
struct RoundRobin
A round-robin balancer (← go-zero's roundRobinBalancer) over a resolved endpoint set: successive picks cycle through the instances, spreading load evenly. Holds only a cursor, so it is cheap to keep per client.
fn RoundRobin::new() -> RoundRobin
A round-robin balancer starting at the first instance.
fn RoundRobin::pick( self : RoundRobin, endpoints : Array[Endpoint]) -> Endpoint?
Pick the next endpoint in rotation, or None if the set is empty. The cursor advances modulo the set size, so it stays valid as instances come and go.
struct WeightedRoundRobin
A weighted balancer over Endpoint::weight, using smooth weighted round-robin: every pick credits each instance with its own weight, serves the highest-credited one, then charges it the total weight of the set. Across one full cycle each instance is served exactly its share of the traffic, and the picks interleave instead of arriving in runs — a weight-5 instance is not handed five requests back to back. Credit is keyed by address(), so an instance that leaves and returns resumes where it was rather than jumping the queue, and two instances sharing an address are treated as one. An instance whose weight is zero or negative is never picked; a set where every weight is non-positive yields None.
fn WeightedRoundRobin::new() -> WeightedRoundRobin
A weighted balancer with no credit accrued yet.
fn WeightedRoundRobin::pick( self : WeightedRoundRobin, endpoints : Array[Endpoint]) -> Endpoint?
Pick the next endpoint in weight order, or None if nothing is eligible.
fn pick_first(endpoints : Array[Endpoint]) -> Endpoint?
Pick the first endpoint (← gRPC's pick_first), or None if the set is empty. A stable choice that only moves when the head instance goes away.
extend Endpoint with Debug::
extend Endpoint with Eq::
fn Endpoint::parse(s : String) -> Endpoint?
Parse a host:port dial string into an Endpoint, splitting on the last colon so IPv6-ish hosts still work; None on a missing or non-numeric port.
§Persisted registry & load-balanced client
A PersistentRegistry that adds watch, events_since catch-up, and snapshot/restore through an etcd v3 RangeResponse-shaped JSON document, and a LoadBalancedChannel that resolves a service through the Resolve interface, balances to a live instance, and dials it over the h2c transport.
type Resolve = (String) -> Array[Endpoint]
The resolve half of go-zero's discovery (← discov.Discovery): a function from a service name to its live endpoints. Any store — the in-memory InMemoryRegistry, the persisted PersistentRegistry, or a future etcd/consul client — exposes one via resolver(), so a balancer and the load-balanced channel are written once against the interface and the backing store swaps by swapping the closure.
fn InMemoryRegistry::resolver(self : InMemoryRegistry) -> Resolve
This registry as a Resolve interface value.
fn PersistentRegistry::resolver(self : PersistentRegistry) -> Resolve
This registry as a Resolve interface value.
enum RegistryEvent
A change to the registry keyspace, in etcd v3's watch shape: a Put carries the instance key and its endpoint, a Delete carries the key that went away, and both carry the store revision the change produced. A watcher receives these in revision order, so a client can rebuild the live set incrementally instead of re-resolving the whole service.
fn RegistryEvent::revision(self : RegistryEvent) -> Int64
The store revision a registry event was produced at.
struct PersistentRegistry
A persisted, watchable service registry (← go-zero's etcd discov publisher): the same two-level service -> instance-id -> endpoint store as InMemoryRegistry, plus a per-key mod-revision, an append-only event log for catch-up watchers, live watcher callbacks fired on every mutation, and snapshot/restore that round-trip the whole keyspace through an etcd v3 RangeResponse-shaped JSON document — the bytes a file- or etcd-backed deployment persists and reloads without losing a revision.
fn PersistentRegistry::new() -> PersistentRegistry
A fresh, empty persisted registry at revision 0.
fn PersistentRegistry::revision(self : PersistentRegistry) -> Int64
The store revision, bumped on each register/deregister.
fn PersistentRegistry::register( self : PersistentRegistry, service : String, endpoint : Endpoint) -> String
Register endpoint under service, mint a fresh instance key, bump the revision, and emit a Put. Returns the instance key (<service>/<n>).
fn PersistentRegistry::deregister( self : PersistentRegistry, service : String, key : String) -> Bool
Remove the instance at key from service. On success bumps the revision and emits a Delete; an unknown service or key is a no-op returning false.
fn PersistentRegistry::resolve( self : PersistentRegistry, service : String) -> Array[Endpoint]
The endpoints registered for service, in registration order.
fn PersistentRegistry::services(self : PersistentRegistry) -> Array[String]
Every service name with at least one live instance.
fn PersistentRegistry::keyed_entries( self : PersistentRegistry) -> Array[(String, Endpoint, Int64)]
Every registered instance as (instance-key, endpoint, mod-revision), across all services — the flat keyspace a file- or etcd-backed reader diffs one load against the next to compute the Put/Delete events a change produced.
fn PersistentRegistry::watch( self : PersistentRegistry, on_event : (RegistryEvent) -> Unit) -> Unit
Register a live watcher fired on every subsequent mutation, in revision order (← etcd's Watch with no start revision). To also see changes already applied, replay events_since first.
fn PersistentRegistry::events_since( self : PersistentRegistry, revision : Int64) -> Array[RegistryEvent]
Every event with a revision greater than revision (← etcd's watch start_revision): the catch-up a client replays to reach the current state before switching to live watch callbacks.
fn PersistentRegistry::snapshot(self : PersistentRegistry) -> String
Serialize the whole keyspace as an etcd v3 RangeResponse-shaped JSON document: a header carrying the store revision and the id counter, and one key/value entry per instance carrying its endpoint and mod-revision. This is the exact payload a file- or etcd-backed deployment persists; restore rebuilds an identical registry from it, revisions intact.
fn PersistentRegistry::restore( src : String) -> PersistentRegistry raise @conf.ConfigError
Rebuild a registry from a snapshot document, preserving instance keys, their endpoints and mod-revisions, the id counter, and the store revision — so a reloaded registry mints the next key exactly where the persisted one left off and a watcher's events_since(old_revision) still lines up.
enum Balancer
The choice of balancer for a load-balanced channel: round-robin cycles through the resolved instances (spreading load evenly), WeightedBalancer shares them out in proportion to Endpoint::weight, PickFirst pins the head instance (← gRPC's pick_first).
fn Balancer::round_robin() -> Balancer
A round-robin balancer, cursor at the first instance.
fn Balancer::weighted() -> Balancer
A weighted balancer, with no credit accrued yet.
fn Balancer::pick( self : Balancer, endpoints : Array[Endpoint]) -> Endpoint?
Pick one endpoint from a resolved set, or None if it is empty.
struct RpcCluster
The in-process dial table: an endpoint address maps to the RpcServer listening there. It stands in for DNS resolution plus a socket dial in the in-process h2c transport — a real deployment opens a connection to the address instead of looking the server up here, but the resolve→balance→call path above it is the same.
fn RpcCluster::new() -> RpcCluster
An empty cluster.
fn RpcCluster::add( self : RpcCluster, endpoint : Endpoint, server : @zrpc.RpcServer) -> Unit
Bind the server reachable at endpoint's address.
fn RpcCluster::dial( self : RpcCluster, endpoint : Endpoint) -> @zrpc.RpcChannel?
Open a channel to the server bound at endpoint, or None if nothing is reachable there (a stale registry entry pointing at a gone instance).
struct LoadBalancedChannel
A load-balanced zRPC client (← go-zero's zrpc.Client over a discovery target): it resolves a service through the Resolver, picks a live instance with the Balancer, dials it on the RpcCluster, and makes the call. The whole resolve→balance→dial→call path runs per call, so instances registering or deregistering between calls take effect on the next one.
fn LoadBalancedChannel::new( resolve : Resolve, cluster : RpcCluster, service : String, balancer~ : Balancer = Balancer::round_robin()) -> LoadBalancedChannel
Build a load-balanced channel for service over a Resolve interface, dialing through cluster with balancer (round-robin by default).
fn LoadBalancedChannel::call( self : LoadBalancedChannel, path : String, request : Bytes) -> Result[Bytes, @moonrpc.Status] raise
Make a unary call to path, resolving and balancing to a live instance first.
fn LoadBalancedChannel::call_server_streaming( self : LoadBalancedChannel, path : String, request : Bytes) -> Result[Array[Bytes], @moonrpc.Status] raise
Make a server-streaming call to path, resolving and balancing to a live instance first.
fn LoadBalancedChannel::call_bidi_streaming( self : LoadBalancedChannel, path : String, requests : Array[Bytes]) -> Result[Array[Bytes], @moonrpc.Status] raise
Make a bidirectional-streaming call to path, resolving and balancing to a live instance first, then driving the whole requests exchange to completion.
extend RegistryEvent with Debug::
extend RegistryEvent with Eq::
§Real file-backed registry I/O
The native discov driver (go-zero's discov publisher/subscriber over the filesystem instead of etcd's network): persist_registry writes the snapshot to a real file through moonbitlang/async's fs, FileRegistry loads it back and exposes a resolver(), reload returns the Put/Delete diff since the last load, and watch/watch_once reload on every real filesystem change.
async fn persist_registry( path : String, reg : @moonzero.PersistentRegistry) -> Unit
Persist a registry's whole keyspace to path as its etcd v3 RangeResponse- shaped snapshot document (← go-zero's discov.Publisher writing instance keys into etcd). The file is truncated and rewritten, so it always holds the current state and its revisions; a reader loads or reloads from exactly these bytes.
struct FileRegistry
A file-backed view of a service registry (← go-zero's discov.Subscriber): it holds the last-loaded PersistentRegistry and the file it came from. resolve answers from the in-memory copy; reload re-reads the file and returns the Put/Delete events that the change produced; watch_once/watch block on a real filesystem watcher and reload when the file changes. The reader and the publisher share only the file, exactly as an etcd subscriber and publisher share only the etcd keyspace.
async fn FileRegistry::load(path : String) -> FileRegistry
Read the registry snapshot at path into a fresh FileRegistry. A missing file is treated as an empty registry, so a reader can start before the publisher has written anything and pick the state up on the first reload/watch.
fn FileRegistry::path(self : FileRegistry) -> String
The current file path this registry loads from.
fn FileRegistry::revision(self : FileRegistry) -> Int64
The store revision of the last-loaded snapshot.
fn FileRegistry::resolve( self : FileRegistry, service : String) -> Array[@moonzero.Endpoint]
The endpoints registered for service in the last-loaded snapshot.
fn FileRegistry::resolver(self : FileRegistry) -> @moonzero.Resolve
This file registry as a Resolve interface value, so a balancer and the load-balanced channel drive it exactly as they drive the in-memory registry. The closure reads whatever snapshot was last loaded, so a reload/watch in between calls is reflected on the next resolve.
async fn FileRegistry::reload( self : FileRegistry) -> Array[@moonzero.RegistryEvent]
Re-read the file and adopt it as the current state, returning the RegistryEvent diff from the previously loaded state: a Put for every instance that is new or whose endpoint changed, a Delete for every instance that went away. The events carry the mod-revision the reloaded snapshot recorded, so a client can stay in revision order across reloads.
async fn FileRegistry::watch_once( self : FileRegistry, watcher : @fs.Watcher) -> Array[@moonzero.RegistryEvent]
Block on watcher until the watched directory changes, then reload and return the diff — one watch cycle. The caller owns the @fs.Watcher (built over the directory holding the registry file) and its lifetime, so a single cycle is easy to drive to completion and join; watch loops this for a long-running reader.
async fn FileRegistry::watch( self : FileRegistry, dir : String, on_event : (@moonzero.RegistryEvent) -> Unit) -> Unit
Watch dir (the directory holding the registry file) and invoke on_event for every RegistryEvent produced by every change, indefinitely — the long-running subscribe loop (← go-zero's discov.Subscriber watch goroutine). Runs until the surrounding task group is torn down; drive it with TaskGroup::spawn.
§Metrics
A CounterVec of per-method/route/status request tallies and a cumulative latency Histogram (Prometheus le buckets), wired by the metrics middleware that times each request on the clock.
struct CounterVec
A monotonic counter (← go-zero's metric.CounterVec) partitioned by a label string. Each inc/add accrues against one label (e.g. "GET /ping 200"), so a single vector holds the per-method/route/status request tallies Prometheus scrapes. Counters only ever go up.
fn CounterVec::new() -> CounterVec
A fresh counter vector with no labels seen yet.
fn CounterVec::add( self : CounterVec, label : String, delta : Int64) -> Unit
Add delta to label's count (creating the series on first sight).
fn CounterVec::inc(self : CounterVec, label : String) -> Unit
Increment label's count by one.
fn CounterVec::value(self : CounterVec, label : String) -> Int64
The current count for label, 0 if never touched.
fn CounterVec::total(self : CounterVec) -> Int64
The sum of every label's count — the total number of observations.
fn CounterVec::labels(self : CounterVec) -> Array[String]
The set of labels that have been observed.
struct Histogram
A cumulative histogram (← go-zero's metric.HistogramVec, Prometheus semantics): a sorted list of le (less-than-or-equal) upper bounds and, for each, the count of observations that fell at or below it, plus the running sum and total count. An observation above every bound still lands in the implicit +Inf bucket that count represents.
let default_latency_buckets : Array[Double] = [
The default latency buckets go-zero ships (milliseconds): a request spends most of its time under a second, so the bounds cluster there.
fn Histogram::new( bounds~ : Array[Double] = default_latency_buckets) -> Histogram
A histogram over bounds (defaulting to default_latency_buckets). The bounds are taken as given; supply them in ascending order, as Prometheus requires.
fn Histogram::observe(self : Histogram, value : Double) -> Unit
Record one observation: it lands in every bucket whose le bound it does not exceed (cumulative), and updates the sum and count.
fn Histogram::total(self : Histogram) -> Int64
The total number of observations (the +Inf bucket count).
fn Histogram::sum_value(self : Histogram) -> Double
The sum of all observed values (Prometheus _sum).
fn Histogram::bucket_count(self : Histogram, i : Int32) -> Int64
The cumulative count in the bucket bounded by bounds[i] — how many observations were <= that bound.
fn Histogram::bounds(self : Histogram) -> Array[Double]
The upper bounds this histogram partitions on.
fn Histogram::mean(self : Histogram) -> Double
The mean of the observations, or 0 when none have been recorded.
struct ServerMetrics
The request metrics an HTTP service exposes (← go-zero's server metrics): a request counter partitioned by method/route/status and a latency histogram. Held by the caller so it can be read out for a /metrics scrape after serving.
fn ServerMetrics::new() -> ServerMetrics
Fresh server metrics: an empty counter and a default-bucket latency histogram.
fn ServerMetrics::requests(self : ServerMetrics) -> CounterVec
The request counter, labelled "<METHOD> <path> <status>".
fn ServerMetrics::latency(self : ServerMetrics) -> Histogram
The request-latency histogram, in milliseconds.
let exposition_content_type : String = "text/plain; version=0.0.4; charset=utf-8"
The content type a /metrics scrape carries so a Prometheus server parses the body as the text exposition format (version=0.0.4).
fn CounterVec::to_exposition( self : CounterVec, name~ : String, help~ : String, label~ : String = "label") -> String
Render this counter vector as Prometheus text exposition: a # HELP line, a # TYPE <name> counter line, then one <name>{<label>="<value>"} <count> series per observed label (sorted for a stable scrape). label names the single dimension the vector partitions on.
fn Histogram::to_exposition( self : Histogram, name~ : String, help~ : String) -> String
Render this histogram as Prometheus text exposition: a # HELP line, a # TYPE <name> histogram line, the cumulative <name>_bucket{le="<bound>"} series capped by the le="+Inf" bucket (every observation, including those above the last bound), then <name>_sum and <name>_count.
fn ServerMetrics::to_exposition(self : ServerMetrics) -> String
Render the whole server metric set as one exposition document a Prometheus server can scrape: the latency histogram followed by the request counter, under go-zero's canonical metric names. go-zero splits the counter into path/method/code labels; moonzero keeps the request signature as one request label.
§Trace-id propagation
W3C traceparent parsing and formatting with SplitMix64-derived trace/span ids, and the tracing middleware that continues an inbound trace or starts a new one and stamps traceparent + x-trace-id onto the response.
fn generate_trace_id(seed : Int64) -> String
A 32-hex-char (128-bit) trace id from seed, mixing two independent words.
fn generate_span_id(seed : Int64) -> String
A 16-hex-char (64-bit) span id from seed.
struct TraceContext
A W3C Trace Context (← go-zero's OpenTelemetry propagation): the 128-bit trace id shared across a request's whole call tree, the 64-bit span id of the current hop, and the 8-bit sampling flags.
fn TraceContext::to_traceparent(self : TraceContext) -> String
Format as a W3C traceparent header value: 00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>.
fn TraceContext::trace_id(self : TraceContext) -> String
The trace id (the value propagated unchanged down the call tree).
fn TraceContext::span_id(self : TraceContext) -> String
The span id of this hop.
fn parse_traceparent(value : String) -> TraceContext?
Parse a W3C traceparent value, or None if it is malformed. Only the four canonical fields with correct lengths are accepted; the flags default to 0 if unparseable.
fn next_trace_context(inbound : String?, seed : Int64) -> TraceContext
Derive the outgoing trace context for a request: reuse the inbound traceparent's trace id if the client sent a valid one (continuing the distributed trace), else start a new trace, and always mint a fresh child span id from seed. This is the propagation decision, pulled out as a pure function so it is testable without the transport.
§Clock abstraction
A millisecond time source injected into the resilience middlewares so their timing is a pure function of an explicit clock; ManualClock drives them deterministically in tests.
struct Clock
A monotonic time source in **milliseconds**, injected into the resilience middlewares (rate-limit, breaker, timeout) so their timing logic is a pure function of an explicit clock rather than a hidden wall-clock read. go-zero reads timex.Now() directly; because that is neither portable across MoonBit's backends nor testable, moonzero threads the clock as a value — the same pattern Go's clockwork/x/time/rate accept for a Clock.
fn Clock::new(now_ms : () -> Int64) -> Clock
Wrap a now-in-milliseconds thunk as a Clock.
fn Clock::now(self : Clock) -> Int64
The current time in milliseconds, as reported by the wrapped source.
fn Clock::system() -> Clock
The platform clock, in milliseconds since the Unix epoch (← go-zero's timex.Now()). Every backend can read it, so a service that has no reason to inject its own time source can take this one.
struct ManualClock
A deterministic, hand-advanced clock for tests and for driving the rate-limit / breaker cores without a real time source. Wall time is replaced by an explicit advance, so a token bucket's refill or a breaker's open window can be exercised exactly.
fn ManualClock::new(start~ : Int64 = 0) -> ManualClock
A manual clock starting at start milliseconds (default 0).
fn ManualClock::advance(self : ManualClock, delta : Int64) -> Unit
Move the manual clock forward by delta milliseconds.
fn ManualClock::as_clock(self : ManualClock) -> Clock
A Clock view over this manual clock: reading it reflects every advance.
§Rate limiting
go-zero's token-bucket limiter over moonward's bucket, on the millisecond clock, and the rate_limit middleware, which answers 429 when it is empty.
struct TokenBucket
A token-bucket rate limiter kept in this process: the bucket holds up to burst tokens and refills continuously at rate a second, and each admitted request spends one. Every decision is a function of the state and now, so the limiter is testable without a real clock. The bucket is local, so N replicas admit N times the rate. RedisTokenLimit is the same limiter with its bucket in redis and is what a fleet should run; this one serves a single process, and is what RedisTokenLimit itself falls back to when redis is unreachable (← go-zero's rescueLimiter).
fn TokenBucket::new( rate : Double, burst~ : Double = -1.0, now~ : Int64 = 0) -> TokenBucket
Build a bucket admitting rate requests per second on average with room for a burst of that many back-to-back (default burst = rate). It starts full at time now, in milliseconds. The bucket is moonward's; this keeps go-zero's shape and its millisecond clock. A capacity below one token is raised to one, because a bucket that can never hold a whole token admits nothing at all — which is what a fractional rate with no burst used to give.
fn TokenBucket::allow(self : TokenBucket, now : Int64) -> Bool
Try to admit one request at time now. Returns true when admitted, false when the bucket is empty.
fn TokenBucket::allow_n( self : TokenBucket, n : Double, now : Int64) -> Bool
Try to admit a request costing n tokens at time now. Returns false (spending nothing) when fewer than n tokens are available.
fn TokenBucket::available(self : TokenBucket, now : Int64) -> Double
The (fractional) number of tokens currently available at now. Useful for metrics and tests.
let token_script : String =
go-zero's core/limit/tokenscript.lua, verbatim: read the bucket and the second it was last touched, refill by the elapsed seconds capped at capacity, spend requested if that many are there, and write both back under a TTL of two fill times. Public so a caller can pre-load it, and so a redis double can evaluate the very text the limiter ships.
struct RedisTokenLimit
A token-bucket rate limiter whose bucket lives in redis (← go-zero's limit.TokenLimiter). The token count and the second it was last refilled sit under {key}.tokens and {key}.ts, so every replica pointed at the same redis and key spends from one bucket. When redis cannot be reached the limiter keeps limiting from rescue, its process-local TokenBucket, the way go-zero drops to its in-process rescueLimiter rather than letting an outage open the gate.
fn RedisTokenLimit::new( client : @redis.RedisClient, rate~ : Int32, burst~ : Int32, key~ : String) -> RedisTokenLimit
A limiter admitting rate requests per second with room for a burst of that many back-to-back, over the bucket key names in client's redis.
fn RedisTokenLimit::rescue(self : RedisTokenLimit) -> TokenBucket
The process-local bucket this limiter falls back to, so a caller can inspect it.
async fn RedisTokenLimit::allow( self : RedisTokenLimit, now_ms : Int64) -> Bool
Try to admit one request at now_ms.
async fn RedisTokenLimit::allow_n( self : RedisTokenLimit, n : Int32, now_ms : Int64) -> Bool
Try to admit a request costing n tokens at now_ms. The script is handed whole seconds because go-zero hands it now.Unix(): the shared bucket refills at one-second granularity however finely the clock is read. Lua's false arrives as a null reply and its true as 1; anything else, and any redis failure, is served by the local bucket instead.
§Circuit breaker
go-zero's googleBreaker over moonward's: Google SRE client-side throttling on the last ten seconds of outcomes, which sheds a computed fraction of calls rather than opening, and the breaker middleware that answers 503 for a shed one.
struct Breaker
Google SRE's client-side throttling breaker (← go-zero's googleBreaker). The algorithm is moonward's Breaker; this binds it to a Clock and a source of draws, the shape go-zero's breaker has, so a call site asks with no arguments. go-zero's constants are moonward's preset sre.
fn Breaker::new( clock : @clock.Clock, rand? : () -> Double, buckets~ : Int32 = 40, bucket_ms~ : Int64 = 250L) -> Breaker
A breaker over a buckets × bucket_ms window (go-zero's ten seconds in forty slices by default) reading time from clock. rand supplies the shed roll as a draw from [0, 1); without it the breaker draws from a system-seeded generator.
fn Breaker::drop_ratio(self : Breaker) -> Double
The fraction of new calls the breaker is shedding now, zero while healthy.
fn Breaker::allow(self : Breaker) -> Promise?
Ask to make one call. Some promise means it may proceed and the caller settles the promise when it finishes; None means the throttle shed it.
struct Promise
The receipt for an admitted call (← go-zero's breaker.Promise).
fn Promise::accept(self : Promise) -> Unit
fn Promise::reject(self : Promise) -> Unit
suberror Unavailable ///| /// Run `req` under the breaker (← go-zero's `Breaker.Do`): a shed call raises /// `Unavailable` and `req` never runs; a raise from `req` counts as a failure. pub fn[T] Breaker::run(self : Breaker, req : () -> T raise) -> T raise
Raised by Breaker::run for a call the throttle shed (← go-zero's ErrServiceUnavailable).
§Timeout & max-bytes
The timeout middleware, which starts a moonward Deadline per request and enforces it on the response path (preemptive cancel is the async boundary), and maxbytes, which rejects an over-limit Content-Length with 413.
fn timeout( budget_ms : Int64, clock : @clock.Clock, refusal~ : Refusal = timed_out) -> Middleware
Timeout middleware (← go-zero's TimeoutHandler): start a moonward Deadline of budget_ms at clock.now() for each request to the wrapped app. **Async boundary (faithful model).** *Preemptively* aborting an in-flight handler the instant its deadline fires requires racing the handler against a timer and cancelling the loser — in MoonBit that is @async.any([handler, timer]) with structured cancellation, which only runs under the native async runtime and cannot be driven synchronously. What this middleware does portably: it installs the deadline and enforces it on the response path — if the handler blows its budget before emitting its first event, the client receives a 503 timeout (from timeout_events) and the late response is suppressed. The remaining gap (a handler that hangs and never emits) is closed by the race/cancel wired at the async server edge. A budget_ms <= 0 disables the timeout, passing straight through.
fn maxbytes(limit : Int32, refusal~ : Refusal = too_large) -> Middleware
Max-bytes middleware (← go-zero's MaxBytesHandler): reject any HTTP request whose declared Content-Length exceeds limit bytes with 413 Payload Too Large, before the wrapped app runs. A limit <= 0 disables the check. Non- HTTP scopes pass through untouched.
§Structured logging
RequestLog captures typed access-log fields (method, path, status, duration, request-id, client-ip, user-agent) and renders one JSON line per request; the structured_logging middleware emits it, timed on the clock.
struct RequestLog
A structured access-log record (← go-zero's logx HTTP access fields). Rather than a free-form line, each request is captured as typed fields and rendered as one JSON object per line — the format go-zero emits under logx and the shape log collectors (ELK, Loki) expect.
fn RequestLog::to_json(self : RequestLog) -> Json
The record as a JSON value with a stable field order, duration_ms rendered as a number of milliseconds.
fn RequestLog::render(self : RequestLog) -> String
The record rendered as a single-line JSON string, ready to write to a log sink.
fn RequestLog::fields(self : RequestLog) -> Array[(String, Json)]
The record as logx fields, for logging it through a @logx.Logger — the method and path make the entry's message, so they are not repeated here.
fn structured_logging( clock : @clock.Clock, logger? : @logx.Logger) -> Middleware
Structured-logging middleware (← go-zero's LogHandler): time each HTTP request on the shared clock, capture its method/path/client-ip/user-agent and the response status observed on HttpResponseStart, and write one access entry through logger (the process logx by default) when the response starts. Access logs are info, so a service configured at error or above emits none of them. Non-HTTP scopes pass through without logging.
§etcd
The etcd v3 client and the discovery driver over it: leases keep a registered instance alive, watches stream membership changes, and a lapsed lease is what removes a dead instance.
struct EtcdKeyValue
An etcd KeyValue (mvccpb.KeyValue): a key, its value, the create/mod revisions and version that track its history, and the lease it is attached to.
fn EtcdKeyValue::empty() -> EtcdKeyValue
The empty key/value with all-zero metadata.
fn EtcdKeyValue::encode(self : EtcdKeyValue) -> Bytes
Encode a KeyValue to its protobuf wire bytes (field numbers per etcd mvccpb.proto: key=1, create_revision=2, mod_revision=3, version=4, value=5, lease=6). Proto3 default (empty / zero) fields are omitted.
fn EtcdKeyValue::decode( data : Bytes) -> EtcdKeyValue raise @moonrpc.PbError
Decode a KeyValue from protobuf wire bytes; unknown fields are skipped.
struct EtcdRangeRequest
A RangeRequest (etcdserverpb): read the key at key, or the half-open range [key, range_end) when range_end is set, up to limit results (0 = no limit).
fn EtcdRangeRequest::encode(self : EtcdRangeRequest) -> Bytes
Encode a RangeRequest (key=1, range_end=2, limit=3).
fn EtcdRangeRequest::decode( data : Bytes) -> EtcdRangeRequest raise @moonrpc.PbError
Decode a RangeRequest.
struct EtcdRangeResponse
A RangeResponse: the matched key/values and the total count in the range (which may exceed the returned kvs when a limit capped them).
fn EtcdRangeResponse::encode(self : EtcdRangeResponse) -> Bytes
Encode a RangeResponse (kvs=2 repeated, count=4).
fn EtcdRangeResponse::decode( data : Bytes) -> EtcdRangeResponse raise @moonrpc.PbError
Decode a RangeResponse; each kvs entry is a nested KeyValue message.
struct EtcdPutRequest
A PutRequest: store value at key, optionally under lease.
fn EtcdPutRequest::encode(self : EtcdPutRequest) -> Bytes
Encode a PutRequest (key=1, value=2, lease=3).
fn EtcdPutRequest::decode( data : Bytes) -> EtcdPutRequest raise @moonrpc.PbError
Decode a PutRequest.
struct EtcdLeaseGrantRequest
A LeaseGrantRequest: ask etcd for a lease living ttl seconds (id 0 lets the server assign one). A registered service key attaches to the lease and vanishes when the lease expires — go-zero's instance liveness mechanism.
fn EtcdLeaseGrantRequest::encode(self : EtcdLeaseGrantRequest) -> Bytes
Encode a LeaseGrantRequest (TTL=1, ID=2).
fn EtcdLeaseGrantRequest::decode( data : Bytes) -> EtcdLeaseGrantRequest raise @moonrpc.PbError
Decode a LeaseGrantRequest.
struct EtcdLeaseGrantResponse
A LeaseGrantResponse: the granted lease id, its actual ttl, and an error string when the grant failed.
fn EtcdLeaseGrantResponse::encode(self : EtcdLeaseGrantResponse) -> Bytes
Encode a LeaseGrantResponse (ID=2, TTL=3, error=4).
fn EtcdLeaseGrantResponse::decode( data : Bytes) -> EtcdLeaseGrantResponse raise @moonrpc.PbError
Decode a LeaseGrantResponse.
struct EtcdLeaseKeepAliveRequest
A LeaseKeepAliveRequest: renew lease id before it expires. A discovery client streams these to keep its instance registered.
fn EtcdLeaseKeepAliveRequest::encode( self : EtcdLeaseKeepAliveRequest) -> Bytes
Encode a LeaseKeepAliveRequest (ID=1).
fn EtcdLeaseKeepAliveRequest::decode( data : Bytes) -> EtcdLeaseKeepAliveRequest raise @moonrpc.PbError
Decode a LeaseKeepAliveRequest.
struct EtcdLeaseKeepAliveResponse
A LeaseKeepAliveResponse: the renewed lease id and its remaining ttl (0 = the lease has expired).
fn EtcdLeaseKeepAliveResponse::encode( self : EtcdLeaseKeepAliveResponse) -> Bytes
Encode a LeaseKeepAliveResponse (ID=2, TTL=3).
fn EtcdLeaseKeepAliveResponse::decode( data : Bytes) -> EtcdLeaseKeepAliveResponse raise @moonrpc.PbError
Decode a LeaseKeepAliveResponse.
enum EtcdEventType
The kind of change a watch Event reports (mvccpb.Event.EventType): a key was Put (created or updated) or Deleted.
fn EtcdEventType::to_int(self : EtcdEventType) -> Int32
The protobuf enum number of an event type (PUT = 0, DELETE = 1).
fn EtcdEventType::from_int(n : Int32) -> EtcdEventType
The event type for a protobuf enum number; unknown numbers read as Put.
struct EtcdEvent
A watch Event (mvccpb.Event): a change to one key, carrying the resulting KeyValue (for a delete, the key with cleared metadata). This is what a discovery watcher folds into add/remove of a service instance.
fn EtcdEvent::encode(self : EtcdEvent) -> Bytes
Encode an Event (type=1, kv=2). PUT (0) is the proto3 default and omitted.
fn EtcdEvent::decode(data : Bytes) -> EtcdEvent raise @moonrpc.PbError
Decode an Event.
struct EtcdWatchCreateRequest
A WatchCreateRequest: subscribe to changes on key, or on the half-open range [key, range_end), from start_revision (0 = current). A discovery watcher opens one over the service's key prefix.
fn EtcdWatchCreateRequest::encode(self : EtcdWatchCreateRequest) -> Bytes
Encode a WatchCreateRequest (key=1, range_end=2, start_revision=3).
fn EtcdWatchCreateRequest::decode( data : Bytes) -> EtcdWatchCreateRequest raise @moonrpc.PbError
Decode a WatchCreateRequest.
struct EtcdWatchCancelRequest
A WatchCancelRequest: stop the watch stream identified by watch_id.
fn EtcdWatchCancelRequest::encode(self : EtcdWatchCancelRequest) -> Bytes
Encode a WatchCancelRequest (watch_id=1).
fn EtcdWatchCancelRequest::decode( data : Bytes) -> EtcdWatchCancelRequest raise @moonrpc.PbError
Decode a WatchCancelRequest.
enum EtcdWatchRequest
A WatchRequest, the request_union oneof of the bidi Watch stream: either a Create to open a watch or a Cancel to close one.
fn EtcdWatchRequest::encode(self : EtcdWatchRequest) -> Bytes
Encode a WatchRequest (create_request=1, cancel_request=2).
fn EtcdWatchRequest::decode( data : Bytes) -> EtcdWatchRequest raise @moonrpc.PbError
Decode a WatchRequest; the last-set oneof arm wins, defaulting to an empty Create.
struct EtcdWatchResponse
A WatchResponse: the server-assigned watch_id, the created / canceled lifecycle flags, and the batch of change events since the last response. A discovery watcher folds each event into add/remove of a service instance.
fn EtcdWatchResponse::encode(self : EtcdWatchResponse) -> Bytes
Encode a WatchResponse (watch_id=2, created=3, canceled=4, events=11 repeated).
fn EtcdWatchResponse::decode( data : Bytes) -> EtcdWatchResponse raise @moonrpc.PbError
Decode a WatchResponse.
struct EtcdDeleteRangeRequest
A DeleteRangeRequest: delete the key at key, or the half-open range [key, range_end); prev_kv asks the server to return the deleted key/values. A discovery client sends this to deregister an instance.
fn EtcdDeleteRangeRequest::encode(self : EtcdDeleteRangeRequest) -> Bytes
Encode a DeleteRangeRequest (key=1, range_end=2, prev_kv=3).
fn EtcdDeleteRangeRequest::decode( data : Bytes) -> EtcdDeleteRangeRequest raise @moonrpc.PbError
Decode a DeleteRangeRequest.
struct EtcdDeleteRangeResponse
A DeleteRangeResponse: the number of keys deleted and, when prev_kv was requested, the prev_kvs that were removed.
fn EtcdDeleteRangeResponse::encode(self : EtcdDeleteRangeResponse) -> Bytes
Encode a DeleteRangeResponse (deleted=2, prev_kvs=3 repeated).
fn EtcdDeleteRangeResponse::decode( data : Bytes) -> EtcdDeleteRangeResponse raise @moonrpc.PbError
Decode a DeleteRangeResponse.
struct EtcdPutResponse
A PutResponse: when prev_kv was requested on the PutRequest, the key/value that the put replaced (its key is empty when there was none).
fn EtcdPutResponse::encode(self : EtcdPutResponse) -> Bytes
Encode a PutResponse (prev_kv=2); an absent previous value (empty key) is omitted.
fn EtcdPutResponse::decode( data : Bytes) -> EtcdPutResponse raise @moonrpc.PbError
Decode a PutResponse.
extend EtcdKeyValue with Eq::
extend EtcdRangeRequest with Eq::
extend EtcdRangeResponse with Eq::
extend EtcdPutRequest with Eq::
extend EtcdLeaseGrantRequest with Eq::
extend EtcdLeaseGrantResponse with Eq::
extend EtcdLeaseKeepAliveRequest with Eq::
extend EtcdLeaseKeepAliveResponse with Eq::
extend EtcdEventType with Eq::
extend EtcdEvent with Eq::
extend EtcdWatchCreateRequest with Eq::
extend EtcdWatchCancelRequest with Eq::
extend EtcdWatchRequest with Eq::
extend EtcdWatchResponse with Eq::
extend EtcdDeleteRangeRequest with Eq::
extend EtcdDeleteRangeResponse with Eq::
extend EtcdPutResponse with Eq::
suberror EtcdErrorAn etcd gRPC call that returned a non-OK grpc-status.
struct EtcdClient
An etcd v3 client bound to a gRPC channel (real etcd, or a mock server in tests).
fn EtcdClient::new(channel : @zrpc.RpcChannel) -> EtcdClient
An etcd client over channel.
fn EtcdClient::range( self : EtcdClient, req : EtcdRangeRequest) -> EtcdRangeResponse raise
KV.Range: read the key or prefix range in req.
fn EtcdClient::put( self : EtcdClient, req : EtcdPutRequest) -> EtcdPutResponse raise
KV.Put: store the key/value (optionally under a lease) in req.
fn EtcdClient::delete_range( self : EtcdClient, req : EtcdDeleteRangeRequest) -> EtcdDeleteRangeResponse raise
KV.DeleteRange: delete the key or range in req (deregister).
fn EtcdClient::lease_grant( self : EtcdClient, req : EtcdLeaseGrantRequest) -> EtcdLeaseGrantResponse raise
Lease.LeaseGrant: obtain a lease with the requested TTL.
fn EtcdClient::watch( self : EtcdClient, req : EtcdWatchRequest) -> Array[EtcdWatchResponse] raise
Watch.Watch: open a watch with req and read the stream of responses the server produces (etcd's Watch is a bidi stream; this drives the common open-then-observe direction over the server-streaming path). Each WatchResponse carries the batch of change events since the last one.
struct EtcdDiscovery
An etcd-backed service registry / resolver. Instances of one service live under <prefix><service>/, so a Range over that prefix returns them all.
fn EtcdDiscovery::new( client : EtcdClient, prefix~ : String = "moonzero/") -> EtcdDiscovery
An etcd discovery bound to client; keys live under prefix (default "moonzero/", mirroring go-zero's configurable discovery key root).
fn etcd_service_prefix(prefix : String, service : String) -> String
The key prefix a service's instances live under: <prefix><service>/. Exposed so the native gRPC-socket path builds the exact same keys as the in-process driver.
fn etcd_prefix_end(prefix : Bytes) -> Bytes
The etcd range-end for a prefix scan: the prefix with its last byte incremented, which is the smallest key greater than every key sharing the prefix (etcd's getPrefix). An all-0xff tail scans to the end of the keyspace (\x00).
fn EtcdDiscovery::register( self : EtcdDiscovery, service : String, endpoint : @registry.Endpoint, ttl~ : Int64 = 10) -> Int64 raise
Register endpoint for service under a fresh lease living ttl seconds, and return the granted lease id (renew it with the client's keep-alive to stay registered). The instance key is <prefix><service>/<lease-id> and its value is the host:port dial string, so a resolver reads the endpoints straight back.
fn EtcdDiscovery::deregister( self : EtcdDiscovery, service : String) -> Unit raise
Remove every instance of service (deregister the whole service prefix).
fn EtcdDiscovery::resolve( self : EtcdDiscovery, service : String) -> Array[@registry.Endpoint] raise
Resolve service to its live endpoints: Range the service prefix and parse each value as a host:port endpoint. Malformed values are skipped.
fn EtcdDiscovery::resolver(self : EtcdDiscovery) -> @registry.Resolve
This etcd discovery as a Resolve interface value, so the balancer and the load-balanced channel written against Resolve run against real etcd unchanged. A resolve error surfaces as an empty endpoint set (the balancer's no-instance case), matching how the in-memory resolver behaves for an unknown service.
§consul
The consul agent API and the discovery driver over it: a service registers with a TTL check, a keep-alive passes that check, and deregistering removes it — the lease pattern consul spells differently.
suberror ConsulErrorA consul API call that failed — a transport error, or a non-2xx agent response.
struct ConsulResponse
A consul agent's HTTP response: the status code and the raw body bytes.
fn ConsulResponse::new(status : Int32, body : Bytes) -> ConsulResponse
A consul response.
fn ConsulResponse::status(self : ConsulResponse) -> Int32
The HTTP status code.
fn ConsulResponse::body(self : ConsulResponse) -> Bytes
The raw response body bytes.
pub(open) trait ConsulHttpA transport to a consul agent: it performs one HTTP request (method and path, with body for writes) and returns the response. Implementations own the medium — a real HTTP socket, or an in-memory fake.
struct ConsulClient
A consul client over a ConsulHttp, exposing the agent operations discovery uses.
fn ConsulClient::new(http : &ConsulHttp) -> ConsulClient
A client over http.
fn consul_check_id(service_id : String) -> String
The check id consul assigns an inline service TTL check: service:<service-id>.
fn ConsulClient::register_service( self : ConsulClient, id : String, name : String, address : String, port : Int32, ttl_secs : Int32) -> Unit raise ConsulError
PUT /v1/agent/service/register: register an instance under name at address:port with an id, held alive by a TTL check that consul deregisters ttl*3 seconds after it stops passing. Pass the check with check_pass before each TTL lapses to stay healthy.
fn consul_register_body( id : String, name : String, address : String, port : Int32, ttl_secs : Int32) -> Bytes
The JSON body of a service/register request: the instance's id, name, address, and port, plus a TTL check that consul deregisters ttl*3 seconds after it stops passing. Exposed so the native HTTP-socket path builds the exact same body.
fn ConsulClient::check_pass( self : ConsulClient, service_id : String) -> Unit raise ConsulError
PUT /v1/agent/check/pass/service:<id>: mark an instance's TTL check passing, the keep-alive that renews its lease.
fn ConsulClient::deregister_service( self : ConsulClient, id : String) -> Unit raise ConsulError
PUT /v1/agent/service/deregister/<id>: deregister one instance.
fn ConsulClient::health_service( self : ConsulClient, name : String) -> Array[@registry.Endpoint] raise ConsulError
GET /v1/health/service/<name>?passing=true: the healthy instances of name, each as its Service.Address:Service.Port endpoint (falling back to Node.Address when the service advertises no address of its own, as consul's clients do).
fn ConsulClient::agent_services( self : ConsulClient) -> Array[(String, String)] raise ConsulError
GET /v1/agent/services: every service instance registered on this agent, as (instance-id, service-name) pairs — the list a by-name deregister filters to find the ids to drop.
fn consul_parse_health( body : Bytes) -> Array[@registry.Endpoint] raise ConsulError
Parse a consul health/service JSON body into endpoints — exposed so the native HTTP-socket path decodes a real agent's response exactly as the client does.
fn consul_str_field(obj : Json, key : String) -> String
The string value of key in a JSON object, or "".
fn consul_int_field(obj : Json, key : String) -> Int32
The integer value of a JSON number key, truncated toward zero, or 0.
struct ConsulDiscovery
A consul-backed service registry / resolver over a ConsulClient. Each instance is a consul service named service, uniquely identified per agent by its address.
fn ConsulDiscovery::new(client : ConsulClient) -> ConsulDiscovery
A consul discovery over client.
fn ConsulDiscovery::register( self : ConsulDiscovery, service : String, endpoint : @registry.Endpoint, ttl~ : Int32 = 10) -> String raise
Register endpoint for service with a ttl-second TTL check and immediately pass the check so the instance is healthy at once (a fresh TTL check starts critical). Returns the instance id; renew it with keepalive before the TTL lapses.
fn ConsulDiscovery::keepalive( self : ConsulDiscovery, service : String, endpoint : @registry.Endpoint) -> Unit raise
Refresh an instance's lease by passing its TTL check.
fn ConsulDiscovery::resolve( self : ConsulDiscovery, service : String) -> Array[@registry.Endpoint] raise
Resolve service to its healthy endpoints.
fn ConsulDiscovery::deregister_instance( self : ConsulDiscovery, service : String, endpoint : @registry.Endpoint) -> Unit raise
Deregister one instance of service.
fn ConsulDiscovery::deregister( self : ConsulDiscovery, service : String) -> Unit raise
Deregister every instance of service registered on this agent.
fn ConsulDiscovery::resolver(self : ConsulDiscovery) -> @registry.Resolve
This consul discovery as a Resolve interface value, so the balancer and the load-balanced channel run against consul unchanged. A resolve error surfaces as an empty endpoint set, matching the other drivers.
§redis
The RESP protocol codec, the redis client over it, and the discovery driver that keeps instances in a keyed set with an expiry.
suberror RedisErrorA redis command that failed — a connection-level error, or a redis error reply.
pub(open) trait RedisConnA live redis connection: it sends an encoded command (an array of argument byte strings) and returns the decoded reply. Implementations own the transport — a real socket speaking RESP, or an in-memory fake. It is open and async so that a socket can be one: real redis I/O suspends, and a sealed synchronous trait left every implementation a test double.
struct RedisClient
A redis client over a RedisConn, exposing the typed commands the discovery driver uses. Every command maps a redis error reply to RedisError.
fn RedisClient::new(conn : &RedisConn) -> RedisClient
A client over conn.
async fn RedisClient::command( self : RedisClient, args : Array[Bytes]) -> RespValue raise RedisError
Send args as a command and return the reply, turning a connection failure or a redis -ERR reply into RedisError.
async fn RedisClient::ping(self : RedisClient) -> String raise RedisError
PING: the server's PONG liveness reply.
async fn RedisClient::set_ex( self : RedisClient, key : Bytes, value : Bytes, ttl_secs : Int32) -> Unit raise RedisError
SET key value EX ttl: store value at key with a ttl-second expiry, the lease a registered instance is held alive by.
async fn RedisClient::get( self : RedisClient, key : Bytes) -> Bytes? raise RedisError
GET key: the value at key, or None if the key is absent or expired.
async fn RedisClient::expire( self : RedisClient, key : Bytes, ttl_secs : Int32) -> Bool raise RedisError
EXPIRE key ttl: refresh a key's expiry (the discovery keep-alive). true if the key existed and its TTL was set.
async fn RedisClient::del( self : RedisClient, keys : Array[Bytes]) -> Int64 raise RedisError
DEL key...: delete the given keys, returning how many existed (deregistration).
async fn RedisClient::scan_match( self : RedisClient, pattern : Bytes, count : Int32) -> Array[Bytes] raise RedisError
SCAN-iterate every key matching pattern (a glob like prefix/*), following the cursor to completion so the whole keyspace is covered without ever blocking the server on a KEYS scan. count is the per-step hint passed to redis.
async fn RedisClient::script_load( self : RedisClient, source : String) -> String raise RedisError
SCRIPT LOAD source: put source in the server's script cache and return the SHA1 it answers to from then on.
async fn RedisClient::eval( self : RedisClient, source : String, keys : Array[Bytes], args : Array[Bytes]) -> RespValue raise RedisError
EVAL source numkeys key... arg...: run a Lua script server-side, where the whole script travels with the call.
async fn RedisClient::evalsha( self : RedisClient, sha : String, keys : Array[Bytes], args : Array[Bytes]) -> RespValue raise RedisError
EVALSHA sha numkeys key... arg...: run a script the server already holds. A server that has forgotten it answers NOSCRIPT, which surfaces as a RedisError.
struct RedisScript
A Lua script bound to its SHA1 (← go-redis's Script, the shape go-zero's ScriptRun drives). The first run SCRIPT LOADs the source and keeps the digest, every later run is an EVALSHA that ships a 40-byte hash instead of the whole program, and a server that has forgotten it — a restart, a SCRIPT FLUSH — answers NOSCRIPT, which loads it again and retries once.
fn RedisScript::new(source : String) -> RedisScript
A script over source, not yet loaded anywhere.
fn RedisScript::source(self : RedisScript) -> String
The Lua this script runs.
async fn RedisScript::run( self : RedisScript, client : RedisClient, keys : Array[Bytes], args : Array[Bytes]) -> RespValue raise RedisError
Run the script on client over keys and args, loading it first if its digest is not known yet or the server has dropped it.
struct RedisDiscovery
A redis-backed service registry / resolver. Instances of one service live under <prefix><service>/, one key per instance keyed by its dial address, so a SCAN of that prefix returns them all.
fn RedisDiscovery::new( client : RedisClient, prefix~ : String = "moonzero/") -> RedisDiscovery
A redis discovery over client; keys live under prefix (default "moonzero/").
fn redis_service_prefix(prefix : String, service : String) -> String
The key prefix a service's instances live under: <prefix><service>/. Exposed so the native RESP-socket path builds the exact same keys as the in-process driver.
fn redis_instance_key( prefix : String, service : String, endpoint : @registry.Endpoint) -> String
The key one instance of service lives at: <prefix><service>/<address>.
fn redis_service_pattern(prefix : String, service : String) -> String
The SCAN MATCH glob for every instance of service: <prefix><service>/*.
async fn RedisDiscovery::register( self : RedisDiscovery, service : String, endpoint : @registry.Endpoint, ttl~ : Int32 = 10) -> String
Register endpoint for service with a ttl-second lease and return its instance key. Renew it with keepalive before the TTL lapses to stay registered; let it lapse and redis drops the key, deregistering the instance automatically.
async fn RedisDiscovery::keepalive( self : RedisDiscovery, service : String, endpoint : @registry.Endpoint, ttl~ : Int32 = 10) -> Bool
Refresh an instance's lease, extending its key's expiry by ttl seconds. false if the key had already lapsed (the instance must re-register).
async fn RedisDiscovery::resolve( self : RedisDiscovery, service : String) -> Array[@registry.Endpoint]
Resolve service to its live endpoints: SCAN the service prefix and read each instance's value as its host:port endpoint. Keys that lapse mid-scan and malformed values are skipped. The answer is also kept as the service's last known set, which is what resolver hands a balancer.
fn RedisDiscovery::last( self : RedisDiscovery, service : String) -> Array[@registry.Endpoint]
The endpoints the last resolve of service found, without going to redis.
async fn RedisDiscovery::deregister_instance( self : RedisDiscovery, service : String, endpoint : @registry.Endpoint) -> Unit
Deregister one instance of service by deleting its key.
async fn RedisDiscovery::deregister( self : RedisDiscovery, service : String) -> Unit
Deregister every instance of service (delete the whole service prefix).
fn RedisDiscovery::resolver(self : RedisDiscovery) -> @registry.Resolve
This redis discovery as a Resolve interface value, so the balancer and the load-balanced channel run against redis unchanged. Resolve is synchronous and a SCAN over a socket is not, so the closure reads the set the last resolve of that service found — the same arrangement discov's file registry uses, where the async reload and the synchronous resolve are separate steps. A service not resolved yet balances over nothing.
suberror RespErrorA malformed or truncated RESP frame.
enum RespValue
A decoded RESP value. RESP2's null bulk string ($-1) and null array (*-1) both decode to Null, unifying with RESP3's explicit null (_). BulkString carries raw bytes (redis values are binary-safe); the text-line types carry the decoded string.
fn resp_kind(value : RespValue) -> String
A short name for a value's RESP type, for error messages ("unexpected <kind> reply") — RespValue derives Eq/Debug but not a renderable form.
fn resp_encode_command(args : Array[Bytes]) -> Bytes
Encode a command as the client frame redis expects: an array of bulk strings, one per argument (*<n>\r\n$<len>\r\n<arg>\r\n...). Arguments are raw bytes, so binary keys and values round-trip unchanged.
fn resp_command(args : Array[String]) -> Bytes
Encode a command given string arguments (the common case — command names and keys are text), UTF-8 encoding each.
fn RespValue::decode(data : Bytes) -> RespValue raise RespError
Decode exactly one RESP value from data, requiring it to consume the whole input. Trailing bytes after a complete value are a framing error.
struct RespReader
A cursor over a RESP byte stream, decoding one value at a time. A socket client feeds it a buffered reply; read advances past exactly one value, so several pipelined replies decode in sequence.
fn RespReader::new(data : Bytes) -> RespReader
A reader positioned at the start of data.
fn RespReader::at_end(self : RespReader) -> Bool
Whether every byte has been consumed.
fn RespReader::read(self : RespReader) -> RespValue raise RespError
Decode the next RESP value, advancing the cursor past it.
extend RespValue with Debug::
extend RespValue with Eq::
§Native transports
What actually talks to a real server: the etcd gRPC socket, the consul HTTP socket and the redis socket. The HTTP/1.1 messages are moonhttp's. Native-only, which is why the portable core is written against traits instead.
suberror EtcdSocketErrorAn etcd gRPC call that returned a non-OK grpc-status.
struct EtcdSocket
A live etcd v3 client over a real gRPC socket connection.
async fn EtcdSocket::connect(host : String, port : Int32) -> EtcdSocket
Open a gRPC connection to the etcd at host:port (etcd serves client gRPC on 2379).
fn EtcdSocket::close(self : EtcdSocket) -> Unit
Close the connection.
async fn EtcdSocket::range( self : EtcdSocket, req : @moonzero.EtcdRangeRequest) -> @moonzero.EtcdRangeResponse
KV.Range: read the key or prefix range in req.
async fn EtcdSocket::put( self : EtcdSocket, req : @moonzero.EtcdPutRequest) -> @moonzero.EtcdPutResponse
KV.Put: store the key/value (optionally under a lease) in req.
async fn EtcdSocket::delete_range( self : EtcdSocket, req : @moonzero.EtcdDeleteRangeRequest) -> @moonzero.EtcdDeleteRangeResponse
KV.DeleteRange: delete the key or range in req (deregister).
async fn EtcdSocket::watch( self : EtcdSocket, req : @moonzero.EtcdWatchCreateRequest, count : Int32) -> Array[@moonzero.EtcdWatchResponse]
Watch.Watch: subscribe to changes on the key or range in req and read the first count WatchResponses off the stream — the created acknowledgement, then events as the watched keys change. The responses are read inline (no background reader), so the caller collects what it needs and closes the connection; a live subscriber loops calling this. Changes are driven elsewhere (a Put on another connection), the way go-zero's discov subscriber watches etcd while publishers register.
async fn EtcdSocket::lease_keep_alive( self : EtcdSocket, req : @moonzero.EtcdLeaseKeepAliveRequest) -> @moonzero.EtcdLeaseKeepAliveResponse
Lease.LeaseKeepAlive: renew id before its TTL runs out. Without this every registration lapses at its TTL and the instance silently disappears from discovery — the grant is the easy half. etcd defines this as a bidirectional stream so a client can hold one open for every lease it owns; this sends one renewal and reads its answer, which is what a timer-driven publisher needs. A returned ttl of zero means the lease was already gone and the caller has to register again rather than keep renewing.
async fn EtcdSocket::lease_grant( self : EtcdSocket, req : @moonzero.EtcdLeaseGrantRequest) -> @moonzero.EtcdLeaseGrantResponse
Lease.LeaseGrant: obtain a lease with the requested TTL.
extend EtcdSocket with LeaseKeeper::
struct ConsulSocket
A consul agent reachable over HTTP at host:port. Each request opens a fresh connection (HTTP/1.0 with Connection: close), the simplest correct model for the agent's short JSON round trips.
fn ConsulSocket::new(host : String, port~ : Int32 = 8500) -> ConsulSocket
A transport to the consul agent at host:port (default port 8500).
async fn ConsulSocket::request( self : ConsulSocket, verb : String, path : String, body : Bytes) -> @moonzero.ConsulResponse
Perform one HTTP request against the agent and return its status and body.
suberror RedisSocketErrorA framing or connection error on a RESP socket.
struct RedisSocket
A live connection to a redis server over TCP, speaking RESP.
async fn RedisSocket::connect(host : String, port : Int32) -> RedisSocket
Open a RESP connection to host:port.
fn RedisSocket::close(self : RedisSocket) -> Unit
Close the connection.
async fn RedisSocket::execute( self : RedisSocket, args : Array[Bytes]) -> @moonzero.RespValue
Send args as a RESP command and read back the decoded reply — the same wire a redis client speaks, carried over the real socket.
fn RedisSocket::client(self : RedisSocket) -> @moonzero.RedisClient
A RedisClient on this socket: the handle production code builds a RedisDiscovery, a RedisPeriodLimit or a RedisTokenLimit over to have them run against a real server.
async fn RedisSocket::command( self : RedisSocket, args : Array[String]) -> @moonzero.RespValue
Send a command given string arguments (command names, keys, and dial addresses are text) and read back the decoded reply.
§Admission control
The concurrency limiter that sheds load past a ceiling — releasing its permit with defer, so cancellation cannot wedge it shut — and the period limiter that counts requests per window.
struct MaxConns
A permit pool of max concurrent slots (← go-zero's syncx.Limit), moonward's Gate under go-zero's names. A max of zero or less admits everything, as go-zero's server does when MaxConns is unset.
fn MaxConns::new(max : Int32) -> MaxConns
A pool admitting at most max requests at once.
fn MaxConns::try_acquire(self : MaxConns) -> Bool
Take a permit if one is free (TryBorrow), returning whether it was taken.
fn MaxConns::release(self : MaxConns) -> Unit
Return a permit taken by try_acquire (Return).
fn MaxConns::in_flight(self : MaxConns) -> Int32
The number of requests currently holding a permit.
enum PeriodResult
The outcome of a PeriodLimit.take (← go-zero's period-limit result codes): a request within quota, the one that reaches it exactly (still admitted), or one beyond it (rejected).
struct PeriodLimit
A fixed-window rate limiter kept in this process: each key may make up to quota requests per period, counted by one moonward Period per key. A key's window opens on its first request, or with align on a multiple of the period (← go-zero's Align()), and starts again once the period has passed. The window is local, so N replicas admit N quotas. RedisPeriodLimit is the same limiter with its window in redis and is what a fleet should run; this one is for a single process, and for the fallback a caller wants when redis is unreachable.
fn PeriodLimit::new( period_secs~ : Int32, quota~ : Int32, align~ : Bool = false) -> PeriodLimit
A limiter admitting quota requests per period_secs seconds, per key.
fn PeriodLimit::take( self : PeriodLimit, key : String, now : Int64) -> PeriodResult
Account for one request under key at now, in milliseconds, and report its outcome: PeriodAllowed below quota, PeriodHitQuota at exactly the quota (the last admitted request), and PeriodOverQuota beyond it.
let period_script : String =
go-zero's core/limit/periodscript.lua, verbatim: INCRBY the key, hang the window on it the first time it appears, and answer 1 below quota, 2 exactly at it, 0 past it. Public so a caller can pre-load it, and so a redis double can evaluate the very text the limiter ships.
struct RedisPeriodLimit
A fixed-window rate limiter whose window lives in redis (← go-zero's limit.PeriodLimit). One INCRBY-ed counter per key, given a period-second expiry the first time it appears, so every replica pointed at the same redis and key draws on one quota instead of each getting its own.
fn RedisPeriodLimit::new( client : @redis.RedisClient, period_secs~ : Int32, quota~ : Int32, prefix~ : String = "") -> RedisPeriodLimit
A limiter admitting quota requests per period_secs seconds per key, counting in client's redis under prefix-prefixed keys.
async fn RedisPeriodLimit::take( self : RedisPeriodLimit, key : String) -> PeriodResult raise @redis.RedisError
Account for one request under key and report its outcome, in the same three states the local limiter reports. A code the script cannot have returned, or a reply that is not an integer at all, raises — go-zero answers Unknown with ErrUnknownCode there, and a caller has to decide what an undecided limiter means.
extend PeriodResult with Debug::
extend PeriodResult with Eq::
§Config keys
go-zero's canonical key form: lowercased, `_` and `-` dropped, so MaxBytes, maxBytes, max_bytes and max-bytes all name one field and a genuine etc/*.yaml loads unchanged.
fn canonical_key(key : String) -> String
go-zero's canonical form for a config key (← the WithCanonicalKeyFunc that conf.Load installs): lowercased, with _ and - dropped. Every lookup compares on this form, so MaxBytes, maxBytes, max_bytes and max-bytes all name one field — which is what lets a genuine go-zero etc/*.yaml, whose keys are all PascalCase, load into a config instead of silently yielding defaults.
struct Conf
A loaded config document (← go-zero's conf.Load): a parsed mapping read through canonical keys and dotted paths, with the ,env=NAME override and the default= / options= / range= constraints go-zero spells as struct tags. A field that is absent and has no default is an error, and so is a value that violates its options= or range= constraint — a bad value never degrades into the default.
fn Conf::of_json(src : String) -> Conf raise ConfigError
Load a document from JSON. Raises ConfigError on malformed JSON or a non-object root.
fn Conf::of_yaml(src : String) -> Conf raise ConfigError
Load a document from the YAML go-zero ships as etc/*.yaml. Raises ConfigError on malformed YAML or a non-mapping root.
fn Conf::at(self : Conf, path : String) -> Json?
The value at a dotted path, or None if any segment is missing or a non-mapping is walked into.
fn Conf::string( self : Conf, path : String, default? : String, options? : Array[String], env? : String, also~ : Array[String] = []) -> String raise ConfigError
Read a string field, honouring env=, default= and options=.
fn Conf::int( self : Conf, path : String, default? : Int32, range? : String, env? : String, also~ : Array[String] = []) -> Int32 raise ConfigError
Read an Int field, honouring env=, default= and range=. A fractional value is truncated toward zero after the range check.
fn Conf::int64( self : Conf, path : String, default? : Int64, range? : String, env? : String, also~ : Array[String] = []) -> Int64 raise ConfigError
Read an Int64 field, honouring env=, default= and range=.
fn Conf::bool( self : Conf, path : String, default? : Bool, env? : String, also~ : Array[String] = []) -> Bool raise ConfigError
Read a Bool field, honouring env= and default=. true/false spelled as a string decode too, which is how an env override arrives.
fn Conf::strings( self : Conf, path : String, default? : Array[String], env? : String, also~ : Array[String] = []) -> Array[String] raise ConfigError
Read a string-list field, honouring env= and default=. An env override is comma-separated, since a shell variable carries one string.
fn Conf::list(self : Conf, path : String) -> Array[Conf] raise ConfigError
The list of sub-documents at path — the shape a []Struct config field takes, each element read back through the same constraint-checked accessors. An absent path is an empty list; a non-list, or a list holding anything but mappings, is an error.
§Engine configuration
Which built-in layers the engine installs (<- MiddlewaresConf), every flag on by default, so an etc/*.yaml that says nothing about middleware still gets the whole chain.
struct MiddlewaresConf
Which of go-zero's built-in layers the engine installs (← MiddlewaresConf). Every flag defaults to true, so an etc/*.yaml that says nothing about middleware still gets the whole chain.
fn MiddlewaresConf::new( trace~ : Bool = true, log~ : Bool = true, prometheus~ : Bool = true, max_conns~ : Bool = true, breaker~ : Bool = true, shedding~ : Bool = true, timeout~ : Bool = true, recover~ : Bool = true, metrics~ : Bool = true, max_bytes~ : Bool = true, gunzip~ : Bool = true) -> MiddlewaresConf
The full chain, which is what go-zero defaults to.
struct PrivateKeyConf
One signing key the request-signature check would verify against (← rest.PrivateKeyConf).
struct SignatureConf
Request-signature settings (← rest.SignatureConf): whether an unsigned or badly-signed request is refused outright, how long a signature stays valid, and the keys it is checked against. moonzero loads and validates this config — a strict service with no keys is refused at assembly, as go-zero's ErrSignatureConfig does — but ships no content-signature layer to consume it.
fn SignatureConf::new( strict~ : Bool = false, expiry_ms~ : Int64 = 3600000L, private_keys~ : Array[PrivateKeyConf] = []) -> SignatureConf
Signature settings that verify nothing: not strict, go-zero's one-hour expiry, no keys.
struct RestConf
A REST service's configuration (← go-zero's rest.RestConf), embedding ServiceConf the way go-zero's does. The name, bind address and request timeout live on that embedded config — host(), port() and timeout_ms() read them — and everything else here is RestConf's own. max_bytes is an Int where go-zero uses int64: it is compared against a request's Content-Length, and its own range= tag caps it at 32 MiB.
fn RestConf::new( service~ : ServiceConf = ServiceConf::new(), cert_file~ : String = "", key_file~ : String = "", verbose~ : Bool = false, max_conns~ : Int32 = 10000, max_bytes~ : Int32 = 1048576, cpu_threshold~ : Int64 = 900L, signature~ : SignatureConf = SignatureConf::new(), middlewares~ : MiddlewaresConf = MiddlewaresConf::new(), trace_ignore_paths~ : Array[String] = []) -> RestConf
A REST config with go-zero's defaults: no TLS, not verbose, 10000 connections, a 1 MiB body cap, a 90% CPU shed threshold, and the full middleware chain.
fn RestConf::host(self : RestConf) -> String
The address the service binds.
fn RestConf::port(self : RestConf) -> Int32
The port the service binds.
fn RestConf::timeout_ms(self : RestConf) -> Int32
The per-request timeout budget in milliseconds; 0 disables it.
fn RestConf::tls(self : RestConf) -> Bool
Whether TLS is configured — go-zero serves HTTPS once both files are named.
fn RestConf::from_yaml(src : String) -> RestConf raise ConfigError
Load a RestConf from the YAML go-zero ships as etc/*.yaml. Keys are matched canonically, MOONZERO_* env variables override the file, and a value outside its options=/range= constraint is an error. Name and Port carry no default, exactly as in go-zero: a rest service that does not say who it is or where to listen fails to load.
fn RestConf::from_json(src : String) -> RestConf raise ConfigError
Load a RestConf from a JSON config string, with the same semantics as from_yaml.
extend MiddlewaresConf with Debug::
extend MiddlewaresConf with Eq::
extend PrivateKeyConf with Debug::
extend PrivateKeyConf with Eq::
extend SignatureConf with Debug::
extend SignatureConf with Eq::
§Logging
go-zero's logx over moonlog's sink: the four level names its etc/*.yaml carries, the @timestamp / content keys its collectors index, the WithDuration view, and a logger whose level and destination move at runtime.
struct Logger
A leveled, structured logger (← go-zero's logx): entries below the configured level are dropped without being rendered, and everything else reaches the sink as one record carrying @timestamp, level, content, an optional duration, and any extra fields. The timestamp is milliseconds from the injected Clock, not a formatted calendar time: MoonBit has no portable date formatter, and the clock is already how every other timed layer here reads the time.
let stamp_key : String = "@timestamp"
The key the entry's time is written under (← go-zero's logx, which names it for Elasticsearch's own field).
let message_key : String = "content"
The key the message is written under (← go-zero's logx.content, where zap and slog would write msg).
let duration_key : String = "duration"
The key the elapsed time is written under.
fn entry(record : @moonlog.Record) -> String
One entry as the single-line JSON object go-zero emits: @timestamp, level, content, then whatever else the entry carries. This is the default sink's format, not a rule — hand Logger::new another moonlog sink and the same records are rendered that sink's way.
let level_key : String = "level"
The key the level is written under.
fn level_name(level : @moonlog.Level) -> String
go-zero's name for a level. fatal reads as severe, which is what go-zero calls its top level and what its collectors filter on.
fn Logger::new( clock : @clock.Clock, level~ : LogLevel = Info, sink~ : &@moonlog.Sink = @moonlog.Lines::new(line => println(line))) -> Logger
A logger writing to sink at level and above. Every record reaches the sink with its [entry] line already rendered; the default sink prints that line. Logger::new(clock, sink=@moonlog.Discard::new()) is how a test silences one.
fn Logger::level(self : Logger) -> LogLevel
The level below which entries are dropped.
fn Logger::set_level(self : Logger, level : LogLevel) -> Unit
Raise or lower the threshold (← logx.SetLevel), for this logger and every view derived from it.
fn Logger::set_sink(self : Logger, sink : &@moonlog.Sink) -> Unit
Send entries to another sink, for this logger and every view derived from it — a file, a collector client, a buffer under test. The sink is handed the record and go-zero's line, and may write either.
fn Logger::set_writer(self : Logger, writer : (String) -> Unit) -> Unit
Send entries somewhere other than stdout (← logx.SetWriter), keeping go-zero's line format. [Logger::set_sink] changes the format too.
fn Logger::enabled(self : Logger, level : LogLevel) -> Bool
Whether an entry at level would be written.
fn Logger::with_duration(self : Logger, ms : Int64) -> Logger
A view of this logger that stamps duration on every entry (← logx.WithDuration), sharing the level and sink with the original.
fn Logger::log( self : Logger, level : LogLevel, content : String, fields~ : Array[(String, Json)] = []) -> Unit
Render and write one entry, unless level is below the threshold.
fn Logger::debug( self : Logger, content : String, fields~ : Array[(String, Json)] = []) -> Unit
Write a debug entry.
fn Logger::info( self : Logger, content : String, fields~ : Array[(String, Json)] = []) -> Unit
Write an info entry.
fn Logger::error( self : Logger, content : String, fields~ : Array[(String, Json)] = []) -> Unit
Write an error entry.
fn Logger::severe( self : Logger, content : String, fields~ : Array[(String, Json)] = []) -> Unit
Write a severe entry.
let logx : Logger = Logger::new(@clock.Clock::system())
The process-wide logger (← logx's package-level logger): what the middlewares write to when no logger is handed to them, on the system clock at info. RestEngine::new points its level at the loaded Log.Level.
fn set_log_level(level : LogLevel) -> Unit
Set the process logger's level (← logx.SetLevel).
fn set_log_writer(writer : (String) -> Unit) -> Unit
Send the process logger's entries to writer (← logx.SetWriter).
fn set_log_sink(sink : &@moonlog.Sink) -> Unit
Send the process logger's entries to sink.
§Turning a request away
What a request refused by a middleware receives: 503 for an open breaker, a spent connection budget or a passed deadline, 429 for a rate limit, 413 for a body too large. Each is a parameter.
struct Refusal
The response a refused request receives: a status, a body, and the headers sent with it.
let plain_text : Array[(String, String)] = [
The headers a refusal carries unless told otherwise.
fn Refusal::new( status~ : Int32 = 503, body~ : Bytes = b"503 Service Unavailable", headers~ : Array[(String, String)] = plain_text) -> Refusal
Build a refusal. Left alone it is the 503 Service Unavailable the breaker, the shedder and the connection limiter all answer with.
let refusal : Refusal = Refusal::new()
The generic refusal: 503 Service Unavailable.
let unavailable : Refusal = refusal
What an open breaker, a shed request and a spent connection budget answer with (← go-zero's breaker, sheddingHandler, maxConns).
let too_many : Refusal = Refusal::new(
What a rate-limited request answers with (← go-zero's periodlimit and tokenlimit handlers).
let timed_out : Refusal = Refusal::new(body=b"service timeout")
What a request past its deadline answers with. go-zero's timeout middleware answers 503 with this body rather than 504, so that is the default.
let too_large : Refusal = Refusal::new(
What an over-large request body answers with (← go-zero's maxBytes).
fn Refusal::events(self : Refusal) -> Array[@moonasgi.Event]
The refusal as the ASGI events that send it. A pure value, so a middleware's answer is testable without driving the async transport.
§Load shedding
go-zero's load.AdaptiveShedder over moonward's, wired from RestConf.CpuThreshold: while the CPU is above the threshold, or was within the last second, it sheds once more requests are in flight than recent throughput and latency can carry.
struct Shedder
Load shedding under pressure (← go-zero's load.AdaptiveShedder, wired from RestConf.CpuThreshold). A request is refused only when the server is hot, its CPU at or over threshold per-mille or recently so, and full, with more requests in flight than it has shown it can finish. The algorithm is moonward's Shedder; this binds it to a Clock and to usage, the CPU meter. Without a meter usage reads zero and nothing is shed, since no MoonBit backend can read one alone.
fn Shedder::new( clock : @clock.Clock, threshold~ : Int64 = 900L, usage? : () -> Int64) -> Shedder
fn Shedder::allow(self : Shedder) -> Ticket?
Ask to admit one request. Some ticket means go ahead, and the caller settles it when the request finishes; None means shed it.
fn Shedder::usage(self : Shedder) -> Int64
The CPU usage the shedder is reading, per mille.
struct Ticket
An admitted request (← go-zero's load.Promise).
fn Ticket::pass(self : Ticket) -> Unit
The request succeeded; its duration counts towards the server's capacity.
fn Ticket::fail(self : Ticket) -> Unit