moonsd
The three traits, Endpoint, Lease,
Update, hold, View,
an in-process registry and the conformance suite.
Service discovery · MoonBit
A service has to say it is here, keep saying it, and be found. moonsd writes that down as three traits — a registrar, a discovery, a watcher — and leaves every registry to implement them.
Article one
A registry is a place a service writes its address and a caller reads it back. That is all the contract fixes. What the registry does with a lapsed lease, how a watcher learns of a change, whether the store is a process, a cluster or a key-value server — none of it reaches the caller.
An address is known for its lease and no longer. Renewing extends it; a lease
the registry has let go raises Refused::Expired.
One answer now, or a stream of answers. A service nobody registered has no endpoints; that is not an error.
Whole snapshots, never events. Reconnecting and recovering from a compacted history are the backend's business, so what comes out is what is true now.
Article two
An endpoint is alive because something keeps saying so. hold
renews at a third of the lease and registers again when the registry has forgotten it.
View follows a service in one task and hands the latest list to
whoever is on the request path, without a round trip.
Both are written once, against the traits, so a backend gets them by implementing six methods:
async fn serve(registry : &@moonsd.Registrar) -> Unit { let endpoint = @moonsd.Endpoint::new("10.0.0.1", 12000, metadata=Map([("zone", "a")])) @async.with_task_group(group => { group.spawn_bg(() => @moonsd.hold(registry, "greeter", endpoint, ttl=15_000)) // serve here; leaving the group cancels hold, which deregisters }) }
Article three
The contract is a module of its own, and each registry is a client library that
implements it — a full client for that registry, with a small sd
package where the client meets the contract.
The three traits, Endpoint, Lease,
Update, hold, View,
an in-process registry and the conformance suite.
Keys that expire carry the endpoint, so a lapsed lease needs no sweeper. A watcher reads the key set again on an interval.
Keys on leases, so the key goes when the lease does; the revision etcd stamps on every write is where a watcher resumes.
Services with TTL checks, so other Consul clients see ordinary services; a watcher follows them with blocking queries.
Article four
A backend is not finished when it compiles. conformance.check
runs every behaviour the contract promises against one registrar and one discovery:
the fields that survive a round trip, the lease that lapses, the watcher that hears a
change and then hears the service empty. Each client's suite runs it against the real
server, in CI.
| Backend | Where the endpoint lives | Lease ends by | Watcher learns by | Check |
|---|---|---|---|---|
memory | a map in this process | a clock, and the next call that looks | a queue per follower | both ways: with and without a clock |
moonredis | a key with a TTL | Redis expiring the key | reading the key set again | live Redis 8, standalone and clustered |
moonetcd | a key attached to a lease | the lease being revoked or lost | a watch from the last revision | live etcd 3.5, three nodes, members killed |
moonconsul | a service with a TTL check | the check going critical and being reaped | a blocking query | live Consul 1.20, two agents |