Skip to content

Commit 5c90921

Browse files
authored
scheduler: add reconciler annotations to completed evals (#26188)
The output of the reconciler stage of scheduling is only visible via debug-level logs, typically accessible only to the cluster admin. We can give job authors better ability to understand what's happening to their jobs if we expose this information to them in the `eval status` command. Add the reconciler's desired updates to the evaluation struct so it can be exposed in the API. This increases the size of evals by roughly 15% in the state store, or a bit more when there are preemptions (but we expect this will be a small minority of evals). Ref: https://hashicorp.atlassian.net/browse/NMD-818 Fixes: #15564
1 parent 60a953c commit 5c90921

File tree

11 files changed

+144
-40
lines changed

11 files changed

+144
-40
lines changed

.changelog/26188.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:improvement
2+
scheduler: Add reconciler annotations to the output of the `eval status` command
3+
```

api/evaluations.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ type Evaluation struct {
116116
BlockedEval string
117117
RelatedEvals []*EvaluationStub
118118
FailedTGAllocs map[string]*AllocationMetric
119+
PlanAnnotations *PlanAnnotations
119120
ClassEligibility map[string]bool
120121
EscapedComputedClass bool
121122
QuotaLimitReached string

command/eval_status.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/hashicorp/nomad/api"
1313
"github.com/hashicorp/nomad/api/contexts"
1414
"github.com/posener/complete"
15+
"github.com/ryanuber/columnize"
1516
)
1617

1718
type EvalStatusCommand struct {
@@ -257,6 +258,36 @@ func (c *EvalStatusCommand) formatEvalStatus(eval *api.Evaluation, placedAllocs
257258
c.Ui.Output(c.Colorize().Color("\n[bold]Related Evaluations[reset]"))
258259
c.Ui.Output(formatRelatedEvalStubs(eval.RelatedEvals, length))
259260
}
261+
if eval.PlanAnnotations != nil {
262+
263+
if len(eval.PlanAnnotations.DesiredTGUpdates) > 0 {
264+
c.Ui.Output(c.Colorize().Color("\n[bold]Reconciler Annotations[reset]"))
265+
annotations := make([]string, len(eval.PlanAnnotations.DesiredTGUpdates)+1)
266+
annotations[0] = "Task Group|Ignore|Place|Stop|Migrate|InPlace|Destructive|Canary|Preemptions"
267+
i := 1
268+
for tg, updates := range eval.PlanAnnotations.DesiredTGUpdates {
269+
annotations[i] = fmt.Sprintf("%s|%d|%d|%d|%d|%d|%d|%d|%d",
270+
tg,
271+
updates.Ignore,
272+
updates.Place,
273+
updates.Stop,
274+
updates.Migrate,
275+
updates.InPlaceUpdate,
276+
updates.DestructiveUpdate,
277+
updates.Canary,
278+
updates.Preemptions,
279+
)
280+
i++
281+
}
282+
c.Ui.Output(columnize.SimpleFormat(annotations))
283+
}
284+
285+
if len(eval.PlanAnnotations.PreemptedAllocs) > 0 {
286+
c.Ui.Output(c.Colorize().Color("\n[bold]Preempted Allocations[reset]"))
287+
allocsOut := formatPreemptedAllocListStubs(eval.PlanAnnotations.PreemptedAllocs, length)
288+
c.Ui.Output(allocsOut)
289+
}
290+
}
260291
if len(placedAllocs) > 0 {
261292
c.Ui.Output(c.Colorize().Color("\n[bold]Placed Allocations[reset]"))
262293
allocsOut := formatAllocListStubs(placedAllocs, false, length)
@@ -323,3 +354,27 @@ func formatRelatedEvalStubs(evals []*api.EvaluationStub, length int) string {
323354

324355
return formatList(out)
325356
}
357+
358+
// formatPreemptedAllocListStubs formats alloc stubs but assumes they don't all
359+
// belong to the same job, as is the case when allocs are preempted by another
360+
// job
361+
func formatPreemptedAllocListStubs(stubs []*api.AllocationListStub, uuidLength int) string {
362+
allocs := make([]string, len(stubs)+1)
363+
allocs[0] = "ID|Job ID|Node ID|Task Group|Version|Desired|Status|Created|Modified"
364+
for i, alloc := range stubs {
365+
now := time.Now()
366+
createTimePretty := prettyTimeDiff(time.Unix(0, alloc.CreateTime), now)
367+
modTimePretty := prettyTimeDiff(time.Unix(0, alloc.ModifyTime), now)
368+
allocs[i+1] = fmt.Sprintf("%s|%s|%s|%s|%d|%s|%s|%s|%s",
369+
limit(alloc.ID, uuidLength),
370+
alloc.JobID,
371+
limit(alloc.NodeID, uuidLength),
372+
alloc.TaskGroup,
373+
alloc.JobVersion,
374+
alloc.DesiredStatus,
375+
alloc.ClientStatus,
376+
createTimePretty,
377+
modTimePretty)
378+
}
379+
return formatList(allocs)
380+
}

command/eval_status_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,22 @@ func TestEvalStatusCommand_Format(t *testing.T) {
150150
CoalescedFailures: 0,
151151
ScoreMetaData: []*api.NodeScoreMeta{},
152152
}},
153+
PlanAnnotations: &api.PlanAnnotations{
154+
DesiredTGUpdates: map[string]*api.DesiredUpdates{"web": {Place: 10}},
155+
PreemptedAllocs: []*api.AllocationListStub{
156+
{
157+
ID: uuid.Generate(),
158+
JobID: "another",
159+
NodeID: uuid.Generate(),
160+
TaskGroup: "db",
161+
DesiredStatus: "evict",
162+
JobVersion: 3,
163+
ClientStatus: "complete",
164+
CreateTime: now.Add(-10 * time.Minute).UnixNano(),
165+
ModifyTime: now.Add(-2 * time.Second).UnixNano(),
166+
},
167+
},
168+
},
153169
ClassEligibility: map[string]bool{},
154170
EscapedComputedClass: true,
155171
QuotaLimitReached: "",
@@ -207,4 +223,6 @@ Task Group "web" (failed to place 1 allocation):
207223

208224
must.StrContains(t, out, `Related Evaluations`)
209225
must.StrContains(t, out, `Placed Allocations`)
226+
must.StrContains(t, out, `Reconciler Annotations`)
227+
must.StrContains(t, out, `Preempted Allocations`)
210228
}

nomad/structs/structs.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12428,6 +12428,9 @@ type Evaluation struct {
1242812428
// to determine the cause.
1242912429
FailedTGAllocs map[string]*AllocMetric
1243012430

12431+
// PlanAnnotations represents the output of the reconciliation step.
12432+
PlanAnnotations *PlanAnnotations
12433+
1243112434
// ClassEligibility tracks computed node classes that have been explicitly
1243212435
// marked as eligible or ineligible.
1243312436
ClassEligibility map[string]bool

scheduler/generic_sched.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@ type GenericScheduler struct {
7373

7474
deployment *structs.Deployment
7575

76-
blocked *structs.Evaluation
77-
failedTGAllocs map[string]*structs.AllocMetric
78-
queuedAllocs map[string]int
76+
blocked *structs.Evaluation
77+
failedTGAllocs map[string]*structs.AllocMetric
78+
queuedAllocs map[string]int
79+
planAnnotations *structs.PlanAnnotations
7980
}
8081

8182
// NewServiceScheduler is a factory function to instantiate a new service scheduler
@@ -132,7 +133,7 @@ func (s *GenericScheduler) Process(eval *structs.Evaluation) (err error) {
132133
desc := fmt.Sprintf("scheduler cannot handle '%s' evaluation reason",
133134
eval.TriggeredBy)
134135
return setStatus(s.logger, s.planner, s.eval, nil, s.blocked,
135-
s.failedTGAllocs, structs.EvalStatusFailed, desc, s.queuedAllocs,
136+
s.failedTGAllocs, s.planAnnotations, structs.EvalStatusFailed, desc, s.queuedAllocs,
136137
s.deployment.GetID())
137138
}
138139

@@ -151,7 +152,7 @@ func (s *GenericScheduler) Process(eval *structs.Evaluation) (err error) {
151152
mErr.Errors = append(mErr.Errors, err)
152153
}
153154
if err := setStatus(s.logger, s.planner, s.eval, nil, s.blocked,
154-
s.failedTGAllocs, statusErr.EvalStatus, err.Error(),
155+
s.failedTGAllocs, s.planAnnotations, statusErr.EvalStatus, err.Error(),
155156
s.queuedAllocs, s.deployment.GetID()); err != nil {
156157
mErr.Errors = append(mErr.Errors, err)
157158
}
@@ -173,7 +174,7 @@ func (s *GenericScheduler) Process(eval *structs.Evaluation) (err error) {
173174

174175
// Update the status to complete
175176
return setStatus(s.logger, s.planner, s.eval, nil, s.blocked,
176-
s.failedTGAllocs, structs.EvalStatusComplete, "", s.queuedAllocs,
177+
s.failedTGAllocs, s.planAnnotations, structs.EvalStatusComplete, "", s.queuedAllocs,
177178
s.deployment.GetID())
178179
}
179180

@@ -285,6 +286,9 @@ func (s *GenericScheduler) process() (bool, error) {
285286
}
286287

287288
// Submit the plan and store the results.
289+
if s.eval.AnnotatePlan {
290+
s.plan.Annotations = s.planAnnotations
291+
}
288292
result, newState, err := s.planner.SubmitPlan(s.plan)
289293
s.planResult = result
290294
if err != nil {
@@ -359,10 +363,8 @@ func (s *GenericScheduler) computeJobAllocs() error {
359363
s.logger.Debug("reconciled current state with desired state", result.Fields()...)
360364
}
361365

362-
if s.eval.AnnotatePlan {
363-
s.plan.Annotations = &structs.PlanAnnotations{
364-
DesiredTGUpdates: result.DesiredTGUpdates,
365-
}
366+
s.planAnnotations = &structs.PlanAnnotations{
367+
DesiredTGUpdates: result.DesiredTGUpdates,
366368
}
367369

368370
// Add the deployment changes to the plan
@@ -912,10 +914,10 @@ func (s *GenericScheduler) handlePreemptions(option *feasible.RankedNode, alloc
912914
s.plan.AppendPreemptedAlloc(stop, alloc.ID)
913915
preemptedAllocIDs = append(preemptedAllocIDs, stop.ID)
914916

915-
if s.eval.AnnotatePlan && s.plan.Annotations != nil {
916-
s.plan.Annotations.PreemptedAllocs = append(s.plan.Annotations.PreemptedAllocs, stop.Stub(nil))
917-
if s.plan.Annotations.DesiredTGUpdates != nil {
918-
desired := s.plan.Annotations.DesiredTGUpdates[missing.TaskGroup().Name]
917+
if s.planAnnotations != nil {
918+
s.planAnnotations.PreemptedAllocs = append(s.planAnnotations.PreemptedAllocs, stop.Stub(nil))
919+
if s.planAnnotations.DesiredTGUpdates != nil {
920+
desired := s.planAnnotations.DesiredTGUpdates[missing.TaskGroup().Name]
919921
desired.Preemptions += 1
920922
}
921923
}

scheduler/generic_sched_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,12 @@ func TestServiceSched_JobRegister(t *testing.T) {
6464
}
6565
plan := h.Plans[0]
6666

67-
// Ensure the plan doesn't have annotations.
67+
// Ensure the plan doesn't have annotations but the eval does
6868
if plan.Annotations != nil {
6969
t.Fatalf("expected no annotations")
7070
}
71+
must.SliceNotEmpty(t, h.Evals)
72+
must.Eq(t, 10, h.Evals[0].PlanAnnotations.DesiredTGUpdates["web"].Place)
7173

7274
// Ensure the eval has no spawned blocked eval
7375
if len(h.CreateEvals) != 0 {

scheduler/scheduler_system.go

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ type SystemScheduler struct {
5252
limitReached bool
5353
nextEval *structs.Evaluation
5454

55-
failedTGAllocs map[string]*structs.AllocMetric
56-
queuedAllocs map[string]int
55+
failedTGAllocs map[string]*structs.AllocMetric
56+
queuedAllocs map[string]int
57+
planAnnotations *structs.PlanAnnotations
5758
}
5859

5960
// NewSystemScheduler is a factory function to instantiate a new system
@@ -97,7 +98,8 @@ func (s *SystemScheduler) Process(eval *structs.Evaluation) (err error) {
9798
// Verify the evaluation trigger reason is understood
9899
if !s.canHandle(eval.TriggeredBy) {
99100
desc := fmt.Sprintf("scheduler cannot handle '%s' evaluation reason", eval.TriggeredBy)
100-
return setStatus(s.logger, s.planner, s.eval, s.nextEval, nil, s.failedTGAllocs, structs.EvalStatusFailed, desc,
101+
return setStatus(s.logger, s.planner, s.eval, s.nextEval, nil,
102+
s.failedTGAllocs, s.planAnnotations, structs.EvalStatusFailed, desc,
101103
s.queuedAllocs, "")
102104
}
103105

@@ -110,14 +112,16 @@ func (s *SystemScheduler) Process(eval *structs.Evaluation) (err error) {
110112
progress := func() bool { return progressMade(s.planResult) }
111113
if err := retryMax(limit, s.process, progress); err != nil {
112114
if statusErr, ok := err.(*SetStatusError); ok {
113-
return setStatus(s.logger, s.planner, s.eval, s.nextEval, nil, s.failedTGAllocs, statusErr.EvalStatus, err.Error(),
115+
return setStatus(s.logger, s.planner, s.eval, s.nextEval, nil,
116+
s.failedTGAllocs, s.planAnnotations, statusErr.EvalStatus, err.Error(),
114117
s.queuedAllocs, "")
115118
}
116119
return err
117120
}
118121

119122
// Update the status to complete
120-
return setStatus(s.logger, s.planner, s.eval, s.nextEval, nil, s.failedTGAllocs, structs.EvalStatusComplete, "",
123+
return setStatus(s.logger, s.planner, s.eval, s.nextEval, nil,
124+
s.failedTGAllocs, s.planAnnotations, structs.EvalStatusComplete, "",
121125
s.queuedAllocs, "")
122126
}
123127

@@ -186,6 +190,9 @@ func (s *SystemScheduler) process() (bool, error) {
186190
}
187191

188192
// Submit the plan
193+
if s.eval.AnnotatePlan {
194+
s.plan.Annotations = s.planAnnotations
195+
}
189196
result, newState, err := s.planner.SubmitPlan(s.plan)
190197
s.planResult = result
191198
if err != nil {
@@ -298,10 +305,8 @@ func (s *SystemScheduler) computeJobAllocs() error {
298305
allocExistsForTaskGroup[inplaceUpdate.TaskGroup.Name] = true
299306
}
300307

301-
if s.eval.AnnotatePlan {
302-
s.plan.Annotations = &structs.PlanAnnotations{
303-
DesiredTGUpdates: desiredUpdates(r, inplaceUpdates, destructiveUpdates),
304-
}
308+
s.planAnnotations = &structs.PlanAnnotations{
309+
DesiredTGUpdates: desiredUpdates(r, inplaceUpdates, destructiveUpdates),
305310
}
306311

307312
// Check if a rolling upgrade strategy is being used
@@ -415,9 +420,9 @@ func (s *SystemScheduler) computePlacements(place []reconciler.AllocTuple, exist
415420

416421
// If we are annotating the plan, then decrement the desired
417422
// placements based on whether the node meets the constraints
418-
if s.eval.AnnotatePlan && s.plan.Annotations != nil &&
419-
s.plan.Annotations.DesiredTGUpdates != nil {
420-
desired := s.plan.Annotations.DesiredTGUpdates[tgName]
423+
if s.planAnnotations != nil &&
424+
s.planAnnotations.DesiredTGUpdates != nil {
425+
desired := s.planAnnotations.DesiredTGUpdates[tgName]
421426
desired.Place -= 1
422427
}
423428

@@ -511,10 +516,10 @@ func (s *SystemScheduler) computePlacements(place []reconciler.AllocTuple, exist
511516
s.plan.AppendPreemptedAlloc(stop, alloc.ID)
512517

513518
preemptedAllocIDs = append(preemptedAllocIDs, stop.ID)
514-
if s.eval.AnnotatePlan && s.plan.Annotations != nil {
515-
s.plan.Annotations.PreemptedAllocs = append(s.plan.Annotations.PreemptedAllocs, stop.Stub(nil))
516-
if s.plan.Annotations.DesiredTGUpdates != nil {
517-
desired := s.plan.Annotations.DesiredTGUpdates[tgName]
519+
if s.eval.AnnotatePlan && s.planAnnotations != nil {
520+
s.planAnnotations.PreemptedAllocs = append(s.planAnnotations.PreemptedAllocs, stop.Stub(nil))
521+
if s.planAnnotations.DesiredTGUpdates != nil {
522+
desired := s.planAnnotations.DesiredTGUpdates[tgName]
518523
desired.Preemptions += 1
519524
}
520525
}

scheduler/scheduler_system_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ func TestSystemSched_JobRegister(t *testing.T) {
5353
must.Len(t, 1, h.Plans)
5454
plan := h.Plans[0]
5555

56-
// Ensure the plan does not have annotations
56+
// Ensure the plan does not have annotations but the eval does
5757
must.Nil(t, plan.Annotations, must.Sprint("expected no annotations"))
58+
must.SliceNotEmpty(t, h.Evals)
59+
must.Eq(t, 10, h.Evals[0].PlanAnnotations.DesiredTGUpdates["web"].Place)
5860

5961
// Ensure the plan allocated
6062
var planned []*structs.Allocation

scheduler/util.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -540,7 +540,9 @@ func renderTemplatesUpdated(a, b *structs.RestartPolicy, msg string) comparison
540540
// setStatus is used to update the status of the evaluation
541541
func setStatus(logger log.Logger, planner sstructs.Planner,
542542
eval, nextEval, spawnedBlocked *structs.Evaluation,
543-
tgMetrics map[string]*structs.AllocMetric, status, desc string,
543+
tgMetrics map[string]*structs.AllocMetric,
544+
annotations *structs.PlanAnnotations,
545+
status, desc string,
544546
queuedAllocs map[string]int, deploymentID string) error {
545547

546548
logger.Debug("setting eval status", "status", status)
@@ -558,6 +560,7 @@ func setStatus(logger log.Logger, planner sstructs.Planner,
558560
if queuedAllocs != nil {
559561
newEval.QueuedAllocations = queuedAllocs
560562
}
563+
newEval.PlanAnnotations = annotations
561564

562565
return planner.UpdateEval(newEval)
563566
}

0 commit comments

Comments
 (0)