fix(R-1250): the line-test hold waits until the held app has really slowed down before asking the agent
gates / gates (push) Successful in 51s
gates / gates (push) Successful in 51s
Measured on demo-hp 2026-10-27: qBittorrent 5.2.4 kept sending at full speed for ~14 s after its limit fell to 1 KiB/s; the agent reads the uplink 5-15 s after the call, so every test on request was refused "not quiet" (four tries in an hour, 4.0-4.2 Mbit/s). waitDrained polls each held app's own upload (qBittorrent up_info_speed, Transmission uploadSpeed) until <= 16 KiB/s twice, 2 s apart, at most 90 s; an unreadable speed waits 20 s. Tests TestR1250_* (five), red-proved by four mutations.
This commit is contained in:
1 parent
9b5bb0a9a6
commit
710ceace12
5 files changed
+289
-3
No files matched your search
@@ -21,9 +21,12 @@ import (
|
||||
// 1. reads the app's global upload limit exactly as the app stored it, and writes it to settings.json
|
||||
// (`upwatch.line_test_hold`) BEFORE it touches the app;
|
||||
// 2. sets the app's upload to HoldValue (1 KiB/s);
|
||||
// 3. asks the agent for a test on request (POST /host/line-test/run: 5 s settle, 10 s quiet check, three 10-second
|
||||
// 3. waits until every held app REPORTS its own upload near zero (waitDrained) — qBittorrent 5.2.4 kept sending at
|
||||
// full speed for ~14 s after its limit fell to 1 KiB/s (measured on demo-hp 2026-10-27, felhom.eu
|
||||
// `audits/linetest-2026-10-27/`), so the agent's quiet check, 5–15 s after the hold, refused every test;
|
||||
// 4. asks the agent for a test on request (POST /host/line-test/run: 5 s settle, 10 s quiet check, three 10-second
|
||||
// runs 5 s apart — about 55 s);
|
||||
// 4. puts the old limit back — the household's own limit included, exactly as it was — and clears the record.
|
||||
// 5. puts the old limit back — the household's own limit included, exactly as it was — and clears the record.
|
||||
//
|
||||
// Whatever fails after step 1, the limit is put back first. A controller that stops between 1 and 4 finds the record
|
||||
// at start (RecoverHold) and puts it back then. If the app holds anything but the test value when the limit goes
|
||||
@@ -60,6 +63,8 @@ type Holdable interface {
|
||||
WriteLimit(ctx context.Context, l HeldLimit) (HeldLimit, error)
|
||||
// HoldValue is the test value in prev's slot: very low, never zero (zero is „no limit").
|
||||
HoldValue(prev HeldLimit) HeldLimit
|
||||
// UploadRate is the app's own upload speed now, B/s — read while held, to know when it has really slowed.
|
||||
UploadRate(ctx context.Context) (int64, error)
|
||||
}
|
||||
|
||||
// LineRunResult is the agent's answer to a test on request.
|
||||
@@ -201,6 +206,76 @@ func (s *Service) releaseAll(ctx context.Context, held []Holdable, hs []LineHold
|
||||
}
|
||||
}
|
||||
|
||||
// The drain wait (step 3). The agent refuses a test when the uplink sends 2 Mbit/s or more 5–15 s after the call, so the
|
||||
// controller asks only when every held app reports its own upload at or below drainQuietBps (0.13 Mbit/s) on
|
||||
// drainQuietReadings readings in a row, drainPoll apart. It never waits longer than drainMax, and asks then anyway (the
|
||||
// agent judges the line). An app whose speed cannot be read gets a plain drainFallback wait instead — the measured
|
||||
// drain was ~14 s. Pinned by linehold_r1250_drain_test.go.
|
||||
const (
|
||||
drainPoll = 2 * time.Second
|
||||
drainMax = 90 * time.Second
|
||||
drainFallback = 20 * time.Second
|
||||
drainQuietBps = 16 * 1024
|
||||
drainQuietReadings = 2
|
||||
)
|
||||
|
||||
// drainSleep waits d; false when ctx ends first. A variable so the tests can count the waiting instead of doing it.
|
||||
var drainSleep = func(ctx context.Context, d time.Duration) bool {
|
||||
t := time.NewTimer(d)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
case <-t.C:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// waitDrained returns when every held app has slowed down to the test value, or when it cannot tell (see above).
|
||||
func (s *Service) waitDrained(ctx context.Context, apps []Holdable) {
|
||||
var waited time.Duration
|
||||
quiet := 0
|
||||
for {
|
||||
var worst int64
|
||||
var readErr error
|
||||
for _, app := range apps {
|
||||
r, err := app.UploadRate(ctx)
|
||||
if err != nil {
|
||||
readErr = fmt.Errorf("%s: %v", app.Stack(), err)
|
||||
break
|
||||
}
|
||||
if r > worst {
|
||||
worst = r
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case readErr != nil:
|
||||
logx.Infof(s.Log, "[upwatch] line test: cannot read the upload speed (%v) — waiting %s before asking the agent", readErr, drainFallback)
|
||||
if rest := drainFallback - waited; rest > 0 {
|
||||
drainSleep(ctx, rest)
|
||||
}
|
||||
return
|
||||
case worst <= drainQuietBps:
|
||||
quiet++
|
||||
if quiet >= drainQuietReadings {
|
||||
logx.Infof(s.Log, "[upwatch] line test: the held app(s) slowed down after %s — asking the agent", waited)
|
||||
return
|
||||
}
|
||||
default:
|
||||
quiet = 0
|
||||
logx.Debugf(s.Log, "[upwatch] line test: held, still sending %d B/s after %s", worst, waited)
|
||||
}
|
||||
if waited >= drainMax {
|
||||
logx.Infof(s.Log, "[upwatch] line test: the held app(s) still send %d B/s after %s — asking the agent anyway", worst, waited)
|
||||
return
|
||||
}
|
||||
if !drainSleep(ctx, drainPoll) {
|
||||
return
|
||||
}
|
||||
waited += drainPoll
|
||||
}
|
||||
}
|
||||
|
||||
// SeedLineTestNow runs the hold once when the agent waits for quiet and a torrent app is running (main calls it every
|
||||
// 15 minutes, before QBLimitNow). EVERY running seeder is held — one left seeding keeps the uplink busy and the agent
|
||||
// refuses the test „not quiet" every time (R-1261). A no-op otherwise.
|
||||
@@ -255,6 +330,7 @@ func (s *Service) SeedLineTestNow(ctx context.Context) {
|
||||
// Deferred: a panic in the call puts the limits back too. A killed process does not — that is RecoverHold's.
|
||||
res, err := func() (LineRunResult, error) {
|
||||
defer s.releaseAll(ctx, apps, hs)
|
||||
s.waitDrained(ctx, apps)
|
||||
return s.LineRun(ctx)
|
||||
}()
|
||||
switch {
|
||||
@@ -354,6 +430,28 @@ func (q *QBittorrent) HoldValue(prev HeldLimit) HeldLimit {
|
||||
return HeldLimit{Bytes: 1024, Alt: prev.Alt}
|
||||
}
|
||||
|
||||
// UploadRate: transfer/info `up_info_speed` (B/s, payload and overhead together).
|
||||
func (q *QBittorrent) UploadRate(ctx context.Context) (int64, error) {
|
||||
h, err := q.session(ctx, true)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
code, b, _, err := httpDo(ctx, q.client(), "GET", q.base()+"/api/v2/transfer/info", h, nil, "")
|
||||
if err != nil || code != 200 {
|
||||
return 0, fmt.Errorf("qBittorrent transfer/info: HTTP %d %v", code, err)
|
||||
}
|
||||
var ti struct {
|
||||
UpSpeed *int64 `json:"up_info_speed"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &ti); err != nil {
|
||||
return 0, fmt.Errorf("qBittorrent transfer/info: %v", err)
|
||||
}
|
||||
if ti.UpSpeed == nil {
|
||||
return 0, fmt.Errorf("qBittorrent transfer/info: no up_info_speed")
|
||||
}
|
||||
return *ti.UpSpeed, nil
|
||||
}
|
||||
|
||||
func (t *Transmission) ReadLimit(ctx context.Context) (HeldLimit, error) {
|
||||
var st struct {
|
||||
Arguments struct {
|
||||
@@ -386,3 +484,23 @@ func (t *Transmission) ReadLimitLike(ctx context.Context, _ HeldLimit) (HeldLimi
|
||||
|
||||
// HoldValue: 1 kB/s with the limit switched on.
|
||||
func (t *Transmission) HoldValue(HeldLimit) HeldLimit { return HeldLimit{Enabled: true, KBps: 1} }
|
||||
|
||||
// UploadRate: session-stats `uploadSpeed` (B/s).
|
||||
func (t *Transmission) UploadRate(ctx context.Context) (int64, error) {
|
||||
b, err := t.rpc(ctx, `{"method":"session-stats"}`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
var st struct {
|
||||
Arguments struct {
|
||||
UploadSpeed *int64 `json:"uploadSpeed"`
|
||||
} `json:"arguments"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &st); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if st.Arguments.UploadSpeed == nil {
|
||||
return 0, fmt.Errorf("transmission session-stats: no uploadSpeed")
|
||||
}
|
||||
return *st.Arguments.UploadSpeed, nil
|
||||
}
|
||||
Reference in new issue
Block a user