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:
@@ -25,16 +25,19 @@ const baselineMinMinutes = 1200
|
||||
|
||||
// newUpwatch builds the upload and CPU watch (internal/upwatch; felhom.eu `08` §9). It does not start it.
|
||||
func newUpwatch(sett *settings.Settings, stackMgr *stacks.Manager, backupMgr *backup.Manager, metricsStore *metrics.MetricsStore,
|
||||
notifier *notify.Notifier, measured func(context.Context) (float64, time.Time, bool), logger *log.Logger) *upwatch.Service {
|
||||
notifier *notify.Notifier, line agentLine, logger *log.Logger) *upwatch.Service {
|
||||
box := mediawire.DockerBox{}
|
||||
qb := &upwatch.QBittorrent{Box: box}
|
||||
tr := &upwatch.Transmission{Box: box}
|
||||
svc := &upwatch.Service{Store: sett, QB: qb, QBRunning: func() bool { return box.Running("qbittorrent") }, QBShare: qbLimitShare,
|
||||
Measured: measured, Log: logger}
|
||||
Measured: line.Measured, Log: logger,
|
||||
// R-1250: a box that seeds all day is line-tested by holding its torrent app low for one test on request.
|
||||
Torrents: []upwatch.Holdable{qb, tr}, TorrentRunning: box.Running, LineWaiting: line.Waiting, LineRun: line.Run}
|
||||
w := &upwatch.Watcher{
|
||||
Source: &upwatch.DockerAPI{},
|
||||
Explainers: map[string]upwatch.Explainer{
|
||||
"qbittorrent": qb,
|
||||
"transmission": &upwatch.Transmission{Box: box},
|
||||
"transmission": tr,
|
||||
"plex": &upwatch.Plex{Token: func() string { return mediawire.PlexToken(box) }, URL: mediawire.PlexURL},
|
||||
},
|
||||
Rules: upwatch.DefaultRules(),
|
||||
@@ -58,26 +61,46 @@ func newUpwatch(sett *settings.Settings, stackMgr *stacks.Manager, backupMgr *ba
|
||||
return svc
|
||||
}
|
||||
|
||||
// agentLine is the host's line test as the watch uses it; every field nil without a local API.
|
||||
type agentLine struct {
|
||||
Measured func(context.Context) (float64, time.Time, bool)
|
||||
Waiting func(context.Context) bool // waiting_for_quiet (agent ≥ 0.165.0, R-1250)
|
||||
Run func(context.Context) (upwatch.LineRunResult, error) // POST /host/line-test/run
|
||||
}
|
||||
|
||||
// agentLineTest reads the host's weekly line test through the agent's local API (agent ≥ 0.164.0; an older agent
|
||||
// answers 404 → not measured). nil without a local API.
|
||||
func agentLineTest(endpoint, token, fingerprint string, logger *log.Logger) func(context.Context) (float64, time.Time, bool) {
|
||||
// answers 404 → not measured, and never waiting). Empty without a local API.
|
||||
func agentLineTest(endpoint, token, fingerprint string, logger *log.Logger) agentLine {
|
||||
if endpoint == "" || token == "" {
|
||||
return nil
|
||||
return agentLine{}
|
||||
}
|
||||
ac, err := agentapi.New(endpoint, token, fingerprint)
|
||||
if err != nil {
|
||||
logger.Printf("[WARN] [upwatch] agent client init failed (%v) — no line test; qBittorrent gets no limit", err)
|
||||
return nil
|
||||
return agentLine{}
|
||||
}
|
||||
return func(ctx context.Context) (float64, time.Time, bool) {
|
||||
read := func(ctx context.Context) (agentapi.LineTestState, error) {
|
||||
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
||||
defer cancel()
|
||||
st, err := ac.LineTest(cctx)
|
||||
if err != nil {
|
||||
logger.Printf("[DEBUG] [upwatch] line test read: %v", err)
|
||||
return 0, time.Time{}, false
|
||||
}
|
||||
return st.Mbps, st.MeasuredAtTime(), st.Present
|
||||
return ac.LineTest(cctx)
|
||||
}
|
||||
return agentLine{
|
||||
Measured: func(ctx context.Context) (float64, time.Time, bool) {
|
||||
st, err := read(ctx)
|
||||
if err != nil {
|
||||
logger.Printf("[DEBUG] [upwatch] line test read: %v", err)
|
||||
return 0, time.Time{}, false
|
||||
}
|
||||
return st.Mbps, st.MeasuredAtTime(), st.Present
|
||||
},
|
||||
Waiting: func(ctx context.Context) bool {
|
||||
st, err := read(ctx)
|
||||
return err == nil && st.WaitingForQuiet
|
||||
},
|
||||
Run: func(ctx context.Context) (upwatch.LineRunResult, error) {
|
||||
r, err := ac.LineTestRun(ctx)
|
||||
return upwatch.LineRunResult{OK: r.OK, Mbps: r.Mbps, Refused: r.Refused, Error: r.Error}, err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,8 +126,10 @@ func dailyBaseline(ms *metrics.MetricsStore) func(string, []string) (float64, bo
|
||||
}
|
||||
}
|
||||
|
||||
// startUpwatch runs the minute watcher and the 15-minute qBittorrent limit rule until ctx ends.
|
||||
// startUpwatch runs the minute watcher and the 15-minute qBittorrent limit rule until ctx ends. A torrent limit a
|
||||
// stopped controller left at the line-test value is put back first (R-1250), before any rule reads it.
|
||||
func startUpwatch(ctx context.Context, svc *upwatch.Service, logger *log.Logger) {
|
||||
svc.RecoverHold(ctx)
|
||||
svc.RefreshMeasured(ctx)
|
||||
go svc.Watcher.Run(ctx, time.Minute)
|
||||
go func() {
|
||||
@@ -115,6 +140,7 @@ func startUpwatch(ctx context.Context, svc *upwatch.Service, logger *log.Logger)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
svc.SeedLineTestNow(ctx) // R-1250: holds the torrent app only while the agent waits for quiet
|
||||
svc.QBLimitNow(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package main
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/notify"
|
||||
@@ -35,7 +37,7 @@ func TestNewUpwatch_ExplainersAndEmit(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
svc := newUpwatch(sett, nil, nil, nil, notify.New("", "", "", sett, lg, false), nil, lg)
|
||||
svc := newUpwatch(sett, nil, nil, nil, notify.New("", "", "", sett, lg, false), agentLine{}, lg)
|
||||
for _, a := range []string{"qbittorrent", "transmission", "plex"} {
|
||||
if _, ok := svc.Watcher.Explainers[a]; !ok {
|
||||
t.Errorf("no explainer for %s", a)
|
||||
@@ -47,7 +49,54 @@ func TestNewUpwatch_ExplainersAndEmit(t *testing.T) {
|
||||
if _, ok := svc.Watcher.Source.(*upwatch.DockerAPI); !ok {
|
||||
t.Fatal("the watch must read the Engine API's exact counters, not docker stats' text")
|
||||
}
|
||||
// R-1250: both torrent apps can be held, qBittorrent first; without a local API there is no hold at all.
|
||||
if len(svc.Torrents) != 2 || svc.Torrents[0].Stack() != "qbittorrent" || svc.Torrents[1].Stack() != "transmission" ||
|
||||
svc.TorrentRunning == nil || svc.LineWaiting != nil || svc.LineRun != nil {
|
||||
t.Fatalf("line-test hold not wired: %d torrents, waiting=%v run=%v", len(svc.Torrents), svc.LineWaiting != nil, svc.LineRun != nil)
|
||||
}
|
||||
if svc.Watcher.Baseline != nil {
|
||||
t.Fatal("no metrics store, yet a baseline: an app with no explainer would be judged without history")
|
||||
}
|
||||
}
|
||||
|
||||
// R-1250: the start-up check puts a held limit back BEFORE the first rule reads it, and every tick runs the hold
|
||||
// before the 50 % rule. Asserted on startUpwatch's source order.
|
||||
// RED-PROOF: move svc.RecoverHold below the ticker, or drop SeedLineTestNow → fails.
|
||||
func TestStartUpwatchRecoversTheHoldFirst(t *testing.T) {
|
||||
src, err := os.ReadFile("upwatch_wiring.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := string(src)
|
||||
i := strings.Index(body, "func startUpwatch(")
|
||||
if i < 0 {
|
||||
t.Fatal("no startUpwatch")
|
||||
}
|
||||
body = body[i:]
|
||||
rec, ticker := strings.Index(body, "svc.RecoverHold(ctx)"), strings.Index(body, "time.NewTicker(")
|
||||
seed, rule := strings.Index(body, "svc.SeedLineTestNow(ctx)"), strings.Index(body, "svc.QBLimitNow(ctx)")
|
||||
if rec < 0 || rec > ticker {
|
||||
t.Fatalf("RecoverHold at %d, the ticker at %d: a held limit must go back before anything else runs", rec, ticker)
|
||||
}
|
||||
if seed < 0 || seed > rule {
|
||||
t.Fatalf("SeedLineTestNow at %d, QBLimitNow at %d: the hold must run before the 50 %% rule", seed, rule)
|
||||
}
|
||||
}
|
||||
|
||||
// With a local API, agentLineTest yields all three halves, and newUpwatch hands the hold both agent calls.
|
||||
// RED-PROOF: drop LineRun from newUpwatch's Service, or Run from agentLineTest → fails.
|
||||
func TestUpwatchHoldGetsBothAgentCalls(t *testing.T) {
|
||||
lg := log.New(io.Discard, "", 0)
|
||||
line := agentLineTest("127.0.0.1:1", "token-canary", strings.Repeat("ab", 32), lg)
|
||||
if line.Measured == nil || line.Waiting == nil || line.Run == nil {
|
||||
t.Fatalf("agentLineTest with a local API: measured=%v waiting=%v run=%v", line.Measured != nil, line.Waiting != nil, line.Run != nil)
|
||||
}
|
||||
sett, err := settings.Load(filepath.Join(t.TempDir(), "settings.json"), lg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
svc := newUpwatch(sett, nil, nil, nil, notify.New("", "", "", sett, lg, false), line, lg)
|
||||
if svc.Measured == nil || svc.LineWaiting == nil || svc.LineRun == nil {
|
||||
t.Fatalf("newUpwatch dropped an agent call: measured=%v waiting=%v run=%v", svc.Measured != nil, svc.LineWaiting != nil, svc.LineRun != nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user