Skip to content

Commit 348c49a

Browse files
authored
Distiguishing "finished exec" and "built specs" in TaskStatus (#542)
This fixes the regression reported in https://github.com/sourcegraph/sourcegraph/issues/21230 and introduced by yours truly in d6c876c. With the introduction of the Coordinator and the explicit steps of checking-the-cache-for and building-of ChangesetSpecs outside the executor, the `TaskStatus.ChangesetSpecs` field wasn't set at the time when `FinishedAt` was set. The `batchProgressPrinter` assumed, though, that this was the case and that if `FinishedAt` was set that `len(taskStatus.ChangesetSpecs) == 0` means "No changes". This change here fixes the problem by distinguishing between the two states: finished execution of steps and finished building changeset specs. The problem is that it's still a slight regression in behaviour: previously the diff stats would be printed in the status bar and in the verbose mode *as tasks were finishing*. Now that we build all changeset specs at once, after all of them have been executed, we can't update the status bar to include diff stats and the verbose messages will be logged all at once. I still think that the current code (with the Coordinator) is better than what we had before and the hard problem here is fixed (no wrong information being displayed), but longer term I think there's a solution possible in which we decouple the task execution and its status much more and make it possible to build better UIs for the status of execution. That I think should be approached separately though.
1 parent d6c876c commit 348c49a

File tree

5 files changed

+119
-76
lines changed

5 files changed

+119
-76
lines changed

cmd/src/batch_progress_printer.go

Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,9 @@ func newBatchProgressPrinter(out *output.Output, verbose bool, numParallelism in
2121

2222
numParallelism: numParallelism,
2323

24-
completedTasks: map[string]bool{},
24+
executedTasks: map[string]bool{},
2525
runningTasks: map[string]*executor.TaskStatus{},
26+
completedTasks: map[string]bool{},
2627

2728
repoStatusBar: map[string]int{},
2829
statusBarRepo: map[int]string{},
@@ -45,8 +46,14 @@ type batchProgressPrinter struct {
4546
maxRepoName int
4647
numParallelism int
4748

49+
// runningTasks are the tasks that are currently executing.
50+
runningTasks map[string]*executor.TaskStatus
51+
// executedTasks are the tasks that finished execution but where the
52+
// changesetSpec hasn't been built.
53+
executedTasks map[string]bool
54+
// completedTasks are the tasks that finished execution and finished
55+
// building changesetSpecs.
4856
completedTasks map[string]bool
49-
runningTasks map[string]*executor.TaskStatus
5057

5158
repoStatusBar map[string]int
5259
statusBarRepo map[int]string
@@ -109,7 +116,8 @@ func (p *batchProgressPrinter) PrintStatuses(statuses []*executor.TaskStatus) {
109116
p.numStatusBars = p.initProgressBar(statuses)
110117
}
111118

112-
newlyCompleted := []*executor.TaskStatus{}
119+
newlyFinishedExecution := []*executor.TaskStatus{}
120+
newlyFinishedBuilding := []*executor.TaskStatus{}
113121
currentlyRunning := []*executor.TaskStatus{}
114122
errored := 0
115123

@@ -118,14 +126,14 @@ func (p *batchProgressPrinter) PrintStatuses(statuses []*executor.TaskStatus) {
118126
p.maxRepoName = len(ts.DisplayName())
119127
}
120128

121-
if ts.IsCompleted() {
129+
if ts.FinishedExecution() {
122130
if ts.Err != nil {
123131
errored += 1
124132
}
125133

126-
if !p.completedTasks[ts.DisplayName()] {
127-
p.completedTasks[ts.DisplayName()] = true
128-
newlyCompleted = append(newlyCompleted, ts)
134+
if !p.executedTasks[ts.DisplayName()] {
135+
p.executedTasks[ts.DisplayName()] = true
136+
newlyFinishedExecution = append(newlyFinishedExecution, ts)
129137
}
130138

131139
if _, ok := p.runningTasks[ts.DisplayName()]; ok {
@@ -137,13 +145,20 @@ func (p *batchProgressPrinter) PrintStatuses(statuses []*executor.TaskStatus) {
137145
}
138146
}
139147

148+
if ts.FinishedBuildingSpecs() {
149+
if !p.completedTasks[ts.DisplayName()] {
150+
p.completedTasks[ts.DisplayName()] = true
151+
newlyFinishedBuilding = append(newlyFinishedBuilding, ts)
152+
}
153+
}
154+
140155
if ts.IsRunning() {
141156
currentlyRunning = append(currentlyRunning, ts)
142157
}
143158

144159
}
145160

146-
p.updateProgressBar(len(p.completedTasks), errored, len(statuses))
161+
p.updateProgressBar(len(p.executedTasks), errored, len(statuses))
147162

148163
newlyStarted := map[string]*executor.TaskStatus{}
149164
statusBarIndex := 0
@@ -176,14 +191,35 @@ func (p *batchProgressPrinter) PrintStatuses(statuses []*executor.TaskStatus) {
176191
p.repoStatusBar[ts.DisplayName()] = statusBarIndex
177192
}
178193

179-
for _, ts := range newlyCompleted {
180-
fileDiffs, hasDiffs, err := ts.FileDiffs()
181-
if err != nil {
182-
p.progress.Verbosef("%-*s failed to display status: %s", p.maxRepoName, ts.DisplayName(), err)
183-
continue
194+
for _, ts := range newlyFinishedExecution {
195+
if idx, ok := p.repoStatusBar[ts.DisplayName()]; ok {
196+
// Log that this task completed, but only if there is no
197+
// currently executing one in this bar, to avoid flicker.
198+
if _, ok := p.statusBarRepo[idx]; !ok {
199+
statusText, err := taskStatusBarText(ts)
200+
if err != nil {
201+
p.progress.Verbosef("%-*s failed to display status: %s", p.maxRepoName, ts.DisplayName(), err)
202+
continue
203+
}
204+
205+
if ts.Err != nil {
206+
p.progress.StatusBarFailf(idx, statusText)
207+
} else {
208+
p.progress.StatusBarCompletef(idx, statusText)
209+
}
210+
}
211+
delete(p.repoStatusBar, ts.DisplayName())
184212
}
213+
}
214+
215+
if p.verbose {
216+
for _, ts := range newlyFinishedBuilding {
217+
fileDiffs, hasDiffs, err := ts.FileDiffs()
218+
if err != nil {
219+
p.progress.Verbosef("%-*s failed to display status: %s", p.maxRepoName, ts.DisplayName(), err)
220+
continue
221+
}
185222

186-
if p.verbose {
187223
p.progress.WriteLine(output.Linef("", output.StylePending, "%s", ts.DisplayName()))
188224

189225
if !hasDiffs {
@@ -206,25 +242,6 @@ func (p *batchProgressPrinter) PrintStatuses(statuses []*executor.TaskStatus) {
206242
p.progress.Verbosef(" Execution took %s", ts.ExecutionTime())
207243
p.progress.Verbose("")
208244
}
209-
210-
if idx, ok := p.repoStatusBar[ts.DisplayName()]; ok {
211-
// Log that this task completed, but only if there is no
212-
// currently executing one in this bar, to avoid flicker.
213-
if _, ok := p.statusBarRepo[idx]; !ok {
214-
statusText, err := taskStatusBarText(ts)
215-
if err != nil {
216-
p.progress.Verbosef("%-*s failed to display status: %s", p.maxRepoName, ts.DisplayName(), err)
217-
continue
218-
}
219-
220-
if ts.Err != nil {
221-
p.progress.StatusBarFailf(idx, statusText)
222-
} else {
223-
p.progress.StatusBarCompletef(idx, statusText)
224-
}
225-
}
226-
delete(p.repoStatusBar, ts.DisplayName())
227-
}
228245
}
229246

230247
for statusBar, repo := range p.statusBarRepo {
@@ -255,24 +272,15 @@ type statusTexter interface {
255272
func taskStatusBarText(ts *executor.TaskStatus) (string, error) {
256273
var statusText string
257274

258-
if ts.IsCompleted() {
259-
diffs, hasDiffs, err := ts.FileDiffs()
260-
if err != nil {
261-
return "", err
262-
}
263-
264-
if hasDiffs {
265-
statusText = diffStatDescription(diffs) + " " + diffStatDiagram(sumDiffStats(diffs))
266-
} else {
267-
if ts.Err != nil {
268-
if texter, ok := ts.Err.(statusTexter); ok {
269-
statusText = texter.StatusText()
270-
} else {
271-
statusText = ts.Err.Error()
272-
}
275+
if ts.FinishedExecution() {
276+
if ts.Err != nil {
277+
if texter, ok := ts.Err.(statusTexter); ok {
278+
statusText = texter.StatusText()
273279
} else {
274-
statusText = "No changes"
280+
statusText = ts.Err.Error()
275281
}
282+
} else {
283+
statusText = "Done!"
276284
}
277285
} else if ts.IsRunning() {
278286
if ts.CurrentlyExecuting != "" {

cmd/src/batch_progress_printer_test.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,44 @@ func TestBatchProgressPrinterIntegration(t *testing.T) {
8989
t.Fatalf("wrong output:\n%s", cmp.Diff(want, have))
9090
}
9191

92-
// Now mark the last task as completed
92+
// Now mark the last task as finished-execution
9393
statuses[len(statuses)-1] = &executor.TaskStatus{
9494
RepoName: "github.com/sourcegraph/automation-testing",
9595
StartedAt: now.Add(time.Duration(-5) * time.Second),
9696
FinishedAt: now.Add(time.Duration(5) * time.Second),
9797
CurrentlyExecuting: "",
9898
Err: nil,
99+
}
100+
101+
printer.PrintStatuses(statuses)
102+
have = buf.Lines()
103+
want = []string{
104+
"⠋ Executing... (1/3, 0 errored) ███████████████▍",
105+
"│ ",
106+
"├── github.com/sourcegraph/sourcegraph echo Hello World > README.md 0s",
107+
"├── github.com/sourcegraph/src-cli Downloading archive 0s",
108+
"└── github.com/sourcegraph/automati... Done! 0s",
109+
"",
110+
}
111+
if !cmp.Equal(want, have) {
112+
t.Fatalf("wrong output:\n%s", cmp.Diff(want, have))
113+
}
114+
115+
// Print again to make sure we get the same result
116+
printer.PrintStatuses(statuses)
117+
have = buf.Lines()
118+
if !cmp.Equal(want, have) {
119+
t.Fatalf("wrong output:\n%s", cmp.Diff(want, have))
120+
}
121+
122+
// Mark the last task as finished-building-specs
123+
statuses[len(statuses)-1] = &executor.TaskStatus{
124+
RepoName: "github.com/sourcegraph/automation-testing",
125+
StartedAt: now.Add(time.Duration(-5) * time.Second),
126+
FinishedAt: now.Add(time.Duration(5) * time.Second),
127+
CurrentlyExecuting: "",
128+
Err: nil,
129+
ChangesetSpecsDone: true,
99130
ChangesetSpecs: []*batches.ChangesetSpec{
100131
{
101132
BaseRepository: "graphql-id",
@@ -132,19 +163,12 @@ func TestBatchProgressPrinterIntegration(t *testing.T) {
132163
"│ ",
133164
"├── github.com/sourcegraph/sourcegraph echo Hello World > README.md 0s",
134165
"├── github.com/sourcegraph/src-cli Downloading archive 0s",
135-
"└── github.com/sourcegraph/automati... 3 files changed ++++ 0s",
166+
"└── github.com/sourcegraph/automati... Done! 0s",
136167
"",
137168
}
138169
if !cmp.Equal(want, have) {
139170
t.Fatalf("wrong output:\n%s", cmp.Diff(want, have))
140171
}
141-
142-
// Print again to make sure we get the same result
143-
printer.PrintStatuses(statuses)
144-
have = buf.Lines()
145-
if !cmp.Equal(want, have) {
146-
t.Fatalf("wrong output:\n%s", cmp.Diff(want, have))
147-
}
148172
}
149173

150174
type ttyBuf struct {

internal/batches/executor/coordinator.go

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,15 @@ func (c *Coordinator) checkCacheForTask(ctx context.Context, task *Task) (specs
139139
return specs, true, nil
140140
}
141141

142-
func (c *Coordinator) cacheAndBuildSpec(ctx context.Context, taskResult taskResult, status taskStatusHandler) ([]*batches.ChangesetSpec, error) {
142+
func (c *Coordinator) cacheAndBuildSpec(ctx context.Context, taskResult taskResult, status taskStatusHandler) (specs []*batches.ChangesetSpec, err error) {
143+
defer func() {
144+
// Set these two fields in any case
145+
status.Update(taskResult.task, func(status *TaskStatus) {
146+
status.ChangesetSpecsDone = true
147+
status.ChangesetSpecs = specs
148+
})
149+
}()
150+
143151
// Add to the cache, even if no diff was produced.
144152
cacheKey := taskResult.task.cacheKey()
145153
if err := c.cache.Set(ctx, cacheKey, taskResult.result); err != nil {
@@ -153,18 +161,11 @@ func (c *Coordinator) cacheAndBuildSpec(ctx context.Context, taskResult taskResu
153161
}
154162

155163
// Build the changeset specs.
156-
specs, err := createChangesetSpecs(taskResult.task, taskResult.result, c.opts.AutoAuthorDetails)
164+
specs, err = createChangesetSpecs(taskResult.task, taskResult.result, c.opts.AutoAuthorDetails)
157165
if err != nil {
158166
return specs, err
159167
}
160168

161-
// Update the status of Task
162-
status.Update(taskResult.task, func(status *TaskStatus) {
163-
status.ChangesetSpecs = specs
164-
// TODO(mrnugget): Ideally we'd get rid of this dependency on the task
165-
// status handler here.
166-
})
167-
168169
return specs, nil
169170
}
170171

@@ -205,10 +206,6 @@ func (c *Coordinator) Execute(ctx context.Context, tasks []*Task, spec *batches.
205206
// Run executor
206207
c.exec.Start(ctx, tasks, status)
207208
results, err := c.exec.Wait(ctx)
208-
if printer != nil {
209-
status.CopyStatuses(printer)
210-
done <- struct{}{}
211-
}
212209
if err != nil {
213210
if c.opts.SkipErrors {
214211
errs = multierror.Append(errs, err)
@@ -227,6 +224,12 @@ func (c *Coordinator) Execute(ctx context.Context, tasks []*Task, spec *batches.
227224
specs = append(specs, taskSpecs...)
228225
}
229226

227+
// Now that we've built the specs too we can mark the progress as done
228+
if printer != nil {
229+
status.CopyStatuses(printer)
230+
done <- struct{}{}
231+
}
232+
230233
// Add external changeset specs.
231234
for _, ic := range spec.ImportChangesets {
232235
repo, err := c.opts.ResolveRepoName(ctx, ic.Repository)

internal/batches/executor/executor.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,6 @@ func (x *executor) do(ctx context.Context, task *Task, status taskStatusHandler)
139139
// Ensure that the status is updated when we're done.
140140
defer func() {
141141
status.Update(task, func(status *TaskStatus) {
142-
status.FinishedAt = time.Now()
143142
status.CurrentlyExecuting = ""
144143
status.Err = err
145144
})

internal/batches/executor/task_status.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,14 @@ type TaskStatus struct {
6767
CurrentlyExecuting string
6868

6969
// ChangesetSpecs are the specs produced by executing the Task in a
70-
// repository. With the introduction of `transformChanges` to the batch
71-
// spec, one Task can produce multiple ChangesetSpecs.
70+
// repository. One Task can produce multiple ChangesetSpecs (see
71+
// createChangesetSpec).
72+
// Only check this field once ChangesetSpecsDone is set.
7273
ChangesetSpecs []*batches.ChangesetSpec
74+
// ChangesetSpecsDone is set after the Coordinator attempted to build the
75+
// ChangesetSpecs of a task.
76+
ChangesetSpecsDone bool
77+
7378
// Err is set if executing the Task lead to an error.
7479
Err error
7580

@@ -89,10 +94,14 @@ func (ts *TaskStatus) IsRunning() bool {
8994
return !ts.StartedAt.IsZero() && ts.FinishedAt.IsZero()
9095
}
9196

92-
func (ts *TaskStatus) IsCompleted() bool {
97+
func (ts *TaskStatus) FinishedExecution() bool {
9398
return !ts.StartedAt.IsZero() && !ts.FinishedAt.IsZero()
9499
}
95100

101+
func (ts *TaskStatus) FinishedBuildingSpecs() bool {
102+
return ts.ChangesetSpecsDone
103+
}
104+
96105
func (ts *TaskStatus) ExecutionTime() time.Duration {
97106
return ts.FinishedAt.Sub(ts.StartedAt).Truncate(time.Millisecond)
98107
}
@@ -102,7 +111,7 @@ func (ts *TaskStatus) ExecutionTime() time.Duration {
102111
// If no file diffs were produced, the task resulted in an error, or the task
103112
// hasn't finished execution yet, the second return value is false.
104113
func (ts *TaskStatus) FileDiffs() ([]*diff.FileDiff, bool, error) {
105-
if !ts.IsCompleted() || len(ts.ChangesetSpecs) == 0 || ts.Err != nil {
114+
if !ts.FinishedBuildingSpecs() || len(ts.ChangesetSpecs) == 0 || ts.Err != nil {
106115
return nil, false, nil
107116
}
108117

0 commit comments

Comments
 (0)