rproxy: circuit breaker for asks to waf (closes #392)
check / check (push) Successful in 5m31s

Five asks that get no answer in time within five seconds open the
breaker. While open, a request takes
SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION without asking waf, except
one ask a second; the first answer closes it. Slow answers count as
failures. SPEC.md describes it.

The sww test that stops and restarts waf now waits for the breaker to
close before it expects the ban again.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-15 13:49:26 +00:00
1 parent 8efd6339a6
commit 969383894c
10 files changed
+391 -18

No files matched your search

+8 -5
View File
@@ -4,10 +4,11 @@
// waf goes to the visitor as waf gave it.
// SWW_RPROXY_WAF_BACKEND_NO_ANSWER_DISPOSITION decides only when waf cannot
// be connected to, does not answer at all, or does not answer within
// SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS. rproxy itself refuses a request with a
// method it does not pass on, or one it cannot put to waf as the visitor
// sent it. It keeps nothing between requests and writes nothing to disk. "rproxy" in
// SPEC.md says what it does.
// SWW_RPROXY_WAF_BACKEND_TIMEOUT_MS, or while the circuit breaker for asks to
// waf is open. rproxy itself refuses a request with a method it does not pass
// on, or one it cannot put to waf as the visitor sent it. It keeps nothing
// between requests but the circuit breaker, and writes nothing to disk.
// "rproxy" in SPEC.md says what it does.
package rproxy
import (
@@ -132,6 +133,8 @@ type handler struct {
wafTransport http.RoundTripper
// reportURL is where the reports go.
reportURL string
// breaker is the circuit breaker for asks to waf.
breaker breaker
// reports waits for the reports being sent. reportsMu guards starting one
// against reportsStopped, which is set once nothing is to wait for another.
reports sync.WaitGroup
@@ -196,7 +199,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
answer, err := rq.ask(r.Context())
answer, err := rq.askUnlessBreakerOpen(r.Context())
if err != nil {
rq.handleNoUsableAnswer(r.Context(), err)