merge: sync Rongsong's scheduler-framework into chatbot

Full merge of origin/scheduler-framework (scheduler-core restructure,
failover-adoption, WAL_JOB_DISPATCH, apiserver backend health/heartbeat,
build-time versioning, bug-1673/1676/1678/1679/1680).

Conflict resolution:
- scheduler: adopt Rongsong's scheduler-core architecture (driver loop,
  queue project/picker fields, resource bug-1569 fix)
- worker/controller_connector.c: keep Rongsong's reconnect/rejoin
  structure, PORT our bug-1509 resend_job_status onto the rejoin success
  path (his reconnect didn't re-report job state)
- apiserver backend: take Rongsong's health/heartbeat (supersedes our
  e505066 circuit breaker)
- group.c: keep our bug-1513 comment
- misc: take theirs for additive fixes (go_build versioning,
  test_group.c, persist.c RUSAGE_UPDATE, FilterRows bug-1680)

NOTE: our scheduler fixes (bug-1494 family dispatch-fail requeue,
PEND-reason classification) are NOT in Rongsong's branch — needs
verification/porting onto scheduler-core (separate follow-up).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-25 12:33:16 +00:00
commit 4e894d2184
395 changed files with 88517 additions and 5742 deletions

View file

@ -4,6 +4,7 @@ import (
"apiserver/models"
"apiserver/store"
logger "apiserver/utils"
"sync"
"go.uber.org/zap"
)
@ -11,6 +12,66 @@ import (
// DefaultHandler implements NotificationHandler by writing to stores.
type DefaultHandler struct{}
// defaultHostConfigName is the cluster-wide config sentinel in hosts.yaml
// (never a real worker) whose fields worker.c's apply_join_state()/
// find_matched_hostconfig() already falls back to on the C side for any
// worker with no host-specific hosts.yaml entry of its own.
const defaultHostConfigName = "default"
// bug-1673: a dynamically-joined worker with no hosts.yaml entry under its
// own literal hostname never receives its own OnHostConfigUpdated call, so
// MaxSlot/HostModel/HostType/DisptWindow stayed at Go's zero-value forever
// -- decoupled from the C-side scheduling path, which already resolves the
// real value via the "default" wildcard entry. These package-level vars
// cache that entry's fields plus which hosts have their OWN explicit
// (non-default) config, so OnHostUpdated can apply the same fallback for
// display purposes that worker.c already applies for real dispatch.
var (
hostConfigMu sync.Mutex
defaultHostConfig *models.HostInfo
hostsWithExplicitConfig = map[string]bool{}
)
// resetHostConfigCache clears the cached "default" pseudo-host config and
// explicit-config tracking. Test-only: called from this package's _test.go
// store-reset helpers, so bug-1673 fallback state doesn't leak between
// tests the way store.InitStores() already prevents for the stores
// themselves.
func resetHostConfigCache() {
hostConfigMu.Lock()
defaultHostConfig = nil
hostsWithExplicitConfig = map[string]bool{}
hostConfigMu.Unlock()
}
// applyDefaultHostConfigFallback copies the cached "default" pseudo-host's
// config-sourced fields onto host, if one has been observed yet. No-op
// otherwise (nothing to fall back to).
//
// A negative MaxSlot is HOSTCONFIG_MXJ_AUTO_DETECT (bug-1556): "no explicit
// cap, use this host's real CPU count" -- the same resolution worker.c's
// apply_join_state()/find_matched_hostconfig() already performs for actual
// dispatch ("if (nslots<0) nslots = maxcpus"). Resolving it here too means
// this display fallback shows the host's real usable capacity (matching
// what dispatch will actually honor) instead of a bare "-1" that would
// leave the reporter's original "is ncpus equal slots?" question (gitea
// #40) just as unanswered as the "0" it replaces.
func applyDefaultHostConfigFallback(host *models.HostInfo) {
hostConfigMu.Lock()
def := defaultHostConfig
hostConfigMu.Unlock()
if def == nil {
return
}
host.HostModel = def.HostModel
host.HostType = def.HostType
host.MaxSlot = def.MaxSlot
if host.MaxSlot < 0 && host.NumCPUs > 0 {
host.MaxSlot = host.NumCPUs
}
host.DisptWindow = def.DisptWindow
}
func (h *DefaultHandler) OnJobCreated(job *models.JobInfo) {
store.Jobs.Put(uint64(job.Unique_JobID), *job)
DefaultBroker.Publish(Event{Type: EventJobCreated, JobID: uint64(job.Unique_JobID), Status: models.JobStateStr(job.JobStatus), Job: job})
@ -46,7 +107,10 @@ func (h *DefaultHandler) OnQueueUpdated(queue *models.QueueInfo) {
// OnHostUpdated merges worker-sourced fields into an existing host entry.
// Config-sourced fields (HostModel, HostType, MaxSlot, DisptWindow) are
// preserved from the prior entry if one exists.
// preserved from the prior entry if one exists. If this is the host's
// first-ever registration (no prior entry), fall back to the cached
// "default" pseudo-host's config (bug-1673) rather than leaving those
// fields at Go's zero-value -- mirrors the C-side wildcard fallback.
func (h *DefaultHandler) OnHostUpdated(incoming *models.HostInfo) {
merged := *incoming
if existing, ok := store.Hosts.Get(merged.HostName); ok {
@ -54,6 +118,8 @@ func (h *DefaultHandler) OnHostUpdated(incoming *models.HostInfo) {
merged.HostType = existing.HostType
merged.MaxSlot = existing.MaxSlot
merged.DisptWindow = existing.DisptWindow
} else {
applyDefaultHostConfigFallback(&merged)
}
store.Hosts.Put(merged.HostName, merged)
}
@ -61,7 +127,35 @@ func (h *DefaultHandler) OnHostUpdated(incoming *models.HostInfo) {
// OnHostConfigUpdated applies config-sourced fields onto an existing worker-registered host.
// Hosts not yet registered by a worker are skipped — config entries like "default" (which sets
// cluster-wide defaults) or hosts whose daemons haven't started must not appear in the host list.
//
// The "default" pseudo-host entry itself is cached (bug-1673) rather than
// simply discarded, then retroactively backfilled onto any already-
// registered host with no explicit config of its own -- "default" can
// arrive either before or after a given worker's own OnHostUpdated call.
func (h *DefaultHandler) OnHostConfigUpdated(incoming *models.HostInfo) {
if incoming.HostName == defaultHostConfigName {
hostConfigMu.Lock()
cp := *incoming
defaultHostConfig = &cp
hostConfigMu.Unlock()
store.Hosts.Range(func(name string, host models.HostInfo) bool {
hostConfigMu.Lock()
explicit := hostsWithExplicitConfig[name]
hostConfigMu.Unlock()
if !explicit {
applyDefaultHostConfigFallback(&host)
store.Hosts.Put(name, host)
}
return true
})
return
}
hostConfigMu.Lock()
hostsWithExplicitConfig[incoming.HostName] = true
hostConfigMu.Unlock()
existing, ok := store.Hosts.Get(incoming.HostName)
if !ok {
return
@ -96,6 +190,36 @@ func (h *DefaultHandler) OnResourceUpdated(res *models.ResourceDefinitionInfo) {
func (h *DefaultHandler) OnResourceLocationUpdated(res *models.ResourceDistributionInfo) {
key := res.ResourceName + ":" + res.Location
store.ResourceLocations.Put(key, *res)
// resource-value-verification task 4.2 (Requirements 5.1, 5.2, 5.4): trace
// the stored value at its point of application, mirroring the scheduler's
// sibling trace log added in task 4.1 (core_events.c). One typed "value"
// field carrying whichever of StringValue/NumericValue/BooleanValue is
// live for res.ValueType, so a downstream grep sees exactly what was
// stored, not every possibly-zero field on the struct.
//
// DO NOT change the "resource-location stored" message string, or the
// resource/host/value field names below, without also updating
// query_apiserver_resvals() in
// .claude/skills/xce-test-resources/run-resource-verify.sh (design.md's
// own Revalidation Triggers list this exact pair) -- that script greps
// this exact message and parses zap's console-encoder trailing JSON
// block for these field names; an unrelated logging cleanup here
// silently turns every future verification run into a false
// UNREACHABLE/timeout, indistinguishable from a real regression.
var valueField zap.Field
switch res.ValueType {
case int(models.STRING):
valueField = zap.String("value", res.StringValue)
case int(models.BOOLEAN):
valueField = zap.Bool("value", res.BooleanValue)
default:
valueField = zap.Float64("value", res.NumericValue)
}
logger.Debug("resource-location stored",
zap.String("resource", res.ResourceName),
zap.String("host", res.Location),
valueField)
}
// dispatchJobArray delivers a locally-expanded compact array push to the