| 1 | package edge |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "net/http" |
| 6 | "time" |
| 7 | ) |
| 8 | |
| 9 | // answeredKey carries the flag the edge attaches so it can tell a 503 the |
| 10 | // activator wrote on the wake path from one the upstream returned. The |
| 11 | // wake failure already reaches the member as a platform log error |
| 12 | // (emitEvent, logsink.go); the edge must not also file it as an edge |
| 13 | // error (spec 2026-09-07 §3.2, "exactly once"). |
| 14 | // |
| 15 | // The flag lives HERE, in the edge, rather than in the activator, because |
| 16 | // the import direction between the two is already fixed the other way: |
| 17 | // internal/activator imports internal/edge (backend_exec.go, for |
| 18 | // edge.Cache and edge.ListenControl) while the edge reaches the activator |
| 19 | // only through the Activator interface (activator.go's doc: "internal/edge |
| 20 | // is deliberately not" imported). So the activator calls edge.MarkAnswered |
| 21 | // before each of its four wake-path 503s; putting these two functions in |
| 22 | // the activator instead would be an import cycle. |
| 23 | type answeredKey struct{} |
| 24 | |
| 25 | // outcome is what the serving path learns about a request that its status |
| 26 | // alone does not say: whether the activator answered it itself, and, for a |
| 27 | // 502/503/504 the edge produced, why (an apperr.Cause*, or causeClientGone). |
| 28 | // The observing defer in ServeHTTP reads both once the response is written. |
| 29 | type outcome struct { |
| 30 | answered bool |
| 31 | cause string |
| 32 | start time.Time // when the edge began serving, so a proxy failure knows how long the client waited |
| 33 | // genHost and gen name the generation whose connection pool carried the |
| 34 | // request (genproxy.go), set only by an activator-managed route with |
| 35 | // generations on. The cache tags what it stores with them (spec §7.2). |
| 36 | genHost string |
| 37 | gen uint64 |
| 38 | // woke is set by the activator on the one request that started a wake |
| 39 | // (MarkWoke), so a request record can say which request woke the app. |
| 40 | // Requests that queued behind the same wake are not marked. |
| 41 | woke bool |
| 42 | } |
| 43 | |
| 44 | // causeClientGone is the cause noted when a proxy fails because the CLIENT |
| 45 | // went away (its request context is done) soon after asking: ReverseProxy |
| 46 | // still writes a 502, but to nobody, and a visitor navigating away mid-load |
| 47 | // is not an outage. It is never filed. |
| 48 | const causeClientGone = "client_gone" |
| 49 | |
| 50 | // clientGoneGrace is how long a client may wait before its leaving stops |
| 51 | // being its own business. Past it the wait is the finding: a dead unix |
| 52 | // socket is redialled for 13s (dialUnixWithRetry) and a wedged instance is |
| 53 | // never timed out at all, so a monitor or a visitor that gives up first is |
| 54 | // the only way those outages end — and filing nothing for them would hide |
| 55 | // exactly the worst ones (apperr.CauseAbandoned). |
| 56 | var clientGoneGrace = 5 * time.Second // a var only so a test can shorten it |
| 57 | |
| 58 | // withOutcome returns r carrying a fresh outcome and the outcome itself. |
| 59 | func withOutcome(r *http.Request) (*http.Request, *outcome) { |
| 60 | o := &outcome{start: time.Now()} |
| 61 | return r.WithContext(context.WithValue(r.Context(), answeredKey{}, o)), o |
| 62 | } |
| 63 | |
| 64 | // WithAnswerFlag returns r carrying a fresh flag and the flag itself. |
| 65 | func WithAnswerFlag(r *http.Request) (*http.Request, *bool) { |
| 66 | r, o := withOutcome(r) |
| 67 | return r, &o.answered |
| 68 | } |
| 69 | |
| 70 | // MarkAnswered sets the flag when r carries one; a no-op otherwise — so the |
| 71 | // activator can call it unconditionally, including on the paths (a |
| 72 | // non-hibernating route, a direct unit test) where the edge attached none. |
| 73 | func MarkAnswered(r *http.Request) { |
| 74 | if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { |
| 75 | o.answered = true |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | // MarkWoke records that r started a wake of its instance; a no-op when r is |
| 80 | // nil or carries no outcome, like MarkAnswered, so the activator can call it |
| 81 | // for its platform callers (a carlos run, a test) that have no request. |
| 82 | func MarkWoke(r *http.Request) { |
| 83 | if r == nil { |
| 84 | return |
| 85 | } |
| 86 | if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { |
| 87 | o.woke = true |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | // Woke reports whether MarkWoke was called for r. |
| 92 | func Woke(r *http.Request) bool { |
| 93 | o, ok := r.Context().Value(answeredKey{}).(*outcome) |
| 94 | return ok && o.woke |
| 95 | } |
| 96 | |
| 97 | // waited reports how long r has been served for, when the edge attached an |
| 98 | // outcome; zero otherwise. |
| 99 | func waited(r *http.Request) time.Duration { |
| 100 | if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { |
| 101 | return time.Since(o.start) |
| 102 | } |
| 103 | return 0 |
| 104 | } |
| 105 | |
| 106 | // noteCause records why the edge could not get an answer for r; a no-op |
| 107 | // when r carries no outcome, like MarkAnswered. |
| 108 | func noteCause(r *http.Request, cause string) { |
| 109 | if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { |
| 110 | o.cause = cause |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | // noteServedGen records on r's outcome that generation n of host carried |
| 115 | // the request; a no-op when r carries no outcome, like MarkAnswered. |
| 116 | func noteServedGen(r *http.Request, host string, n uint64) { |
| 117 | if o, ok := r.Context().Value(answeredKey{}).(*outcome); ok { |
| 118 | o.genHost, o.gen = host, n |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | // servedGen is what noteServedGen recorded: ok false when nothing was. |
| 123 | func servedGen(r *http.Request) (host string, n uint64, ok bool) { |
| 124 | if o, isOutcome := r.Context().Value(answeredKey{}).(*outcome); isOutcome && o.gen != 0 { |
| 125 | return o.genHost, o.gen, true |
| 126 | } |
| 127 | return "", 0, false |
| 128 | } |
| 129 | |