moonzero

    A service framework for MoonBit.

    go-zero
    microservice
    microservices
    service-framework
    framework
    rest
    rpc
    zrpc
    grpc
    middleware
    circuit-breaker
    rate-limit
    load-shedding
    load-balancing
    service-discovery
    metrics
    tracing
    logging
    config
    yaml
    jwt
    signature
    cors
    gzip
    sse
    file-server
    p2c
    singleflight
    cache
    redis
    sql
    prometheus
    opentelemetry
    graceful-shutdown
    moonapi
    server
    moonbit
    Download zip
    Version
    0.14.7
    License
    Apache-2.0
    Last updated
    8 hours ago
    Downloads
    234

    #moonzero

    A service framework for MoonBit — configuration, middleware, zRPC, resilience and discovery in one place.

    License mooncakes

    moonzero assembles a service from a config document: it builds the application, wraps it in the middleware the config asks for, and hands back an ASGI application that any host can run. It also speaks zRPC — the h2c gRPC profile with its own client and server interceptor chains — and it carries the resilience primitives, the service discovery glue and the small utilities a service ends up needing.

    flowchart LR conf["RestConf"] --> engine["RestEngine"] app["application"] --> engine engine -->|"build()"| server["Server"] server -->|"to_asgi()"| asgi(["AsgiApp"]) asgi --> host["any ASGI host"]

    #Install

    moon add moonbitstack/moonzero

    #Quickstart

    let app = @moonapi.App::new()
    let api = @moonzero.Group::new(app, "/api/v1") // a prefix for a set of routes
    api.get("/ping", _ctx => @moonapi.text(200, "pong"))

    let conf = @moonzero.ServiceConf::new(
    name="greet", host="127.0.0.1", port=12000, timeout_ms=3000, log_level=Info,
    )
    let server = @moonzero.Server::new(conf, app)
    .use_(@moonzero.cors(@moonzero.CorsConf::new())) // Access-Control-* headers
    .use_(@moonzero.request_id()) // x-request-id per request
    .use_(@moonzero.recovery) // 500 while nothing is sent
    .use_(@moonzero.logging)

    println(server.describe()) // "greet listening on 127.0.0.1:12000"

    server.to_asgi() is an AsgiApp: hand it to a server, or drive it in a test with no socket in sight.

    #Packages

    PackageWhat it does
    restthe config-driven engine, the server, route groups and route options
    rest/handlerthe middlewares: CORS, recovery, request ids, deadlines, body caps, gzip, JWT, content signatures, breaker, shedding, rate limits, tracing, metrics, access logs
    rest/filesreading a directory into the static file set
    zrpcthe zRPC server, the client channel, the interceptor chains, per-method metrics
    zrpc/socketserving a zRPC server on a port, and calling one over TCP (native)
    balanceround-robin, weighted and power-of-two-choices balancers, the load-balanced channel
    confthe config loader and the service, REST, RPC and signature blocks
    breakerthe client-side adaptive breaker
    shedderthe adaptive load shedder
    limittoken bucket, fixed window and connection limit, in memory or on Redis
    stores/cachecache-aside over an open store contract, one load per key
    stores/redisthat contract over Redis
    stores/sqlstatements, rows and transactions over a database driver
    syncxone call in flight per key, a body that runs once, an object pool
    collectionan LRU cache with lifetimes, a fixed-size ring
    mrmap-reduce, sequential and parallel
    fxretry, backoff, deadlines, grouping
    threadinga fan-out group, a queue drained by one consumer, a newest-only queue
    serviceseveral services with one lifetime
    telemetrythe tracer a service runs on when nobody configured one
    clockthe millisecond clock every timing decision is a function of
    logxlevelled, structured logging
    procgraceful shutdown
    discovfile-backed discovery for native hosts

    The root re-exports the surface: @moonzero.Breaker, @moonzero.Server, @moonzero.jwt_sign and the rest all work from one import.

    #Ports

    The REST engine listens on 12000 and the zRPC server on 12001. Both are ordinary parameters over public presets (@moonzero.port, @moonzero.rpc_port), so a service that wants 8080 writes ServiceConf::new(port=8080). A process that runs both binds them separately — two listeners cannot hold one port.

    CertFile and KeyFile live in the same document and are validated as a pair: a document that names one without the other is refused at load, so a service cannot quietly listen in the clear while its operator believes otherwise. Terminating TLS is the host's job, since the object this library builds is an application.

    #Configuration

    RestConf loads the etc/*.yaml a scaffolder writes: PascalCase keys, nested blocks, MOONZERO_* environment overrides, and default=/options=/range= constraints. A missing required field, or a value outside its range, is an error rather than a silent default.

    let conf = @moonzero.RestConf::from_yaml(
    #|Name: greet-api
    #|Host: 127.0.0.1
    #|Port: 12000
    #|Timeout: 5000
    #|Middlewares:
    #| Gunzip: false
    #|Log:
    #| Level: error
    )
    let engine = @moonzero.RestEngine::new(conf)
    println(engine.names().join(", "))

    The engine installs exactly the layers the config's Middlewares flags ask for, in a fixed order, and leaves out a layer whose own setting disables it — no shedder without a CPU threshold, no deadline without a budget.

    RestConf fieldMeaning
    Servicename, bind address, request budget, log level
    CertFile, KeyFileTLS material; both or neither, refused otherwise
    Verboseaccess logs carry both header sets
    MaxConns, MaxBytesconnection permits, declared-body cap
    CpuThresholdthe CPU reading at which shedding starts
    Signaturecontent-security keys, tolerance, strict or lenient
    Middlewaresone flag per layer
    TraceIgnorePathspaths left untraced

    #Route options

    A route carries its own settings, applied where it is registered:

    let api = @moonzero.Group::new(app, "/api/v1")
    let admin = api.sub("/admin").options_(
    @moonzero.RouteOptions::new()
    .jwt(secret="current", prev_secret=Some("retiring"))
    .timeout(500)
    .max_bytes(4096)
    .priority(engine.priority_shedder()),
    )
    admin.get("/stats", ctx => @moonapi.text(200, "ok"))

    OptionEffect
    jwtrequires a valid bearer token; prev_secret keeps a rotation working
    signatureverifies the content-security MAC before the handler runs
    timeoutabandons the handler at the deadline and answers 503
    max_bytesrefuses a declared body past the cap with 413
    prioritydraws the call from the priority shedder
    prefixjoins another prefix onto the group's
    ssestamps the event-stream media type and no-cache headers

    #Middlewares

    rest/handler exports each layer as an AsgiApp transformer, so they compose in any order and none needs the engine to exist:

    let clock = @moonzero.Clock::new(() => 0L) // inject time; no hidden reads
    let server = @moonzero.Server::new(conf, app)
    .use_(@moonzero.maxbytes(1 << 20)) // 413 past 1 MiB
    .use_(@moonzero.rate_limit(@moonzero.TokenBucket::new(100.0), clock))
    .use_(@moonzero.breaker(@moonzero.Breaker::new(clock)))
    .use_(@moonzero.timeout(3000L, clock))
    .use_(@moonzero.tracing()) // W3C traceparent in/out
    .use_(@moonzero.metrics(metrics, clock)) // path/method/code families

    • cors matches an origin against the allowed set and its subdomains, answers a preflight with 204, and never pairs * with credentials.
    • recovery answers 500 only while nothing of the response has gone out; once the status line is on the wire the error is re-raised for the server to close on.
    • gunzip inflates a Content-Encoding: gzip body, with an optional cap on the inflated size; a body that will not inflate is answered 400.
    • content_security verifies X-Content-Security: RSA-sealed envelope, HS256-class MAC over the request, a timestamp inside the tolerance, and an AES-ECB payload when the envelope says the body is sealed.
    • metrics records two families labelled path, method and code; the labels stay three labels, so a dashboard can select on any one of them.
    • layer adapts any mid.Layer from the ASGI toolkit, so a layer written for the host drops in unchanged.

    Refusal is what a turned-away request receives — status, body and headers — and every refusing middleware takes one, so a gateway can answer 429 where a service alone answers 503.

    #zRPC

    Both halves of the wire run through one chain: an invoker turns a call into a result, an interceptor wraps an invoker, and the transport is the innermost one.

    let rpc = @moonzero.RpcServer::of(@moonzero.RpcServerConf::new(name="greeter"))
    rpc.group("hello.Greeter").register("SayHello", req => req)

    let ch = @moonzero.RpcChannel::connect(rpc)
    .with_interceptors(@moonzero.client_interceptors(conf.middlewares, deps))
    let reply = ch.call_through("/hello.Greeter/SayHello", b"world")

    SideInterceptors
    clientclient_tracing, client_duration, client_prometheus, client_breaker, client_timeout
    serverserver_auth, server_tracing, server_recover, server_stat, server_prometheus, server_breaker, server_shedding, server_timeout

    RpcServerConf and RpcClientConf carry the timeout, both CPU thresholds, authentication and the per-side flags, so both chains are built from config.

    The channel above is in process — it drives a server object directly, which is what a test wants. zrpc/socket carries the same calls over TCP: Listener puts a RpcServer on a port with its interceptor chain intact, and SocketChannel speaks h2c to any gRPC peer.

    // native: a listener is a socket
    let listener = @socket.Listener::new(rpc)
    @async.with_task_group(g => {
    g.spawn_bg(() => listener.serve(port=12001))
    let ch = @socket.SocketChannel::connect("127.0.0.1", 12001)
    let reply = ch.call("/hello.Greeter/SayHello", b"world")
    ch.close()
    listener.stop()
    })

    #Balancing

    Balancer chooses an instance three ways: in turn, by weight, or by power of two choices over each instance's measured load — sqrt(lag + 1) * (inflight + 1), where the lag and the success count are moving averages that decay over ten seconds. A LoadBalancedChannel resolves, picks, dials and calls, and reports the outcome back so the next pick is informed.

    let ch = @moonzero.LoadBalancedChannel::new(
    _ => view.endpoints(),
    cluster,
    "greeter",
    balancer=@moonzero.Balancer::p2c(),
    )

    #Stores

    // with `moonbitstack/moonzero/stores/redis` as @redis and `.../stores/cache` as @cache
    let store = @redis.RedisStore::new(client, prefix="orders:")
    let cache : @cache.Cache[Order] = @cache.Cache::new(
    store, encode=order_to_bytes, decode=order_of_bytes, ttl_ms=60_000L,
    )
    let order = cache.take("42", () => load_order(42))

    take runs the loader once per key however many callers arrive, and writes the answer back with a jittered lifetime so a fleet warmed at the same moment does not expire together. An absent value is not cached.

    #Resilience

    Three primitives, each a pure decision over an injected clock so every decision is reproducible:

    • Breaker counts the last ten seconds in forty buckets and sheds a fraction of calls once the failure ratio is past its budget — a struggling backend keeps the load it can still serve.
    • Shedder refuses work only when the machine is both hot and full: the CPU at or above the threshold, and more calls in flight than the recent throughput and latency can carry.
    • limit offers a token bucket, a fixed window and a connection count, each in memory or on Redis. The Redis token limiter marks the shared bucket down the moment a call fails and keeps limiting from its local bucket until a probe finds Redis again.

    #Discovery

    @moonzero.hold keeps an instance registered for as long as its task runs, and a View follows a service in a task of its own, so a client reads the current set without waiting. Any registry that implements the discovery contract plugs in without a change here; the file-backed driver for native hosts ships in discov.

    @moonzero.hold(registry, "greeter", @moonzero.Endpoint::new("10.0.0.1", 12001))
    let view = @moonzero.View::new()
    group.spawn_bg(no_wait=true, () => view.follow(registry, "greeter"))

    #Observability

    Metrics come from moonp8s and spans from moontel, so the numbers and the traces a service emits are the ones the rest of the toolchain reads. The engine keeps the spans its default tracer records — engine.spans() — so a test can assert on a trace without a collector.

    mount_metrics(app, engine.metrics()) // GET /metrics

    #Licence

    Apache-2.0. See LICENSE.

    Balancer

    How a load-balanced channel chooses: in turn, in proportion to Endpoint::weight, or always the first (gRPC's pick_first). Weighted keeps its credit by Endpoint::id.

    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).

    BidiStreamFactory

    A factory that mints one BidiStreamHandler per call, so each stream gets its own handler state (← the fresh ServerStream gRPC hands every bidi invocation).

    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.

    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.

    Call

    One unary call: its gRPC path (/package.Service/Method) and the request message's wire bytes.

    ClientDeps

    Everything the client chain needs that is not a flag: the seam values a framework injects rather than looks up.

    ClientStreamHandler

    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.

    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.

    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.

    CorsConf

    CORS configuration: which origins may call the service, and what a preflight is told about the rest of the policy.

    Discovery

    Where a service's callers find its instances.

    resolve answers once, with no endpoints for a service nobody registered. watch follows one service: its watcher's first next gives the service as it stands, and each later one waits for a change. Implement these two and View works for the backend.

    Endpoint

    One instance of a service. id tells it from the other instances of that service, and is the address unless given. weight and metadata are carried for whoever picks among endpoints; nothing here reads them.

    FileEntry

    One servable file: its bytes and the media type it is announced with. The ETag is derived from the content, so a conditional request works without the loader having to know a modification time.

    Files

    The files a service serves, keyed by path relative to the mount prefix.

    Group

    A route group: registers a set of routes on an underlying moonapi.App under a shared path prefix and a shared set of route options, so related endpoints (everything under /api/v1, say) are declared without repeating either.

    Invoker

    The innermost of a chain: makes the call.

    JwtRoute

    JWT settings attached to a route: the current secret and, during a rotation, the previous one.

    Layer

    One layer of the assembled chain: go-zero's name for the handler, and the middleware that stands in for it.

    Lease

    A registration: what Registrar::register hands back and takes again to renew or remove it. token is the backend's own handle (etcd's lease id, a Consul check), opaque to everyone else.

    LoadBalancedChannel

    A load-balanced zRPC client (← go-zero's zrpc.Client over a discovery target): it resolves a service through Resolve, 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.

    LogLevel

    Log verbosity (← go-zero's LogConf.Level), ordered from most to least verbose. Compare follows that order so thresholds can be tested directly.

    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.

    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.

    Mapper

    A mapper: map sees one item and emits values with emit; finish runs after the last item, for whatever an aggregate can only produce once the stream is done.

    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.

    MethodTimeout

    A per-method deadline: path gets budget_ms instead of the service's.

    Middleware

    An AsgiApp transformer — one layer of the middleware onion.

    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.

    Once

    A body that runs at most once: the first caller runs it, every later one is a no-op. Guards one-time initialisation, which in a service is a pool, a table or a connection that must not be built twice.

    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.

    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).

    Pool

    A pool of reusable objects: a caller takes one, uses it, and hands it back. Building is deferred until the first take, so a pool nobody touches costs nothing — which is what makes it usable for a buffer or a connection that only some code paths need.

    PrivateKeyConf

    One signing key the request-signature check would verify against (← rest.PrivateKeyConf).

    Promise

    The receipt for an admitted call (← go-zero's breaker.Promise).

    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.

    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.

    Refusal

    The response a refused request receives: a status, a body, and the headers sent with it.

    Refused

    A config-loading failure (← go-zero's conf.Load errors): malformed JSON or YAML, a non-mapping root, a field of the wrong type, a required field with no value, or a value outside its options=/range= constraint, with a human-readable reason.

    Registrar

    Where a service's instances announce themselves.

    register makes endpoint known under service for ttl milliseconds; renew extends that, and raises Expired once the backend has let it go; deregister removes it at once, and is harmless on a lease already gone. Implement these three and hold works for the backend.

    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 a log collector (ELK, Loki) expects.

    Resolve

    type Resolve = (String) -> Array[
    Endpoint
    ]

    The instances of a service. It runs on every call, so it must answer without waiting: a program passes @moonsd.View::endpoints here and runs View::follow in a task of its own.

    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.

    RestEngine

    The engine that turns a RestConf into a runnable service. It owns the state the built-in layers need — the connection permits, the breaker window, the metric set, the two shedders — so every request shares one of each, and it installs exactly the layers Middlewares asks for.

    Ring

    A fixed-size circular buffer: pushing past the capacity overwrites the oldest sample. What a service keeps its last N events, timings or failures in.

    RouteOptions

    The per-route settings. Every one of them defaults to "inherit": no timeout, no body cap, no JWT, no signature, ordinary priority, no SSE.

    RoutineGroup

    A set of tasks that are started together and joined together.

    Bodies are collected first and run at wait, which is what makes the group usable while the tasks they depend on are still being decided: a fan-out whose width comes from the data does not have to know that width up front.

    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.

    RpcClientConf

    zRPC client configuration (← go-zero's RpcClientConf): where a service is — a direct Target, a list of Endpoints, or a name the discovery layer resolves — plus the call timeout and which interceptors run.

    RpcClientMiddlewaresConf

    Which of the zRPC client's interceptors are installed (← RpcClientMiddlewaresConf). Five, and all on by default.

    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.

    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.

    RpcHandler

    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.

    RpcMetrics

    The per-method call metrics of one side of the wire.

    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.

    RpcServerConf

    zRPC server configuration (← go-zero's RpcServerConf): the service name, the address it listens on, a per-call timeout in milliseconds (0 disables it), the two CPU thresholds, whether peers must authenticate, and which interceptors run.

    The registry block of the reference implementation's conf is modelled by the discovery layer instead; everything transport-facing is here.

    RpcServerMiddlewaresConf

    Which of the zRPC server's own interceptors are installed (← go-zero's RpcServerMiddlewaresConf). All eight default to on; a service narrows the chain from its config rather than from code.

    Server

    A moonzero service: its config plus the assembled application (a moonapi App with any middleware already wrapped around it).

    ServerDeps

    Everything the server chain needs that is not a flag.

    ServerMetrics

    The request metrics an HTTP service exposes: http_server_requests_duration_ms and http_server_requests_code_total, each partitioned by path, method and code. Held by the caller so it can be scraped after serving, and shared with RestEngine so the prometheus layer and the /metrics route read one set.

    ServerStreamHandler

    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.

    Service

    One service in a group: a name, so a failure can say which one it was, and the body that runs it until it is cancelled.

    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.

    ServiceGroup

    Several services run as one process (← go-zero's service.Group).

    Every service is a task that runs until it returns or is cancelled, so a server loop and a background refresher share one lifetime: when the first fails, the rest are cancelled and the failure reaches the caller rather than leaving half a process running.

    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.

    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.

    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.

    SignatureInput

    A request as the signature check sees it.

    SignatureKey

    One verification key: the fingerprint a caller names in the header, and the decrypter that opens envelopes sealed for it.

    SignatureVerifier

    The verification policy: the keys, how far a timestamp may drift, and whether a request that fails verification is refused or merely ignored.

    Single

    One call in flight per key: a second caller asking for the same key while the first is still running waits for that answer instead of issuing a second call, and every one of them receives the same result — or the same failure.

    This is the primitive a cache-aside store is built on: a miss under load must put one query on the database, not one per request.

    StableRunner

    A runner that keeps only the newest pending item: pushing while the consumer is busy replaces what it will take next, so a burst collapses to its last value.

    A service uses it for work that is idempotent and worth doing only once — a configuration reload, a cache refresh, a re-index triggered by a write.

    StatConf

    What the stat interceptor logs: the slow-call threshold, and the methods whose payload must not reach a log line.

    TaskRunner

    A queue drained by one consumer: push hands work over and returns, run is the loop that takes it. What a service uses when the work must not run on the request's own task — a write-behind, a slow notification.

    Ticket

    An admitted request (← go-zero's load.Promise).

    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).

    Unavailable

    Raised by Breaker::run for a call the throttle shed (← go-zero's ErrServiceUnavailable).

    Update

    A service as it stands: all its live endpoints, and a revision that grows with every change a backend can number. A backend that numbers nothing counts its own updates.

    VerifiedContent

    The plain content a verified request carries: the HMAC key the envelope named, whether the body was sealed, and the body itself — already decrypted when it was.

    View

    using @moonbitstack/moonsd { type View }

    The latest word on a service, readable without waiting: one task runs follow, and the request path reads endpoints. Choosing an endpoint must not cost a round trip.

    Watcher

    One service followed. next gives whole snapshots rather than events, so losing a connection costs an implementation a reconnect and a caller nothing: reconnecting, and recovering from a compacted history, are the implementation's business, and what comes out is always what is true now. next raises when the watcher cannot go on; close ends it.

    auth

    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.

    breaker

    Circuit-breaker middleware (← go-zero's breaker interceptor): gate each HTTP request through a shared Breaker. An admitted request's outbound HttpResponseStart status is observed — a 5xx settles its promise as a failure, anything else as a success, and that is what feeds the window. A shed request is answered 503 Service Unavailable without the wrapped app running. Non-HTTP scopes pass through untouched.

    The promise starts out a failure and only the observed status makes it a success, on a defer — the same shape go-zero's doReq uses, so a handler that raises or is cancelled before answering counts against the window instead of quietly not counting at all.

    canonical_key

    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.

    chain

    Fold interceptors around base, first in the array outermost — the order ChainUnaryInterceptor gives them.

    chunk

    fn[A] chunk(items : Array[A], size : Int) -> Array[Array[A]]

    Cut items into consecutive chunks of size, the last one possibly shorter.

    client_breaker

    The breaker interceptor: a call the throttle sheds never reaches the transport, and answers Unavailable; a failure the peer caused is counted against it.

    client_duration

    The duration interceptor: a failed call is logged at error with how long it took, and a successful one slower than slow_ms is logged as a slow call. A method listed in mute is logged without its request body, the way a login or a payment call is.

    client_prometheus

    The Prometheus interceptor: time the call and count it under its method and status code.

    client_timeout

    The timeout interceptor: a call that ran past budget_ms is reported as DeadlineExceeded.

    The deadline is checked when the call returns rather than enforced against a timer: this transport runs the peer in the caller's own task, so there is no frame to abandon. A socket-backed client hands the budget to the transport, which does preempt; the decision this records is the same either way.

    client_tracing

    The tracing interceptor: open a client span for the call, run it as a child of whatever context the caller carried, and record the resulting status code on the span.

    content_security

    Content-security middleware: verify the X-Content-Security signature on DELETE, GET, POST and PUT requests, the methods the reference implementation checks.

    A strict verifier answers 403 Forbidden when the check fails. A lenient one serves the request anyway, which is what lets a fleet roll signing out one client at a time. When the envelope declares an encrypted payload, the body served to the application is the decrypted one.

    cors

    CORS middleware: answer a preflight OPTIONS with 204 No Content and the policy headers, and stamp the same headers onto every other response.

    A request from an origin the policy does not allow is served without the Access-Control-* headers rather than refused — the browser is the one that enforces the policy, and a server-side refusal would break non-browser clients that never asked for CORS at all.

    default_latency_buckets

    let default_latency_buckets : Array[Double]

    The latency buckets go-zero registers for a server route, in milliseconds.

    duration_key

    let duration_key : String

    The key the elapsed time is written under.

    entry

    fn entry(record :
    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.

    exposition_content_type

    let exposition_content_type : String

    The content type a /metrics scrape carries so a Prometheus server reads the body as the text exposition format.

    file_server

    The file-serving layer: a GET under prefix whose remaining path names a registered file is answered with it, and every other request reaches the application. A conditional request whose If-None-Match names the file's tag is answered 304 without the body.

    This is a middleware rather than a route so it composes with the assembled chain exactly as the reference implementation's does: the file response still carries the CORS, trace and access-log layers.

    group_by

    fn[A, K : Hash + Eq] group_by(items : Array[A], key : (A) -> K) -> Array[(K, Array[A])]

    Group items by key, keeping the items of each group in the order they arrived. The groups come out in the order their first item did, so a report over them is stable.

    hold

    async fn hold(registrar : &
    Registrar
    , service : String, endpoint :
    Endpoint
    , ttl? : Int, report? : (Error) -> Unit) -> Unit

    Keep endpoint registered under service until the task is cancelled, then remove it.

    It renews at a third of ttl, the interval etcd's own keep-alive uses. The first register failing is raised: nothing has been announced, and the caller should hear. After that a failure is a backend that went away, which is no reason to stop serving: it goes to report and the next round registers again, so the endpoint comes back with the backend.

    jwt_authorized

    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.

    jwt_sign

    fn jwt_sign(claims : Map[String,
    Json
    ], secret : String, typ? : String?, extra? : Map[String,
    Json
    ], wins? :
    Wins
    , clash? :
    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.

    jwt_verify

    fn jwt_verify(token : String, secret : String, now_secs : Int64, policy? :
    Policy
    ) -> Map[String,
    Json
    ] raise
    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.

    layer

    Adapt a moonasgi mid.Layer into a moonzero Middleware. The two shapes are the same ASGI transformer, so this is the whole bridge — a layer written for moonasgi (cors, gzip, body_limit, server_errors, trusted_hosts, sessions, or a user's own) drops into a moonzero chain unchanged.

    level_name

    fn level_name(level :
    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.

    logging

    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.

    logx

    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.

    map_reduce

    fn[A, B, C] map_reduce(source : Array[A], mapper :
    Mapper
    [A, B], reducer : (Array[B]) -> C) -> C

    Run mapper over source and hand every emitted value to reducer, in the order the items were visited.

    map_reduce_parallel

    async fn[A, B, C] map_reduce_parallel(source : Array[A], mapper :
    Mapper
    [A, B], reducer : (Array[B]) -> C, max_concurrent? : Int) -> C

    map_reduce with the mapping split across at most max_concurrent tasks.

    The items are cut into one run per task, each run emits into its own array, and the arrays are concatenated in slice order — so the result is the same one the sequential run produces, whatever order the tasks happened to finish in.

    max_conns

    Max-connections middleware (← go-zero's MaxConns): hold a permit for the wrapped app's duration, or answer 503 Service Unavailable when every permit is taken. Non-HTTP scopes (lifespan, websocket) pass through untouched.

    maxbytes

    Max-bytes middleware (← go-zero's MaxBytesHandler): reject any HTTP request whose declared Content-Length exceeds limit bytes with 413 Payload TooLarge, before the wrapped app runs. A limit <= 0 disables the check. Non- HTTP scopes pass through untouched.

    media_type_of

    fn media_type_of(path : String) -> String

    The media type for a path, from its extension. The table covers the types a static bundle actually carries; anything else is served as application/octet-stream, which every client downloads rather than guesses.

    message_key

    let message_key : String

    The key the message is written under (← go-zero's logx.content, where zap and slog would write msg).

    metrics

    Metrics middleware: time each HTTP request on the shared clock and, when the response starts, count it under its path, method and status and record its latency in milliseconds. The record is taken once per request even if a downstream emits a second start. Non-HTTP scopes pass through unmeasured.

    metrics_handler

    A handler that serves the current metric set in the text exposition format, with the Prometheus content type.

    mount_metrics

    Register the GET /metrics scrape endpoint on app, so a Prometheus server can pull the exposition.

    not_allowed

    Replace the reply a request with a known path but an unsupported method gets (← WithNotAllowedHandler), normally 405 Method Not Allowed.

    not_found

    Replace the reply an unmatched request gets (← WithNotFoundHandler). The handler runs inside the application, so the chain around it — tracing, access logging, CORS — has already run.

    period_limit

    Fixed-window rate-limit middleware over the process-local limiter: account for each HTTP request under key and answer 429 Too Many Requests once the key is over quota for the current window, otherwise delegating to the wrapped app. The limiter is captured once per assembly, so its windows are shared across every request this layer serves — but only within this process; use redis_period_limit to share them across replicas. Non-HTTP scopes pass through untouched.

    period_script

    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.

    plain_text

    let plain_text : Array[(String, String)]

    The headers a refusal carries unless told otherwise.

    port

    let port : Int

    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.

    rate_limit

    Rate-limit middleware over the process-local bucket: admit each HTTP request against a shared TokenBucket read at clock.now(), answering 429 Too ManyRequests when the bucket is empty and otherwise delegating to the wrapped app. The bucket is captured once per assembly, so its state is shared across every request this layer serves — but only within this process; use redis_rate_limit to share it across replicas. Non-HTTP scopes (lifespan, websocket) pass through untouched.

    recovery

    Recovery middleware: run the wrapped application inside a try, and if it raises before any part of the response has gone out, emit a 500 InternalServer Error instead of letting the failure escape to the server.

    Once the response has started, a 500 can no longer be written — the status line is already on the wire — so the error is re-raised for the server to close the connection on. Answering late would append a second status to a response the client is already reading.

    redis_period_limit

    Fixed-window rate-limit middleware over a shared redis: period_limit with the window in redis, so replicas behind one redis spend one quota between them. A redis that cannot be reached admits the request — the limiter is a guard on the service, not a dependency that should be able to close it.

    redis_rate_limit

    Rate-limit middleware over a shared redis: rate_limit with the bucket in redis, so replicas behind one redis spend from one bucket. A redis that cannot be reached leaves the limiter running on its local bucket, so the layer keeps limiting either way. Non-HTTP scopes pass through untouched.

    refusal

    The generic refusal: 503 Service Unavailable.

    request_id

    Request-ID middleware: 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.

    retry

    fn[A] retry(body : () -> A raise, attempts? : Int, fatal? : (Error) -> Bool) -> A raise

    Call body until it stops raising, at most attempts times, and report the last failure when every attempt raised.

    fatal stops the ladder early for a failure that will not change — a malformed request is not worth three attempts — and by default nothing is fatal.

    retry_with_backoff

    async fn[A] retry_with_backoff(body : async () -> A, backoff? :
    Backoff
    , fatal? : (Error) -> Bool) -> A

    retry with a backoff between attempts: backoff decides how long to wait, and the nth wait is its nth delay. An attempt that raised the last time waits before the next one, so the first call is never delayed.

    rpc_port

    let rpc_port : Int

    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.

    server_auth

    The authentication interceptor: a peer that cannot prove itself is refused with Unauthenticated and its call never reaches the handler.

    It runs outermost, ahead of the breaker and the metric families, so a stream of unauthenticated calls cannot push the breaker open or fill the counters with traffic the service never served.

    server_breaker

    The server breaker interceptor: a shed call never reaches the handler and answers Unavailable. A deadline or a shed on the way in says nothing about this service, so it does not count as a failure.

    server_interceptors

    The server chain conf asks for, outermost first: authentication, tracing, recovery, the stat log, the metric families, the breaker, shedding and the deadline.

    server_prometheus

    The server Prometheus interceptor: time the call and count it under its method and status code.

    server_recover

    The recovery interceptor: an error raised anywhere below is reported as Internal instead of reaching the transport, and logged with its method so the reason is not lost. A handler itself cannot raise — the transport hands it a request and takes back bytes — but a layer added below can.

    server_shedding

    The shedding interceptor: a call is admitted against the adaptive shedder, and refused with ResourceExhausted when the machine is past its threshold. A refused call is reported to the shedder as a drop; an admitted one is reported by how it ended.

    server_stat

    The stat interceptor: every call is timed, and a slow one is logged with its method. A method in ignore_content_methods is logged without the payload size, so a credential never reaches a log file.

    server_timeout

    The timeout interceptor: a call that ran past its budget is reported as DeadlineExceeded. methods overrides the service budget for named paths.

    The deadline is checked when the handler returns, not raced against a timer: this transport runs the peer in the caller's task, so there is no frame to abandon. A socket-backed server hands the same budget to its read deadline, which does preempt.

    server_tracing

    The server tracing interceptor: open a server span for the call, as a child of the context the client sent, and record the resulting status on it.

    set_log_level

    fn set_log_level(level :
    LogLevel
    ) -> Unit

    Set the process logger's level (← logx.SetLevel).

    set_log_sink

    fn set_log_sink(sink : &
    Sink
    ) -> Unit

    Send the process logger's entries to sink.

    set_log_writer

    fn set_log_writer(writer : (String) -> Unit) -> Unit

    Send the process logger's entries to writer (← logx.SetWriter).

    shedding

    Shedding middleware (← go-zero's SheddingHandler): answer 503 ServiceUnavailable without running the app while the shedder is refusing, and pass everything else through. Non-HTTP scopes are never shed.

    An admitted request whose app answers 503 is settled as failed, any other as passed, which is the one status go-zero tells its shedder apart.

    stamp_key

    let stamp_key : String

    The key the entry's time is written under (← go-zero's logx, which names it for Elasticsearch's own field).

    status_code

    fn status_code(status :
    Status
    ) -> Int

    The numeric grpc-status of a status, the value that goes on the wire.

    status_name

    fn status_name(status :
    Status
    ) -> String

    The grpc-status name of a status, for a log line or a span attribute.

    status_of_code

    fn status_of_code(code : Int) ->
    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.

    structured_logging

    Structured-logging middleware: 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.

    verbose adds both header sets and a preview of the request body to the entry — what a debugging deployment switches on and an ordinary one leaves off. The preview is the first verbose_body_preview bytes: go-zero dumps the whole body, which for a service that accepts uploads means a log line the size of the upload. Non-HTTP scopes pass through without logging.

    timed_out

    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.

    timeout

    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.

    token_script

    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.

    too_large

    What an over-large request body answers with (← go-zero's maxBytes).

    too_many

    What a rate-limited request answers with (← go-zero's periodlimit and tokenlimit handlers).

    trace_clock

    fn trace_clock() -> (() -> Int64)

    The clock the tracer is driven by, in nanoseconds: the platform millisecond clock scaled up, which is the resolution every span in this framework is measured at anyway.

    trace_id_of

    fn trace_id_of(traceparent : String) -> String?

    The trace id carried by a W3C traceparent value, 00-<32 hex trace-id>-<16 hex span-id>-<2 hex flags>, or None when the value is not one.

    tracer

    A tracer over sink and sampler, drawing ids from the platform entropy source.

    tracing

    fn tracing(tracer? :
    Tracer
    , now? : () -> Int64, header? : String, ignore_paths? : Array[String]) -> ((async (
    Scope
    , async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit) -> (async (
    Scope
    , async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit))

    Tracing middleware: join the trace the request arrived on, or start one, and open a server span that reaches sink when the request finishes. Paths in ignore_paths are left untraced. The span and the propagation headers come from moontel's ASGI layer; this adds the convenience x-trace-id response header on top.

    The defaults — a discarding sink, always sampling, ids from the platform entropy source — make tracing() a working layer with nothing configured; a service that wants spans kept hands in a tracer over its own sink.

    unavailable

    What an open breaker, a shed request and a spent connection budget answer with (← go-zero's breaker, sheddingHandler, maxConns).

    verify_signature

    Verify one request against verifier at now_ms.

    On success the returned VerifiedContent carries the body to serve — the plaintext when the request declared an encrypted payload. A failure is the single check that refused it; the caller decides whether that is a 403 or a pass-through, per strict.

    Source Files