Setup asks for webhooks, but one left empty was never mentioned again: a lost node or a judge down was told nobody. With no notify.webhooks in the cluster's settings, ctl status and the console's Overview say so, and how to set one. BACKLOG: the judge's refusal after a placement change, tried again on idle VMs, did not happen; a cold judge's first answer is 2-3x slower but inside its budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2c32a58a06
commit
a00c92648e
10 files changed
+60
-4
No files matched your search
+8
-3
@@ -463,8 +463,9 @@ portal is not done — the page shows; the console and ctl change.
|
||||
5. **Done (branch `features-c`):** `notify.webhooks` (model parked, node
|
||||
lost, budget spent, update available; signed JSON or Slack; retries,
|
||||
hourly de-duplication), `ctl notify test`, Alertmanager receiver
|
||||
templates. Left: `server init` and the console do not yet ask for a
|
||||
receiver — the templates are filled in by hand. Events are claimed in
|
||||
templates. `server init` and both setup screens ask for webhooks and test
|
||||
each (d1e9186, 2026-10-14); with none set, `ctl status` and the console's
|
||||
Overview say the alerts go nowhere (2026-11-12). Events are claimed in
|
||||
the store before they are sent (2026-10-10), so two control planes send
|
||||
each once.
|
||||
**Alerting out of the box (S).** `deploy/monitoring/alertmanager.yml`
|
||||
@@ -2056,7 +2057,11 @@ lent, partition`); the leader tells each named node's agent (root) what to be, t
|
||||
node (the controller is its node), so a site wanting control planes without nodes chooses the
|
||||
controller later with `ctl slurm setup`; batch output lands on the job's first node until there is
|
||||
shared storage; on shared CPUs the judge refuses the first request after a placement change (fail
|
||||
closed, ~10 s), and its advice repeats `--cores 4` to a judge that has 4.
|
||||
closed, ~10 s), and its advice repeats `--cores 4` to a judge that has 4. Tried again on the idle
|
||||
VMs: one judge restarted in place, then both at once, requests every second: 48 of 48 served; the
|
||||
first after a cold start took 7.6–8.9 s end to end against ~3 s, within the 5 s judge budget. So the
|
||||
refusal was the install's load on the CPUs; if it is seen again, warm a new judge with one judgment
|
||||
before it takes requests.
|
||||
- Shared files and users, built 2026-11-12: `slurm.shared` {path, nfs | server} — the site's export
|
||||
mounted (a systemd mount unit, hard, nconnect=4), an llmgw node exporting the path itself
|
||||
(/etc/exports.d, to the Slurm machines' addresses, root_squash; 2049 in the firewall), or the site's
|
||||
|
||||
@@ -9595,6 +9595,10 @@
|
||||
"guard_off": {
|
||||
"type": "boolean",
|
||||
"description": "GuardOff says the guardrails are turned off (guard.enabled: false): no request is checked, whatever the policies say."
|
||||
},
|
||||
"no_alerts": {
|
||||
"type": "boolean",
|
||||
"description": "NoAlerts says the cluster's settings name no webhook: a model parked, a node lost or a judge down is told nobody."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
-1
@@ -67,7 +67,8 @@ or with the mirror unticked, is said in the form and nothing starts.
|
||||
## Watch
|
||||
|
||||
**Overview** is `llmgw ctl status`: nodes, GPUs by class, fabrics, replicas,
|
||||
models, the store's members, and anything parked, unplaced or idle.
|
||||
models, the store's members, and anything parked, unplaced or idle. With no
|
||||
webhook in the cluster's settings it says the alerts go nowhere.
|
||||
|
||||
**Monitor** draws the cluster the way btop draws a machine: CPU, memory,
|
||||
GPU busy and GPU memory, requests and tokens a second, time to first token,
|
||||
|
||||
@@ -84,3 +84,19 @@ func TestTheSummaryIsWrittenWhereItIsToldNotToTheTerminal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusSaysWhenAlertsGoNowhere(t *testing.T) {
|
||||
render := func(s adminapi.Summary) string {
|
||||
cmd := &cobra.Command{}
|
||||
var b bytes.Buffer
|
||||
cmd.SetOut(&b)
|
||||
printSummary(cmd, s)
|
||||
return b.String()
|
||||
}
|
||||
if out := render(adminapi.Summary{NoAlerts: true}); !strings.Contains(out, "ALERTS nowhere") {
|
||||
t.Errorf("no webhook was not called out:\n%s", out)
|
||||
}
|
||||
if out := render(adminapi.Summary{}); strings.Contains(out, "ALERTS") {
|
||||
t.Errorf("an alerts line with webhooks set:\n%s", out)
|
||||
}
|
||||
}
|
||||
@@ -417,6 +417,7 @@ func newServerCmd() *cobra.Command {
|
||||
adminAPI = api
|
||||
api.SetReleases(cfg.Releases)
|
||||
api.SetNotifier(notifier)
|
||||
api.SetWebhooks(func() int { return len(current.Load().Notify.Webhooks) })
|
||||
api.SetReplayer(rt.Replay)
|
||||
// Quality suites, asked of a model the way a replay is.
|
||||
evals = newEvalService(cfg, rt, admin, log)
|
||||
|
||||
@@ -156,6 +156,10 @@ func printSummary(cmd *cobra.Command, s adminapi.Summary) {
|
||||
if s.Stopped {
|
||||
fmt.Fprintln(out, "SERVICE STOPPED: no model runs and requests are answered 503 (llmgw start)")
|
||||
}
|
||||
if s.NoAlerts {
|
||||
fmt.Fprintln(out, "ALERTS nowhere: a lost node or a judge down is told nobody "+
|
||||
"(llmgw ctl config set notify.webhooks, then llmgw ctl notify test)")
|
||||
}
|
||||
if s.GuardOff {
|
||||
fmt.Fprintln(out, "GUARD off: no request is checked, whatever the policies say (llmgw ctl guard on)")
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@ type API struct {
|
||||
ctl *control.Controller
|
||||
// slurmUsers are the Linux users made from the accounts; nil without a store.
|
||||
slurmUsers SlurmUsers
|
||||
// webhooks counts the settings' notify.webhooks; nil does not say.
|
||||
webhooks func() int
|
||||
}
|
||||
|
||||
// New builds the API over a controller.
|
||||
|
||||
@@ -6,6 +6,10 @@ import (
|
||||
"gitea.mynodes.xyz/ilker/llmgw/internal/notify"
|
||||
)
|
||||
|
||||
// SetWebhooks has the summary say when the settings name no webhook:
|
||||
// count is how many they name now.
|
||||
func (a *API) SetWebhooks(count func() int) { a.webhooks = count }
|
||||
|
||||
// SetNotifier gives the API the webhooks `ctl notify test` reaches.
|
||||
func (a *API) SetNotifier(n *notify.Notifier) { a.notifier = n }
|
||||
|
||||
|
||||
@@ -50,6 +50,9 @@ type Summary struct {
|
||||
// GuardOff says the guardrails are turned off (guard.enabled: false):
|
||||
// no request is checked, whatever the policies say.
|
||||
GuardOff bool `json:"guard_off,omitempty"`
|
||||
// NoAlerts says the cluster's settings name no webhook: a model parked,
|
||||
// a node lost or a judge down is told nobody.
|
||||
NoAlerts bool `json:"no_alerts,omitempty"`
|
||||
}
|
||||
|
||||
// StoreMember is one member of a replicated store, as its Patroni reports it.
|
||||
@@ -160,6 +163,9 @@ func (a *API) Summary() Summary {
|
||||
if a.guard != nil && a.guard.Library != nil && !a.guard.Library().On() {
|
||||
s.GuardOff = true
|
||||
}
|
||||
if a.webhooks != nil && a.webhooks() == 0 {
|
||||
s.NoAlerts = true
|
||||
}
|
||||
for _, j := range a.Judges() {
|
||||
if j.Problem != "" {
|
||||
s.Judges = append(s.Judges, j)
|
||||
|
||||
@@ -153,3 +153,16 @@ func TestOneSharedCopyCountsAsMuchAsTwenty(t *testing.T) {
|
||||
t.Errorf("counted %d models; three nodes seeing one file is one model", f.Models)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheSummarySaysWhenNoWebhookIsSet(t *testing.T) {
|
||||
a, _ := setup(t, testModels(), gpuNode("n1", 8))
|
||||
n := 0
|
||||
a.SetWebhooks(func() int { return n })
|
||||
if !a.Summary().NoAlerts {
|
||||
t.Error("no webhook, not said")
|
||||
}
|
||||
n = 1
|
||||
if a.Summary().NoAlerts {
|
||||
t.Error("a webhook set, yet said there is none")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user