feat(job-array-one-based-index): control by identity validates first and dispatches once (task 7.4)

ControlJobByID parses every selection with ParseIdentitySelection before
any store access (first rejection: 400, nothing dispatched), collects
the jobs to act on de-duplicated by unique_job_id plus one not-found
result per missing number per selection (carrying the would-be element
ID), validates each job once after the loop and dispatches each
accepted job once. foundJob gains an explicit-index flag so
<array-id>[0] matches only element 0 and never falls back to the whole
array or an ordinary job. Fixes bug-1899 (earlier selections were
re-dispatched for every later one).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-10-01 12:29:19 +00:00
parent c3962e8f61
commit 4020740208
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
4 changed files with 475 additions and 241 deletions

View file

@ -976,3 +976,261 @@ func TestJobHandler_GetJobByIDsSelections(t *testing.T) {
assert400(t, "5.14", f.getJobs("64-0-1-0"), "wider than allowed")
assert400(t, "5.5", f.getJobs(X+"[3-1]"), "reversed")
}
// ========== Control by identity (task 7.4, bug-1899) ==========
// controlItem is one accepted or failed item of a control reply.
type controlItem struct {
UniqueJobID uint64
HasUnique bool
JobID uint64
ArrayIdx int
Msg string
}
// controlReply is the data of a 202 control reply.
type controlReply struct {
Status string
Accepted []controlItem
Failed []controlItem
}
// decodeControlReply decodes a successful (202) control reply.
func decodeControlReply(t *testing.T, w *httptest.ResponseRecorder) controlReply {
t.Helper()
if w.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String())
}
dec := json.NewDecoder(bytes.NewReader(w.Body.Bytes()))
dec.UseNumber()
var raw struct {
Code int `json:"code"`
Data struct {
Status string `json:"status"`
Accepted []map[string]interface{} `json:"accepted"`
Failed []map[string]interface{} `json:"failed"`
} `json:"data"`
}
if err := dec.Decode(&raw); err != nil {
t.Fatalf("decode control reply: %v; body: %s", err, w.Body.String())
}
if raw.Code != 202 {
t.Fatalf("code = %d, want 202; body: %s", raw.Code, w.Body.String())
}
num := func(m map[string]interface{}, k string) (uint64, bool) {
n, ok := m[k].(json.Number)
if !ok {
return 0, false
}
v, err := strconv.ParseUint(n.String(), 10, 64)
if err != nil {
// job_id/unique_job_id are int64 on the wire; keep the bit pattern
s, err2 := strconv.ParseInt(n.String(), 10, 64)
if err2 != nil {
t.Fatalf("%s %q: %v", k, n, err)
}
v = uint64(s)
}
return v, true
}
conv := func(in []map[string]interface{}) []controlItem {
out := make([]controlItem, 0, len(in))
for _, m := range in {
var it controlItem
it.UniqueJobID, it.HasUnique = num(m, "unique_job_id")
it.JobID, _ = num(m, "job_id")
idx, _ := num(m, "array_idx")
it.ArrayIdx = int(idx)
it.Msg, _ = m["msg"].(string)
out = append(out, it)
}
return out
}
return controlReply{Status: raw.Data.Status, Accepted: conv(raw.Data.Accepted), Failed: conv(raw.Data.Failed)}
}
// itemIDs returns the unique_job_id of every item, in reply order.
func itemIDs(t *testing.T, items []controlItem) []uint64 {
t.Helper()
ids := make([]uint64, 0, len(items))
for _, it := range items {
if !it.HasUnique {
t.Errorf("result item without unique_job_id: %+v", it)
}
ids = append(ids, it.UniqueJobID)
}
return ids
}
// assertExactIDs checks got equals want element by element (order kept).
func assertExactIDs(t *testing.T, name string, got, want []uint64) {
t.Helper()
if len(got) != len(want) {
t.Errorf("%s: %v, want %v", name, got, want)
return
}
for i := range got {
if got[i] != want[i] {
t.Errorf("%s: %v, want %v", name, got, want)
return
}
}
}
// assertSeeded fails the test unless every id is in store.Jobs, so an
// act-on-no-job assertion cannot pass on an empty store.
func assertSeeded(t *testing.T, ids ...uint64) {
t.Helper()
for _, id := range ids {
if !store.Jobs.Has(id) {
t.Fatalf("fixture row %d missing from store.Jobs", id)
}
}
}
// controlByIDEscaped posts a control action for a selection list, escaping
// the path segment.
func (f *handlerFixture) controlByIDEscaped(jobids, action string) *httptest.ResponseRecorder {
f.t.Helper()
return f.controlByID(url.PathEscape(jobids), action)
}
// TestJobHandler_ControlTwoSelectionsDispatchOnce (bug-1899): a request with
// two selections dispatches each selected element exactly once and lists
// each once; the same holds for two ordinary jobs.
func TestJobHandler_ControlTwoSelectionsDispatchOnce(t *testing.T) {
f := newHandlerFixture(t)
X := handlerTestArrayBase + 30<<15
Y := handlerTestArrayBase + 31<<15
f.seedArrayElements(X, "foo", "RUN", 1, 2)
f.seedArrayElements(Y, "bar", "RUN", 1, 2)
A := handlerTestOrdinaryBase + 1
B := handlerTestOrdinaryBase + 2
f.seedOrdinaryJob(A, "a", "RUN")
f.seedOrdinaryJob(B, "b", "RUN")
x1, y2 := backend.MakeElementID(X, 1), backend.MakeElementID(Y, 2)
r := decodeControlReply(t, f.controlByIDEscaped(jobIDString(X)+"[1],"+jobIDString(Y)+"[2]", "kill"))
assertExactIDs(t, "X[1],Y[2] dispatched", f.dispatchedIDs(), []uint64{x1, y2})
assertExactIDs(t, "X[1],Y[2] accepted", itemIDs(t, r.Accepted), []uint64{x1, y2})
if len(r.Failed) != 0 {
t.Errorf("X[1],Y[2] failed items: %+v", r.Failed)
}
f.dispatched = nil
r = decodeControlReply(t, f.controlByIDEscaped(jobIDString(A)+","+jobIDString(B), "kill"))
assertExactIDs(t, "A,B dispatched", f.dispatchedIDs(), []uint64{A, B})
assertExactIDs(t, "A,B accepted", itemIDs(t, r.Accepted), []uint64{A, B})
// a job selected by two selections is acted on once (de-duplicated by
// unique_job_id)
f.dispatched = nil
x2 := backend.MakeElementID(X, 2)
r = decodeControlReply(t, f.controlByIDEscaped(jobIDString(X)+","+jobIDString(X)+"[2]", "kill"))
assertExactIDs(t, "X,X[2] dispatched", f.dispatchedIDs(), []uint64{x1, x2})
assertExactIDs(t, "X,X[2] accepted", itemIDs(t, r.Accepted), []uint64{x1, x2})
}
// TestJobHandler_ControlRangeReportsMissing (5.3): X[1-3] with element 2
// missing dispatches elements 1 and 3 and reports 2 as not found, carrying
// its would-be element ID.
func TestJobHandler_ControlRangeReportsMissing(t *testing.T) {
f := newHandlerFixture(t)
X := handlerTestArrayBase + 32<<15
f.seedArrayElements(X, "foo", "RUN", 1, 3, 4)
x1, x2, x3 := backend.MakeElementID(X, 1), backend.MakeElementID(X, 2), backend.MakeElementID(X, 3)
r := decodeControlReply(t, f.controlByIDEscaped(jobIDString(X)+"[1-3]", "kill"))
assertExactIDs(t, "dispatched", f.dispatchedIDs(), []uint64{x1, x3})
assertExactIDs(t, "accepted", itemIDs(t, r.Accepted), []uint64{x1, x3})
assertExactIDs(t, "failed", itemIDs(t, r.Failed), []uint64{x2})
if len(r.Failed) == 1 {
it := r.Failed[0]
if it.Msg != "job not found" || it.JobID != X || it.ArrayIdx != 2 {
t.Errorf("not-found item = %+v, want job_id %d, array_idx 2, msg \"job not found\"", it, X)
}
}
if r.Status != "partially_accepted" {
t.Errorf("status = %q, want partially_accepted", r.Status)
}
// a range on an identity no array has: every number not found, no job
f.dispatched = nil
Z := handlerTestArrayBase + 33<<15
r = decodeControlReply(t, f.controlByIDEscaped(jobIDString(Z)+"[1-2]", "kill"))
if got := f.dispatchedIDs(); len(got) != 0 {
t.Errorf("Z[1-2] dispatched %v, want none", got)
}
assertExactIDs(t, "Z[1-2] failed", itemIDs(t, r.Failed),
[]uint64{backend.MakeElementID(Z, 1), backend.MakeElementID(Z, 2)})
}
// TestJobHandler_ControlElementZeroMissing (5.4): X[0] with no element 0
// acts on no job -- not on the array, not on an ordinary job with identity
// X -- and reports element 0 as not found.
func TestJobHandler_ControlElementZeroMissing(t *testing.T) {
f := newHandlerFixture(t)
X := handlerTestArrayBase + 34<<15
elems := f.seedArrayElements(X, "foo", "RUN", 1, 2)
// an ordinary job sharing the number X (array_idx 0)
f.seedOrdinaryJob(X, "ordinary", "RUN")
assertSeeded(t, uint64(elems[0].Unique_JobID), uint64(elems[1].Unique_JobID), X)
for _, sel := range []string{"[0]", "[0-0]"} {
f.dispatched = nil
r := decodeControlReply(t, f.controlByIDEscaped(jobIDString(X)+sel, "kill"))
if got := f.dispatchedIDs(); len(got) != 0 {
t.Errorf("X%s dispatched %v, want none", sel, got)
}
if len(r.Accepted) != 0 {
t.Errorf("X%s accepted %+v, want none", sel, r.Accepted)
}
assertExactIDs(t, "X"+sel+" failed", itemIDs(t, r.Failed), []uint64{backend.MakeElementID(X, 0)})
if len(r.Failed) == 1 && r.Failed[0].Msg != "job not found" {
t.Errorf("X%s failed msg = %q, want \"job not found\"", sel, r.Failed[0].Msg)
}
}
}
// TestJobHandler_ControlPreUpgradeElementZero (8.2, 8.3): a pre-upgrade
// element 0 is dispatched alone, also when the same request selects another
// element of that array, and is reported with its own unique_job_id.
func TestJobHandler_ControlPreUpgradeElementZero(t *testing.T) {
f := newHandlerFixture(t)
X := handlerTestArrayBase + 35<<15
f.seedArrayElements(X, "old", "RUN", 0, 1, 2)
x0, x2 := backend.MakeElementID(X, 0), backend.MakeElementID(X, 2)
r := decodeControlReply(t, f.controlByIDEscaped(jobIDString(X)+"[0]", "kill"))
assertExactIDs(t, "X[0] dispatched", f.dispatchedIDs(), []uint64{x0})
assertExactIDs(t, "X[0] accepted", itemIDs(t, r.Accepted), []uint64{x0})
f.dispatched = nil
r = decodeControlReply(t, f.controlByIDEscaped(jobIDString(X)+"[0],"+jobIDString(X)+"[2]", "kill"))
assertExactIDs(t, "X[0],X[2] dispatched", f.dispatchedIDs(), []uint64{x0, x2})
assertExactIDs(t, "X[0],X[2] accepted", itemIDs(t, r.Accepted), []uint64{x0, x2})
}
// TestJobHandler_ControlValidThenInvalid (Terms): a valid selection followed
// by an invalid one rejects the whole command with 400 and dispatches
// nothing, without "not found" items.
func TestJobHandler_ControlValidThenInvalid(t *testing.T) {
f := newHandlerFixture(t)
X := handlerTestArrayBase + 36<<15
elems := f.seedArrayElements(X, "foo", "RUN", 1, 2)
assertSeeded(t, uint64(elems[0].Unique_JobID))
cases := []struct{ sel, want string }{
{jobIDString(X) + "[1]," + jobIDString(X) + "[40000]", "element numbers must be at most"},
{jobIDString(X) + "[1]," + jobIDString(X) + "[3-1]", "reversed"},
{jobIDString(X) + "[1]," + jobIDString(X) + "[a]", "invalid job identity or element selection"},
{jobIDString(X) + "[1]," + jobIDString(uint64(elems[1].Unique_JobID)), "must be named as"},
}
for _, c := range cases {
f.dispatched = nil
assert400(t, c.sel, f.controlByIDEscaped(c.sel, "kill"), c.want)
if got := f.dispatchedIDs(); len(got) != 0 {
t.Errorf("%s: dispatched %v, want none", c.sel, got)
}
}
}