Pular para o conteúdo

Service Registry Contract

discovery specs/discovery/service-registry-contract.kmd

The runtime service-discovery contract of the Koder Cloud: how a service instance registers/renews/deregisters its endpoint, how the registry expires dead instances, how a consumer opts a service into edge routing, and the health semantics — the single source of truth that the PRODUCER (Koder DNS registry, dns#015) stores and the CONSUMER (Koder Jet, jet#027) reads. Ratified by infra-RFC-007 (Slice 3 = this contract). Replaces hand-edited endpoint maps (koder-service-ports.toml + jet sites.toml) with a registered source of truth.

Quando esta spec se aplica

Triggers primários

Todos os triggers

Corpo da especificação

Spec — Service Registry Contract

Status: v1.0.0. Formalizes the contract already implemented by the producer (Koder DNS, dns#015) and the consumer (Koder Jet, jet#027); both predate this doc and agreed by convention — this spec makes that agreement explicit, versioned, and conformance-testable so the two ends cannot drift (the failure mode behind the voice.koder.dev incident in infra-RFC-007).

Roles

  • Producer = the Koder DNS service registry (infra/net/dns): stores instances, serves them (SRV + the REST API below), and reaps dead ones. The single source of truth for "where is service X right now?".
  • Registrant = any Koder service, which registers its own endpoint on (re)start (auto-register-on-deploy) and heart-beats while alive.
  • Consumer = anything that routes to / resolves a backend (the Koder Jet edge, jet#027; future: other clients).

R1 — ServiceInstance (canonical wire shape)

A registered instance is a JSON object with these fields (the producer's discovery.ServiceInstance):

FieldJSONTypeNotes
IDidstringStable instance id; defaults to <name>-<address>-<port> when omitted
NamenamestringLogical service name (required)
AddressaddressstringHost/IP reachable by consumers (required)
PortportintRequired, non-zero
Protocolprotocolstringdefault tcp
Tagstagsstring[]key=value tags also accepted (see R5)
Metadatametadatamap<string,string>see R5
Weight / Priorityweight/priorityintSRV weighting; defaults 100 / 10
HealthyhealthyboolR6
TTLttl_nsint64 (ns)lease; 0 → registry default (R3)
RegisteredAt / UpdatedAtregistered_at/updated_atRFC3339server-stamped

R2 — Registration is an idempotent upsert

POST /api/v1/services/register with a ServiceInstance body. Registering an ID that already exists refreshes its mutable fields and renews its lease (it does NOT error). This is what makes auto-register-on-deploy safe to call on every (re)start without leaking duplicate or stale entries. Response: {id, created} (created=true only on first registration). name+address+port are required.

R3 — Lease, heartbeat, expiry

  • Each instance has a TTL lease (its ttl_ns, or the registry default — currently 90s — when unset/non-positive). A registry default of 0 means "never expire" (a static entry).
  • POST /api/v1/services/heartbeat {id} renews the lease (bumps updated_at). Renewing an unknown/expired id returns 410 Gone — the registrant must re-register.
  • The registry reaps any instance whose updated_at + effectiveTTL has passed (reaper interval currently 30s). A service that dies stops heart-beating and disappears on its own — the registry never serves a dead endpoint.

R4 — Deregistration

POST /api/v1/services/deregister {id} removes the instance immediately. 404 if unknown. Registrants SHOULD deregister on graceful shutdown (the reaper is the backstop for ungraceful exits).

R5 — Edge-routing opt-in + topology metadata

  • A service opts into edge routing by setting jet.domain (in metadata, or as a jet.domain=<host> tag) to its public vhost (e.g. voice.koder.dev). An instance without jet.domain is internal-only — registered + discoverable but not routed by the edge. (Analogue of Traefik's koder.jet.enable.)
  • metadata.container / metadata.service (or container= tag) carry the topology labels used to GENERATE koder-service-ports.toml (R8).

R6 — Health

healthy reflects the producer's health checks (infra/net/dns HealthChecker: tcp/http). Consumers MUST honour it (route only to healthy instances for new traffic); the flag is preserved end-to-end so a consumer can display/treat unhealthy instances distinctly. GET /api/v1/services?healthy_only=… is the producer's filter; SRV queries return healthy instances only.

R7 — REST API surface (authenticated, X-API-Key)

Method + pathPurpose
POST /api/v1/services/registerupsert an instance (R2)
POST /api/v1/services/heartbeatrenew a lease (R3)
POST /api/v1/services/deregisterremove an instance (R4)
GET /api/v1/serviceslist service names; ?detail=1 → full instances
GET /api/v1/services/exportrender the registry as koder-service-ports.toml (R8)

(SRV/A/TXT DNS resolution of the same registry is the producer's existing service.dns domain — out of scope for this REST contract.)

R8 — Generated artifacts

koder-service-ports.toml (meta/docs/stack/registries/) becomes generated from the registry (/services/export); the hand-maintained file is deprecated. A drift check compares the live registry against a static reference and reports missing / extra / owner_mismatch per port.

Conformance

  • Producerinfra/net/dns (dns#015): R1–R8. Tests: infra/net/dns/internal/discovery/registry_slice1_test.go (upsert/renew/expire/ default-TTL/export/drift) + internal/api/server_slice1_test.go (register→ heartbeat→export→deregister, 410 on expired).
  • Consumerinfra/net/jet (jet#027): R1, R5, R6. Tests: infra/net/jet/internal/discovery/koder_registry_test.go (opt-in via jet.domain metadata/tag; internal-only excluded; Healthy preserved; hot-reload).

Non-goals

  • Sidecar/eBPF mesh data-plane (that's Koder Mesh's own scope; this contract is the registry, not per-hop proxying).
  • Per-service identity / mTLS — see stack-RFC-009 (complementary, not here).

Referências