Skip to content

Commit 4e2603a

Browse files
committed
Stop build graph and watch work on cancellation
Interruption could still wait for the full project reference graph to load, or for watch reconciliation after compilation had already stopped. These phases must observe cancellation too, without leaving a partial graph available to a subsequent build request.
1 parent b3a475b commit 4e2603a

4 files changed

Lines changed: 226 additions & 72 deletions

File tree

‎tsc/internal/execute/build/graph_test.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (b *buildOrderTestCase) run(t *testing.T) {
117117
Sys: sys,
118118
Command: buildCommand,
119119
})
120-
orchestrator.GenerateGraph(nil)
120+
orchestrator.GenerateGraph(t.Context(), nil)
121121
buildOrder := core.Map(orchestrator.Order(), func(config tspath.RootedFilePath) string { return b.projectName(config.AsString()) })
122122
assert.DeepEqual(t, buildOrder, b.expected)
123123
verifyDeps(orchestrator, buildOrder, false)
@@ -141,7 +141,7 @@ func (b *buildOrderTestCase) run(t *testing.T) {
141141
}
142142
}
143143

144-
orchestrator.GenerateGraphReusingOldTasks()
144+
orchestrator.GenerateGraphReusingOldTasks(t.Context())
145145
buildOrder2 := core.Map(orchestrator.Order(), func(config tspath.RootedFilePath) string { return b.projectName(config.AsString()) })
146146
assert.DeepEqual(t, buildOrder2, b.expected)
147147

@@ -151,7 +151,7 @@ func (b *buildOrderTestCase) run(t *testing.T) {
151151
Sys: sys,
152152
Command: buildCommandWatch,
153153
})
154-
orchestrator.GenerateGraph(nil)
154+
orchestrator.GenerateGraph(t.Context(), nil)
155155
buildOrder3 := core.Map(orchestrator.Order(), func(config tspath.RootedFilePath) string { return b.projectName(config.AsString()) })
156156
verifyDeps(orchestrator, buildOrder3, true)
157157
})

‎tsc/internal/execute/build/orchestrator.go‎

Lines changed: 98 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -124,16 +124,22 @@ func (o *Orchestrator) ScheduleOrder() []string {
124124
// picked up may not be done yet, so a builder can take a dependent of a slow project and
125125
// wait on that project while a later project's upstream has already finished. The stable
126126
// sort preserves the original order within a depth, and reporting still follows Order().
127-
func (o *Orchestrator) computeScheduleOrder() []*BuildTask {
127+
func (o *Orchestrator) computeScheduleOrder(ctx context.Context) []*BuildTask {
128128
type scheduleEntry struct {
129129
task *BuildTask
130130
depth int
131131
}
132132
entries := make([]scheduleEntry, len(o.order))
133133
depths := make(map[*BuildTask]int, len(o.order))
134134
for i, task := range o.order {
135+
if ctx.Err() != nil {
136+
return nil
137+
}
135138
depth := 0
136139
for _, upstream := range task.upStream {
140+
if ctx.Err() != nil {
141+
return nil
142+
}
137143
depth = max(depth, depths[upstream.task]+1)
138144
}
139145
depths[task] = depth
@@ -142,9 +148,14 @@ func (o *Orchestrator) computeScheduleOrder() []*BuildTask {
142148
slices.SortStableFunc(entries, func(a, b scheduleEntry) int {
143149
return a.depth - b.depth
144150
})
145-
return core.Map(entries, func(entry scheduleEntry) *BuildTask {
146-
return entry.task
147-
})
151+
order := make([]*BuildTask, len(entries))
152+
for i, entry := range entries {
153+
if ctx.Err() != nil {
154+
return nil
155+
}
156+
order[i] = entry.task
157+
}
158+
return order
148159
}
149160

150161
func (o *Orchestrator) Upstream(configName string) []string {
@@ -171,9 +182,15 @@ func (o *Orchestrator) getTask(path tspath.PathKey) *BuildTask {
171182
return task
172183
}
173184

174-
func (o *Orchestrator) createBuildTasks(oldTasks *collections.SyncMap[tspath.PathKey, *BuildTask], configs []tspath.RootedFilePath, wg core.WorkGroup) {
185+
func (o *Orchestrator) createBuildTasks(ctx context.Context, oldTasks *collections.SyncMap[tspath.PathKey, *BuildTask], configs []tspath.RootedFilePath, wg core.WorkGroup) {
175186
for _, config := range configs {
187+
if ctx.Err() != nil {
188+
return
189+
}
176190
wg.Queue(func() {
191+
if ctx.Err() != nil {
192+
return
193+
}
177194
path := o.caseSensitivity.PathKey(tspath.RootedPath(config))
178195
var task *BuildTask
179196
var buildInfo *buildInfoEntry
@@ -201,20 +218,24 @@ func (o *Orchestrator) createBuildTasks(oldTasks *collections.SyncMap[tspath.Pat
201218
task.resolved = o.host.GetResolvedProjectReference(config, path)
202219
task.upStream = nil
203220
if task.resolved != nil {
204-
o.createBuildTasks(oldTasks, task.resolved.ResolvedProjectReferencePaths(), wg)
221+
o.createBuildTasks(ctx, oldTasks, task.resolved.ResolvedProjectReferencePaths(), wg)
205222
}
206223
})
207224
}
208225
}
209226

210227
func (o *Orchestrator) setupBuildTask(
228+
ctx context.Context,
211229
configName tspath.RootedFilePath,
212230
downStream *BuildTask,
213231
inCircularContext bool,
214232
completed *collections.Set[tspath.PathKey],
215233
analyzing *collections.Set[tspath.PathKey],
216234
circularityStack []string,
217235
) *BuildTask {
236+
if ctx.Err() != nil {
237+
return nil
238+
}
218239
path := o.caseSensitivity.PathKey(tspath.RootedPath(configName))
219240
task := o.getTask(path)
220241
if !completed.Has(path) {
@@ -231,7 +252,10 @@ func (o *Orchestrator) setupBuildTask(
231252
circularityStack = append(circularityStack, configName.AsString())
232253
if task.resolved != nil {
233254
for index, subReference := range task.resolved.ResolvedProjectReferencePaths() {
234-
upstream := o.setupBuildTask(subReference, task, inCircularContext || task.resolved.ProjectReferences()[index].Circular, completed, analyzing, circularityStack)
255+
if ctx.Err() != nil {
256+
return nil
257+
}
258+
upstream := o.setupBuildTask(ctx, subReference, task, inCircularContext || task.resolved.ProjectReferences()[index].Circular, completed, analyzing, circularityStack)
235259
if upstream != nil {
236260
task.upStream = append(task.upStream, &upstreamTask{task: upstream, refIndex: index})
237261
}
@@ -249,41 +273,67 @@ func (o *Orchestrator) setupBuildTask(
249273
return task
250274
}
251275

252-
func (o *Orchestrator) GenerateGraphReusingOldTasks() {
276+
func (o *Orchestrator) GenerateGraphReusingOldTasks(ctx context.Context) {
253277
tasks := o.tasks
254278
o.tasks = &collections.SyncMap[tspath.PathKey, *BuildTask]{}
255279
o.order = nil
256280
o.errors = nil
257-
o.GenerateGraph(tasks)
281+
o.GenerateGraph(ctx, tasks)
258282
}
259283

260-
func (o *Orchestrator) GenerateGraph(oldTasks *collections.SyncMap[tspath.PathKey, *BuildTask]) {
284+
func (o *Orchestrator) GenerateGraph(ctx context.Context, oldTasks *collections.SyncMap[tspath.PathKey, *BuildTask]) {
285+
o.graphGenerated = false
286+
defer func() {
287+
if oldTasks != nil {
288+
oldTasks.Range(func(path tspath.PathKey, oldTask *BuildTask) bool {
289+
if task, ok := o.tasks.Load(path); ok && task == oldTask {
290+
return true
291+
}
292+
if oldTask.contentMapperProject != nil {
293+
_ = oldTask.contentMapperProject.Close()
294+
}
295+
return true
296+
})
297+
}
298+
if ctx.Err() != nil {
299+
// A partial graph must not be reused or built on a subsequent request.
300+
o.tasks.Range(func(_ tspath.PathKey, task *BuildTask) bool {
301+
if task.contentMapperProject != nil {
302+
_ = task.contentMapperProject.Close()
303+
}
304+
return true
305+
})
306+
o.tasks = &collections.SyncMap[tspath.PathKey, *BuildTask]{}
307+
o.order = nil
308+
o.scheduleOrder = nil
309+
o.errors = nil
310+
o.graphGenerated = false
311+
}
312+
}()
313+
if ctx.Err() != nil {
314+
return
315+
}
261316
projects := o.opts.Command.ResolvedProjectPaths()
262317
// Parse all config files in parallel
263318
wg := core.NewWorkGroup(o.opts.Command.CompilerOptions.SingleThreaded.IsTrue())
264-
o.createBuildTasks(oldTasks, projects, wg)
319+
o.createBuildTasks(ctx, oldTasks, projects, wg)
265320
wg.RunAndWait()
321+
if ctx.Err() != nil {
322+
return
323+
}
266324

267325
// Generate the graph
268326
completed := collections.Set[tspath.PathKey]{}
269327
analyzing := collections.Set[tspath.PathKey]{}
270328
circularityStack := []string{}
271329
for _, project := range projects {
272-
o.setupBuildTask(project, nil, false, &completed, &analyzing, circularityStack)
273-
}
274-
o.scheduleOrder = o.computeScheduleOrder()
275-
if oldTasks != nil {
276-
oldTasks.Range(func(path tspath.PathKey, oldTask *BuildTask) bool {
277-
if task, ok := o.tasks.Load(path); ok && task == oldTask {
278-
return true
279-
}
280-
if oldTask.contentMapperProject != nil {
281-
_ = oldTask.contentMapperProject.Close()
282-
}
283-
return true
284-
})
330+
if ctx.Err() != nil {
331+
return
332+
}
333+
o.setupBuildTask(ctx, project, nil, false, &completed, &analyzing, circularityStack)
285334
}
286-
o.graphGenerated = true
335+
o.scheduleOrder = o.computeScheduleOrder(ctx)
336+
o.graphGenerated = ctx.Err() == nil
287337
}
288338

289339
// tsc -b entrypoint
@@ -293,17 +343,20 @@ func (o *Orchestrator) Start(ctx context.Context) tsc.CommandLineResult {
293343

294344
// orchestrator.Build() entrypoint for api
295345
func (o *Orchestrator) Build(ctx context.Context, project string) *OrchestratorResult {
296-
o.recheckAllProjects(project)
346+
o.recheckAllProjects(ctx, project)
297347
return o.start(ctx, project, false /*onlyReferences*/)
298348
}
299349

300350
// orchestrator.BuildReferences() entrypoint for api
301351
func (o *Orchestrator) BuildReferences(ctx context.Context, project string) *OrchestratorResult {
302-
o.recheckAllProjects(project)
352+
o.recheckAllProjects(ctx, project)
303353
return o.start(ctx, project, true /*onlyReferences*/)
304354
}
305355

306356
func (o *Orchestrator) start(ctx context.Context, project string, onlyReferences bool) *OrchestratorResult {
357+
if ctx.Err() != nil {
358+
return &OrchestratorResult{Result: tsc.CommandLineResult{Status: tsc.ExitStatusCancelled}}
359+
}
307360
o.contentMapperHost = tsc.NewContentMapperHost(ctx, o.opts.Sys, o.opts.Command.CompilerOptions)
308361
if o.contentMapperHost != nil && (!o.opts.Command.CompilerOptions.Watch.IsTrue() || o.opts.Testing == nil) {
309362
defer o.contentMapperHost.Close()
@@ -312,9 +365,12 @@ func (o *Orchestrator) start(ctx context.Context, project string, onlyReferences
312365
o.watchStatusReporter(ast.NewCompilerDiagnostic(diagnostics.Starting_compilation_in_watch_mode))
313366
}
314367
if o.graphGenerated {
315-
o.GenerateGraphReusingOldTasks()
368+
o.GenerateGraphReusingOldTasks(ctx)
316369
} else {
317-
o.GenerateGraph(nil)
370+
o.GenerateGraph(ctx, nil)
371+
}
372+
if ctx.Err() != nil {
373+
return &OrchestratorResult{Result: tsc.CommandLineResult{Status: tsc.ExitStatusCancelled}}
318374
}
319375
order, ok := o.getBuildOrderFor(project)
320376
if !ok {
@@ -337,15 +393,18 @@ func (o *Orchestrator) start(ctx context.Context, project string, onlyReferences
337393
return result
338394
}
339395

340-
func (o *Orchestrator) recheckAllProjects(project string) {
341-
if !o.graphGenerated {
396+
func (o *Orchestrator) recheckAllProjects(ctx context.Context, project string) {
397+
if !o.graphGenerated || ctx.Err() != nil {
342398
return
343399
}
344400
order, ok := o.getBuildOrderFor(project)
345401
if !ok {
346402
return
347403
}
348404
o.rangeTasks(order, func(path tspath.PathKey, task *BuildTask) {
405+
if ctx.Err() != nil {
406+
return
407+
}
349408
task.resetStatus()
350409
task.resetConfig(o, path)
351410
})
@@ -365,7 +424,7 @@ func (o *Orchestrator) CleanReferences(project string) *OrchestratorResult {
365424

366425
func (o *Orchestrator) clean(project string, onlyReferences bool) *OrchestratorResult {
367426
if !o.graphGenerated {
368-
o.GenerateGraph(nil)
427+
o.GenerateGraph(context.Background(), nil)
369428
}
370429
if len(o.errors) != 0 {
371430
result := &OrchestratorResult{
@@ -854,10 +913,16 @@ func (o *Orchestrator) DoCycle(ctx context.Context) {
854913
o.watchStatusReporter(ast.NewCompilerDiagnostic(diagnostics.File_change_detected_Starting_incremental_compilation))
855914
if needsConfigUpdate.Load() {
856915
// Generate new tasks
857-
o.GenerateGraphReusingOldTasks()
916+
o.GenerateGraphReusingOldTasks(ctx)
917+
}
918+
if ctx.Err() != nil {
919+
return
858920
}
859921

860922
o.buildOrClean(ctx)
923+
if ctx.Err() != nil {
924+
return
925+
}
861926
o.updateWatch()
862927
desiredDirs := o.computeDesiredWatches()
863928
if err := o.wm.ReconcileWatches(desiredDirs); err != nil {

0 commit comments

Comments
 (0)