carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package edge
2
3import (
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.
23type 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.
29type 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.
48const 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).
56var 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.
59func 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.
65func 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.
73func 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.
82func 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.
92func 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.
99func 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.
108func 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.
116func 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.
123func 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