R-1125: the install page shows the check's own memory reading
gates / gates (push) Successful in 46s
gates / gates (push) Successful in 46s
The deploy page read /proc's host-wide figures (system.GetMemoryMB) while the install check reads the guest cap and the installed apps' committed requests; on 9202 the page said 12,637 MB needed where the check counted 2,648. Both now read stacks.InstallMemory. TestR1125_InstallMemoryIsTheChecksReading, red-proved. Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -1517,15 +1517,20 @@ func randomAlphanumeric(length int) (string, error) {
|
|||||||
// household reads it in its own language). One value where there used to be a sentence plus a
|
// household reads it in its own language). One value where there used to be a sentence plus a
|
||||||
// wrapper at each call site. An unreadable memory reading admits with a WARN, exactly as the deploy
|
// wrapper at each call site. An unreadable memory reading admits with a WARN, exactly as the deploy
|
||||||
// always has.
|
// always has.
|
||||||
func (m *Manager) memoryVerdict(newReqMB, newLimitMB, releasedReqMB, releasedLimitMB int) (refusal error, warningKey string) {
|
// InstallMemory is the ONE reading of the memory the install check judges: the guest's total, the reserve, and the
|
||||||
reservedMB := m.cfg.System.ReservedMemoryMB
|
// installed apps' committed requests („used"). The install page (web deploy handler) shows exactly these, so the page
|
||||||
|
// and the check can never disagree (R-1125, 2026-10-17: the page used /proc's host-wide figures and blocked an app the
|
||||||
|
// check would accept, or the other way round). ok=false when the memory cannot be read. Pinned by
|
||||||
|
// TestR1125_InstallMemoryIsTheChecksReading.
|
||||||
|
func (m *Manager) InstallMemory() (totalMB, reservedMB, usedMB int, ok bool) {
|
||||||
|
reservedMB = m.cfg.System.ReservedMemoryMB
|
||||||
totalMB, usedMB, memErr := system.GetMemoryMB()
|
totalMB, usedMB, memErr := system.GetMemoryMB()
|
||||||
// F1: the controller container cannot read the guest's RAM cap from /proc (no lxcfs) or its own
|
// F1: the controller container cannot read the guest's RAM cap from /proc (no lxcfs) or its own
|
||||||
// cgroup (the cap is on the LXC ancestor). Prefer the guest cap from the Docker daemon (runs in the
|
// cgroup (the cap is on the LXC ancestor). Prefer the guest cap from the Docker daemon (runs in the
|
||||||
// LXC). And use the controller's OWN committed-memory accounting for "used" — accurate and cheap —
|
// LXC). And use the controller's OWN committed-memory accounting for "used" — accurate and cheap —
|
||||||
// rather than host /proc RSS, which is unobservable-per-guest and would otherwise make this guard
|
// rather than host /proc RSS, which is unobservable-per-guest and would otherwise make this guard
|
||||||
// either never fire (host total) or always fire (host used > guest cap).
|
// either never fire (host total) or always fire (host used > guest cap).
|
||||||
if gt, ok := system.GuestMemTotalMB(); ok && gt > 0 {
|
if gt, gok := system.GuestMemTotalMB(); gok && gt > 0 {
|
||||||
totalMB = gt
|
totalMB = gt
|
||||||
memErr = nil
|
memErr = nil
|
||||||
}
|
}
|
||||||
@@ -1534,6 +1539,14 @@ func (m *Manager) memoryVerdict(newReqMB, newLimitMB, releasedReqMB, releasedLim
|
|||||||
}
|
}
|
||||||
if memErr != nil {
|
if memErr != nil {
|
||||||
m.logger.Printf("[WARN] [stacks] Cannot read system memory: %v — skipping memory check", memErr)
|
m.logger.Printf("[WARN] [stacks] Cannot read system memory: %v — skipping memory check", memErr)
|
||||||
|
return 0, 0, 0, false
|
||||||
|
}
|
||||||
|
return totalMB, reservedMB, usedMB, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) memoryVerdict(newReqMB, newLimitMB, releasedReqMB, releasedLimitMB int) (refusal error, warningKey string) {
|
||||||
|
totalMB, reservedMB, usedMB, ok := m.InstallMemory()
|
||||||
|
if !ok {
|
||||||
return nil, ""
|
return nil, ""
|
||||||
}
|
}
|
||||||
usedMB -= releasedReqMB
|
usedMB -= releasedReqMB
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package stacks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gitea.dooplex.hu/admin/felhom-controller/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// R-1125: the install page and the install check read memory the SAME way. „Used" is the installed (running) apps'
|
||||||
|
// committed requests — never /proc's host-wide figure, which the page used before 2026-10-17 (on scratch 9202 it
|
||||||
|
// showed 12,637 MB „needed" while the check counted 2,648 MB).
|
||||||
|
// RED-PROOF (2026-10-17): the deploy handler back on system.GetMemoryMB fails the source half.
|
||||||
|
func TestR1125_InstallMemoryIsTheChecksReading(t *testing.T) {
|
||||||
|
cfg := config.Default()
|
||||||
|
m := &Manager{cfg: cfg, logger: log.New(io.Discard, "", 0), stacks: map[string]*Stack{
|
||||||
|
"a": {Deployed: true, State: StateRunning, Meta: Metadata{Resources: ResourceHints{MemRequest: "600M", MemLimit: "1664M"}}},
|
||||||
|
"b": {Deployed: true, State: StateRunning, Meta: Metadata{Resources: ResourceHints{MemRequest: "2048M", MemLimit: "4096M"}}},
|
||||||
|
"c": {Deployed: true, State: StateStopped, Meta: Metadata{Resources: ResourceHints{MemRequest: "900M"}}},
|
||||||
|
"d": {Deployed: false, Meta: Metadata{Resources: ResourceHints{MemRequest: "500M"}}},
|
||||||
|
}}
|
||||||
|
_, reserved, used, ok := m.InstallMemory()
|
||||||
|
if !ok {
|
||||||
|
t.Skip("no memory reading on this platform")
|
||||||
|
}
|
||||||
|
if used != 2648 {
|
||||||
|
t.Fatalf("„used\" must be the running installed apps' requests (600+2048), got %d", used)
|
||||||
|
}
|
||||||
|
if reserved != cfg.System.ReservedMemoryMB {
|
||||||
|
t.Fatalf("reserve %d, want the configured %d", reserved, cfg.System.ReservedMemoryMB)
|
||||||
|
}
|
||||||
|
src, err := os.ReadFile("../web/handlers.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body := string(src)
|
||||||
|
i := strings.Index(body, "// Memory info for deploy page")
|
||||||
|
if i < 0 {
|
||||||
|
t.Fatal("the deploy page's memory block moved — re-point this test")
|
||||||
|
}
|
||||||
|
block := body[i : i+1200]
|
||||||
|
if !strings.Contains(block, "s.stackMgr.InstallMemory()") || strings.Contains(block, "system.GetMemoryMB()") {
|
||||||
|
t.Fatalf("the install page must read memory through InstallMemory, the check's own reading:\n%s", block)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -688,9 +688,10 @@ func (s *Server) deployPageData(w http.ResponseWriter, r *http.Request, name str
|
|||||||
// Memory info for deploy page (only for non-deployed apps)
|
// Memory info for deploy page (only for non-deployed apps)
|
||||||
if !alreadyDeployed {
|
if !alreadyDeployed {
|
||||||
memInfo := map[string]interface{}{"Available": false}
|
memInfo := map[string]interface{}{"Available": false}
|
||||||
totalMB, usedMB, memErr := system.GetMemoryMB()
|
// R-1125: the SAME reading the install check judges (guest total, reserve, installed apps' requests) — never
|
||||||
if memErr == nil {
|
// /proc's host-wide figures, which made this box and the check disagree.
|
||||||
reservedMB := s.cfg.System.ReservedMemoryMB
|
totalMB, reservedMB, usedMB, memOK := s.stackMgr.InstallMemory()
|
||||||
|
if memOK {
|
||||||
usableMB := totalMB - reservedMB
|
usableMB := totalMB - reservedMB
|
||||||
newReqMB := stacks.ParseMemoryMB(meta.Resources.MemRequest)
|
newReqMB := stacks.ParseMemoryMB(meta.Resources.MemRequest)
|
||||||
afterMB := usedMB + newReqMB
|
afterMB := usedMB + newReqMB
|
||||||
|
|||||||
Reference in New Issue
Block a user