A service framework for MoonBit.
flowchart LR
conf["RestConf"] --> engine["RestEngine"]
app["application"] --> engine
engine -->|"build()"| server["Server"]
server -->|"to_asgi()"| asgi(["AsgiApp"])
asgi --> host["any ASGI host"]moon add moonbitstack/moonzerolet 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"| Package | What it does |
|---|---|
| rest | the config-driven engine, the server, route groups and route options |
| rest/handler | the middlewares: CORS, recovery, request ids, deadlines, body caps, gzip, JWT, content signatures, breaker, shedding, rate limits, tracing, metrics, access logs |
| rest/files | reading a directory into the static file set |
| zrpc | the zRPC server, the client channel, the interceptor chains, per-method metrics |
| zrpc/socket | serving a zRPC server on a port, and calling one over TCP (native) |
| balance | round-robin, weighted and power-of-two-choices balancers, the load-balanced channel |
| conf | the config loader and the service, REST, RPC and signature blocks |
| breaker | the client-side adaptive breaker |
| shedder | the adaptive load shedder |
| limit | token bucket, fixed window and connection limit, in memory or on Redis |
| stores/cache | cache-aside over an open store contract, one load per key |
| stores/redis | that contract over Redis |
| stores/sql | statements, rows and transactions over a database driver |
| syncx | one call in flight per key, a body that runs once, an object pool |
| collection | an LRU cache with lifetimes, a fixed-size ring |
| mr | map-reduce, sequential and parallel |
| fx | retry, backoff, deadlines, grouping |
| threading | a fan-out group, a queue drained by one consumer, a newest-only queue |
| service | several services with one lifetime |
| telemetry | the tracer a service runs on when nobody configured one |
| clock | the millisecond clock every timing decision is a function of |
| logx | levelled, structured logging |
| proc | graceful shutdown |
| discov | file-backed discovery for native hosts |
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(", "))| RestConf field | Meaning |
|---|---|
| Service | name, bind address, request budget, log level |
| CertFile, KeyFile | TLS material; both or neither, refused otherwise |
| Verbose | access logs carry both header sets |
| MaxConns, MaxBytes | connection permits, declared-body cap |
| CpuThreshold | the CPU reading at which shedding starts |
| Signature | content-security keys, tolerance, strict or lenient |
| Middlewares | one flag per layer |
| TraceIgnorePaths | paths left untraced |
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"))| Option | Effect |
|---|---|
| jwt | requires a valid bearer token; prev_secret keeps a rotation working |
| signature | verifies the content-security MAC before the handler runs |
| timeout | abandons the handler at the deadline and answers 503 |
| max_bytes | refuses a declared body past the cap with 413 |
| priority | draws the call from the priority shedder |
| prefix | joins another prefix onto the group's |
| sse | stamps the event-stream media type and no-cache headers |
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 familieslet 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")| Side | Interceptors |
|---|---|
| client | client_tracing, client_duration, client_prometheus, client_breaker, client_timeout |
| server | server_auth, server_tracing, server_recover, server_stat, server_prometheus, server_breaker, server_shedding, server_timeout |
// 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()
})let ch = @moonzero.LoadBalancedChannel::new(
_ => view.endpoints(),
cluster,
"greeter",
balancer=@moonzero.Balancer::p2c(),
)// 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))@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"))mount_metrics(app, engine.metrics()) // GET /metricstype RpcHandler = (Bytes) -> Bytesfn canonical_key(key : String) -> Stringfn client_interceptors(conf : RpcClientMiddlewaresConf, deps : ClientDeps) -> Array[((Call) -> Result[Bytes, Status] raise) -> ((Call) -> Result[Bytes, Status] raise)]let exposition_content_type : Stringfn jwt_authorized(token : String?, secret : String, now_secs : Int64) -> Boolfn media_type_of(path : String) -> Stringlet message_key : Stringlet period_script : Stringlet port : Intfn[A] retry(body : () -> A raise, attempts? : Int, fatal? : (Error) -> Bool) -> A raiseasync fn[A] retry_with_backoff(body : async () -> A, backoff? : Backoff, fatal? : (Error) -> Bool) -> Alet rpc_port : Intfn server_interceptors(conf : RpcServerMiddlewaresConf, deps : ServerDeps) -> Array[((Call) -> Result[Bytes, Status] raise) -> ((Call) -> Result[Bytes, Status] raise)]fn set_log_writer(writer : (String) -> Unit) -> Unitlet stamp_key : Stringlet token_script : Stringfn trace_clock() -> (() -> Int64)fn trace_id_of(traceparent : String) -> String?fn verify_signature(verifier : SignatureVerifier, input : SignatureInput, now_ms : Int64) -> VerifiedContent raise RefusedInstall
Download zipA service framework for MoonBit.