-
Notifications
You must be signed in to change notification settings - Fork 23
fix(cloud-task): refresh run status before emitting watcher error #2201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -946,6 +946,41 @@ export class CloudTaskService extends TypedEventEmitter<CloudTaskEvents> { | |
| watcher.sseAbortController?.abort(); | ||
| watcher.sseAbortController = null; | ||
|
|
||
| void this.refreshStatusThenEmitError(watcher, error); | ||
| } | ||
|
|
||
| /** | ||
| * Refresh the run state one final time before emitting the watcher's error | ||
| * event. The stream typically fails *after* the run has already reached a | ||
| * terminal status (e.g. reconnect attempts exhausted, auth flicker), so a | ||
| * single REST poll often surfaces the real outcome. Clients can then move | ||
| * the task out of the "in_progress" UI even though the watcher is dead. | ||
| * | ||
| * Auth/access failures (401/403/404) are skipped because the same call | ||
| * would just fail again — and `fetchTaskRunSilent` would loop into another | ||
| * `failWatcher` via `shouldFailWatcherForFetchStatus` if we reused | ||
| * `fetchTaskRun` here. | ||
| */ | ||
| private async refreshStatusThenEmitError( | ||
| watcher: WatcherState, | ||
| error: CloudTaskConnectionError, | ||
| ): Promise<void> { | ||
| if (!isAuthFailureError(error)) { | ||
| const { run } = await this.fetchTaskRunSilent(watcher); | ||
| if (run && this.applyTaskRunState(watcher, run)) { | ||
| this.emit(CloudTaskEvent.Update, { | ||
| taskId: watcher.taskId, | ||
| runId: watcher.runId, | ||
| kind: "status", | ||
| status: watcher.lastStatus ?? undefined, | ||
| stage: watcher.lastStage, | ||
| output: watcher.lastOutput, | ||
| errorMessage: watcher.lastErrorMessage, | ||
| branch: watcher.lastBranch, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| this.emit(CloudTaskEvent.Update, { | ||
| taskId: watcher.taskId, | ||
| runId: watcher.runId, | ||
|
|
@@ -1219,6 +1254,26 @@ export class CloudTaskService extends TypedEventEmitter<CloudTaskEvents> { | |
| private async fetchTaskRun( | ||
| watcher: WatcherState, | ||
| ): Promise<TaskRunResponse | null> { | ||
| const { run, httpStatus } = await this.fetchTaskRunSilent(watcher); | ||
| if (run === null && httpStatus !== null) { | ||
| if (shouldFailWatcherForFetchStatus(httpStatus)) { | ||
| this.failWatcher( | ||
| watcherKey(watcher.taskId, watcher.runId), | ||
| createStreamStatusError(httpStatus).details, | ||
| ); | ||
| } | ||
| } | ||
| return run; | ||
| } | ||
|
|
||
| /** | ||
| * Same wire call as `fetchTaskRun` but never escalates a non-OK response | ||
| * into a watcher failure. Used from `failWatcher` so the final status poll | ||
| * can't re-enter watcher teardown. | ||
| */ | ||
| private async fetchTaskRunSilent( | ||
| watcher: WatcherState, | ||
| ): Promise<{ run: TaskRunResponse | null; httpStatus: number | null }> { | ||
| const url = `${watcher.apiHost}/api/projects/${watcher.teamId}/tasks/${watcher.taskId}/runs/${watcher.runId}/`; | ||
|
|
||
| try { | ||
|
|
@@ -1236,23 +1291,28 @@ export class CloudTaskService extends TypedEventEmitter<CloudTaskEvents> { | |
| taskId: watcher.taskId, | ||
| runId: watcher.runId, | ||
| }); | ||
| if (shouldFailWatcherForFetchStatus(authedResponse.status)) { | ||
| this.failWatcher( | ||
| watcherKey(watcher.taskId, watcher.runId), | ||
| createStreamStatusError(authedResponse.status).details, | ||
| ); | ||
| } | ||
| return null; | ||
| return { run: null, httpStatus: authedResponse.status }; | ||
| } | ||
|
|
||
| return (await authedResponse.json()) as TaskRunResponse; | ||
| return { | ||
| run: (await authedResponse.json()) as TaskRunResponse, | ||
| httpStatus: authedResponse.status, | ||
| }; | ||
| } catch (error) { | ||
| log.warn("Cloud task status fetch error", { | ||
| taskId: watcher.taskId, | ||
| runId: watcher.runId, | ||
| error, | ||
| }); | ||
| return null; | ||
| return { run: null, httpStatus: null }; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| function isAuthFailureError(error: CloudTaskConnectionError): boolean { | ||
| return ( | ||
| error.title === "Cloud authentication expired" || | ||
| error.title === "Cloud access denied" || | ||
| error.title === "Cloud run not found" | ||
| ); | ||
| } | ||
|
Comment on lines
+1312
to
+1318
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/code/src/main/services/cloud-task/service.ts
Line: 1312-1318
Comment:
`isAuthFailureError` duplicates the exact title strings defined inside `createStreamStatusError`, violating OnceAndOnlyOnce. If any of those three titles are ever updated in `createStreamStatusError`, `isAuthFailureError` silently returns `false` for what is actually an auth failure, causing an unnecessary `fetchTaskRunSilent` call. Since `CloudTaskConnectionError` is an interface you own, the simplest fix is to add an optional `httpStatus?: number` field to it — `createStreamStatusError` already receives the status code and can populate it, and `isAuthFailureError` can then check `[401, 403, 404].includes(error.httpStatus ?? 0)` instead of comparing title strings.
How can I resolve this? If you propose a fix, please make it concise. |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
isAuthFailureErrorcovers three distinct HTTP statuses (401, 403, 404). Per the team's preference for parameterised tests, all three cases should be verified — a regression in any one of them would go undetected. Consider usingit.each([[401], [403], [404]])(or Vitest'stest.each) to run the same assertions against all three codes.Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Prompt To Fix With AI