fix(schedulerclient): node memory_bytes_total no longer ×1e6 (bug-1514)

The apiserver's host max_mem is already in bytes (the worker reads
sysinfo().totalram × mem_unit → bytes and passes it through unchanged via
controller.capnp), but ListNodes treated it as MB and multiplied by
mbToBytes, inflating each node's reported memory to ~8.2e15 (~7.3 PiB).
Now reports the raw bytes (~7.7 GiB per node).

- httpclient.go: drop ×mbToBytes for node max_mem; clarify mbToBytes is
  job-submit-path only (job max_mem is never populated → stays 0)
- ordermap_test.go: correct assertion that had encoded the buggy value
- httpclient_test.go: regression test TestListNodeMemoryBytesKeepsByteUnit
- buglog.json: bug-1514 + close-tags for the bug-1494 family (prior session)

Also add MCP demo deliverables:
- docs/claude-code-demo-guide.md: live Claude Code demo guide (7 scenes)
- docs/mcp-demo-design.md: leadership demo design
- docs/mcp-demo-run.py: add --slow/--color flags for screen recording

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-22 14:58:56 +00:00
parent bbe8ed3335
commit 06887985b4
7 changed files with 423 additions and 36 deletions

View file

@ -195,7 +195,7 @@ func (hc *HTTPClient) ListNodes(ctx context.Context) ([]*URMNode, error) {
Status: NodeStatusFromString(rawStatus),
CoresTotal: h.rowInt("num_cpus"),
CoresUsed: numRun, // running jobs ≈ cores used
MemoryBytesTotal: int64(h.rowInt("max_mem")) * mbToBytes,
MemoryBytesTotal: int64(h.rowInt("max_mem")), // max_mem is already bytes (worker reports sysinfo().totalram bytes); do NOT ×1e6
MemoryBytesUsed: 0, // apiserver does not expose live used-memory for hosts
GPUCount: 0, // TODO: parse from extensions/topology when available
Features: nil,
@ -393,7 +393,10 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter)
// ── internal types for JSON decoding (apiserver response shapes) ────────────
const mbToBytes = 1_000_000 // MaxMem from apiserver is in MB → stored as bytes in URM
// mbToBytes scales a value that is genuinely in MB to bytes. It applies ONLY
// to the job submit path where mem_limit is formatted as "<MB> MB"
// (write.go). Node host max_mem is already in bytes — do NOT scale it.
const mbToBytes = 1_000_000
// apiEnvelope is the standard top-level response: {"code":200,"msg":"ok","data":...}
type apiEnvelope struct {

View file

@ -0,0 +1,51 @@
package schedulerclient
import (
"context"
"net/http"
"net/http/httptest"
"testing"
)
// TestListNodeMemoryBytesKeepsByteUnit verifies the node total memory is NOT
// re-scaled by the client: the apiserver's `max_mem` field is already in bytes
// (the worker reads sysinfo().totalram * mem_unit → bytes and passes it through
// unchanged), so scheduler_list_nodes must report memory_bytes_total ≈ 8.2e9
// for a ~7.7 GiB node — not the ×1e6-inflated ~8.2e15 that httpclient.go:198
// produced when it wrongly treated apiserver bytes as MB.
func TestListNodeMemoryBytesKeepsByteUnit(t *testing.T) {
// 8224825344 bytes = the real MemTotal of the t1 node (7.66 GiB).
const realMemBytes = 8224825344
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got := r.Header.Get("Authorization"); got == "" {
t.Errorf("expected Authorization header, got empty")
}
if got := r.URL.Path; got != "/xce/v1/hosts" {
t.Errorf("unexpected path %q", got)
}
w.Header().Set("Content-Type", "application/json")
body := `{"code":200,"msg":"ok","data":{"count":1,"rows":[` +
`{"fields":["host_name","host_status","num_cpus","num_run","max_mem"],` +
`"data":{"host_name":"t1","host_status":"idle","num_cpus":8,"num_run":0,"max_mem":8224825344}}` +
`]}}`
_, _ = w.Write([]byte(body))
}))
defer srv.Close()
hc := NewClient(srv.URL, "xce_ak_test")
nodes, err := hc.ListNodes(context.Background())
if err != nil {
t.Fatalf("ListNodes: %v", err)
}
if len(nodes) != 1 {
t.Fatalf("expected 1 node, got %d", len(nodes))
}
n := nodes[0]
if n.Name != "t1" || n.CoresTotal != 8 {
t.Fatalf("unexpected node: %+v", n)
}
if n.MemoryBytesTotal != realMemBytes {
t.Fatalf("MemoryBytesTotal = %d, want %d (apiserver max_mem is already bytes; must not be ×1e6)", n.MemoryBytesTotal, realMemBytes)
}
}

View file

@ -56,8 +56,10 @@ func TestOrderMap_RowsUseColumnarShape(t *testing.T) {
if n.CoresTotal != 8 {
t.Errorf("node.CoresTotal = %d, want 8", n.CoresTotal)
}
if n.MemoryBytesTotal != 8224821248*mbToBytes {
t.Errorf("node.MemoryBytesTotal = %d, want %d", n.MemoryBytesTotal, 8224821248*mbToBytes)
// max_mem from apiserver is already bytes (worker reports sysinfo bytes);
// the client must NOT scale it by mbToBytes (see httpclient.go ListNodes).
if n.MemoryBytesTotal != 8224821248 {
t.Errorf("node.MemoryBytesTotal = %d, want %d", n.MemoryBytesTotal, 8224821248)
}
}