Skip to content

Commit fee6a2f

Browse files
authored
fix: edge-case where cancelled context causes process to hang (#60)
* fix: cancelled context now exits the process with error code * fix: remove listening for <-ctx.Done in root.go * docs: tweak wording of cancel context comments
1 parent 642c8e7 commit fee6a2f

File tree

1 file changed

+14
-6
lines changed

1 file changed

+14
-6
lines changed

cmd/root.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,20 +327,28 @@ func InitConfig(ctx context.Context, clients *shared.ClientFactory, rootCmd *cob
327327
// It listens for process interrupts and sends to IOStreams' GetInterruptChannel() for use in
328328
// in communicating process interrupts elsewhere in the code.
329329
func ExecuteContext(ctx context.Context, rootCmd *cobra.Command, clients *shared.ClientFactory) {
330+
// Derive a cancel context that is cancelled when the main execution is interrupted or cleaned up.
331+
// Sub-commands can register for the cleanup wait group with clients.CleanupWaitGroup.Add(1)
332+
// and listen for <-ctx.Done() to be notified when the main execution is interrupted, in order
333+
// to have a chance to cleanup. This is useful for long running processes and background goroutines,
334+
// such as the activity and upgrade commands.
330335
ctx, cancel := context.WithCancel(ctx)
331336

332337
completedChan := make(chan bool, 1) // completed is used for signalling an end to command
333338
exitChan := make(chan bool, 1) // exit blocks the command from exiting until completed
334339
interruptChan := make(chan os.Signal, 1) // interrupt catches signals to avoid abrupt exits
340+
335341
signal.Notify(interruptChan, os.Interrupt, syscall.SIGTERM)
336342
defer func() {
337343
signal.Stop(interruptChan)
338344
cancel()
339345
}()
346+
347+
// Wait for either an interrupt signal (ctrl+c) or an explicit call to ctx.cancel()
340348
go func() {
341-
// Wait for either an interrupt signal (ctrl+c), or an explicit call to ctx.cancel()
342349
select {
343-
case <-interruptChan: // first interrupt signal, cancel context
350+
// Received interrupt signal, so cancel the context and cleanup
351+
case <-interruptChan:
344352
clients.IO.PrintInfo(ctx, false, "\n") // flush the CTRL + C character
345353
clients.IO.PrintDebug(ctx, "Got process interrupt signal, cancelling context")
346354
cancel()
@@ -355,18 +363,17 @@ func ExecuteContext(ctx context.Context, rootCmd *cobra.Command, clients *shared
355363
clients.IO.PrintDebug(ctx, "Exiting with cancel exit code.")
356364
os.Exit(int(iostreams.ExitCancel))
357365
}()
358-
case <-ctx.Done():
359-
// No interrupt signal sent, command executed to completion
360-
// FIXME - `.Done()` channel is triggered by `cancel()` and not a successfully completion
366+
// Received completed execution, so exit the process successfully
361367
case <-completedChan:
362-
// No canceled context, but command has completed execution
363368
exitChan <- true
364369
}
370+
365371
// If we get a second interrupt, no matter what exit the process
366372
<-interruptChan
367373
clients.IO.PrintDebug(ctx, "Got second process interrupt signal, exiting the process")
368374
os.Exit(int(iostreams.ExitCancel))
369375
}()
376+
370377
// The cleanup() method in the root command will invoke via `defer` from within Execute.
371378
if err := rootCmd.ExecuteContext(ctx); err != nil {
372379
errMsg := err.Error()
@@ -389,6 +396,7 @@ func ExecuteContext(ctx context.Context, rootCmd *cobra.Command, clients *shared
389396
} else {
390397
completedChan <- true
391398
}
399+
392400
<-exitChan
393401
_ = clients.EventTracker.FlushToLogstash(ctx, clients.Config, clients.IO, clients.IO.GetExitCode())
394402
}

0 commit comments

Comments
 (0)