fix(cli): use quartz clock in waitForTaskIdle for immediate first poll (#25648)

waitForTaskIdle used time.NewTicker(5s) which delays the first poll
by 5 seconds. Debugger tracing proved the failure mechanism: on slow
CI (Windows), the first poll at 5s sees "working" (idle patch has not
landed due to goroutine scheduling), needs poll #2 at 10s, but the
25s context expires before it fires.

Two changes:

1. Use r.clock.NewTicker (quartz) with time.Nanosecond initial
   interval and Reset(5s) for immediate first poll. Tests inject a
   mock clock via clitest.NewWithClock for deterministic control.
2. Rewrite WaitsForWorkingAppState test with quartz traps
   (NewTicker + TickerReset) for deterministic synchronization
   instead of racing goroutines. Fix PausedDuringWaitForReady
   sync point.

Closes DEVEX-381
This commit is contained in:
Mathias Fredriksson
2026-05-25 19:14:29 +03:00
committed by GitHub
parent 8652ef3e3b
commit 7958ad6d04
2 changed files with 32 additions and 7 deletions
+6 -3
View File
@@ -11,6 +11,7 @@ import (
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/quartz"
"github.com/coder/serpent"
)
@@ -107,7 +108,7 @@ func (r *RootCmd) taskSend() *serpent.Command {
return xerrors.Errorf("task %q has status %s and cannot be sent input", display, task.Status)
}
if err := waitForTaskIdle(ctx, inv, client, task, workspaceBuildID); err != nil {
if err := waitForTaskIdle(ctx, inv, r.clock, client, task, workspaceBuildID); err != nil {
return xerrors.Errorf("wait for task %q to be idle: %w", display, err)
}
@@ -126,7 +127,7 @@ func (r *RootCmd) taskSend() *serpent.Command {
// then polls until the task becomes active and its app state is idle.
// This merges build-watching and idle-polling into a single loop so
// that status changes (e.g. paused) are never missed between phases.
func waitForTaskIdle(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, task codersdk.Task, workspaceBuildID uuid.UUID) error {
func waitForTaskIdle(ctx context.Context, inv *serpent.Invocation, clk quartz.Clock, client *codersdk.Client, task codersdk.Task, workspaceBuildID uuid.UUID) error {
if workspaceBuildID != uuid.Nil {
if err := cliui.WorkspaceBuild(ctx, inv.Stdout, client, workspaceBuildID); err != nil {
return xerrors.Errorf("watch workspace build: %w", err)
@@ -162,13 +163,15 @@ func waitForTaskIdle(ctx context.Context, inv *serpent.Invocation, client *coder
// TODO(DanielleMaywood):
// When we have a streaming Task API, this should be converted
// away from polling.
ticker := time.NewTicker(5 * time.Second)
const pollInterval = 5 * time.Second
ticker := clk.NewTicker(time.Nanosecond, "task_send", "poll")
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
ticker.Reset(pollInterval, "task_send", "poll")
task, err := client.TaskByID(ctx, task.ID)
if err != nil {
return xerrors.Errorf("get task by id: %w", err)