package edge import ( "context" "net/http" "time" ) // answeredKey carries the flag the edge attaches so it can tell a 503 the // activator wrote on the wake path from one the upstream returned. The // wake failure already reaches the member as a platform log error // (emitEvent, logsink.go); the edge must not also file it as an edge // error (spec 2026-09-07 §3.2, "exactly once"). // // The flag lives HERE, in the edge, rather than in the activator, because // the import direction between the two is already fixed the other way: // internal/activator imports internal/edge (backend_exec.go, for // edge.Cache and edge.ListenControl) while the edge reaches the activator // only through the Activator interface (activator.go's doc: "internal/edge // is deliberately not" imported). So the activator calls edge.MarkAnswered // before each of its four wake-path 503s; putting these two functions in // the activator instead would be an import cycle. type answeredKey struct{} // outcome is what the serving path learns about a request that its status // alone does not say: whether the activator answered it itself, and, for a // 502/503/504 the edge produced, why (an apperr.Cause*, or causeClientGone). // The observing defer in ServeHTTP reads both once the response is written. type outcome struct { answered bool cause string start time.Time // when the edge began serving, so a proxy failure knows how long the client waited // genHost and gen name the generation whose connection pool carried the // request (genproxy.go), set only by an activator-managed route with // generations on. The cache tags what it stores with them (spec §7.2). genHost string gen uint64 // woke is set by the activator on the one request that started a wake // (MarkWoke), so a request record can say which request woke the app. // Requests that queued behind the same wake are not marked. woke bool } // causeClientGone is the cause noted when a proxy fails because the CLIENT // went away (its request context is done) soon after asking: ReverseProxy // still writes a 502, but to nobody, and a visitor navigating away mid-load // is not an outage. It is never filed. const causeClientGone = "client_gone" // clientGoneGrace is how long a client may wait before its leaving stops // being its own business. Past it the wait is the finding: a dead unix // socket is redialled for 13s (dialUnixWithRetry) and a wedged instance is // never timed out at all, so a monitor or a visitor that gives up first is // the only way those outages end — and filing nothing for them would hide // exactly the worst ones (apperr.CauseAbandoned). var clientGoneGrace = 5 * time.Second // a var only so a test can shorten it // withOutcome returns r carrying a fresh outcome and the outcome itself. func withOutcome(r *http.Request) (*http.Request, *outcome) { o := &outcome{start: time.Now()} return r.WithContext(context.WithValue(r.Context(), answeredKey{}, o)), o } // WithAnswerFlag returns r carrying a fresh flag and the flag itself. func WithAnswerFlag(r *http.Request) (*http.Request, *bool) { r, o := withOutcome(r) return r, &o.answered } // MarkAnswered sets the flag when r carries one; a no-op otherwise — so the // activator can call it unconditionally, including on the paths (a // non-hibernating route, a direct unit test) where the edge attached none. func MarkAnswered(r *http.Request) { if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { o.answered = true } } // MarkWoke records that r started a wake of its instance; a no-op when r is // nil or carries no outcome, like MarkAnswered, so the activator can call it // for its platform callers (a carlos run, a test) that have no request. func MarkWoke(r *http.Request) { if r == nil { return } if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { o.woke = true } } // Woke reports whether MarkWoke was called for r. func Woke(r *http.Request) bool { o, ok := r.Context().Value(answeredKey{}).(*outcome) return ok && o.woke } // waited reports how long r has been served for, when the edge attached an // outcome; zero otherwise. func waited(r *http.Request) time.Duration { if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { return time.Since(o.start) } return 0 } // noteCause records why the edge could not get an answer for r; a no-op // when r carries no outcome, like MarkAnswered. func noteCause(r *http.Request, cause string) { if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { o.cause = cause } } // noteServedGen records on r's outcome that generation n of host carried // the request; a no-op when r carries no outcome, like MarkAnswered. func noteServedGen(r *http.Request, host string, n uint64) { if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { o.genHost, o.gen = host, n } } // servedGen is what noteServedGen recorded: ok false when nothing was. func servedGen(r *http.Request) (host string, n uint64, ok bool) { if o, isOutcome := r.Context().Value(answeredKey{}).(*outcome); isOutcome && o.gen != 0 { return o.genHost, o.gen, true } return "", 0, false }