upwatch: hold a seeding torrent app for one line test, then put its limit back exactly (R-1250)
gates / gates (push) Successful in 1m22s
gates / gates (push) Successful in 1m22s
When the agent waits for quiet and qBittorrent or Transmission runs: record the limit first, hold at 1 KiB/s, ask the agent for a test on request, put the old limit back (deferred; a household change during the test stays). Start-up puts back a limit a killed controller left; the 50 % rule skips while a hold record exists. Commit only.
This commit is contained in:
@@ -1291,6 +1291,18 @@ func (c *Client) get(ctx context.Context, path string) (json.RawMessage, error)
|
||||
// post issues an authenticated JSON POST and unwraps the {ok,data,error} envelope. The agent
|
||||
// returns 200 or 202 for accepted requests.
|
||||
func (c *Client) post(ctx context.Context, path string, body any) (json.RawMessage, error) {
|
||||
return c.postWith(c.hc, ctx, path, body)
|
||||
}
|
||||
|
||||
// postSlow is post with its own overall timeout, for the few synchronous routes that outlast the client's 15 s
|
||||
// (POST /host/line-test/run, about 55 s). It shares the pinned transport.
|
||||
func (c *Client) postSlow(ctx context.Context, path string, body any, timeout time.Duration) (json.RawMessage, error) {
|
||||
hc := *c.hc
|
||||
hc.Timeout = timeout
|
||||
return c.postWith(&hc, ctx, path, body)
|
||||
}
|
||||
|
||||
func (c *Client) postWith(hc *http.Client, ctx context.Context, path string, body any) (json.RawMessage, error) {
|
||||
buf, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1302,7 +1314,7 @@ func (c *Client) post(ctx context.Context, path string, body any) (json.RawMessa
|
||||
req.Header.Set("Authorization", "Bearer "+c.token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
start := time.Now()
|
||||
resp, err := c.hc.Do(req)
|
||||
resp, err := hc.Do(req)
|
||||
if err != nil {
|
||||
logx.Debugf(c.logger, "[agentapi] POST %s failed after %dms: %v", path, time.Since(start).Milliseconds(), err)
|
||||
return nil, fmt.Errorf("agentapi: POST %s: %w", path, err)
|
||||
|
||||
@@ -16,6 +16,37 @@ type LineTestState struct {
|
||||
Mbps float64 `json:"mbps,omitempty"`
|
||||
MeasuredAt string `json:"measured_at,omitempty"` // RFC3339 UTC
|
||||
Method string `json:"method,omitempty"`
|
||||
// WaitingForQuiet (agent ≥ 0.165.0, R-1250): a test is due and could start now, but the host's uplink has not been
|
||||
// quiet for 10 minutes. With a seeding torrent app the controller then holds the app's limit and calls LineTestRun.
|
||||
WaitingForQuiet bool `json:"waiting_for_quiet,omitempty"`
|
||||
}
|
||||
|
||||
// LineTestRunResult mirrors POST /host/line-test/run. OK=false with Refused: nothing ran (not-due, outside-window,
|
||||
// tries-used, no-tunnel, heavy-op, not-quiet, server-busy); OK=false without it: the runs failed.
|
||||
type LineTestRunResult struct {
|
||||
OK bool `json:"ok"`
|
||||
Mbps float64 `json:"mbps,omitempty"`
|
||||
RunsMbps []float64 `json:"runs_mbps,omitempty"`
|
||||
Refused string `json:"refused,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// LineTestRunTimeout bounds the synchronous request: about 55 s on the agent (5 s settle, 10 s quiet check, three
|
||||
// 10-second runs 5 s apart), each run itself bounded at 25 s.
|
||||
const LineTestRunTimeout = 2 * time.Minute
|
||||
|
||||
// LineTestRun calls POST /host/line-test/run (R-1250): a test on request while the caller holds its torrent app's
|
||||
// upload down. An agent before the route answers 404 (*StatusError).
|
||||
func (c *Client) LineTestRun(ctx context.Context) (LineTestRunResult, error) {
|
||||
var out LineTestRunResult
|
||||
body, err := c.postSlow(ctx, "/host/line-test/run", struct{}{}, LineTestRunTimeout)
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if err := json.Unmarshal(body, &out); err != nil {
|
||||
return out, fmt.Errorf("agentapi: decode /host/line-test/run: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// LineTest calls GET /host/line-test.
|
||||
|
||||
@@ -34,3 +34,31 @@ func TestLineTestDecodesAndAnOlderAgentIs404(t *testing.T) {
|
||||
t.Fatalf("an older agent must answer a typed 404, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// R-1250: the request decodes its outcome, waiting_for_quiet decodes, and the request outlasts the client's own
|
||||
// timeout (the agent answers after about 55 s; the client's default is 15 s).
|
||||
func TestLineTestRunOutlastsTheClientTimeout(t *testing.T) {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /host/line-test", func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(`{"ok":true,"data":{"present":false,"waiting_for_quiet":true}}`))
|
||||
})
|
||||
mux.HandleFunc("POST /host/line-test/run", func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
_, _ = w.Write([]byte(`{"ok":true,"data":{"ok":true,"mbps":93.5,"runs_mbps":[92,93.5,93]}}`))
|
||||
})
|
||||
s := httptest.NewTLSServer(mux)
|
||||
defer s.Close()
|
||||
c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://"))
|
||||
st, err := c.LineTest(context.Background())
|
||||
if err != nil || st.Present || !st.WaitingForQuiet {
|
||||
t.Fatalf("GET = %+v %v, want present:false waiting_for_quiet:true", st, err)
|
||||
}
|
||||
c.hc.Timeout = 100 * time.Millisecond // stands in for the 15 s default against a 55 s answer
|
||||
if _, err := c.post(context.Background(), "/host/line-test/run", struct{}{}); err == nil {
|
||||
t.Fatal("the plain POST should time out here — the seam does not test anything")
|
||||
}
|
||||
res, err := c.LineTestRun(context.Background())
|
||||
if err != nil || !res.OK || res.Mbps != 93.5 || len(res.RunsMbps) != 3 {
|
||||
t.Fatalf("LineTestRun = %+v %v", res, err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user