From d97f67808a09b7facc4766c5363c534f79bdede7 Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Thu, 10 Oct 2024 07:27:34 -0400 Subject: [PATCH 1/4] test(workflow): Add integration tests for workflowInfo().lastFailure --- .../test/src/test-integration-workflows.ts | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts index ebed2508f..1c179eeac 100644 --- a/packages/test/src/test-integration-workflows.ts +++ b/packages/test/src/test-integration-workflows.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'crypto'; import { ExecutionContext } from 'ava'; import { firstValueFrom, Subject } from 'rxjs'; -import { WorkflowFailedError } from '@temporalio/client'; +import { WorkflowFailedError, WorkflowHandleWithFirstExecutionRunId } from '@temporalio/client'; import * as activity from '@temporalio/activity'; import { msToNumber, tsToMs } from '@temporalio/common/lib/time'; import { TestWorkflowEnvironment } from '@temporalio/testing'; @@ -1124,6 +1124,73 @@ test('Abandon activity cancel before started works', async (t) => { t.pass(); }); +export async function WorkflowWillFail(): Promise { + // We will reset at this point + await workflow.sleep(1); + + const round = await workflow.proxyLocalActivities({ startToCloseTimeout: 1000 }).getRoundNumber(); + if (round === 0) { + throw ApplicationFailure.retryable('WorkflowWillFail', 'WorkflowWillFail'); + } + + workflow.log.warn(`$$$$$$$`, { lastFailure: workflow.workflowInfo().lastFailure }); + + return workflow.workflowInfo().lastFailure?.message; +} + +test("WorkflowInfo().lastFailure contains last run's failure on Workflow Failure", async (t) => { + const { createWorker, startWorkflow } = helpers(t); + let roundNumber = 0; + const worker = await createWorker({ + activities: { + getRoundNumber: () => roundNumber++, + }, + }); + const handle = await startWorkflow(WorkflowWillFail, { retry: { maximumAttempts: 2 } }); + await worker.runUntil(async () => { + const lastFailure = await handle.result(); + t.is(lastFailure, 'WorkflowWillFail'); + }); +}); + +test("WorkflowInfo().lastFailure contains last run's failure on Reset", async (t) => { + const client = t.context.env.client; + const { createWorker, startWorkflow } = helpers(t); + + let roundNumber = 0; + const worker = await createWorker({ + activities: { + getRoundNumber: () => roundNumber++, + }, + }); + + const firstExecHandle = (await startWorkflow(WorkflowWillFail)) as WorkflowHandleWithFirstExecutionRunId; + await worker.runUntil(async () => { + // Wait for the workflow to crash + await firstExecHandle.result().catch(() => undefined); + + const firstExecHistory = await firstExecHandle.fetchHistory(); + const resetPoint = firstExecHistory.events?.filter((ev) => ev.workflowTaskCompletedEventAttributes)?.[0]; + if (!resetPoint?.eventId) throw new Error('Reset point not found'); + + const resetResponse = await client.workflowService.resetWorkflowExecution({ + namespace: worker.options.namespace, + requestId: randomUUID(), + workflowExecution: { + workflowId: firstExecHandle.workflowId, + runId: firstExecHandle.firstExecutionRunId, + }, + reason: 'Some Reset Reason', + workflowTaskFinishEventId: resetPoint.eventId, + }); + + const secondExecHandle = client.workflow.getHandle(firstExecHandle.workflowId, resetResponse.runId); + const result = await secondExecHandle.result(); + + t.is('Some Reset Reason', result); + }); +}); + export const interceptors: workflow.WorkflowInterceptorsFactory = () => { const interceptorsFactoryFunc = module.exports[`${workflow.workflowInfo().workflowType}Interceptors`]; if (typeof interceptorsFactoryFunc === 'function') { From 6c50972ea1b1a58ec993302cf5a8b18b46778af3 Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Thu, 10 Oct 2024 07:31:22 -0400 Subject: [PATCH 2/4] Remove test case for wf reset; remove tracing log --- .../test/src/test-integration-workflows.ts | 44 ------------------- 1 file changed, 44 deletions(-) diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts index 1c179eeac..1982094e0 100644 --- a/packages/test/src/test-integration-workflows.ts +++ b/packages/test/src/test-integration-workflows.ts @@ -1125,16 +1125,10 @@ test('Abandon activity cancel before started works', async (t) => { }); export async function WorkflowWillFail(): Promise { - // We will reset at this point - await workflow.sleep(1); - const round = await workflow.proxyLocalActivities({ startToCloseTimeout: 1000 }).getRoundNumber(); if (round === 0) { throw ApplicationFailure.retryable('WorkflowWillFail', 'WorkflowWillFail'); } - - workflow.log.warn(`$$$$$$$`, { lastFailure: workflow.workflowInfo().lastFailure }); - return workflow.workflowInfo().lastFailure?.message; } @@ -1153,44 +1147,6 @@ test("WorkflowInfo().lastFailure contains last run's failure on Workflow Failure }); }); -test("WorkflowInfo().lastFailure contains last run's failure on Reset", async (t) => { - const client = t.context.env.client; - const { createWorker, startWorkflow } = helpers(t); - - let roundNumber = 0; - const worker = await createWorker({ - activities: { - getRoundNumber: () => roundNumber++, - }, - }); - - const firstExecHandle = (await startWorkflow(WorkflowWillFail)) as WorkflowHandleWithFirstExecutionRunId; - await worker.runUntil(async () => { - // Wait for the workflow to crash - await firstExecHandle.result().catch(() => undefined); - - const firstExecHistory = await firstExecHandle.fetchHistory(); - const resetPoint = firstExecHistory.events?.filter((ev) => ev.workflowTaskCompletedEventAttributes)?.[0]; - if (!resetPoint?.eventId) throw new Error('Reset point not found'); - - const resetResponse = await client.workflowService.resetWorkflowExecution({ - namespace: worker.options.namespace, - requestId: randomUUID(), - workflowExecution: { - workflowId: firstExecHandle.workflowId, - runId: firstExecHandle.firstExecutionRunId, - }, - reason: 'Some Reset Reason', - workflowTaskFinishEventId: resetPoint.eventId, - }); - - const secondExecHandle = client.workflow.getHandle(firstExecHandle.workflowId, resetResponse.runId); - const result = await secondExecHandle.result(); - - t.is('Some Reset Reason', result); - }); -}); - export const interceptors: workflow.WorkflowInterceptorsFactory = () => { const interceptorsFactoryFunc = module.exports[`${workflow.workflowInfo().workflowType}Interceptors`]; if (typeof interceptorsFactoryFunc === 'function') { From ca39486531da3c36af33d6f27bbef751bf843146 Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Fri, 11 Oct 2024 19:25:35 -0400 Subject: [PATCH 3/4] lint --- packages/test/src/test-integration-workflows.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts index 1982094e0..8a232edb2 100644 --- a/packages/test/src/test-integration-workflows.ts +++ b/packages/test/src/test-integration-workflows.ts @@ -1,7 +1,7 @@ import { randomUUID } from 'crypto'; import { ExecutionContext } from 'ava'; import { firstValueFrom, Subject } from 'rxjs'; -import { WorkflowFailedError, WorkflowHandleWithFirstExecutionRunId } from '@temporalio/client'; +import { WorkflowFailedError } from '@temporalio/client'; import * as activity from '@temporalio/activity'; import { msToNumber, tsToMs } from '@temporalio/common/lib/time'; import { TestWorkflowEnvironment } from '@temporalio/testing'; From 596446dd2decdf81a43090146af53f61d0c95efa Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Tue, 15 Oct 2024 11:14:15 -0400 Subject: [PATCH 4/4] Fix flake --- packages/test/src/test-integration-workflows.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/test/src/test-integration-workflows.ts b/packages/test/src/test-integration-workflows.ts index 8a232edb2..d73ee28f9 100644 --- a/packages/test/src/test-integration-workflows.ts +++ b/packages/test/src/test-integration-workflows.ts @@ -1125,21 +1125,15 @@ test('Abandon activity cancel before started works', async (t) => { }); export async function WorkflowWillFail(): Promise { - const round = await workflow.proxyLocalActivities({ startToCloseTimeout: 1000 }).getRoundNumber(); - if (round === 0) { - throw ApplicationFailure.retryable('WorkflowWillFail', 'WorkflowWillFail'); + if (workflow.workflowInfo().attempt > 1) { + return workflow.workflowInfo().lastFailure?.message; } - return workflow.workflowInfo().lastFailure?.message; + throw ApplicationFailure.retryable('WorkflowWillFail', 'WorkflowWillFail'); } test("WorkflowInfo().lastFailure contains last run's failure on Workflow Failure", async (t) => { const { createWorker, startWorkflow } = helpers(t); - let roundNumber = 0; - const worker = await createWorker({ - activities: { - getRoundNumber: () => roundNumber++, - }, - }); + const worker = await createWorker(); const handle = await startWorkflow(WorkflowWillFail, { retry: { maximumAttempts: 2 } }); await worker.runUntil(async () => { const lastFailure = await handle.result();