046b808792
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.
158 lines
5.7 KiB
Go
158 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/mediawire"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/metrics"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/notify"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
|
"gitea.dooplex.hu/admin/felhom-controller/internal/upwatch"
|
|
)
|
|
|
|
// qbLimitShare is qBittorrent's default share of the measured upload line — CC's pick, open for the operator (decide
|
|
// row in felhom.eu STATUS): 50 %.
|
|
const qbLimitShare = 0.5
|
|
|
|
// baselineMinMinutes: an app the box cannot ask is judged only once its history holds ~20 hours of minutes.
|
|
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, 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: 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": tr,
|
|
"plex": &upwatch.Plex{Token: func() string { return mediawire.PlexToken(box) }, URL: mediawire.PlexURL},
|
|
},
|
|
Rules: upwatch.DefaultRules(),
|
|
Line: svc.Line,
|
|
Busy: func(stack string) bool {
|
|
if backupMgr != nil && backupMgr.IsRunning() {
|
|
return true
|
|
}
|
|
if st, ok := stackMgr.GetStack(stack); ok && st != nil && (st.Updating || st.Deploying) {
|
|
return true
|
|
}
|
|
return false
|
|
},
|
|
Emit: notifier.NotifyUnexplained,
|
|
Log: logger,
|
|
}
|
|
if metricsStore != nil {
|
|
w.Baseline = dailyBaseline(metricsStore)
|
|
}
|
|
svc.Watcher = w
|
|
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, and never waiting). Empty without a local API.
|
|
func agentLineTest(endpoint, token, fingerprint string, logger *log.Logger) agentLine {
|
|
if endpoint == "" || token == "" {
|
|
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 agentLine{}
|
|
}
|
|
read := func(ctx context.Context) (agentapi.LineTestState, error) {
|
|
cctx, cancel := context.WithTimeout(ctx, 15*time.Second)
|
|
defer cancel()
|
|
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
|
|
},
|
|
}
|
|
}
|
|
|
|
// dailyBaseline caches each app's p95 for a day (a 7-day query is a few hundred thousand rows).
|
|
func dailyBaseline(ms *metrics.MetricsStore) func(string, []string) (float64, bool) {
|
|
type entry struct {
|
|
at time.Time
|
|
p95 float64
|
|
ok bool
|
|
}
|
|
var mu sync.Mutex
|
|
cache := map[string]entry{}
|
|
return func(stack string, containers []string) (float64, bool) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if e, ok := cache[stack]; ok && time.Since(e.at) < 24*time.Hour {
|
|
return e.p95, e.ok
|
|
}
|
|
p95, minutes, err := ms.StackTxP95Mbps(containers, time.Now().Add(-7*24*time.Hour))
|
|
e := entry{at: time.Now(), p95: p95, ok: err == nil && minutes >= baselineMinMinutes}
|
|
cache[stack] = e
|
|
return e.p95, e.ok
|
|
}
|
|
}
|
|
|
|
// 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() {
|
|
t := time.NewTicker(15 * time.Minute)
|
|
defer t.Stop()
|
|
for {
|
|
select {
|
|
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)
|
|
}
|
|
}
|
|
}()
|
|
l, ok := svc.Line()
|
|
logger.Printf("[INFO] [upwatch] upload and CPU watch started (line %s, qBittorrent share %.0f %%)", lineText(l, ok), qbLimitShare*100)
|
|
}
|
|
|
|
func lineText(l float64, ok bool) string {
|
|
if !ok {
|
|
return "unknown — no weekly line test yet and no counting off-site push"
|
|
}
|
|
return upwatch.MbpsText(int64(l*1e6/8)) + " Mbit/s"
|
|
}
|