fix: surface Temporal dispatch failures
This commit is contained in:
@@ -1,12 +1,107 @@
|
||||
import type { WorkbenchApplication, WorkbenchTurnInput } from "./contracts.ts";
|
||||
import { ApplicationFailure, Context } from "@temporalio/activity";
|
||||
|
||||
export function createWorkbenchActivities(application: WorkbenchApplication) {
|
||||
import type { WorkbenchActivityConfig, WorkbenchApplication, WorkbenchEventPublisher, WorkbenchTurnInput } from "./contracts.ts";
|
||||
|
||||
export function createWorkbenchActivities(application: WorkbenchApplication, options: {
|
||||
activity: WorkbenchActivityConfig;
|
||||
eventPublisher?: WorkbenchEventPublisher | null;
|
||||
attempt?: () => number;
|
||||
}) {
|
||||
return {
|
||||
async dispatchWorkbenchTurn(input: WorkbenchTurnInput) {
|
||||
return application.dispatchTurn(input);
|
||||
try {
|
||||
return await application.dispatchTurn(input);
|
||||
} catch (error) {
|
||||
const attempt = positiveInteger(options.attempt?.() ?? currentActivityAttempt(), 1);
|
||||
const retryable = (error as any)?.retryable !== false;
|
||||
const exhausted = !retryable || attempt >= options.activity.retryMaximumAttempts;
|
||||
await publishDispatchFailure(options.eventPublisher, input, error, {
|
||||
attempt,
|
||||
maxAttempts: options.activity.retryMaximumAttempts,
|
||||
retryable,
|
||||
exhausted,
|
||||
backoffMs: exhausted ? null : retryDelayMs(options.activity, attempt)
|
||||
});
|
||||
throw ApplicationFailure.create({
|
||||
message: errorMessage(error),
|
||||
type: errorCode(error),
|
||||
nonRetryable: !retryable,
|
||||
details: [{
|
||||
code: errorCode(error),
|
||||
layer: (error as any)?.layer ?? null,
|
||||
status: (error as any)?.status ?? null,
|
||||
retryable,
|
||||
attempt,
|
||||
maxAttempts: options.activity.retryMaximumAttempts,
|
||||
valuesPrinted: false
|
||||
}]
|
||||
});
|
||||
}
|
||||
},
|
||||
async cancelWorkbenchTurn(input: { actor: WorkbenchTurnInput["actor"]; traceId: string; params: Record<string, unknown> }) {
|
||||
return application.cancelTurn(input);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function publishDispatchFailure(publisher: WorkbenchEventPublisher | null | undefined, input: WorkbenchTurnInput, error: unknown, state: { attempt: number; maxAttempts: number; retryable: boolean; exhausted: boolean; backoffMs: number | null }) {
|
||||
if (!publisher) return;
|
||||
const code = errorCode(error);
|
||||
const message = userErrorMessage(error);
|
||||
const createdAt = new Date().toISOString();
|
||||
await publisher.publish({
|
||||
traceId: input.traceId,
|
||||
sessionId: input.sessionId,
|
||||
event: {
|
||||
sourceEventId: `workbench:${input.traceId}:dispatch-failure:${state.attempt}`,
|
||||
type: state.exhausted ? "error" : "backend_status",
|
||||
phase: state.exhausted ? "dispatch-failed" : "dispatch-retry-scheduled",
|
||||
failureDomain: (error as any)?.layer === "billing" ? "billing" : "infrastructure",
|
||||
component: "hwlab-cloud-api",
|
||||
code,
|
||||
failureKind: code,
|
||||
summary: message,
|
||||
message,
|
||||
retryable: state.retryable,
|
||||
willRetry: !state.exhausted,
|
||||
retryPhase: state.exhausted ? "retryExhausted" : "retryScheduled",
|
||||
attempt: state.attempt,
|
||||
maxAttempts: state.maxAttempts,
|
||||
backoffMs: state.backoffMs,
|
||||
createdAt,
|
||||
terminal: false,
|
||||
valuesPrinted: false
|
||||
}
|
||||
});
|
||||
if (!state.exhausted) return;
|
||||
await publisher.publish({
|
||||
traceId: input.traceId,
|
||||
sessionId: input.sessionId,
|
||||
event: {
|
||||
sourceEventId: `workbench:${input.traceId}:terminal-failed`,
|
||||
type: "terminal_status",
|
||||
terminalStatus: "failed",
|
||||
status: "failed",
|
||||
failureDomain: (error as any)?.layer === "billing" ? "billing" : "infrastructure",
|
||||
code,
|
||||
failureKind: code,
|
||||
errorCode: code,
|
||||
message,
|
||||
retryable: state.retryable,
|
||||
attempt: state.attempt,
|
||||
maxAttempts: state.maxAttempts,
|
||||
createdAt,
|
||||
terminal: true,
|
||||
valuesPrinted: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function currentActivityAttempt() {
|
||||
try { return Context.current().info.attempt; } catch { return 1; }
|
||||
}
|
||||
function retryDelayMs(activity: WorkbenchActivityConfig, attempt: number) { return Math.min(activity.retryMaximumIntervalMs, activity.retryInitialIntervalMs * (2 ** Math.max(0, attempt - 1))); }
|
||||
function positiveInteger(value: unknown, fallback: number) { const parsed = Number(value); return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback; }
|
||||
function errorCode(error: unknown) { return String((error as any)?.code ?? "workbench_activity_failed").trim() || "workbench_activity_failed"; }
|
||||
function errorMessage(error: unknown) { return String((error as any)?.message ?? "Workbench activity failed").trim() || "Workbench activity failed"; }
|
||||
function userErrorMessage(error: unknown) { return String((error as any)?.userMessage ?? (error as any)?.message ?? "Workbench activity failed").trim() || "Workbench activity failed"; }
|
||||
|
||||
Reference in New Issue
Block a user