-
Notifications
You must be signed in to change notification settings - Fork 0
Dependency State
A dependency is anything the application needs and does not control: a database, a cache, a queue, another service. The state store holds what is currently known about each one.
type DepStateStore interface {
Get(id string) *DepState
GetAll() map[string]*DepState
Register(id string) *DepState
ReportSuccess(id string, latency time.Duration)
ReportFailure(id string, errType string)
SetStatus(id string, status Status, message string)
SetCircuitState(id string, circuitState CircuitState)
Remove(id string)
}store := ext.StateStore()| Meaning | IsHealthy() |
|
|---|---|---|
StatusUp |
fully operational | true |
StatusDegraded |
operational, with problems | true |
StatusDown |
not operational | false |
StatusUnknown |
never determined | false |
Status marshals to and from a string in JSON — "UP", "DEGRADED" — so
responses are readable and stable if the constants are ever reordered.
An active check writes its result into the store on every tick. This is the usual path.
store.ReportSuccess("payments-api", latency)
store.ReportFailure("payments-api", "timeout")Better signal than a synthetic ping, because it reflects the operations you
actually perform. A DepReporter bundles the two with an optional circuit
breaker:
rep := health.NewDepReporter("payments-api", store, breaker)
rep.ReportSuccess(latency)
rep.ReportFailure("timeout")client := health.WrapHTTPClient(http.DefaultClient, "payments-api", store,
health.WithHTTPTimeout(3*time.Second),
health.WithRetries(2, 200*time.Millisecond),
health.WithCircuitBreaker(breaker),
)
resp, err := client.Get(ctx, "https://payments.internal/v1/charges")Every call reports its outcome and latency automatically, and the circuit
breaker refuses calls while it is open — returning *CircuitOpenError rather
than making a request that is going to fail.
health.WithStateStoreConfig(health.DepStateStoreConfig{
FailureThreshold: 5, // consecutive failures → DOWN
DegradedThreshold: 2, // consecutive failures → DEGRADED
WindowDuration: 30 * time.Second, // the counting window
})Those are the defaults. Two consecutive failures mark a dependency degraded; five mark it down; the counters are scoped to a 30-second window, so isolated failures spread over minutes do not accumulate into an outage.
Tune FailureThreshold against your traffic rate. On an endpoint doing 1000
requests a second, five consecutive failures is 5 milliseconds of trouble — you
probably want a higher threshold or a circuit breaker with a longer window.
type DepState struct {
ID string
Status Status
Message string
LastCheck time.Time
LastSuccess *time.Time
LastFailure *time.Time
FailureCount int64
SuccessCount int64
LastLatency time.Duration
AvgLatency time.Duration
Metadata map[string]string
CircuitState CircuitState
}All of it appears under dependencies on /status — which is why
that endpoint should not be public.
Clone() returns a thread-safe copy; the store hands out clones rather than
live pointers, so a reader cannot race a writer.
SetMeta / GetMeta attach arbitrary strings — a region, a replica identifier,
a schema version.
For handlers that want to adapt rather than be refused — serve stale data, skip an enrichment step, degrade a response — inject state into the context without gating:
app.UseOnRouter(rex.DefaultRouterName,
health.DepContextMiddleware(store, "cache", "recommendations"),
rextension.PriorityDefault,
)
func handler(ctx rxroute.Context) {
if health.IsDegraded(ctx, "recommendations") {
_ = ctx.JSON(200, responseWithoutRecommendations())
return
}
…
}| Helper | |
|---|---|
GetDepState(ctx, depID) |
the full state |
IsDegraded(ctx, depID) |
is it marked degraded |
GetDepStateContext(ctx) |
route id, all states, degraded ids |
This is a soft mechanism — nothing is refused. To refuse, use the gate.
Dependency identifiers are free strings, matched exactly between
Dependencies() on a route, ReportFailure calls, and check names when you use
NewDependencyCheck. A typo produces a dependency that is permanently
StatusUnknown — which, with the default TreatUnknownAs: StatusUp, means the
gate silently serves.
Declare them as constants:
const (
DepDatabase = "database"
DepPayments = "payments-api"
)rextension-health — health, readiness and dependency gating for Rex · MIT · © 2026 Kryovyx