waitForTaskIdle optionally watches a workspace build to completion, 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.
(ctx context.Context, inv *serpent.Invocation, clk quartz.Clock, client *codersdk.Client, task codersdk.Task, workspaceBuildID uuid.UUID)
| 128 | // This merges build-watching and idle-polling into a single loop so |
| 129 | // that status changes (e.g. paused) are never missed between phases. |
| 130 | func waitForTaskIdle(ctx context.Context, inv *serpent.Invocation, clk quartz.Clock, client *codersdk.Client, task codersdk.Task, workspaceBuildID uuid.UUID) error { |
| 131 | if workspaceBuildID != uuid.Nil { |
| 132 | if err := cliui.WorkspaceBuild(ctx, inv.Stdout, client, workspaceBuildID); err != nil { |
| 133 | return xerrors.Errorf("watch workspace build: %w", err) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | cliui.Infof(inv.Stdout, "Waiting for task to become idle...") |
| 138 | |
| 139 | // NOTE(DanielleMaywood): |
| 140 | // It has been observed that the `TaskStatusError` state has |
| 141 | // appeared during a typical healthy startup [^0]. To combat |
| 142 | // this, we allow a 5 minute grace period where we allow |
| 143 | // `TaskStatusError` to surface without immediately failing. |
| 144 | // |
| 145 | // TODO(DanielleMaywood): |
| 146 | // Remove this grace period once the upstream agentapi health |
| 147 | // check no longer reports transient error states during normal |
| 148 | // startup. |
| 149 | // |
| 150 | // [0]: https://github.com/coder/coder/pull/22203#discussion_r2858002569 |
| 151 | const errorGracePeriod = 5 * time.Minute |
| 152 | gracePeriodDeadline := time.Now().Add(errorGracePeriod) |
| 153 | |
| 154 | // NOTE(DanielleMaywood): |
| 155 | // On resume the MCP may not report an initial app status, |
| 156 | // leaving CurrentState nil indefinitely. To avoid hanging |
| 157 | // forever we treat Active with nil CurrentState as idle |
| 158 | // after a grace period, giving the MCP time to report |
| 159 | // during normal startup. |
| 160 | const nilStateGracePeriod = 30 * time.Second |
| 161 | var nilStateDeadline time.Time |
| 162 | |
| 163 | // TODO(DanielleMaywood): |
| 164 | // When we have a streaming Task API, this should be converted |
| 165 | // away from polling. |
| 166 | const pollInterval = 5 * time.Second |
| 167 | ticker := clk.NewTicker(time.Nanosecond, "task_send", "poll") |
| 168 | defer ticker.Stop() |
| 169 | for { |
| 170 | select { |
| 171 | case <-ctx.Done(): |
| 172 | return ctx.Err() |
| 173 | case <-ticker.C: |
| 174 | ticker.Reset(pollInterval, "task_send", "poll") |
| 175 | task, err := client.TaskByID(ctx, task.ID) |
| 176 | if err != nil { |
| 177 | return xerrors.Errorf("get task by id: %w", err) |
| 178 | } |
| 179 | |
| 180 | switch task.Status { |
| 181 | case codersdk.TaskStatusInitializing, |
| 182 | codersdk.TaskStatusPending: |
| 183 | // Not yet active, keep polling. |
| 184 | continue |
| 185 | case codersdk.TaskStatusActive: |
| 186 | // Task is active; check app state. |
| 187 | if task.CurrentState == nil { |