rustlavel-discovery 0.8.3

Service registry and discovery: register, resolve, replicate — Eureka-compatible
Documentation

rustlavel-discovery

A service registry and the client that uses it, for the Rustlavel framework. Eureka-compatible on the wire, so a Spring Cloud service can register against it unmodified.

Read this before reaching for it

On Kubernetes, ECS or Nomad you do not need a registry. The platform already knows which instances are ready and already balances across them. A registry there is a second copy of that fact, and two sources for one fact disagree eventually. Use Dns.

This crate is for what the platforms do not cover: services on plain machines, an estate spread across two clouds, or a Spring system already speaking Eureka that a Rustlavel service has to join.

Three answers, one trait

use rustlavel::discovery::{Discovery, Dns, Resolve, Static};
Resolver Where the addresses come from
Static The list you wrote. Where everybody starts, and where a great many applications correctly stay.
Dns A name the platform already resolves. The Kubernetes answer.
Discovery This registry, cached so that the registry being down does not stop traffic.

Moving between them is a line of configuration, not a rewrite.

Running a registry

use rustlavel::prelude::*;
use rustlavel::discovery::{Peers, RegistryServer};

App::new()
    .plugin(
        RegistryServer::new()
            .peers(Peers::new(nodes).excluding(&own_url)),
    )
    .run()
    .await

It serves Eureka's routes — POST /eureka/apps/{app}, PUT /eureka/apps/{app}/{id}, DELETE, the status endpoint and both reads — plus a dashboard at /discovery and its document at /discovery/registry.json.

The dashboard has no authentication in front of it. It lists every host and port you run. Put it behind whatever the application already uses, or .dashboard(None) and read the Registry from a page of your own — which is what the auth kit's screen does.

Joining one

use rustlavel::discovery::{Instance, Registrar};

let registrar = Arc::new(Registrar::new(
    nodes,
    Instance::new("orders", host, port).meta("zone", "jakarta-a"),
));
registrar.clone().start();           // registers, then heartbeats

On shutdown, registrar.deregister().await — the difference between a clean deploy and thirty seconds of failures.

Routing a gateway by name

With both the gateway and discovery features on:

let balancer = Arc::new(Balancer::new(Discovery::new(registries)));

Gateway::new()
    .route("/api/orders/*", Upstream::service("orders", balancer.clone()).strip("/api"))
    .route("/api/billing/*", Upstream::service("billing", balancer).strip("/api"))

A service that resolves to nothing is 503, not a request sent to a host nobody invented.

The decisions, and why

The registry is in memory and nothing is persisted. Every instance re-registers within a heartbeat of a restart, so the state rebuilds itself faster than it could be loaded — and a durable copy would only ever be wrong on the way back up.

Nothing is evicted on a timer. Instances carry a last_seen and a read decides who is alive. That is what lets the registry notice that most of itself went quiet at once and conclude the network broke rather than that the estate died. Eureka calls this self-preservation; it is its most misunderstood feature, and people disable it because "stale instances got traffic" — which is true, and is the smaller of the two failures. A registry that empties during a blink turns the blink into an outage that outlasts it, because everything then has to re-register before anything can find anything.

A registry with fewer than eight instances never enters that state: a proportion threshold applied to a handful fires on the first ordinary failure.

Replication is peer forwarding, not consensus. A registry is a cache of where things are, and the answer is already slightly out of date when it is given. Paying for consensus would buy agreement about a stale fact and would mean a node that loses quorum stops answering — exactly the outage the registry exists to survive. Every forwarded write carries a marker (?isReplication=true, and a header for anything that strips query strings), and a marked write is applied and never forwarded on: one hop, never two.

The client's cache has no expiry. When the registry cannot be reached, Discovery goes on serving what it last read, for however long that lasts. An outage costs you changes — a new instance is not noticed, a departed one is still offered — and the caller's own retries cover the second. A cache that expired would turn a registry outage into a total outage on a timer.

Licence

MIT OR Apache-2.0