feat: P1-1 handle resource value push via OPCode_notify
Add OPCode_notify handler in go_push_handler top-level switch (not inside handlePush) to receive resource value updates from the controller via xce_notify_data_t/schedUpdateResvals. - Add 8 CGO union accessors for xce_val_t (which, i32, i64, u32, u64, fval, str, bool) since CGO cannot access union fields - Add handleNotifyData() dispatching NotifyData_data_schedUpdateResvals events, iterating xce_resval_list_t and calling cResvalToDistribution - Add cResvalToDistribution in convert.go with 7 separate switch cases for Value_u_* union discriminants (no combining i32/i64) - Add rawResvalData + rawResvalToDistribution pure-Go version in convert_pure.go for testability without CGO - Add 10 TDD tests covering all 7 value types, nil val guard, unknown discriminant, and boolean false - All 42 backend tests passing
This commit is contained in:
parent
dc029a6d14
commit
bc3103a6d5
5 changed files with 365 additions and 23 deletions
|
|
@ -48,6 +48,16 @@ static void jobctrl_set_siginfo(xce_jobctrl_t *ctrl, xce_signal_t *sig) {
|
|||
static void jobctrl_set_ckptoption(xce_jobctrl_t *ctrl, xce_ckpt_options_t *ckpt) {
|
||||
ctrl->u.ckptoption = ckpt;
|
||||
}
|
||||
|
||||
// xce_val_t union accessors — CGO cannot access C union fields by name.
|
||||
static int xce_val_which(xce_val_t *v) { return v->which; }
|
||||
static int32_t xce_val_i32(xce_val_t *v) { return v->u.ivalue32; }
|
||||
static int64_t xce_val_i64(xce_val_t *v) { return v->u.ivalue64; }
|
||||
static uint32_t xce_val_u32(xce_val_t *v) { return v->u.uvalue32; }
|
||||
static uint64_t xce_val_u64(xce_val_t *v) { return v->u.uvalue64; }
|
||||
static double xce_val_fval(xce_val_t *v) { return v->u.fvalue; }
|
||||
static char* xce_val_str(xce_val_t *v) { return v->u.svalue; }
|
||||
static int xce_val_bool(xce_val_t *v) { return v->u.bvalue; }
|
||||
*/
|
||||
import "C"
|
||||
|
||||
|
|
@ -160,6 +170,15 @@ func go_push_handler(req *C.xce_request_t, _ unsafe.Pointer) C.int {
|
|||
switch req.opcode {
|
||||
case C.OPCode_push:
|
||||
return handlePush(req)
|
||||
case C.OPCode_notify:
|
||||
if req.body_which != C.Request_body_notifymsg {
|
||||
return 0
|
||||
}
|
||||
nd := (*C.xce_notify_data_t)(unsafe.Pointer(C.xce_request_body(req)))
|
||||
if nd == nil {
|
||||
return 0
|
||||
}
|
||||
return handleNotifyData(nd)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
|
@ -238,6 +257,36 @@ func handleConfigPush(cfgdata *C.xce_configdata_t) C.int {
|
|||
return 0
|
||||
}
|
||||
|
||||
// handleNotifyData processes an OPCode_notify request.
|
||||
// xce_notify_data_t uses 'event' (not 'data_which') as its union discriminant.
|
||||
func handleNotifyData(nd *C.xce_notify_data_t) C.int {
|
||||
if nd == nil {
|
||||
return 0
|
||||
}
|
||||
switch nd.event {
|
||||
case C.NotifyData_data_schedUpdateResvals:
|
||||
rvl := (*C.xce_resval_list_t)(C.xce_notify_data_ptr(nd))
|
||||
if rvl == nil {
|
||||
return 0
|
||||
}
|
||||
n := int(rvl.count)
|
||||
if n == 0 || rvl.vals == nil {
|
||||
return 0
|
||||
}
|
||||
vals := unsafe.Slice(rvl.vals, n)
|
||||
for _, vp := range vals {
|
||||
if vp == nil {
|
||||
continue
|
||||
}
|
||||
rd := cResvalToDistribution(vp)
|
||||
if rd != nil {
|
||||
gHandler.OnResourceLocationUpdated(rd)
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// SubmitJob sends a job submission to the backend via xcecli_jobsubmit.
|
||||
// arraySize=1 for normal jobs, >1 for array jobs.
|
||||
// The store is updated only when the backend pushes the resulting OnJobCreated
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ import "C"
|
|||
|
||||
import (
|
||||
"apiserver/models"
|
||||
logger "apiserver/utils"
|
||||
"unsafe"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// goString safely converts a C string to a Go string, handling nil.
|
||||
|
|
@ -250,3 +253,52 @@ func freeJobSubmit(js *C.xce_job_submit_t, sd *C.job_submit_data_t, si *C.job_st
|
|||
C.free(unsafe.Pointer(js))
|
||||
}
|
||||
}
|
||||
|
||||
// cResvalToDistribution converts a C xce_resval_t pointer to a Go ResourceDistributionInfo.
|
||||
// Returns nil if vp.val is NULL (nil guard prevents CGO panic).
|
||||
// vp.val is a *C.xce_val_t pointer field — pass directly, no &.
|
||||
func cResvalToDistribution(vp *C.xce_resval_t) *models.ResourceDistributionInfo {
|
||||
rd := &models.ResourceDistributionInfo{
|
||||
ResourceName: C.GoString(vp.name),
|
||||
Location: C.GoString(vp.host),
|
||||
IsDynamic: true, // resource values pushed from workers are always dynamic
|
||||
}
|
||||
// Guard: vp.val may be NULL if the controller sends a resval with no value set.
|
||||
// Dereferencing NULL in xce_val_which() would segfault in C.
|
||||
if vp.val == nil {
|
||||
logger.Warn("resval with nil value, skipping",
|
||||
zap.String("resource", rd.ResourceName),
|
||||
zap.String("host", rd.Location))
|
||||
return nil
|
||||
}
|
||||
switch C.xce_val_which(vp.val) {
|
||||
case C.Value_u_ivalue32:
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = float64(C.xce_val_i32(vp.val))
|
||||
case C.Value_u_ivalue64:
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = float64(C.xce_val_i64(vp.val))
|
||||
case C.Value_u_uvalue32:
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = float64(C.xce_val_u32(vp.val))
|
||||
case C.Value_u_uvalue64:
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = float64(C.xce_val_u64(vp.val))
|
||||
case C.Value_u_fvalue:
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = C.xce_val_fval(vp.val) // C.double → float64, no explicit cast needed
|
||||
case C.Value_u_svalue:
|
||||
rd.ValueType = int(models.STRING)
|
||||
rd.StringValue = C.GoString(C.xce_val_str(vp.val))
|
||||
case C.Value_u_bvalue:
|
||||
rd.ValueType = int(models.BOOLEAN)
|
||||
rd.BooleanValue = C.xce_val_bool(vp.val) != 0
|
||||
default:
|
||||
logger.Warn("unknown xce_val_t discriminant, skipping",
|
||||
zap.Int("which", int(C.xce_val_which(vp.val))),
|
||||
zap.String("resource", rd.ResourceName),
|
||||
zap.String("host", rd.Location))
|
||||
return nil
|
||||
}
|
||||
return rd
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,16 +13,16 @@ import (
|
|||
const (
|
||||
nTagBits = 2
|
||||
tagShift = 62
|
||||
nClusterIDBits = 6
|
||||
clusterIDShift = 56
|
||||
nApiserverIDBits = 4
|
||||
apiserverIDShift = 52
|
||||
nTimestampBits = 32
|
||||
timestampShift = 20
|
||||
nArrayIdxBits = 15
|
||||
sequenceHighShift = 15 // mirrors SEQUENCE_HIGH_SHIFT in config.h
|
||||
nSequenceBits = 30 // mirrors N_SEQUENCE_BITS in config.h
|
||||
tagJobArray = 0x01 // mirrors TAG_JOB_ARRAY in config.h
|
||||
nClusterIDBits = 6
|
||||
clusterIDShift = 56
|
||||
nApiserverIDBits = 4
|
||||
apiserverIDShift = 52
|
||||
nTimestampBits = 32
|
||||
timestampShift = 20
|
||||
nArrayIdxBits = 15
|
||||
sequenceHighShift = 15 // mirrors SEQUENCE_HIGH_SHIFT in config.h
|
||||
nSequenceBits = 30 // mirrors N_SEQUENCE_BITS in config.h
|
||||
tagJobArray = 0x01 // mirrors TAG_JOB_ARRAY in config.h
|
||||
)
|
||||
|
||||
// extractBits extracts a bit field from id at the given shift position.
|
||||
|
|
@ -260,14 +260,70 @@ func rawHostConfigToModel(r rawHostConfigData) *models.HostInfo {
|
|||
}
|
||||
}
|
||||
|
||||
// rawResvalData mirrors the C xce_resval_t fields used by the Go model.
|
||||
// IsDynamic is always true for push-sourced values (controller sends live measurements).
|
||||
type rawResvalData struct {
|
||||
name string
|
||||
host string
|
||||
valWhich int // mirrors C xce_val_t.which (Value_u_* discriminant)
|
||||
i32 int32
|
||||
i64 int64
|
||||
u32 uint32
|
||||
u64 uint64
|
||||
fval float64
|
||||
sval string
|
||||
bval bool
|
||||
valNil bool // true when the C xce_val_t* was NULL
|
||||
}
|
||||
|
||||
// rawResvalToDistribution maps a rawResvalData to a models.ResourceDistributionInfo.
|
||||
// Returns nil when valNil is true (C val pointer was NULL) or when valWhich
|
||||
// is an unknown discriminant — callers should skip nil results.
|
||||
func rawResvalToDistribution(r rawResvalData) *models.ResourceDistributionInfo {
|
||||
rd := &models.ResourceDistributionInfo{
|
||||
ResourceName: r.name,
|
||||
Location: r.host,
|
||||
IsDynamic: true, // resource values pushed from workers are always dynamic
|
||||
}
|
||||
if r.valNil {
|
||||
return nil
|
||||
}
|
||||
switch r.valWhich {
|
||||
case 0: // Value_u_ivalue32
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = float64(r.i32)
|
||||
case 2: // Value_u_ivalue64
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = float64(r.i64)
|
||||
case 1: // Value_u_uvalue32
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = float64(r.u32)
|
||||
case 3: // Value_u_uvalue64
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = float64(r.u64)
|
||||
case 5: // Value_u_fvalue
|
||||
rd.ValueType = int(models.NUMERIC)
|
||||
rd.NumericValue = r.fval
|
||||
case 4: // Value_u_svalue
|
||||
rd.ValueType = int(models.STRING)
|
||||
rd.StringValue = r.sval
|
||||
case 6: // Value_u_bvalue
|
||||
rd.ValueType = int(models.BOOLEAN)
|
||||
rd.BooleanValue = r.bval
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return rd
|
||||
}
|
||||
|
||||
// rawClusterData mirrors the C cluster_config_t fields used by the Go model.
|
||||
// Only config-sourced fields are included (heartbeat_interval, vcpus, inactive_heartbeat_count).
|
||||
// Built-in/sourced fields (Version, Status, ControllerName, etc.) come from LoadBuiltinData
|
||||
// and are not part of the config push conversion.
|
||||
type rawClusterData struct {
|
||||
name string
|
||||
heartbeatInterval int
|
||||
vcpus int
|
||||
heartbeatInterval int
|
||||
vcpus int
|
||||
inactiveHeartbeatCount int
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -497,6 +497,188 @@ func TestRawHostConfigToModel(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_FloatValue(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "r1m",
|
||||
host: "node01",
|
||||
valWhich: 5, // Value_u_fvalue
|
||||
fval: 3.14,
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil result for fvalue")
|
||||
}
|
||||
if got.ResourceName != "r1m" {
|
||||
t.Errorf("ResourceName: got %q, want %q", got.ResourceName, "r1m")
|
||||
}
|
||||
if got.Location != "node01" {
|
||||
t.Errorf("Location: got %q, want %q", got.Location, "node01")
|
||||
}
|
||||
if !got.IsDynamic {
|
||||
t.Errorf("IsDynamic: got false, want true")
|
||||
}
|
||||
if got.ValueType != int(models.NUMERIC) {
|
||||
t.Errorf("ValueType: got %d, want %d (NUMERIC)", got.ValueType, models.NUMERIC)
|
||||
}
|
||||
if got.NumericValue != 3.14 {
|
||||
t.Errorf("NumericValue: got %f, want 3.14", got.NumericValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_StringValue(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "type",
|
||||
host: "node02",
|
||||
valWhich: 4, // Value_u_svalue
|
||||
sval: "LINUX64",
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil result for svalue")
|
||||
}
|
||||
if got.ValueType != int(models.STRING) {
|
||||
t.Errorf("ValueType: got %d, want %d (STRING)", got.ValueType, models.STRING)
|
||||
}
|
||||
if got.StringValue != "LINUX64" {
|
||||
t.Errorf("StringValue: got %q, want %q", got.StringValue, "LINUX64")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_BooleanValue(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "available",
|
||||
host: "node03",
|
||||
valWhich: 6, // Value_u_bvalue
|
||||
bval: true,
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil result for bvalue")
|
||||
}
|
||||
if got.ValueType != int(models.BOOLEAN) {
|
||||
t.Errorf("ValueType: got %d, want %d (BOOLEAN)", got.ValueType, models.BOOLEAN)
|
||||
}
|
||||
if !got.BooleanValue {
|
||||
t.Errorf("BooleanValue: got false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_Int32Value(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "slots",
|
||||
host: "node04",
|
||||
valWhich: 0, // Value_u_ivalue32
|
||||
i32: 16,
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil result for ivalue32")
|
||||
}
|
||||
if got.ValueType != int(models.NUMERIC) {
|
||||
t.Errorf("ValueType: got %d, want %d (NUMERIC)", got.ValueType, models.NUMERIC)
|
||||
}
|
||||
if got.NumericValue != 16 {
|
||||
t.Errorf("NumericValue: got %f, want 16", got.NumericValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_Int64Value(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "bigcounter",
|
||||
host: "node05",
|
||||
valWhich: 2, // Value_u_ivalue64
|
||||
i64: 9223372036854775807, // max int64
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil result for ivalue64")
|
||||
}
|
||||
if got.ValueType != int(models.NUMERIC) {
|
||||
t.Errorf("ValueType: got %d, want %d (NUMERIC)", got.ValueType, models.NUMERIC)
|
||||
}
|
||||
if got.NumericValue != float64(int64(9223372036854775807)) {
|
||||
t.Errorf("NumericValue: got %f, want max int64 as float64", got.NumericValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_Uint32Value(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "uint32res",
|
||||
host: "node06",
|
||||
valWhich: 1, // Value_u_uvalue32
|
||||
u32: 4294967295, // max uint32
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil result for uvalue32")
|
||||
}
|
||||
if got.ValueType != int(models.NUMERIC) {
|
||||
t.Errorf("ValueType: got %d, want %d", got.ValueType, models.NUMERIC)
|
||||
}
|
||||
if got.NumericValue != float64(uint32(4294967295)) {
|
||||
t.Errorf("NumericValue: got %f, want 4294967295", got.NumericValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_Uint64Value(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "uint64res",
|
||||
host: "node07",
|
||||
valWhich: 3, // Value_u_uvalue64
|
||||
u64: 18446744073709551615, // max uint64
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil result for uvalue64")
|
||||
}
|
||||
if got.ValueType != int(models.NUMERIC) {
|
||||
t.Errorf("ValueType: got %d, want %d", got.ValueType, models.NUMERIC)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_NilVal(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "nilres",
|
||||
host: "node08",
|
||||
valNil: true,
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got != nil {
|
||||
t.Errorf("expected nil for nil val, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_UnknownDiscriminant(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "unkres",
|
||||
host: "node09",
|
||||
valWhich: 99, // unknown
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got != nil {
|
||||
t.Errorf("expected nil for unknown discriminant, got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawResvalToDistribution_BooleanFalse(t *testing.T) {
|
||||
r := rawResvalData{
|
||||
name: "available",
|
||||
host: "node10",
|
||||
valWhich: 6, // Value_u_bvalue
|
||||
bval: false,
|
||||
}
|
||||
got := rawResvalToDistribution(r)
|
||||
if got == nil {
|
||||
t.Fatal("expected non-nil result for bvalue false")
|
||||
}
|
||||
if got.ValueType != int(models.BOOLEAN) {
|
||||
t.Errorf("ValueType: got %d, want %d (BOOLEAN)", got.ValueType, models.BOOLEAN)
|
||||
}
|
||||
if got.BooleanValue {
|
||||
t.Errorf("BooleanValue: got true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRawClusterToModel(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
@ -506,9 +688,9 @@ func TestRawClusterToModel(t *testing.T) {
|
|||
{
|
||||
name: "normal values",
|
||||
raw: rawClusterData{
|
||||
name: "mycluster",
|
||||
heartbeatInterval: 30,
|
||||
vcpus: 16,
|
||||
name: "mycluster",
|
||||
heartbeatInterval: 30,
|
||||
vcpus: 16,
|
||||
inactiveHeartbeatCount: 3,
|
||||
},
|
||||
want: models.ClusterInfo{
|
||||
|
|
@ -536,9 +718,9 @@ func TestRawClusterToModel(t *testing.T) {
|
|||
{
|
||||
name: "built-in fields remain zero",
|
||||
raw: rawClusterData{
|
||||
name: "test",
|
||||
heartbeatInterval: 45,
|
||||
vcpus: 32,
|
||||
name: "test",
|
||||
heartbeatInterval: 45,
|
||||
vcpus: 32,
|
||||
inactiveHeartbeatCount: 2,
|
||||
},
|
||||
want: models.ClusterInfo{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue