From 5421b70354bf6e74105f8577a526ba36d92a9b5f Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Wed, 2 Apr 2025 11:53:22 -0700 Subject: [PATCH 1/9] update core to latest, 93471ac --- packages/core-bridge/sdk-core | 2 +- packages/core-bridge/src/conversions.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core-bridge/sdk-core b/packages/core-bridge/sdk-core index a3cd3fa8d..93471ac6d 160000 --- a/packages/core-bridge/sdk-core +++ b/packages/core-bridge/sdk-core @@ -1 +1 @@ -Subproject commit a3cd3fa8df9a52ef291d4266d721f1e04df4310d +Subproject commit 93471ac6d8bbf62839148a1bf97ef6dfd49aead0 diff --git a/packages/core-bridge/src/conversions.rs b/packages/core-bridge/src/conversions.rs index d76dfa65f..2c2da9d07 100644 --- a/packages/core-bridge/src/conversions.rs +++ b/packages/core-bridge/src/conversions.rs @@ -10,7 +10,7 @@ use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; use temporal_client::HttpConnectProxyOptions; use temporal_sdk_core::api::{ telemetry::{HistogramBucketOverrides, OtlpProtocol}, - worker::SlotKind, + worker::{PollerBehavior, SlotKind}, }; use temporal_sdk_core::{ ClientOptions, ClientOptionsBuilder, ClientTlsConfig, ResourceBasedSlotsOptions, @@ -523,8 +523,8 @@ impl ObjectHandleConversionsExt for Handle<'_, JsObject> { .use_worker_versioning(js_value_getter!(cx, self, "useVersioning", JsBoolean)) .no_remote_activities(!enable_remote_activities) .tuner(tuner) - .max_concurrent_wft_polls(max_concurrent_wft_polls) - .max_concurrent_at_polls(max_concurrent_at_polls) + .workflow_task_poller_behavior(PollerBehavior::SimpleMaximum(max_concurrent_wft_polls)) + .activity_task_poller_behavior(PollerBehavior::SimpleMaximum(max_concurrent_at_polls)) .nonsticky_to_sticky_poll_ratio(nonsticky_to_sticky_poll_ratio) .max_cached_workflows(max_cached_workflows) .sticky_queue_schedule_to_start_timeout(sticky_queue_schedule_to_start_timeout) From f3cd287205fb7a76b2f94cb002b990117e4335c5 Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Thu, 3 Apr 2025 14:49:13 -0700 Subject: [PATCH 2/9] need to figure out why workflow info doesn't show correct priority --- packages/activity/src/index.ts | 5 + packages/client/src/workflow-options.ts | 8 +- packages/common/src/activity-options.ts | 8 +- .../test/src/test-integration-split-two.ts | 1479 +++++++++-------- packages/test/src/workflows/index.ts | 1 + packages/test/src/workflows/priority.ts | 28 + packages/testing/src/index.ts | 1 + packages/worker/src/worker.ts | 3 + packages/workflow/src/interfaces.ts | 8 +- 9 files changed, 828 insertions(+), 713 deletions(-) create mode 100644 packages/test/src/workflows/priority.ts diff --git a/packages/activity/src/index.ts b/packages/activity/src/index.ts index b58644701..63a4e887d 100644 --- a/packages/activity/src/index.ts +++ b/packages/activity/src/index.ts @@ -73,6 +73,7 @@ import { AsyncLocalStorage } from 'node:async_hooks'; import { Logger, Duration, LogLevel, LogMetadata } from '@temporalio/common'; import { msToNumber } from '@temporalio/common/lib/time'; import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers'; +import { coresdk, temporal } from '@temporalio/proto'; export { ActivityFunction, @@ -198,6 +199,10 @@ export interface Info { * For Local Activities, this is set to the Workflow's Task Queue. */ readonly taskQueue: string; + /** + * Priority for Activity + */ + readonly priority?: temporal.api.common.v1.Priority } /** diff --git a/packages/client/src/workflow-options.ts b/packages/client/src/workflow-options.ts index 521fa6d37..9b2b7937d 100644 --- a/packages/client/src/workflow-options.ts +++ b/packages/client/src/workflow-options.ts @@ -1,7 +1,7 @@ import { CommonWorkflowOptions, SignalDefinition, WithWorkflowArgs, Workflow } from '@temporalio/common'; import { Duration, msOptionalToTs } from '@temporalio/common/lib/time'; import { Replace } from '@temporalio/common/lib/type-helpers'; -import { google } from '@temporalio/proto'; +import { google, temporal } from '@temporalio/proto'; export * from '@temporalio/common/lib/workflow-options'; @@ -41,6 +41,12 @@ export interface WorkflowOptions extends CommonWorkflowOptions { * */ startDelay?: Duration; + + /** + * Priority of this workflow. + * + */ + priority?: temporal.api.common.v1.Priority } export type WithCompiledWorkflowOptions = Replace< diff --git a/packages/common/src/activity-options.ts b/packages/common/src/activity-options.ts index 8ad6e2b89..e4e5090b5 100644 --- a/packages/common/src/activity-options.ts +++ b/packages/common/src/activity-options.ts @@ -1,8 +1,9 @@ -import type { coresdk } from '@temporalio/proto'; +import { coresdk, temporal } from '@temporalio/proto'; import { RetryPolicy } from './retry-policy'; import { Duration } from './time'; import { VersioningIntent } from './versioning-intent'; import { makeProtoEnumConverters } from './internal-workflow'; +import Priority = temporal.api.common.v1.Priority; export const ActivityCancellationType = { TRY_CANCEL: 'TRY_CANCEL', @@ -122,6 +123,11 @@ export interface ActivityOptions { * @experimental The Worker Versioning API is still being designed. Major changes are expected. */ versioningIntent?: VersioningIntent; + + /** + * Priority of an activity + */ + priority?: Priority; } /** diff --git a/packages/test/src/test-integration-split-two.ts b/packages/test/src/test-integration-split-two.ts index 80e5f1530..53e0553a0 100644 --- a/packages/test/src/test-integration-split-two.ts +++ b/packages/test/src/test-integration-split-two.ts @@ -15,10 +15,12 @@ import { msToNumber, tsToMs } from '@temporalio/common/lib/time'; import { decode as payloadDecode, decodeFromPayloadsAtIndex } from '@temporalio/common/lib/internal-non-workflow'; import { condition, defineQuery, defineSignal, setDefaultQueryHandler, setHandler, sleep } from '@temporalio/workflow'; +import { temporal } from '@temporalio/proto'; import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration'; import * as activities from './activities'; import * as workflows from './workflows'; import { makeTestFn, configMacro } from './helpers-integration-multi-codec'; +import Priority = temporal.api.common.v1.Priority; // Note: re-export shared workflows (or long workflows) // - review the files where these workflows are shared @@ -27,727 +29,784 @@ export * from './workflows'; const test = makeTestFn(() => createTestWorkflowBundle({ workflowsPath: __filename })); test.macro(configMacro); -test('WorkflowOptions are passed correctly with defaults', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.argsAndReturn, { - args: ['hey', undefined, Buffer.from('def')], - }); - await worker.runUntil(handle.result()); - const execution = await handle.describe(); - t.deepEqual(execution.type, 'argsAndReturn'); - const indexedFields = execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!; - const indexedFieldKeys = Object.keys(indexedFields); - - let encodedId: any; - if (indexedFieldKeys.includes('BinaryChecksums')) { - encodedId = indexedFields.BinaryChecksums!; - } else { - encodedId = indexedFields.BuildIds!; - } - t.true(encodedId != null); - - const checksums = searchAttributePayloadConverter.fromPayload(encodedId); - console.log(checksums); - t.true(Array.isArray(checksums)); - t.regex((checksums as string[]).pop()!, /@temporalio\/worker@\d+\.\d+\.\d+/); - t.is(execution.raw.executionConfig?.taskQueue?.name, taskQueue); - t.is( - execution.raw.executionConfig?.taskQueue?.kind, - iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL - ); - t.is(execution.raw.executionConfig?.workflowRunTimeout, null); - t.is(execution.raw.executionConfig?.workflowExecutionTimeout, null); -}); - -test('WorkflowOptions are passed correctly', configMacro, async (t, config) => { +// test('WorkflowOptions are passed correctly with defaults', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(workflows.argsAndReturn, { +// args: ['hey', undefined, Buffer.from('def')], +// }); +// await worker.runUntil(handle.result()); +// const execution = await handle.describe(); +// t.deepEqual(execution.type, 'argsAndReturn'); +// const indexedFields = execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!; +// const indexedFieldKeys = Object.keys(indexedFields); +// +// let encodedId: any; +// if (indexedFieldKeys.includes('BinaryChecksums')) { +// encodedId = indexedFields.BinaryChecksums!; +// } else { +// encodedId = indexedFields.BuildIds!; +// } +// t.true(encodedId != null); +// +// const checksums = searchAttributePayloadConverter.fromPayload(encodedId); +// console.log(checksums); +// t.true(Array.isArray(checksums)); +// t.regex((checksums as string[]).pop()!, /@temporalio\/worker@\d+\.\d+\.\d+/); +// t.is(execution.raw.executionConfig?.taskQueue?.name, taskQueue); +// t.is( +// execution.raw.executionConfig?.taskQueue?.kind, +// iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL +// ); +// t.is(execution.raw.executionConfig?.workflowRunTimeout, null); +// t.is(execution.raw.executionConfig?.workflowExecutionTimeout, null); +// }); +// // +// test('WorkflowOptions are passed correctly', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// // Throws because we use a different task queue +// const worker = await createWorkerWithDefaults(t); +// const options = { +// memo: { a: 'b' }, +// searchAttributes: { CustomIntField: [3] }, +// workflowRunTimeout: '2s', +// workflowExecutionTimeout: '3s', +// workflowTaskTimeout: '1s', +// taskQueue: 'diff-task-queue', +// priority: Priority.create({ priorityKey: 1 }) +// } as const; +// const handle = await startWorkflow(workflows.sleeper, options); +// async function fromPayload(payload: Payload) { +// const payloadCodecs = env.client.options.dataConverter.payloadCodecs ?? []; +// const [decodedPayload] = await payloadDecode(payloadCodecs, [payload]); +// return defaultPayloadConverter.fromPayload(decodedPayload); +// } +// await t.throwsAsync(worker.runUntil(handle.result()), { +// instanceOf: WorkflowFailedError, +// message: 'Workflow execution timed out', +// }); +// const execution = await handle.describe(); +// t.deepEqual( +// execution.raw.workflowExecutionInfo?.type, +// iface.temporal.api.common.v1.WorkflowType.create({ name: 'sleeper' }) +// ); +// t.deepEqual(await fromPayload(execution.raw.workflowExecutionInfo!.memo!.fields!.a!), 'b'); +// t.deepEqual( +// searchAttributePayloadConverter.fromPayload( +// execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!.CustomIntField! +// ), +// [3] +// ); +// t.deepEqual(execution.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation +// t.is(execution.raw.executionConfig?.taskQueue?.name, 'diff-task-queue'); +// t.is( +// execution.raw.executionConfig?.taskQueue?.kind, +// iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL +// ); +// +// t.is(tsToMs(execution.raw.executionConfig!.workflowRunTimeout!), msToNumber(options.workflowRunTimeout)); +// t.is(tsToMs(execution.raw.executionConfig!.workflowExecutionTimeout!), msToNumber(options.workflowExecutionTimeout)); +// t.is(tsToMs(execution.raw.executionConfig!.defaultWorkflowTaskTimeout!), msToNumber(options.workflowTaskTimeout)); +// }); +// +// test('WorkflowHandle.result() throws if terminated', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(workflows.sleeper, { +// args: [1000000], +// }); +// await t.throwsAsync( +// worker.runUntil(async () => { +// await handle.terminate('hasta la vista baby'); +// await handle.result(); +// }), +// { +// instanceOf: WorkflowFailedError, +// message: 'hasta la vista baby', +// } +// ); +// }); +// +// test('WorkflowHandle.result() throws if continued as new', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// await worker.runUntil(async () => { +// const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewSameWorkflow, { +// followRuns: false, +// }); +// let err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); +// +// if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion +// const client = env.client; +// let continueWorkflowHandle = client.workflow.getHandle( +// originalWorkflowHandle.workflowId, +// err.newExecutionRunId, +// { +// followRuns: false, +// } +// ); +// +// await continueWorkflowHandle.signal(workflows.continueAsNewSignal); +// err = await t.throwsAsync(continueWorkflowHandle.result(), { +// instanceOf: WorkflowContinuedAsNewError, +// }); +// if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion +// +// continueWorkflowHandle = client.workflow.getHandle( +// continueWorkflowHandle.workflowId, +// err.newExecutionRunId +// ); +// await continueWorkflowHandle.result(); +// }); +// }); +// +// test('WorkflowHandle.result() follows chain of execution', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// await worker.runUntil( +// executeWorkflow(workflows.continueAsNewSameWorkflow, { +// args: ['execute', 'none'], +// }) +// ); +// t.pass(); +// }); +// +// test('continue-as-new-to-different-workflow', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults, loadedDataConverter } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// await worker.runUntil(async () => { +// const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { +// followRuns: false, +// }); +// const err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); +// if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion +// const workflow = client.workflow.getHandle( +// originalWorkflowHandle.workflowId, +// err.newExecutionRunId, +// { +// followRuns: false, +// } +// ); +// await workflow.result(); +// const info = await workflow.describe(); +// t.is(info.raw.workflowExecutionInfo?.type?.name, 'sleeper'); +// const history = await workflow.fetchHistory(); +// const timeSlept = await decodeFromPayloadsAtIndex( +// loadedDataConverter, +// 0, +// history?.events?.[0].workflowExecutionStartedEventAttributes?.input?.payloads +// ); +// t.is(timeSlept, 1); +// }); +// }); +// +// test('continue-as-new-to-same-workflow keeps memo and search attributes', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(workflows.continueAsNewSameWorkflow, { +// memo: { +// note: 'foo', +// }, +// searchAttributes: { +// CustomKeywordField: ['test-value'], +// CustomIntField: [1], +// }, +// followRuns: true, +// }); +// await worker.runUntil(async () => { +// await handle.signal(workflows.continueAsNewSignal); +// await handle.result(); +// const execution = await handle.describe(); +// t.not(execution.runId, handle.firstExecutionRunId); +// t.deepEqual(execution.memo, { note: 'foo' }); +// t.deepEqual(execution.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation +// t.deepEqual(execution.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation +// }); +// }); +// +// test( +// 'continue-as-new-to-different-workflow keeps memo and search attributes by default', +// configMacro, +// async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { +// followRuns: true, +// memo: { +// note: 'foo', +// }, +// searchAttributes: { +// CustomKeywordField: ['test-value'], +// CustomIntField: [1], +// }, +// }); +// await worker.runUntil(async () => { +// await handle.result(); +// const info = await handle.describe(); +// t.is(info.type, 'sleeper'); +// t.not(info.runId, handle.firstExecutionRunId); +// t.deepEqual(info.memo, { note: 'foo' }); +// t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation +// t.deepEqual(info.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation +// }); +// } +// ); +// +// test('continue-as-new-to-different-workflow can set memo and search attributes', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { +// args: [ +// 1, +// { +// memo: { +// note: 'bar', +// }, +// searchAttributes: { +// CustomKeywordField: ['test-value-2'], +// CustomIntField: [3], +// }, +// }, +// ], +// followRuns: true, +// memo: { +// note: 'foo', +// }, +// searchAttributes: { +// CustomKeywordField: ['test-value'], +// CustomIntField: [1], +// }, +// }); +// await worker.runUntil(async () => { +// await handle.result(); +// const info = await handle.describe(); +// t.is(info.type, 'sleeper'); +// t.not(info.runId, handle.firstExecutionRunId); +// t.deepEqual(info.memo, { note: 'bar' }); +// t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value-2']); // eslint-disable-line deprecation/deprecation +// t.deepEqual(info.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation +// }); +// }); +// +// test('signalWithStart works as intended and returns correct runId', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// const originalWorkflowHandle = await client.workflow.signalWithStart(workflows.interruptableWorkflow, { +// taskQueue, +// workflowId: uuid4(), +// signal: workflows.interruptSignal, +// signalArgs: ['interrupted from signalWithStart'], +// }); +// await worker.runUntil(async () => { +// let err: WorkflowFailedError | undefined = await t.throwsAsync(originalWorkflowHandle.result(), { +// instanceOf: WorkflowFailedError, +// }); +// if (!(err?.cause instanceof ApplicationFailure)) { +// return t.fail('Expected err.cause to be an instance of ApplicationFailure'); +// } +// t.is(err.cause.message, 'interrupted from signalWithStart'); +// +// // Test returned runId +// const handle = client.workflow.getHandle( +// originalWorkflowHandle.workflowId, +// originalWorkflowHandle.signaledRunId +// ); +// err = await t.throwsAsync(handle.result(), { +// instanceOf: WorkflowFailedError, +// }); +// if (!(err?.cause instanceof ApplicationFailure)) { +// return t.fail('Expected err.cause to be an instance of ApplicationFailure'); +// } +// t.is(err.cause.message, 'interrupted from signalWithStart'); +// }); +// }); +// +// test('activity-failures', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t, { activities }); +// await worker.runUntil(executeWorkflow(workflows.activityFailures)); +// t.pass(); +// }); +// +// export async function sleepInvalidDuration(): Promise { +// await sleep(0); +// await new Promise((resolve) => setTimeout(resolve, -1)); +// } +// +// test('sleepInvalidDuration is caught in Workflow runtime', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// +// const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// await worker.runUntil(executeWorkflow(sleepInvalidDuration)); +// t.pass(); +// }); +// +// test('unhandledRejection causes WFT to fail', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(workflows.throwUnhandledRejection, { +// // throw an exception that our worker can associate with a running workflow +// args: [{ crashWorker: false }], +// }); +// await worker.runUntil( +// asyncRetry( +// async () => { +// const history = await handle.fetchHistory(); +// const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); +// if (wftFailedEvent === undefined) { +// throw new Error('No WFT failed event'); +// } +// const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; +// if (!failure) { +// t.fail(); +// return; +// } +// t.is(failure.message, 'Unhandled Promise rejection: Error: unhandled rejection'); +// t.true(failure.stackTrace?.includes(`Error: unhandled rejection`)); +// t.is(failure.cause?.cause?.message, 'root failure'); +// }, +// { minTimeout: 300, factor: 1, retries: 100 } +// ) +// ); +// await handle.terminate(); +// }); +// +// export async function throwObject(): Promise { +// throw { plainObject: true }; +// } +// +// test('throwObject includes message with our recommendation', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(throwObject); +// await worker.runUntil( +// asyncRetry( +// async () => { +// const history = await handle.fetchHistory(); +// const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); +// if (wftFailedEvent === undefined) { +// throw new Error('No WFT failed event'); +// } +// const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; +// if (!failure) { +// t.fail(); +// return; +// } +// t.is( +// failure.message, +// '{"plainObject":true} [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' +// ); +// }, +// { minTimeout: 300, factor: 1, retries: 100 } +// ) +// ); +// await handle.terminate(); +// }); +// +// export async function throwBigInt(): Promise { +// throw 42n; +// } +// +// test('throwBigInt includes message with our recommendation', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(throwBigInt); +// await worker.runUntil( +// asyncRetry( +// async () => { +// const history = await handle.fetchHistory(); +// const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); +// if (wftFailedEvent === undefined) { +// throw new Error('No WFT failed event'); +// } +// const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; +// if (!failure) { +// t.fail(); +// return; +// } +// t.is( +// failure.message, +// '42 [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' +// ); +// }, +// { minTimeout: 300, factor: 1, retries: 100 } +// ) +// ); +// await handle.terminate(); +// }); +// +// test('Workflow RetryPolicy kicks in with retryable failure', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(workflows.throwAsync, { +// args: ['retryable'], +// retry: { +// initialInterval: 1, +// maximumInterval: 1, +// maximumAttempts: 2, +// }, +// }); +// await worker.runUntil(async () => { +// await t.throwsAsync(handle.result()); +// // Verify retry happened +// const { runId } = await handle.describe(); +// t.not(runId, handle.firstExecutionRunId); +// }); +// }); +// +// test('Workflow RetryPolicy ignored with nonRetryable failure', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(workflows.throwAsync, { +// args: ['nonRetryable'], +// retry: { +// initialInterval: 1, +// maximumInterval: 1, +// maximumAttempts: 2, +// }, +// }); +// await worker.runUntil(async () => { +// await t.throwsAsync(handle.result()); +// const res = await handle.describe(); +// t.is( +// res.raw.workflowExecutionInfo?.status, +// iface.temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED +// ); +// // Verify retry did not happen +// const { runId } = await handle.describe(); +// t.is(runId, handle.firstExecutionRunId); +// }); +// }); +// +// test('WorkflowClient.start fails with WorkflowExecutionAlreadyStartedError', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// const handle = await startWorkflow(workflows.sleeper, { +// args: [10000000], +// }); +// try { +// await worker.runUntil( +// t.throwsAsync( +// client.workflow.start(workflows.sleeper, { +// taskQueue, +// workflowId: handle.workflowId, +// }), +// { +// instanceOf: WorkflowExecutionAlreadyStartedError, +// message: 'Workflow execution already started', +// } +// ) +// ); +// } finally { +// await handle.terminate(); +// } +// }); +// +// test( +// 'WorkflowClient.signalWithStart fails with WorkflowExecutionAlreadyStartedError', +// configMacro, +// async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// const handle = await startWorkflow(workflows.sleeper); +// await worker.runUntil(async () => { +// await handle.result(); +// await t.throwsAsync( +// client.workflow.signalWithStart(workflows.sleeper, { +// taskQueue: 'test', +// workflowId: handle.workflowId, +// signal: workflows.interruptSignal, +// signalArgs: ['interrupted from signalWithStart'], +// workflowIdReusePolicy: 'REJECT_DUPLICATE', +// }), +// { +// instanceOf: WorkflowExecutionAlreadyStartedError, +// message: 'Workflow execution already started', +// } +// ); +// }); +// } +// ); +// +// test('Handle from WorkflowClient.start follows only own execution chain', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); +// const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); +// await worker.runUntil(async () => { +// await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); +// const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { +// taskQueue: 'test', +// workflowId: handleFromThrowerStart.workflowId, +// args: [1_000_000], +// }); +// try { +// await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); +// } finally { +// await handleFromSleeperStart.terminate(); +// } +// }); +// }); +// +// test('Handle from WorkflowClient.signalWithStart follows only own execution chain', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// const handleFromThrowerStart = await client.workflow.signalWithStart(workflows.throwAsync, { +// taskQueue, +// workflowId: uuid4(), +// signal: 'unblock', +// }); +// const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); +// await worker.runUntil(async () => { +// await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); +// const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { +// taskQueue, +// workflowId: handleFromThrowerStart.workflowId, +// args: [1_000_000], +// }); +// try { +// await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); +// } finally { +// await handleFromSleeperStart.terminate(); +// } +// }); +// }); +// +// test('Handle from WorkflowClient.getHandle follows only own execution chain', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); +// const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId, undefined, { +// firstExecutionRunId: handleFromThrowerStart.firstExecutionRunId, +// }); +// await worker.runUntil(async () => { +// await t.throwsAsync(handleFromThrowerStart.result(), { message: /.*/ }); +// const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { +// taskQueue, +// workflowId: handleFromThrowerStart.workflowId, +// args: [1_000_000], +// }); +// try { +// await t.throwsAsync(handleFromGet.result(), { message: 'Workflow execution failed' }); +// } finally { +// await handleFromSleeperStart.terminate(); +// } +// }); +// }); +// +// test('Handle from WorkflowClient.start terminates run after continue as new', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { +// args: [1_000_000], +// }); +// const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId, { +// followRuns: false, +// }); +// await worker.runUntil(async () => { +// await t.throwsAsync(handleFromGet.result(), { instanceOf: WorkflowContinuedAsNewError }); +// await handleFromStart.terminate(); +// await t.throwsAsync(handleFromStart.result(), { message: 'Workflow execution terminated' }); +// }); +// }); +// +// test( +// 'Handle from WorkflowClient.getHandle does not terminate run after continue as new if given runId', +// configMacro, +// async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const client = env.client; +// const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { +// args: [1_000_000], +// followRuns: false, +// }); +// const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId); +// await worker.runUntil(async () => { +// await t.throwsAsync(handleFromStart.result(), { instanceOf: WorkflowContinuedAsNewError }); +// try { +// await t.throwsAsync(handleFromGet.terminate(), { +// instanceOf: WorkflowNotFoundError, +// message: 'workflow execution already completed', +// }); +// } finally { +// await client.workflow.getHandle(handleFromStart.workflowId).terminate(); +// } +// }); +// } +// ); +// +// test( +// 'Runtime does not issue cancellations for activities and timers that throw during validation', +// configMacro, +// async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// await worker.runUntil(executeWorkflow(workflows.cancelScopeOnFailedValidation)); +// t.pass(); +// } +// ); +// +// const mutateWorkflowStateQuery = defineQuery('mutateWorkflowState'); +// export async function queryAndCondition(): Promise { +// let mutated = false; +// // Not a valid query, used to verify that condition isn't triggered for query jobs +// setHandler(mutateWorkflowStateQuery, () => void (mutated = true)); +// await condition(() => mutated); +// } +// +// test('Query does not cause condition to be triggered', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t); +// const handle = await startWorkflow(queryAndCondition); +// await worker.runUntil(handle.query(mutateWorkflowStateQuery)); +// await handle.terminate(); +// // Worker did not crash +// t.pass(); +// }); +// +// const completeSignal = defineSignal('complete'); +// const definedQuery = defineQuery('query-handler-type'); +// +// interface QueryNameAndArgs { +// name: string; +// queryName?: string; +// args: any[]; +// } +// +// export async function workflowWithMaybeDefinedQuery(useDefinedQuery: boolean): Promise { +// let complete = false; +// setHandler(completeSignal, () => { +// complete = true; +// }); +// setDefaultQueryHandler((queryName: string, ...args: any[]) => { +// return { name: 'default', queryName, args }; +// }); +// if (useDefinedQuery) { +// setHandler(definedQuery, (...args: any[]) => { +// return { name: definedQuery.name, args }; +// }); +// } +// +// await condition(() => complete); +// } +// +// test('default query handler is used if requested query does not exist', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t, { activities }); +// const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { +// args: [false], +// }); +// await worker.runUntil(async () => { +// const args = ['test', 'args']; +// const result = await handle.query(definedQuery, ...args); +// t.deepEqual(result, { name: 'default', queryName: definedQuery.name, args }); +// }); +// }); +// +// test('default query handler is not used if requested query exists', configMacro, async (t, config) => { +// const { env, createWorkerWithDefaults } = config; +// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); +// const worker = await createWorkerWithDefaults(t, { activities }); +// const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { +// args: [true], +// }); +// await worker.runUntil(async () => { +// const args = ['test', 'args']; +// const result = await handle.query('query-handler-type', ...args); +// t.deepEqual(result, { name: definedQuery.name, args }); +// }); +// }); + +test('Priority', configMacro, async (t, config) => { const { env, createWorkerWithDefaults } = config; const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - // Throws because we use a different task queue const worker = await createWorkerWithDefaults(t); const options = { - memo: { a: 'b' }, - searchAttributes: { CustomIntField: [3] }, - workflowRunTimeout: '2s', - workflowExecutionTimeout: '3s', - workflowTaskTimeout: '1s', - taskQueue: 'diff-task-queue', + args: [false, 1], + priority: Priority.create({ priorityKey: 1 }) } as const; - const handle = await startWorkflow(workflows.sleeper, options); - async function fromPayload(payload: Payload) { - const payloadCodecs = env.client.options.dataConverter.payloadCodecs ?? []; - const [decodedPayload] = await payloadDecode(payloadCodecs, [payload]); - return defaultPayloadConverter.fromPayload(decodedPayload); - } + const handle = await startWorkflow(workflows.priorityWorkflow, options); await t.throwsAsync(worker.runUntil(handle.result()), { instanceOf: WorkflowFailedError, message: 'Workflow execution timed out', }); - const execution = await handle.describe(); - t.deepEqual( - execution.raw.workflowExecutionInfo?.type, - iface.temporal.api.common.v1.WorkflowType.create({ name: 'sleeper' }) - ); - t.deepEqual(await fromPayload(execution.raw.workflowExecutionInfo!.memo!.fields!.a!), 'b'); - t.deepEqual( - searchAttributePayloadConverter.fromPayload( - execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!.CustomIntField! - ), - [3] - ); - t.deepEqual(execution.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation - t.is(execution.raw.executionConfig?.taskQueue?.name, 'diff-task-queue'); - t.is( - execution.raw.executionConfig?.taskQueue?.kind, - iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL - ); - - t.is(tsToMs(execution.raw.executionConfig!.workflowRunTimeout!), msToNumber(options.workflowRunTimeout)); - t.is(tsToMs(execution.raw.executionConfig!.workflowExecutionTimeout!), msToNumber(options.workflowExecutionTimeout)); - t.is(tsToMs(execution.raw.executionConfig!.defaultWorkflowTaskTimeout!), msToNumber(options.workflowTaskTimeout)); -}); - -test('WorkflowHandle.result() throws if terminated', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.sleeper, { - args: [1000000], - }); - await t.throwsAsync( - worker.runUntil(async () => { - await handle.terminate('hasta la vista baby'); - await handle.result(); - }), - { - instanceOf: WorkflowFailedError, - message: 'hasta la vista baby', - } - ); -}); - -test('WorkflowHandle.result() throws if continued as new', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(async () => { - const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewSameWorkflow, { - followRuns: false, - }); - let err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); - - if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion - const client = env.client; - let continueWorkflowHandle = client.workflow.getHandle( - originalWorkflowHandle.workflowId, - err.newExecutionRunId, - { - followRuns: false, - } - ); - - await continueWorkflowHandle.signal(workflows.continueAsNewSignal); - err = await t.throwsAsync(continueWorkflowHandle.result(), { - instanceOf: WorkflowContinuedAsNewError, - }); - if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion - - continueWorkflowHandle = client.workflow.getHandle( - continueWorkflowHandle.workflowId, - err.newExecutionRunId - ); - await continueWorkflowHandle.result(); - }); -}); - -test('WorkflowHandle.result() follows chain of execution', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil( - executeWorkflow(workflows.continueAsNewSameWorkflow, { - args: ['execute', 'none'], - }) - ); - t.pass(); -}); - -test('continue-as-new-to-different-workflow', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults, loadedDataConverter } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - await worker.runUntil(async () => { - const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - followRuns: false, - }); - const err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); - if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion - const workflow = client.workflow.getHandle( - originalWorkflowHandle.workflowId, - err.newExecutionRunId, - { - followRuns: false, - } - ); - await workflow.result(); - const info = await workflow.describe(); - t.is(info.raw.workflowExecutionInfo?.type?.name, 'sleeper'); - const history = await workflow.fetchHistory(); - const timeSlept = await decodeFromPayloadsAtIndex( - loadedDataConverter, - 0, - history?.events?.[0].workflowExecutionStartedEventAttributes?.input?.payloads - ); - t.is(timeSlept, 1); - }); -}); - -test('continue-as-new-to-same-workflow keeps memo and search attributes', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.continueAsNewSameWorkflow, { - memo: { - note: 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - }, - followRuns: true, - }); - await worker.runUntil(async () => { - await handle.signal(workflows.continueAsNewSignal); - await handle.result(); - const execution = await handle.describe(); - t.not(execution.runId, handle.firstExecutionRunId); - t.deepEqual(execution.memo, { note: 'foo' }); - t.deepEqual(execution.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation - t.deepEqual(execution.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation - }); -}); - -test( - 'continue-as-new-to-different-workflow keeps memo and search attributes by default', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - followRuns: true, - memo: { - note: 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - }, - }); - await worker.runUntil(async () => { - await handle.result(); - const info = await handle.describe(); - t.is(info.type, 'sleeper'); - t.not(info.runId, handle.firstExecutionRunId); - t.deepEqual(info.memo, { note: 'foo' }); - t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation - t.deepEqual(info.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation - }); - } -); - -test('continue-as-new-to-different-workflow can set memo and search attributes', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - args: [ - 1, - { - memo: { - note: 'bar', - }, - searchAttributes: { - CustomKeywordField: ['test-value-2'], - CustomIntField: [3], - }, - }, - ], - followRuns: true, - memo: { - note: 'foo', - }, - searchAttributes: { - CustomKeywordField: ['test-value'], - CustomIntField: [1], - }, - }); - await worker.runUntil(async () => { - await handle.result(); - const info = await handle.describe(); - t.is(info.type, 'sleeper'); - t.not(info.runId, handle.firstExecutionRunId); - t.deepEqual(info.memo, { note: 'bar' }); - t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value-2']); // eslint-disable-line deprecation/deprecation - t.deepEqual(info.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation - }); -}); - -test('signalWithStart works as intended and returns correct runId', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const originalWorkflowHandle = await client.workflow.signalWithStart(workflows.interruptableWorkflow, { - taskQueue, - workflowId: uuid4(), - signal: workflows.interruptSignal, - signalArgs: ['interrupted from signalWithStart'], - }); - await worker.runUntil(async () => { - let err: WorkflowFailedError | undefined = await t.throwsAsync(originalWorkflowHandle.result(), { - instanceOf: WorkflowFailedError, - }); - if (!(err?.cause instanceof ApplicationFailure)) { - return t.fail('Expected err.cause to be an instance of ApplicationFailure'); - } - t.is(err.cause.message, 'interrupted from signalWithStart'); - - // Test returned runId - const handle = client.workflow.getHandle( - originalWorkflowHandle.workflowId, - originalWorkflowHandle.signaledRunId - ); - err = await t.throwsAsync(handle.result(), { - instanceOf: WorkflowFailedError, - }); - if (!(err?.cause instanceof ApplicationFailure)) { - return t.fail('Expected err.cause to be an instance of ApplicationFailure'); - } - t.is(err.cause.message, 'interrupted from signalWithStart'); - }); -}); - -test('activity-failures', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - await worker.runUntil(executeWorkflow(workflows.activityFailures)); - t.pass(); -}); - -export async function sleepInvalidDuration(): Promise { - await sleep(0); - await new Promise((resolve) => setTimeout(resolve, -1)); -} - -test('sleepInvalidDuration is caught in Workflow runtime', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(executeWorkflow(sleepInvalidDuration)); - t.pass(); -}); - -test('unhandledRejection causes WFT to fail', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.throwUnhandledRejection, { - // throw an exception that our worker can associate with a running workflow - args: [{ crashWorker: false }], - }); - await worker.runUntil( - asyncRetry( - async () => { - const history = await handle.fetchHistory(); - const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); - if (wftFailedEvent === undefined) { - throw new Error('No WFT failed event'); + // const execution = await handle.describe(); + let firstChild = true; + const history = await handle.fetchHistory(); + for (const event of history?.events ?? []) { + switch (event.eventType) { + case temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: + t.deepEqual( + event.workflowExecutionStartedEventAttributes?.priority?.priorityKey, 1 + ) + break; + case temporal.api.enums.v1.EventType.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED: { + const pri = event.startChildWorkflowExecutionInitiatedEventAttributes?.priority?.priorityKey; + if (firstChild) { + t.deepEqual(pri, 4); + firstChild = false; + } else { + t.deepEqual(pri, 2); + } + break; } - const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; - if (!failure) { - t.fail(); - return; - } - t.is(failure.message, 'Unhandled Promise rejection: Error: unhandled rejection'); - t.true(failure.stackTrace?.includes(`Error: unhandled rejection`)); - t.is(failure.cause?.cause?.message, 'root failure'); - }, - { minTimeout: 300, factor: 1, retries: 100 } - ) - ); - await handle.terminate(); -}); - -export async function throwObject(): Promise { - throw { plainObject: true }; -} - -test('throwObject includes message with our recommendation', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(throwObject); - await worker.runUntil( - asyncRetry( - async () => { - const history = await handle.fetchHistory(); - const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); - if (wftFailedEvent === undefined) { - throw new Error('No WFT failed event'); - } - const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; - if (!failure) { - t.fail(); - return; - } - t.is( - failure.message, - '{"plainObject":true} [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' + case temporal.api.enums.v1.EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: + t.deepEqual( + event.activityTaskScheduledEventAttributes?.priority?.priorityKey, 5 ); - }, - { minTimeout: 300, factor: 1, retries: 100 } - ) - ); - await handle.terminate(); -}); - -export async function throwBigInt(): Promise { - throw 42n; -} - -test('throwBigInt includes message with our recommendation', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(throwBigInt); - await worker.runUntil( - asyncRetry( - async () => { - const history = await handle.fetchHistory(); - const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); - if (wftFailedEvent === undefined) { - throw new Error('No WFT failed event'); - } - const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; - if (!failure) { - t.fail(); - return; - } - t.is( - failure.message, - '42 [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' - ); - }, - { minTimeout: 300, factor: 1, retries: 100 } - ) - ); - await handle.terminate(); -}); - -test('Workflow RetryPolicy kicks in with retryable failure', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.throwAsync, { - args: ['retryable'], - retry: { - initialInterval: 1, - maximumInterval: 1, - maximumAttempts: 2, - }, - }); - await worker.runUntil(async () => { - await t.throwsAsync(handle.result()); - // Verify retry happened - const { runId } = await handle.describe(); - t.not(runId, handle.firstExecutionRunId); - }); -}); - -test('Workflow RetryPolicy ignored with nonRetryable failure', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(workflows.throwAsync, { - args: ['nonRetryable'], - retry: { - initialInterval: 1, - maximumInterval: 1, - maximumAttempts: 2, - }, - }); - await worker.runUntil(async () => { - await t.throwsAsync(handle.result()); - const res = await handle.describe(); - t.is( - res.raw.workflowExecutionInfo?.status, - iface.temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED - ); - // Verify retry did not happen - const { runId } = await handle.describe(); - t.is(runId, handle.firstExecutionRunId); - }); -}); - -test('WorkflowClient.start fails with WorkflowExecutionAlreadyStartedError', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handle = await startWorkflow(workflows.sleeper, { - args: [10000000], - }); - try { - await worker.runUntil( - t.throwsAsync( - client.workflow.start(workflows.sleeper, { - taskQueue, - workflowId: handle.workflowId, - }), - { - instanceOf: WorkflowExecutionAlreadyStartedError, - message: 'Workflow execution already started', - } - ) - ); - } finally { - await handle.terminate(); - } -}); - -test( - 'WorkflowClient.signalWithStart fails with WorkflowExecutionAlreadyStartedError', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handle = await startWorkflow(workflows.sleeper); - await worker.runUntil(async () => { - await handle.result(); - await t.throwsAsync( - client.workflow.signalWithStart(workflows.sleeper, { - taskQueue: 'test', - workflowId: handle.workflowId, - signal: workflows.interruptSignal, - signalArgs: ['interrupted from signalWithStart'], - workflowIdReusePolicy: 'REJECT_DUPLICATE', - }), - { - instanceOf: WorkflowExecutionAlreadyStartedError, - message: 'Workflow execution already started', - } - ); - }); - } -); - -test('Handle from WorkflowClient.start follows only own execution chain', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); - const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); - const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { - taskQueue: 'test', - workflowId: handleFromThrowerStart.workflowId, - args: [1_000_000], - }); - try { - await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); - } finally { - await handleFromSleeperStart.terminate(); - } - }); -}); - -test('Handle from WorkflowClient.signalWithStart follows only own execution chain', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromThrowerStart = await client.workflow.signalWithStart(workflows.throwAsync, { - taskQueue, - workflowId: uuid4(), - signal: 'unblock', - }); - const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); - const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { - taskQueue, - workflowId: handleFromThrowerStart.workflowId, - args: [1_000_000], - }); - try { - await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); - } finally { - await handleFromSleeperStart.terminate(); - } - }); -}); - -test('Handle from WorkflowClient.getHandle follows only own execution chain', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); - const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId, undefined, { - firstExecutionRunId: handleFromThrowerStart.firstExecutionRunId, - }); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromThrowerStart.result(), { message: /.*/ }); - const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { - taskQueue, - workflowId: handleFromThrowerStart.workflowId, - args: [1_000_000], - }); - try { - await t.throwsAsync(handleFromGet.result(), { message: 'Workflow execution failed' }); - } finally { - await handleFromSleeperStart.terminate(); + break; } - }); -}); - -test('Handle from WorkflowClient.start terminates run after continue as new', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - args: [1_000_000], - }); - const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId, { - followRuns: false, - }); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromGet.result(), { instanceOf: WorkflowContinuedAsNewError }); - await handleFromStart.terminate(); - await t.throwsAsync(handleFromStart.result(), { message: 'Workflow execution terminated' }); - }); -}); - -test( - 'Handle from WorkflowClient.getHandle does not terminate run after continue as new if given runId', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const client = env.client; - const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { - args: [1_000_000], - followRuns: false, - }); - const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId); - await worker.runUntil(async () => { - await t.throwsAsync(handleFromStart.result(), { instanceOf: WorkflowContinuedAsNewError }); - try { - await t.throwsAsync(handleFromGet.terminate(), { - instanceOf: WorkflowNotFoundError, - message: 'workflow execution already completed', - }); - } finally { - await client.workflow.getHandle(handleFromStart.workflowId).terminate(); - } - }); - } -); - -test( - 'Runtime does not issue cancellations for activities and timers that throw during validation', - configMacro, - async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - await worker.runUntil(executeWorkflow(workflows.cancelScopeOnFailedValidation)); - t.pass(); - } -); - -const mutateWorkflowStateQuery = defineQuery('mutateWorkflowState'); -export async function queryAndCondition(): Promise { - let mutated = false; - // Not a valid query, used to verify that condition isn't triggered for query jobs - setHandler(mutateWorkflowStateQuery, () => void (mutated = true)); - await condition(() => mutated); -} - -test('Query does not cause condition to be triggered', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t); - const handle = await startWorkflow(queryAndCondition); - await worker.runUntil(handle.query(mutateWorkflowStateQuery)); - await handle.terminate(); - // Worker did not crash - t.pass(); -}); - -const completeSignal = defineSignal('complete'); -const definedQuery = defineQuery('query-handler-type'); - -interface QueryNameAndArgs { - name: string; - queryName?: string; - args: any[]; -} - -export async function workflowWithMaybeDefinedQuery(useDefinedQuery: boolean): Promise { - let complete = false; - setHandler(completeSignal, () => { - complete = true; - }); - setDefaultQueryHandler((queryName: string, ...args: any[]) => { - return { name: 'default', queryName, args }; - }); - if (useDefinedQuery) { - setHandler(definedQuery, (...args: any[]) => { - return { name: definedQuery.name, args }; - }); + const handle = await startWorkflow(workflows.priorityWorkflow, { + args: [false], + }) + await handle.result() } - - await condition(() => complete); -} - -test('default query handler is used if requested query does not exist', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { - args: [false], - }); - await worker.runUntil(async () => { - const args = ['test', 'args']; - const result = await handle.query(definedQuery, ...args); - t.deepEqual(result, { name: 'default', queryName: definedQuery.name, args }); - }); -}); - -test('default query handler is not used if requested query exists', configMacro, async (t, config) => { - const { env, createWorkerWithDefaults } = config; - const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); - const worker = await createWorkerWithDefaults(t, { activities }); - const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { - args: [true], - }); - await worker.runUntil(async () => { - const args = ['test', 'args']; - const result = await handle.query('query-handler-type', ...args); - t.deepEqual(result, { name: definedQuery.name, args }); - }); -}); + // TODO: + // # Verify a workflow started without priorities sees None for the key + // handle = await client.start_workflow( + // WorkflowUsingPriorities.run, + // args=[None, True], + // id=f"workflow-{uuid.uuid4()}", + // task_queue=worker.task_queue, + // ) + // await handle.result() +}) diff --git a/packages/test/src/workflows/index.ts b/packages/test/src/workflows/index.ts index cc01d985f..6df78ec71 100644 --- a/packages/test/src/workflows/index.ts +++ b/packages/test/src/workflows/index.ts @@ -51,6 +51,7 @@ export * from './noncancellable-shields-children'; export * from './partial-noncancelable'; export * from './patched'; export * from './patched-top-level'; +export * from './priority'; export * from './promise-all'; export * from './promise-race'; export * from './promise-then-promise'; diff --git a/packages/test/src/workflows/priority.ts b/packages/test/src/workflows/priority.ts new file mode 100644 index 000000000..8c8a6f371 --- /dev/null +++ b/packages/test/src/workflows/priority.ts @@ -0,0 +1,28 @@ +// @@@SNIPSTART typescript-priority-workflow +import { executeChild, proxyActivities, startChild, workflowInfo } from '@temporalio/workflow'; +import { temporal } from '@temporalio/proto'; +import type * as activities from '../activities'; +import Priority = temporal.api.common.v1.Priority; + +const { echo } = proxyActivities({ startToCloseTimeout: '5s', priority: Priority.create({ priorityKey: 5}) }); + +export async function priorityWorkflow(stopAfterCheck: boolean, expectedPriority?: number): Promise { + const info = workflowInfo(); + if (info.priority?.priorityKey !== expectedPriority) { + console.log("priorityKey", info.priority?.priorityKey) + console.log("expectedPriority", expectedPriority) + throw new Error('workflow priority doesn\'t match expected priority'); + } + if (stopAfterCheck) { + return; + } + + await executeChild(priorityWorkflow, {args: [true, 4]}); + + const child = await startChild(priorityWorkflow, {args: [true, 2],}); + await child.result(); + + await echo("hi") +} +// @@@SNIPEND + diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index dd9878333..ff56e6165 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -427,6 +427,7 @@ export const defaultActivityInfo: activity.Info = { startToCloseTimeoutMs: 1000, scheduleToCloseTimeoutMs: 1000, currentAttemptScheduledTimestampMs: 1, + priority: undefined, }; /** diff --git a/packages/worker/src/worker.ts b/packages/worker/src/worker.ts index b9b32106f..6f87ad94d 100644 --- a/packages/worker/src/worker.ts +++ b/packages/worker/src/worker.ts @@ -1268,6 +1268,7 @@ export class Worker { cronSchedule, workflowExecutionExpirationTime, cronScheduleToScheduleInterval, + priority } = initWorkflowJob; // Note that we can't do payload convertion here, as there's no guarantee that converted payloads would be safe to @@ -1304,6 +1305,7 @@ export class Worker { now: () => Date.now(), // re-set in initRuntime isReplaying: activation.isReplaying, }, + priority: temporal.api.common.v1.Priority.create(priority || undefined), }; const logAttributes = workflowLogAttributes(workflowInfo); this.logger.trace('Creating workflow', logAttributes); @@ -1898,6 +1900,7 @@ async function extractActivityInfo( start.currentAttemptScheduledTime, 'currentAttemptScheduledTime' ), + priority: temporal.api.common.v1.Priority.create(start.priority), }; } diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index cc9bb285c..1f4f45ee3 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -15,7 +15,8 @@ import { } from '@temporalio/common'; import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers'; import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow/enums-helpers'; -import type { coresdk } from '@temporalio/proto'; +import { coresdk, temporal } from '@temporalio/proto'; +import Priority = temporal.api.common.v1.Priority; /** * Workflow Execution information @@ -183,6 +184,11 @@ export interface WorkflowInfo { readonly currentBuildId?: string; readonly unsafe: UnsafeWorkflowInfo; + + /** + * Priority for a workflow + */ + readonly priority?: Priority; } /** From 8d0bacbfcf067c495b5e222b5751f0c669ca57ed Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Fri, 4 Apr 2025 11:31:14 -0700 Subject: [PATCH 3/9] Priority shows up, something's still missing --- packages/activity/src/index.ts | 5 +- packages/client/src/helpers.ts | 3 +- packages/client/src/types.ts | 3 +- packages/client/src/workflow-client.ts | 2 + packages/client/src/workflow-options.ts | 6 +- packages/common/src/activity-options.ts | 4 +- packages/common/src/index.ts | 1 + packages/common/src/priority.ts | 60 +++++++++++++++++++ packages/common/src/workflow-options.ts | 6 ++ packages/test/src/helpers.ts | 5 +- .../test/src/test-integration-split-two.ts | 8 ++- packages/test/src/workflows/priority.ts | 11 ++-- packages/worker/src/worker.ts | 5 +- packages/workflow/src/interfaces.ts | 4 +- packages/workflow/src/workflow.ts | 1 + 15 files changed, 103 insertions(+), 21 deletions(-) create mode 100644 packages/common/src/priority.ts diff --git a/packages/activity/src/index.ts b/packages/activity/src/index.ts index 63a4e887d..2c00ecded 100644 --- a/packages/activity/src/index.ts +++ b/packages/activity/src/index.ts @@ -70,10 +70,9 @@ */ import { AsyncLocalStorage } from 'node:async_hooks'; -import { Logger, Duration, LogLevel, LogMetadata } from '@temporalio/common'; +import { Logger, Duration, LogLevel, LogMetadata, Priority } from '@temporalio/common'; import { msToNumber } from '@temporalio/common/lib/time'; import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers'; -import { coresdk, temporal } from '@temporalio/proto'; export { ActivityFunction, @@ -202,7 +201,7 @@ export interface Info { /** * Priority for Activity */ - readonly priority?: temporal.api.common.v1.Priority + readonly priority?: Priority; } /** diff --git a/packages/client/src/helpers.ts b/packages/client/src/helpers.ts index 403073241..10f283363 100644 --- a/packages/client/src/helpers.ts +++ b/packages/client/src/helpers.ts @@ -1,5 +1,5 @@ import { ServiceError as GrpcServiceError, status as grpcStatus } from '@grpc/grpc-js'; -import { LoadedDataConverter, NamespaceNotFoundError } from '@temporalio/common'; +import { LoadedDataConverter, NamespaceNotFoundError, Priority } from '@temporalio/common'; import { decodeSearchAttributes, decodeTypedSearchAttributes, @@ -79,6 +79,7 @@ export async function executionInfoFromRaw( } : undefined, raw: rawDataToEmbed, + priority: Priority.fromProto(raw.priority), }; } diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 7289f0e05..eb0a63dfe 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -1,5 +1,5 @@ import type * as grpc from '@grpc/grpc-js'; -import type { TypedSearchAttributes, SearchAttributes, SearchAttributeValue } from '@temporalio/common'; +import type { TypedSearchAttributes, SearchAttributes, SearchAttributeValue, Priority } from '@temporalio/common'; import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow'; import * as proto from '@temporalio/proto'; import { Replace } from '@temporalio/common/lib/type-helpers'; @@ -52,6 +52,7 @@ export interface WorkflowExecutionInfo { typedSearchAttributes: TypedSearchAttributes; parentExecution?: Required; raw: RawWorkflowExecutionInfo; + priority?: Priority; } export interface CountWorkflowExecution { diff --git a/packages/client/src/workflow-client.ts b/packages/client/src/workflow-client.ts index 15b75ce3e..e559827e6 100644 --- a/packages/client/src/workflow-client.ts +++ b/packages/client/src/workflow-client.ts @@ -1225,6 +1225,7 @@ export class WorkflowClient extends BaseClient { : undefined, cronSchedule: options.cronSchedule, header: { fields: headers }, + priority: options.priority, }; try { return (await this.workflowService.signalWithStartWorkflowExecution(req)).runId; @@ -1293,6 +1294,7 @@ export class WorkflowClient extends BaseClient { : undefined, cronSchedule: opts.cronSchedule, header: { fields: headers }, + priority: opts.priority, }; } diff --git a/packages/client/src/workflow-options.ts b/packages/client/src/workflow-options.ts index 9b2b7937d..b6d54ce99 100644 --- a/packages/client/src/workflow-options.ts +++ b/packages/client/src/workflow-options.ts @@ -1,7 +1,7 @@ -import { CommonWorkflowOptions, SignalDefinition, WithWorkflowArgs, Workflow } from '@temporalio/common'; +import { CommonWorkflowOptions, Priority, SignalDefinition, WithWorkflowArgs, Workflow } from '@temporalio/common'; import { Duration, msOptionalToTs } from '@temporalio/common/lib/time'; import { Replace } from '@temporalio/common/lib/type-helpers'; -import { google, temporal } from '@temporalio/proto'; +import { google } from '@temporalio/proto'; export * from '@temporalio/common/lib/workflow-options'; @@ -46,7 +46,7 @@ export interface WorkflowOptions extends CommonWorkflowOptions { * Priority of this workflow. * */ - priority?: temporal.api.common.v1.Priority + priority?: Priority; } export type WithCompiledWorkflowOptions = Replace< diff --git a/packages/common/src/activity-options.ts b/packages/common/src/activity-options.ts index e4e5090b5..3f19f505a 100644 --- a/packages/common/src/activity-options.ts +++ b/packages/common/src/activity-options.ts @@ -1,9 +1,9 @@ -import { coresdk, temporal } from '@temporalio/proto'; +import type { coresdk } from '@temporalio/proto'; import { RetryPolicy } from './retry-policy'; import { Duration } from './time'; import { VersioningIntent } from './versioning-intent'; import { makeProtoEnumConverters } from './internal-workflow'; -import Priority = temporal.api.common.v1.Priority; +import { Priority } from './priority'; export const ActivityCancellationType = { TRY_CANCEL: 'TRY_CANCEL', diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index f4d44abba..1c45a5a90 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -19,6 +19,7 @@ export * from './failure'; export { Headers, Next } from './interceptors'; export * from './interfaces'; export * from './logger'; +export * from './priority'; export * from './retry-policy'; export type { Timestamp, Duration, StringValue } from './time'; export * from './workflow-handle'; diff --git a/packages/common/src/priority.ts b/packages/common/src/priority.ts new file mode 100644 index 000000000..54fc0eb96 --- /dev/null +++ b/packages/common/src/priority.ts @@ -0,0 +1,60 @@ +// TODO: type import here? +import { temporal } from '@temporalio/proto'; + +/** + * Priority contains metadata that controls relative ordering of task processing when tasks are + * backlogged in a queue. Initially, Priority will be used in activity and workflow task queues, + * which are typically where backlogs exist. + * Priority is (for now) attached to workflows and activities. Activities and child workflows + * inherit Priority from the workflow that created them, but may override fields when they are + * started or modified. For each field of a Priority on an activity/workflow, not present or equal + * to zero/empty string means to inherit the value from the calling workflow, or if there is no + * calling workflow, then use the default (documented on the field). + * The overall semantics of Priority are: + * 1. First, consider "priority_key": lower number goes first. + * (more will be added here later) + */ +export class Priority { + /** + * Priority key is a positive integer from 1 to n, where smaller integers + * correspond to higher priorities (tasks run sooner). In general, tasks in + * a queue should be processed in close to priority order, although small + * deviations are possible. + * + * The maximum priority value (minimum priority) is determined by server configuration, and + * defaults to 5. + * + * The default priority is (min+max)/2. With the default max of 5 and min of 1, that comes out to 3. + */ + public readonly priorityKey?: number; + + static readonly default = new Priority(undefined); + + constructor(priorityKey?: number) { + if (priorityKey !== undefined && priorityKey !== null) { + if (!Number.isInteger(priorityKey)) { + throw new TypeError('priorityKey must be an integer'); + } + if (priorityKey < 1) { + throw new RangeError('priorityKey must be a positive integer'); + } + } + this.priorityKey = priorityKey ?? undefined; + } + + /** + * Create a `Priority` instance from the protobuf message. + */ + static fromProto(proto: temporal.api.common.v1.IPriority | null | undefined): Priority { + return new Priority(proto?.priorityKey ?? undefined); + } + + /** + * Convert this instance to a protobuf message. + */ + toProto(): temporal.api.common.v1.Priority { + return temporal.api.common.v1.Priority.create({ + priorityKey: this.priorityKey ?? 0, + }); + } +} \ No newline at end of file diff --git a/packages/common/src/workflow-options.ts b/packages/common/src/workflow-options.ts index 6206a9861..aeb3aa349 100644 --- a/packages/common/src/workflow-options.ts +++ b/packages/common/src/workflow-options.ts @@ -4,6 +4,7 @@ import { RetryPolicy } from './retry-policy'; import { Duration } from './time'; import { makeProtoEnumConverters } from './internal-workflow'; import { SearchAttributePair, SearchAttributes, TypedSearchAttributes } from './search-attributes'; +import { Priority } from './priority'; /** * Defines what happens when trying to start a Workflow with the same ID as a *Closed* Workflow. @@ -190,6 +191,11 @@ export interface BaseWorkflowOptions { * by {@link typedSearchAttributes}. */ typedSearchAttributes?: SearchAttributePair[] | TypedSearchAttributes; + + /** + * Priority of a workflow + */ + priority?: Priority; } export type WithWorkflowArgs = T & diff --git a/packages/test/src/helpers.ts b/packages/test/src/helpers.ts index 97eed9524..9a819d52d 100644 --- a/packages/test/src/helpers.ts +++ b/packages/test/src/helpers.ts @@ -38,7 +38,10 @@ export const REUSE_V8_CONTEXT = inWorkflowContext() || isSet(process.env.REUSE_V export const RUN_TIME_SKIPPING_TESTS = inWorkflowContext() || !(process.platform === 'linux' && process.arch === 'arm64'); -export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : process.env.TESTS_CLI_VERSION; +// export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : process.env.TESTS_CLI_VERSION; +// TODO: Remove after next CLI release +export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : "v1.3.1-priority.0"; + export const TESTS_TIME_SKIPPING_SERVER_VERSION = inWorkflowContext() ? '' : process.env.TESTS_TIME_SKIPPING_SERVER_VERSION; diff --git a/packages/test/src/test-integration-split-two.ts b/packages/test/src/test-integration-split-two.ts index 53e0553a0..e42874feb 100644 --- a/packages/test/src/test-integration-split-two.ts +++ b/packages/test/src/test-integration-split-two.ts @@ -7,6 +7,7 @@ import { ApplicationFailure, defaultPayloadConverter, Payload, + Priority, WorkflowExecutionAlreadyStartedError, WorkflowNotFoundError, } from '@temporalio/common'; @@ -75,7 +76,7 @@ test.macro(configMacro); // workflowExecutionTimeout: '3s', // workflowTaskTimeout: '1s', // taskQueue: 'diff-task-queue', -// priority: Priority.create({ priorityKey: 1 }) +// priority: new Priority(1), // } as const; // const handle = await startWorkflow(workflows.sleeper, options); // async function fromPayload(payload: Payload) { @@ -761,17 +762,20 @@ test('Priority', configMacro, async (t, config) => { const worker = await createWorkerWithDefaults(t); const options = { args: [false, 1], - priority: Priority.create({ priorityKey: 1 }) + priority: new Priority(1), } as const; const handle = await startWorkflow(workflows.priorityWorkflow, options); await t.throwsAsync(worker.runUntil(handle.result()), { instanceOf: WorkflowFailedError, message: 'Workflow execution timed out', }); + await handle.result(); // const execution = await handle.describe(); let firstChild = true; const history = await handle.fetchHistory(); + console.log("events"); for (const event of history?.events ?? []) { + console.log("\t", event) switch (event.eventType) { case temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: t.deepEqual( diff --git a/packages/test/src/workflows/priority.ts b/packages/test/src/workflows/priority.ts index 8c8a6f371..da36e4323 100644 --- a/packages/test/src/workflows/priority.ts +++ b/packages/test/src/workflows/priority.ts @@ -1,16 +1,18 @@ // @@@SNIPSTART typescript-priority-workflow import { executeChild, proxyActivities, startChild, workflowInfo } from '@temporalio/workflow'; import { temporal } from '@temporalio/proto'; +import { Priority } from '@temporalio/common'; import type * as activities from '../activities'; -import Priority = temporal.api.common.v1.Priority; -const { echo } = proxyActivities({ startToCloseTimeout: '5s', priority: Priority.create({ priorityKey: 5}) }); + +const { echo } = proxyActivities({ startToCloseTimeout: '5s', priority: new Priority(5) }); export async function priorityWorkflow(stopAfterCheck: boolean, expectedPriority?: number): Promise { const info = workflowInfo(); + console.log("info.priority", info.priority) + console.log("priorityKey", info.priority?.priorityKey) + console.log("expectedPriority", expectedPriority) if (info.priority?.priorityKey !== expectedPriority) { - console.log("priorityKey", info.priority?.priorityKey) - console.log("expectedPriority", expectedPriority) throw new Error('workflow priority doesn\'t match expected priority'); } if (stopAfterCheck) { @@ -23,6 +25,7 @@ export async function priorityWorkflow(stopAfterCheck: boolean, expectedPriority await child.result(); await echo("hi") + return; } // @@@SNIPEND diff --git a/packages/worker/src/worker.ts b/packages/worker/src/worker.ts index 6f87ad94d..4e617914d 100644 --- a/packages/worker/src/worker.ts +++ b/packages/worker/src/worker.ts @@ -32,6 +32,7 @@ import { ApplicationFailure, ensureApplicationFailure, TypedSearchAttributes, + Priority, } from '@temporalio/common'; import { decodeArrayFromPayloads, @@ -1305,7 +1306,7 @@ export class Worker { now: () => Date.now(), // re-set in initRuntime isReplaying: activation.isReplaying, }, - priority: temporal.api.common.v1.Priority.create(priority || undefined), + priority: Priority.fromProto(initWorkflowJob.priority), }; const logAttributes = workflowLogAttributes(workflowInfo); this.logger.trace('Creating workflow', logAttributes); @@ -1900,7 +1901,7 @@ async function extractActivityInfo( start.currentAttemptScheduledTime, 'currentAttemptScheduledTime' ), - priority: temporal.api.common.v1.Priority.create(start.priority), + priority: Priority.fromProto(start.priority), }; } diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 1f4f45ee3..956acab06 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -12,11 +12,11 @@ import { VersioningIntent, TypedSearchAttributes, SearchAttributePair, + Priority, } from '@temporalio/common'; import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers'; import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow/enums-helpers'; -import { coresdk, temporal } from '@temporalio/proto'; -import Priority = temporal.api.common.v1.Priority; +import { coresdk } from '@temporalio/proto'; /** * Workflow Execution information diff --git a/packages/workflow/src/workflow.ts b/packages/workflow/src/workflow.ts index 711b71b04..c5c6717bb 100644 --- a/packages/workflow/src/workflow.ts +++ b/packages/workflow/src/workflow.ts @@ -393,6 +393,7 @@ function startChildWorkflowExecutionNextHandler({ : undefined, memo: options.memo && mapToPayloads(activator.payloadConverter, options.memo), versioningIntent: versioningIntentToProto(options.versioningIntent), + priority: options.priority, }, }); activator.completions.childWorkflowStart.set(seq, { From 89b8112d876f7501d41eeba6376e172df720f47b Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Mon, 7 Apr 2025 10:26:37 -0700 Subject: [PATCH 4/9] switched to an interface --- packages/client/src/helpers.ts | 4 +- packages/client/src/schedule-helpers.ts | 11 +- packages/client/src/schedule-types.ts | 1 + packages/client/src/workflow-client.ts | 5 +- packages/client/src/workflow-options.ts | 8 +- packages/common/src/priority.ts | 54 +- packages/test/package.json | 4 +- packages/test/src/helpers.ts | 2 +- .../test/src/test-integration-split-three.ts | 54 + .../test/src/test-integration-split-two.ts | 1483 ++++++++--------- packages/test/src/workflows/priority.ts | 23 +- packages/worker/src/worker.ts | 8 +- packages/workflow/src/interfaces.ts | 2 +- packages/workflow/src/workflow.ts | 4 +- 14 files changed, 825 insertions(+), 838 deletions(-) diff --git a/packages/client/src/helpers.ts b/packages/client/src/helpers.ts index 10f283363..a695f8f9c 100644 --- a/packages/client/src/helpers.ts +++ b/packages/client/src/helpers.ts @@ -1,5 +1,5 @@ import { ServiceError as GrpcServiceError, status as grpcStatus } from '@grpc/grpc-js'; -import { LoadedDataConverter, NamespaceNotFoundError, Priority } from '@temporalio/common'; +import { decodePriority, LoadedDataConverter, NamespaceNotFoundError } from '@temporalio/common'; import { decodeSearchAttributes, decodeTypedSearchAttributes, @@ -79,7 +79,7 @@ export async function executionInfoFromRaw( } : undefined, raw: rawDataToEmbed, - priority: Priority.fromProto(raw.priority), + priority: decodePriority(raw.priority), }; } diff --git a/packages/client/src/schedule-helpers.ts b/packages/client/src/schedule-helpers.ts index e403b7cce..cd6bf2fc8 100644 --- a/packages/client/src/schedule-helpers.ts +++ b/packages/client/src/schedule-helpers.ts @@ -1,5 +1,12 @@ import Long from 'long'; // eslint-disable-line import/no-named-as-default -import { compileRetryPolicy, decompileRetryPolicy, extractWorkflowType, LoadedDataConverter } from '@temporalio/common'; +import { + compilePriority, + compileRetryPolicy, + decodePriority, + decompileRetryPolicy, + extractWorkflowType, + LoadedDataConverter, +} from '@temporalio/common'; import { encodeUnifiedSearchAttributes, decodeSearchAttributes, @@ -263,6 +270,7 @@ export async function encodeScheduleAction( } : undefined, header: { fields: headers }, + priority: action.priority ? compilePriority(action.priority) : undefined, }, }; } @@ -328,6 +336,7 @@ export async function decodeScheduleAction( workflowExecutionTimeout: optionalTsToMs(pb.startWorkflow.workflowExecutionTimeout), workflowRunTimeout: optionalTsToMs(pb.startWorkflow.workflowRunTimeout), workflowTaskTimeout: optionalTsToMs(pb.startWorkflow.workflowTaskTimeout), + priority: decodePriority(pb.startWorkflow.priority), }; } throw new TypeError('Unsupported schedule action'); diff --git a/packages/client/src/schedule-types.ts b/packages/client/src/schedule-types.ts index 176e62e0a..e75e53676 100644 --- a/packages/client/src/schedule-types.ts +++ b/packages/client/src/schedule-types.ts @@ -815,6 +815,7 @@ export type ScheduleDescriptionStartWorkflowAction = ScheduleSummaryStartWorkflo | 'workflowExecutionTimeout' | 'workflowRunTimeout' | 'workflowTaskTimeout' + | 'priority' >; // Invariant: an existing ScheduleDescriptionAction can be used as is to create or update a schedule diff --git a/packages/client/src/workflow-client.ts b/packages/client/src/workflow-client.ts index e559827e6..7b4b9a77f 100644 --- a/packages/client/src/workflow-client.ts +++ b/packages/client/src/workflow-client.ts @@ -22,6 +22,7 @@ import { decodeRetryState, encodeWorkflowIdConflictPolicy, WorkflowIdConflictPolicy, + compilePriority, } from '@temporalio/common'; import { encodeUnifiedSearchAttributes } from '@temporalio/common/lib/converter/payload-search-attributes'; import { composeInterceptors } from '@temporalio/common/lib/interceptors'; @@ -1225,7 +1226,7 @@ export class WorkflowClient extends BaseClient { : undefined, cronSchedule: options.cronSchedule, header: { fields: headers }, - priority: options.priority, + priority: options.priority ? compilePriority(options.priority) : undefined, }; try { return (await this.workflowService.signalWithStartWorkflowExecution(req)).runId; @@ -1294,7 +1295,7 @@ export class WorkflowClient extends BaseClient { : undefined, cronSchedule: opts.cronSchedule, header: { fields: headers }, - priority: opts.priority, + priority: opts.priority ? compilePriority(opts.priority) : undefined, }; } diff --git a/packages/client/src/workflow-options.ts b/packages/client/src/workflow-options.ts index b6d54ce99..521fa6d37 100644 --- a/packages/client/src/workflow-options.ts +++ b/packages/client/src/workflow-options.ts @@ -1,4 +1,4 @@ -import { CommonWorkflowOptions, Priority, SignalDefinition, WithWorkflowArgs, Workflow } from '@temporalio/common'; +import { CommonWorkflowOptions, SignalDefinition, WithWorkflowArgs, Workflow } from '@temporalio/common'; import { Duration, msOptionalToTs } from '@temporalio/common/lib/time'; import { Replace } from '@temporalio/common/lib/type-helpers'; import { google } from '@temporalio/proto'; @@ -41,12 +41,6 @@ export interface WorkflowOptions extends CommonWorkflowOptions { * */ startDelay?: Duration; - - /** - * Priority of this workflow. - * - */ - priority?: Priority; } export type WithCompiledWorkflowOptions = Replace< diff --git a/packages/common/src/priority.ts b/packages/common/src/priority.ts index 54fc0eb96..5e6cc8f92 100644 --- a/packages/common/src/priority.ts +++ b/packages/common/src/priority.ts @@ -1,5 +1,4 @@ -// TODO: type import here? -import { temporal } from '@temporalio/proto'; +import type { temporal } from '@temporalio/proto'; /** * Priority contains metadata that controls relative ordering of task processing when tasks are @@ -14,7 +13,7 @@ import { temporal } from '@temporalio/proto'; * 1. First, consider "priority_key": lower number goes first. * (more will be added here later) */ -export class Priority { +export interface Priority { /** * Priority key is a positive integer from 1 to n, where smaller integers * correspond to higher priorities (tasks run sooner). In general, tasks in @@ -26,35 +25,30 @@ export class Priority { * * The default priority is (min+max)/2. With the default max of 5 and min of 1, that comes out to 3. */ - public readonly priorityKey?: number; + priorityKey?: number; +} - static readonly default = new Priority(undefined); +/** + * Turn a proto compatible Priority into a TS Priority + */ +export function decodePriority(proto: temporal.api.common.v1.IPriority | null | undefined): Priority { + return { priorityKey: proto?.priorityKey ?? undefined }; +} - constructor(priorityKey?: number) { - if (priorityKey !== undefined && priorityKey !== null) { - if (!Number.isInteger(priorityKey)) { - throw new TypeError('priorityKey must be an integer'); - } - if (priorityKey < 1) { - throw new RangeError('priorityKey must be a positive integer'); - } +/** + * Turn a TS Priority into a proto compatible Priority + */ +export function compilePriority(priority: Priority): temporal.api.common.v1.IPriority { + if (priority.priorityKey !== undefined && priority.priorityKey !== null) { + if (!Number.isInteger(priority.priorityKey)) { + throw new TypeError('priorityKey must be an integer'); + } + if (priority.priorityKey < 1) { + throw new RangeError('priorityKey must be a positive integer'); } - this.priorityKey = priorityKey ?? undefined; - } - - /** - * Create a `Priority` instance from the protobuf message. - */ - static fromProto(proto: temporal.api.common.v1.IPriority | null | undefined): Priority { - return new Priority(proto?.priorityKey ?? undefined); } - /** - * Convert this instance to a protobuf message. - */ - toProto(): temporal.api.common.v1.Priority { - return temporal.api.common.v1.Priority.create({ - priorityKey: this.priorityKey ?? 0, - }); - } -} \ No newline at end of file + return { + priorityKey: priority.priorityKey ?? 0, + }; +} diff --git a/packages/test/package.json b/packages/test/package.json index 044a46c7e..332e801e6 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -8,7 +8,9 @@ "build:ts": "tsc --build", "build:protos": "node ./scripts/compile-proto.js", "test": "ava ./lib/test-*.js", - "test.watch": "ava --watch ./lib/test-*.js" + "test.watch": "ava --watch ./lib/test-*.js", + "test-priority": "ava ./lib/test-integration-split-three.js", + "test-priority-one": "ava ./lib/test-integration-split-one.js" }, "ava": { "timeout": "60s", diff --git a/packages/test/src/helpers.ts b/packages/test/src/helpers.ts index 9a819d52d..c21ce2b68 100644 --- a/packages/test/src/helpers.ts +++ b/packages/test/src/helpers.ts @@ -40,7 +40,7 @@ export const RUN_TIME_SKIPPING_TESTS = // export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : process.env.TESTS_CLI_VERSION; // TODO: Remove after next CLI release -export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : "v1.3.1-priority.0"; +export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : 'v1.3.1-priority.0'; export const TESTS_TIME_SKIPPING_SERVER_VERSION = inWorkflowContext() ? '' diff --git a/packages/test/src/test-integration-split-three.ts b/packages/test/src/test-integration-split-three.ts index 1edf8387e..5539a5765 100644 --- a/packages/test/src/test-integration-split-three.ts +++ b/packages/test/src/test-integration-split-three.ts @@ -3,6 +3,7 @@ import v8 from 'node:v8'; import { readFileSync } from 'node:fs'; import pkg from '@temporalio/worker/lib/pkg'; import { bundleWorkflowCode } from '@temporalio/worker'; +import { temporal } from '@temporalio/proto'; import { configMacro, makeTestFn } from './helpers-integration-multi-codec'; import { configurableHelpers } from './helpers-integration'; import { withZeroesHTTPServer } from './zeroes-http-server'; @@ -140,3 +141,56 @@ if ('promiseHooks' in v8) { t.deepEqual(Object.entries(enhancedStack.sources), expectedSources); }); } + +test( + 'priorities can be specified and propagated across child workflows and activities', + configMacro, + async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t, { activities }); + const handle = await startWorkflow(workflows.priorityWorkflow, { + args: [false, 1], + priority: { priorityKey: 1 }, + }); + await worker.runUntil(handle.result()); + let firstChild = true; + const history = await handle.fetchHistory(); + console.log('events'); + for (const event of history?.events ?? []) { + switch (event.eventType) { + case temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: + t.deepEqual(event.workflowExecutionStartedEventAttributes?.priority?.priorityKey, 1); + break; + case temporal.api.enums.v1.EventType.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED: { + const pri = event.startChildWorkflowExecutionInitiatedEventAttributes?.priority?.priorityKey; + if (firstChild) { + t.deepEqual(pri, 4); + firstChild = false; + } else { + t.deepEqual(pri, 2); + } + break; + } + case temporal.api.enums.v1.EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: + t.deepEqual(event.activityTaskScheduledEventAttributes?.priority?.priorityKey, 5); + break; + } + } + } +); + +test('workflow start without priorities sees undefined for the key', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t, { activities }); + console.log('STARTING WORKFLOW'); + + const handle1 = await startWorkflow(workflows.priorityWorkflow, { + args: [true, undefined], + }); + await worker.runUntil(handle1.result()); + + // check occurs in the workflow, need an assert in the test itself in order to run + t.true(true); +}); diff --git a/packages/test/src/test-integration-split-two.ts b/packages/test/src/test-integration-split-two.ts index e42874feb..80e5f1530 100644 --- a/packages/test/src/test-integration-split-two.ts +++ b/packages/test/src/test-integration-split-two.ts @@ -7,7 +7,6 @@ import { ApplicationFailure, defaultPayloadConverter, Payload, - Priority, WorkflowExecutionAlreadyStartedError, WorkflowNotFoundError, } from '@temporalio/common'; @@ -16,12 +15,10 @@ import { msToNumber, tsToMs } from '@temporalio/common/lib/time'; import { decode as payloadDecode, decodeFromPayloadsAtIndex } from '@temporalio/common/lib/internal-non-workflow'; import { condition, defineQuery, defineSignal, setDefaultQueryHandler, setHandler, sleep } from '@temporalio/workflow'; -import { temporal } from '@temporalio/proto'; import { configurableHelpers, createTestWorkflowBundle } from './helpers-integration'; import * as activities from './activities'; import * as workflows from './workflows'; import { makeTestFn, configMacro } from './helpers-integration-multi-codec'; -import Priority = temporal.api.common.v1.Priority; // Note: re-export shared workflows (or long workflows) // - review the files where these workflows are shared @@ -30,787 +27,727 @@ export * from './workflows'; const test = makeTestFn(() => createTestWorkflowBundle({ workflowsPath: __filename })); test.macro(configMacro); -// test('WorkflowOptions are passed correctly with defaults', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(workflows.argsAndReturn, { -// args: ['hey', undefined, Buffer.from('def')], -// }); -// await worker.runUntil(handle.result()); -// const execution = await handle.describe(); -// t.deepEqual(execution.type, 'argsAndReturn'); -// const indexedFields = execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!; -// const indexedFieldKeys = Object.keys(indexedFields); -// -// let encodedId: any; -// if (indexedFieldKeys.includes('BinaryChecksums')) { -// encodedId = indexedFields.BinaryChecksums!; -// } else { -// encodedId = indexedFields.BuildIds!; -// } -// t.true(encodedId != null); -// -// const checksums = searchAttributePayloadConverter.fromPayload(encodedId); -// console.log(checksums); -// t.true(Array.isArray(checksums)); -// t.regex((checksums as string[]).pop()!, /@temporalio\/worker@\d+\.\d+\.\d+/); -// t.is(execution.raw.executionConfig?.taskQueue?.name, taskQueue); -// t.is( -// execution.raw.executionConfig?.taskQueue?.kind, -// iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL -// ); -// t.is(execution.raw.executionConfig?.workflowRunTimeout, null); -// t.is(execution.raw.executionConfig?.workflowExecutionTimeout, null); -// }); -// // -// test('WorkflowOptions are passed correctly', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// // Throws because we use a different task queue -// const worker = await createWorkerWithDefaults(t); -// const options = { -// memo: { a: 'b' }, -// searchAttributes: { CustomIntField: [3] }, -// workflowRunTimeout: '2s', -// workflowExecutionTimeout: '3s', -// workflowTaskTimeout: '1s', -// taskQueue: 'diff-task-queue', -// priority: new Priority(1), -// } as const; -// const handle = await startWorkflow(workflows.sleeper, options); -// async function fromPayload(payload: Payload) { -// const payloadCodecs = env.client.options.dataConverter.payloadCodecs ?? []; -// const [decodedPayload] = await payloadDecode(payloadCodecs, [payload]); -// return defaultPayloadConverter.fromPayload(decodedPayload); -// } -// await t.throwsAsync(worker.runUntil(handle.result()), { -// instanceOf: WorkflowFailedError, -// message: 'Workflow execution timed out', -// }); -// const execution = await handle.describe(); -// t.deepEqual( -// execution.raw.workflowExecutionInfo?.type, -// iface.temporal.api.common.v1.WorkflowType.create({ name: 'sleeper' }) -// ); -// t.deepEqual(await fromPayload(execution.raw.workflowExecutionInfo!.memo!.fields!.a!), 'b'); -// t.deepEqual( -// searchAttributePayloadConverter.fromPayload( -// execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!.CustomIntField! -// ), -// [3] -// ); -// t.deepEqual(execution.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation -// t.is(execution.raw.executionConfig?.taskQueue?.name, 'diff-task-queue'); -// t.is( -// execution.raw.executionConfig?.taskQueue?.kind, -// iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL -// ); -// -// t.is(tsToMs(execution.raw.executionConfig!.workflowRunTimeout!), msToNumber(options.workflowRunTimeout)); -// t.is(tsToMs(execution.raw.executionConfig!.workflowExecutionTimeout!), msToNumber(options.workflowExecutionTimeout)); -// t.is(tsToMs(execution.raw.executionConfig!.defaultWorkflowTaskTimeout!), msToNumber(options.workflowTaskTimeout)); -// }); -// -// test('WorkflowHandle.result() throws if terminated', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(workflows.sleeper, { -// args: [1000000], -// }); -// await t.throwsAsync( -// worker.runUntil(async () => { -// await handle.terminate('hasta la vista baby'); -// await handle.result(); -// }), -// { -// instanceOf: WorkflowFailedError, -// message: 'hasta la vista baby', -// } -// ); -// }); -// -// test('WorkflowHandle.result() throws if continued as new', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// await worker.runUntil(async () => { -// const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewSameWorkflow, { -// followRuns: false, -// }); -// let err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); -// -// if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion -// const client = env.client; -// let continueWorkflowHandle = client.workflow.getHandle( -// originalWorkflowHandle.workflowId, -// err.newExecutionRunId, -// { -// followRuns: false, -// } -// ); -// -// await continueWorkflowHandle.signal(workflows.continueAsNewSignal); -// err = await t.throwsAsync(continueWorkflowHandle.result(), { -// instanceOf: WorkflowContinuedAsNewError, -// }); -// if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion -// -// continueWorkflowHandle = client.workflow.getHandle( -// continueWorkflowHandle.workflowId, -// err.newExecutionRunId -// ); -// await continueWorkflowHandle.result(); -// }); -// }); -// -// test('WorkflowHandle.result() follows chain of execution', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// await worker.runUntil( -// executeWorkflow(workflows.continueAsNewSameWorkflow, { -// args: ['execute', 'none'], -// }) -// ); -// t.pass(); -// }); -// -// test('continue-as-new-to-different-workflow', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults, loadedDataConverter } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// await worker.runUntil(async () => { -// const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { -// followRuns: false, -// }); -// const err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); -// if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion -// const workflow = client.workflow.getHandle( -// originalWorkflowHandle.workflowId, -// err.newExecutionRunId, -// { -// followRuns: false, -// } -// ); -// await workflow.result(); -// const info = await workflow.describe(); -// t.is(info.raw.workflowExecutionInfo?.type?.name, 'sleeper'); -// const history = await workflow.fetchHistory(); -// const timeSlept = await decodeFromPayloadsAtIndex( -// loadedDataConverter, -// 0, -// history?.events?.[0].workflowExecutionStartedEventAttributes?.input?.payloads -// ); -// t.is(timeSlept, 1); -// }); -// }); -// -// test('continue-as-new-to-same-workflow keeps memo and search attributes', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(workflows.continueAsNewSameWorkflow, { -// memo: { -// note: 'foo', -// }, -// searchAttributes: { -// CustomKeywordField: ['test-value'], -// CustomIntField: [1], -// }, -// followRuns: true, -// }); -// await worker.runUntil(async () => { -// await handle.signal(workflows.continueAsNewSignal); -// await handle.result(); -// const execution = await handle.describe(); -// t.not(execution.runId, handle.firstExecutionRunId); -// t.deepEqual(execution.memo, { note: 'foo' }); -// t.deepEqual(execution.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation -// t.deepEqual(execution.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation -// }); -// }); -// -// test( -// 'continue-as-new-to-different-workflow keeps memo and search attributes by default', -// configMacro, -// async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { -// followRuns: true, -// memo: { -// note: 'foo', -// }, -// searchAttributes: { -// CustomKeywordField: ['test-value'], -// CustomIntField: [1], -// }, -// }); -// await worker.runUntil(async () => { -// await handle.result(); -// const info = await handle.describe(); -// t.is(info.type, 'sleeper'); -// t.not(info.runId, handle.firstExecutionRunId); -// t.deepEqual(info.memo, { note: 'foo' }); -// t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation -// t.deepEqual(info.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation -// }); -// } -// ); -// -// test('continue-as-new-to-different-workflow can set memo and search attributes', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { -// args: [ -// 1, -// { -// memo: { -// note: 'bar', -// }, -// searchAttributes: { -// CustomKeywordField: ['test-value-2'], -// CustomIntField: [3], -// }, -// }, -// ], -// followRuns: true, -// memo: { -// note: 'foo', -// }, -// searchAttributes: { -// CustomKeywordField: ['test-value'], -// CustomIntField: [1], -// }, -// }); -// await worker.runUntil(async () => { -// await handle.result(); -// const info = await handle.describe(); -// t.is(info.type, 'sleeper'); -// t.not(info.runId, handle.firstExecutionRunId); -// t.deepEqual(info.memo, { note: 'bar' }); -// t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value-2']); // eslint-disable-line deprecation/deprecation -// t.deepEqual(info.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation -// }); -// }); -// -// test('signalWithStart works as intended and returns correct runId', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// const originalWorkflowHandle = await client.workflow.signalWithStart(workflows.interruptableWorkflow, { -// taskQueue, -// workflowId: uuid4(), -// signal: workflows.interruptSignal, -// signalArgs: ['interrupted from signalWithStart'], -// }); -// await worker.runUntil(async () => { -// let err: WorkflowFailedError | undefined = await t.throwsAsync(originalWorkflowHandle.result(), { -// instanceOf: WorkflowFailedError, -// }); -// if (!(err?.cause instanceof ApplicationFailure)) { -// return t.fail('Expected err.cause to be an instance of ApplicationFailure'); -// } -// t.is(err.cause.message, 'interrupted from signalWithStart'); -// -// // Test returned runId -// const handle = client.workflow.getHandle( -// originalWorkflowHandle.workflowId, -// originalWorkflowHandle.signaledRunId -// ); -// err = await t.throwsAsync(handle.result(), { -// instanceOf: WorkflowFailedError, -// }); -// if (!(err?.cause instanceof ApplicationFailure)) { -// return t.fail('Expected err.cause to be an instance of ApplicationFailure'); -// } -// t.is(err.cause.message, 'interrupted from signalWithStart'); -// }); -// }); -// -// test('activity-failures', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t, { activities }); -// await worker.runUntil(executeWorkflow(workflows.activityFailures)); -// t.pass(); -// }); -// -// export async function sleepInvalidDuration(): Promise { -// await sleep(0); -// await new Promise((resolve) => setTimeout(resolve, -1)); -// } -// -// test('sleepInvalidDuration is caught in Workflow runtime', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// -// const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// await worker.runUntil(executeWorkflow(sleepInvalidDuration)); -// t.pass(); -// }); -// -// test('unhandledRejection causes WFT to fail', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(workflows.throwUnhandledRejection, { -// // throw an exception that our worker can associate with a running workflow -// args: [{ crashWorker: false }], -// }); -// await worker.runUntil( -// asyncRetry( -// async () => { -// const history = await handle.fetchHistory(); -// const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); -// if (wftFailedEvent === undefined) { -// throw new Error('No WFT failed event'); -// } -// const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; -// if (!failure) { -// t.fail(); -// return; -// } -// t.is(failure.message, 'Unhandled Promise rejection: Error: unhandled rejection'); -// t.true(failure.stackTrace?.includes(`Error: unhandled rejection`)); -// t.is(failure.cause?.cause?.message, 'root failure'); -// }, -// { minTimeout: 300, factor: 1, retries: 100 } -// ) -// ); -// await handle.terminate(); -// }); -// -// export async function throwObject(): Promise { -// throw { plainObject: true }; -// } -// -// test('throwObject includes message with our recommendation', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(throwObject); -// await worker.runUntil( -// asyncRetry( -// async () => { -// const history = await handle.fetchHistory(); -// const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); -// if (wftFailedEvent === undefined) { -// throw new Error('No WFT failed event'); -// } -// const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; -// if (!failure) { -// t.fail(); -// return; -// } -// t.is( -// failure.message, -// '{"plainObject":true} [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' -// ); -// }, -// { minTimeout: 300, factor: 1, retries: 100 } -// ) -// ); -// await handle.terminate(); -// }); -// -// export async function throwBigInt(): Promise { -// throw 42n; -// } -// -// test('throwBigInt includes message with our recommendation', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(throwBigInt); -// await worker.runUntil( -// asyncRetry( -// async () => { -// const history = await handle.fetchHistory(); -// const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); -// if (wftFailedEvent === undefined) { -// throw new Error('No WFT failed event'); -// } -// const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; -// if (!failure) { -// t.fail(); -// return; -// } -// t.is( -// failure.message, -// '42 [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' -// ); -// }, -// { minTimeout: 300, factor: 1, retries: 100 } -// ) -// ); -// await handle.terminate(); -// }); -// -// test('Workflow RetryPolicy kicks in with retryable failure', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(workflows.throwAsync, { -// args: ['retryable'], -// retry: { -// initialInterval: 1, -// maximumInterval: 1, -// maximumAttempts: 2, -// }, -// }); -// await worker.runUntil(async () => { -// await t.throwsAsync(handle.result()); -// // Verify retry happened -// const { runId } = await handle.describe(); -// t.not(runId, handle.firstExecutionRunId); -// }); -// }); -// -// test('Workflow RetryPolicy ignored with nonRetryable failure', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(workflows.throwAsync, { -// args: ['nonRetryable'], -// retry: { -// initialInterval: 1, -// maximumInterval: 1, -// maximumAttempts: 2, -// }, -// }); -// await worker.runUntil(async () => { -// await t.throwsAsync(handle.result()); -// const res = await handle.describe(); -// t.is( -// res.raw.workflowExecutionInfo?.status, -// iface.temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED -// ); -// // Verify retry did not happen -// const { runId } = await handle.describe(); -// t.is(runId, handle.firstExecutionRunId); -// }); -// }); -// -// test('WorkflowClient.start fails with WorkflowExecutionAlreadyStartedError', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// const handle = await startWorkflow(workflows.sleeper, { -// args: [10000000], -// }); -// try { -// await worker.runUntil( -// t.throwsAsync( -// client.workflow.start(workflows.sleeper, { -// taskQueue, -// workflowId: handle.workflowId, -// }), -// { -// instanceOf: WorkflowExecutionAlreadyStartedError, -// message: 'Workflow execution already started', -// } -// ) -// ); -// } finally { -// await handle.terminate(); -// } -// }); -// -// test( -// 'WorkflowClient.signalWithStart fails with WorkflowExecutionAlreadyStartedError', -// configMacro, -// async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// const handle = await startWorkflow(workflows.sleeper); -// await worker.runUntil(async () => { -// await handle.result(); -// await t.throwsAsync( -// client.workflow.signalWithStart(workflows.sleeper, { -// taskQueue: 'test', -// workflowId: handle.workflowId, -// signal: workflows.interruptSignal, -// signalArgs: ['interrupted from signalWithStart'], -// workflowIdReusePolicy: 'REJECT_DUPLICATE', -// }), -// { -// instanceOf: WorkflowExecutionAlreadyStartedError, -// message: 'Workflow execution already started', -// } -// ); -// }); -// } -// ); -// -// test('Handle from WorkflowClient.start follows only own execution chain', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); -// const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); -// await worker.runUntil(async () => { -// await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); -// const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { -// taskQueue: 'test', -// workflowId: handleFromThrowerStart.workflowId, -// args: [1_000_000], -// }); -// try { -// await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); -// } finally { -// await handleFromSleeperStart.terminate(); -// } -// }); -// }); -// -// test('Handle from WorkflowClient.signalWithStart follows only own execution chain', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// const handleFromThrowerStart = await client.workflow.signalWithStart(workflows.throwAsync, { -// taskQueue, -// workflowId: uuid4(), -// signal: 'unblock', -// }); -// const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); -// await worker.runUntil(async () => { -// await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); -// const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { -// taskQueue, -// workflowId: handleFromThrowerStart.workflowId, -// args: [1_000_000], -// }); -// try { -// await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); -// } finally { -// await handleFromSleeperStart.terminate(); -// } -// }); -// }); -// -// test('Handle from WorkflowClient.getHandle follows only own execution chain', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); -// const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId, undefined, { -// firstExecutionRunId: handleFromThrowerStart.firstExecutionRunId, -// }); -// await worker.runUntil(async () => { -// await t.throwsAsync(handleFromThrowerStart.result(), { message: /.*/ }); -// const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { -// taskQueue, -// workflowId: handleFromThrowerStart.workflowId, -// args: [1_000_000], -// }); -// try { -// await t.throwsAsync(handleFromGet.result(), { message: 'Workflow execution failed' }); -// } finally { -// await handleFromSleeperStart.terminate(); -// } -// }); -// }); -// -// test('Handle from WorkflowClient.start terminates run after continue as new', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { -// args: [1_000_000], -// }); -// const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId, { -// followRuns: false, -// }); -// await worker.runUntil(async () => { -// await t.throwsAsync(handleFromGet.result(), { instanceOf: WorkflowContinuedAsNewError }); -// await handleFromStart.terminate(); -// await t.throwsAsync(handleFromStart.result(), { message: 'Workflow execution terminated' }); -// }); -// }); -// -// test( -// 'Handle from WorkflowClient.getHandle does not terminate run after continue as new if given runId', -// configMacro, -// async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const client = env.client; -// const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { -// args: [1_000_000], -// followRuns: false, -// }); -// const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId); -// await worker.runUntil(async () => { -// await t.throwsAsync(handleFromStart.result(), { instanceOf: WorkflowContinuedAsNewError }); -// try { -// await t.throwsAsync(handleFromGet.terminate(), { -// instanceOf: WorkflowNotFoundError, -// message: 'workflow execution already completed', -// }); -// } finally { -// await client.workflow.getHandle(handleFromStart.workflowId).terminate(); -// } -// }); -// } -// ); -// -// test( -// 'Runtime does not issue cancellations for activities and timers that throw during validation', -// configMacro, -// async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// await worker.runUntil(executeWorkflow(workflows.cancelScopeOnFailedValidation)); -// t.pass(); -// } -// ); -// -// const mutateWorkflowStateQuery = defineQuery('mutateWorkflowState'); -// export async function queryAndCondition(): Promise { -// let mutated = false; -// // Not a valid query, used to verify that condition isn't triggered for query jobs -// setHandler(mutateWorkflowStateQuery, () => void (mutated = true)); -// await condition(() => mutated); -// } -// -// test('Query does not cause condition to be triggered', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t); -// const handle = await startWorkflow(queryAndCondition); -// await worker.runUntil(handle.query(mutateWorkflowStateQuery)); -// await handle.terminate(); -// // Worker did not crash -// t.pass(); -// }); -// -// const completeSignal = defineSignal('complete'); -// const definedQuery = defineQuery('query-handler-type'); -// -// interface QueryNameAndArgs { -// name: string; -// queryName?: string; -// args: any[]; -// } -// -// export async function workflowWithMaybeDefinedQuery(useDefinedQuery: boolean): Promise { -// let complete = false; -// setHandler(completeSignal, () => { -// complete = true; -// }); -// setDefaultQueryHandler((queryName: string, ...args: any[]) => { -// return { name: 'default', queryName, args }; -// }); -// if (useDefinedQuery) { -// setHandler(definedQuery, (...args: any[]) => { -// return { name: definedQuery.name, args }; -// }); -// } -// -// await condition(() => complete); -// } -// -// test('default query handler is used if requested query does not exist', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t, { activities }); -// const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { -// args: [false], -// }); -// await worker.runUntil(async () => { -// const args = ['test', 'args']; -// const result = await handle.query(definedQuery, ...args); -// t.deepEqual(result, { name: 'default', queryName: definedQuery.name, args }); -// }); -// }); -// -// test('default query handler is not used if requested query exists', configMacro, async (t, config) => { -// const { env, createWorkerWithDefaults } = config; -// const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); -// const worker = await createWorkerWithDefaults(t, { activities }); -// const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { -// args: [true], -// }); -// await worker.runUntil(async () => { -// const args = ['test', 'args']; -// const result = await handle.query('query-handler-type', ...args); -// t.deepEqual(result, { name: definedQuery.name, args }); -// }); -// }); - -test('Priority', configMacro, async (t, config) => { +test('WorkflowOptions are passed correctly with defaults', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(workflows.argsAndReturn, { + args: ['hey', undefined, Buffer.from('def')], + }); + await worker.runUntil(handle.result()); + const execution = await handle.describe(); + t.deepEqual(execution.type, 'argsAndReturn'); + const indexedFields = execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!; + const indexedFieldKeys = Object.keys(indexedFields); + + let encodedId: any; + if (indexedFieldKeys.includes('BinaryChecksums')) { + encodedId = indexedFields.BinaryChecksums!; + } else { + encodedId = indexedFields.BuildIds!; + } + t.true(encodedId != null); + + const checksums = searchAttributePayloadConverter.fromPayload(encodedId); + console.log(checksums); + t.true(Array.isArray(checksums)); + t.regex((checksums as string[]).pop()!, /@temporalio\/worker@\d+\.\d+\.\d+/); + t.is(execution.raw.executionConfig?.taskQueue?.name, taskQueue); + t.is( + execution.raw.executionConfig?.taskQueue?.kind, + iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL + ); + t.is(execution.raw.executionConfig?.workflowRunTimeout, null); + t.is(execution.raw.executionConfig?.workflowExecutionTimeout, null); +}); + +test('WorkflowOptions are passed correctly', configMacro, async (t, config) => { const { env, createWorkerWithDefaults } = config; const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + // Throws because we use a different task queue const worker = await createWorkerWithDefaults(t); const options = { - args: [false, 1], - priority: new Priority(1), + memo: { a: 'b' }, + searchAttributes: { CustomIntField: [3] }, + workflowRunTimeout: '2s', + workflowExecutionTimeout: '3s', + workflowTaskTimeout: '1s', + taskQueue: 'diff-task-queue', } as const; - const handle = await startWorkflow(workflows.priorityWorkflow, options); + const handle = await startWorkflow(workflows.sleeper, options); + async function fromPayload(payload: Payload) { + const payloadCodecs = env.client.options.dataConverter.payloadCodecs ?? []; + const [decodedPayload] = await payloadDecode(payloadCodecs, [payload]); + return defaultPayloadConverter.fromPayload(decodedPayload); + } await t.throwsAsync(worker.runUntil(handle.result()), { instanceOf: WorkflowFailedError, message: 'Workflow execution timed out', }); - await handle.result(); - // const execution = await handle.describe(); - let firstChild = true; - const history = await handle.fetchHistory(); - console.log("events"); - for (const event of history?.events ?? []) { - console.log("\t", event) - switch (event.eventType) { - case temporal.api.enums.v1.EventType.EVENT_TYPE_WORKFLOW_EXECUTION_STARTED: - t.deepEqual( - event.workflowExecutionStartedEventAttributes?.priority?.priorityKey, 1 - ) - break; - case temporal.api.enums.v1.EventType.EVENT_TYPE_START_CHILD_WORKFLOW_EXECUTION_INITIATED: { - const pri = event.startChildWorkflowExecutionInitiatedEventAttributes?.priority?.priorityKey; - if (firstChild) { - t.deepEqual(pri, 4); - firstChild = false; - } else { - t.deepEqual(pri, 2); - } - break; + const execution = await handle.describe(); + t.deepEqual( + execution.raw.workflowExecutionInfo?.type, + iface.temporal.api.common.v1.WorkflowType.create({ name: 'sleeper' }) + ); + t.deepEqual(await fromPayload(execution.raw.workflowExecutionInfo!.memo!.fields!.a!), 'b'); + t.deepEqual( + searchAttributePayloadConverter.fromPayload( + execution.raw.workflowExecutionInfo!.searchAttributes!.indexedFields!.CustomIntField! + ), + [3] + ); + t.deepEqual(execution.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation + t.is(execution.raw.executionConfig?.taskQueue?.name, 'diff-task-queue'); + t.is( + execution.raw.executionConfig?.taskQueue?.kind, + iface.temporal.api.enums.v1.TaskQueueKind.TASK_QUEUE_KIND_NORMAL + ); + + t.is(tsToMs(execution.raw.executionConfig!.workflowRunTimeout!), msToNumber(options.workflowRunTimeout)); + t.is(tsToMs(execution.raw.executionConfig!.workflowExecutionTimeout!), msToNumber(options.workflowExecutionTimeout)); + t.is(tsToMs(execution.raw.executionConfig!.defaultWorkflowTaskTimeout!), msToNumber(options.workflowTaskTimeout)); +}); + +test('WorkflowHandle.result() throws if terminated', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(workflows.sleeper, { + args: [1000000], + }); + await t.throwsAsync( + worker.runUntil(async () => { + await handle.terminate('hasta la vista baby'); + await handle.result(); + }), + { + instanceOf: WorkflowFailedError, + message: 'hasta la vista baby', + } + ); +}); + +test('WorkflowHandle.result() throws if continued as new', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + await worker.runUntil(async () => { + const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewSameWorkflow, { + followRuns: false, + }); + let err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); + + if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion + const client = env.client; + let continueWorkflowHandle = client.workflow.getHandle( + originalWorkflowHandle.workflowId, + err.newExecutionRunId, + { + followRuns: false, + } + ); + + await continueWorkflowHandle.signal(workflows.continueAsNewSignal); + err = await t.throwsAsync(continueWorkflowHandle.result(), { + instanceOf: WorkflowContinuedAsNewError, + }); + if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion + + continueWorkflowHandle = client.workflow.getHandle( + continueWorkflowHandle.workflowId, + err.newExecutionRunId + ); + await continueWorkflowHandle.result(); + }); +}); + +test('WorkflowHandle.result() follows chain of execution', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + await worker.runUntil( + executeWorkflow(workflows.continueAsNewSameWorkflow, { + args: ['execute', 'none'], + }) + ); + t.pass(); +}); + +test('continue-as-new-to-different-workflow', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults, loadedDataConverter } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + await worker.runUntil(async () => { + const originalWorkflowHandle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { + followRuns: false, + }); + const err = await t.throwsAsync(originalWorkflowHandle.result(), { instanceOf: WorkflowContinuedAsNewError }); + if (!(err instanceof WorkflowContinuedAsNewError)) return; // Type assertion + const workflow = client.workflow.getHandle( + originalWorkflowHandle.workflowId, + err.newExecutionRunId, + { + followRuns: false, + } + ); + await workflow.result(); + const info = await workflow.describe(); + t.is(info.raw.workflowExecutionInfo?.type?.name, 'sleeper'); + const history = await workflow.fetchHistory(); + const timeSlept = await decodeFromPayloadsAtIndex( + loadedDataConverter, + 0, + history?.events?.[0].workflowExecutionStartedEventAttributes?.input?.payloads + ); + t.is(timeSlept, 1); + }); +}); + +test('continue-as-new-to-same-workflow keeps memo and search attributes', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(workflows.continueAsNewSameWorkflow, { + memo: { + note: 'foo', + }, + searchAttributes: { + CustomKeywordField: ['test-value'], + CustomIntField: [1], + }, + followRuns: true, + }); + await worker.runUntil(async () => { + await handle.signal(workflows.continueAsNewSignal); + await handle.result(); + const execution = await handle.describe(); + t.not(execution.runId, handle.firstExecutionRunId); + t.deepEqual(execution.memo, { note: 'foo' }); + t.deepEqual(execution.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation + t.deepEqual(execution.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation + }); +}); + +test( + 'continue-as-new-to-different-workflow keeps memo and search attributes by default', + configMacro, + async (t, config) => { + const { env, createWorkerWithDefaults } = config; + + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { + followRuns: true, + memo: { + note: 'foo', + }, + searchAttributes: { + CustomKeywordField: ['test-value'], + CustomIntField: [1], + }, + }); + await worker.runUntil(async () => { + await handle.result(); + const info = await handle.describe(); + t.is(info.type, 'sleeper'); + t.not(info.runId, handle.firstExecutionRunId); + t.deepEqual(info.memo, { note: 'foo' }); + t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value']); // eslint-disable-line deprecation/deprecation + t.deepEqual(info.searchAttributes!.CustomIntField, [1]); // eslint-disable-line deprecation/deprecation + }); + } +); + +test('continue-as-new-to-different-workflow can set memo and search attributes', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { + args: [ + 1, + { + memo: { + note: 'bar', + }, + searchAttributes: { + CustomKeywordField: ['test-value-2'], + CustomIntField: [3], + }, + }, + ], + followRuns: true, + memo: { + note: 'foo', + }, + searchAttributes: { + CustomKeywordField: ['test-value'], + CustomIntField: [1], + }, + }); + await worker.runUntil(async () => { + await handle.result(); + const info = await handle.describe(); + t.is(info.type, 'sleeper'); + t.not(info.runId, handle.firstExecutionRunId); + t.deepEqual(info.memo, { note: 'bar' }); + t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value-2']); // eslint-disable-line deprecation/deprecation + t.deepEqual(info.searchAttributes!.CustomIntField, [3]); // eslint-disable-line deprecation/deprecation + }); +}); + +test('signalWithStart works as intended and returns correct runId', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + const originalWorkflowHandle = await client.workflow.signalWithStart(workflows.interruptableWorkflow, { + taskQueue, + workflowId: uuid4(), + signal: workflows.interruptSignal, + signalArgs: ['interrupted from signalWithStart'], + }); + await worker.runUntil(async () => { + let err: WorkflowFailedError | undefined = await t.throwsAsync(originalWorkflowHandle.result(), { + instanceOf: WorkflowFailedError, + }); + if (!(err?.cause instanceof ApplicationFailure)) { + return t.fail('Expected err.cause to be an instance of ApplicationFailure'); + } + t.is(err.cause.message, 'interrupted from signalWithStart'); + + // Test returned runId + const handle = client.workflow.getHandle( + originalWorkflowHandle.workflowId, + originalWorkflowHandle.signaledRunId + ); + err = await t.throwsAsync(handle.result(), { + instanceOf: WorkflowFailedError, + }); + if (!(err?.cause instanceof ApplicationFailure)) { + return t.fail('Expected err.cause to be an instance of ApplicationFailure'); + } + t.is(err.cause.message, 'interrupted from signalWithStart'); + }); +}); + +test('activity-failures', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t, { activities }); + await worker.runUntil(executeWorkflow(workflows.activityFailures)); + t.pass(); +}); + +export async function sleepInvalidDuration(): Promise { + await sleep(0); + await new Promise((resolve) => setTimeout(resolve, -1)); +} + +test('sleepInvalidDuration is caught in Workflow runtime', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + + const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + await worker.runUntil(executeWorkflow(sleepInvalidDuration)); + t.pass(); +}); + +test('unhandledRejection causes WFT to fail', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(workflows.throwUnhandledRejection, { + // throw an exception that our worker can associate with a running workflow + args: [{ crashWorker: false }], + }); + await worker.runUntil( + asyncRetry( + async () => { + const history = await handle.fetchHistory(); + const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); + if (wftFailedEvent === undefined) { + throw new Error('No WFT failed event'); } - case temporal.api.enums.v1.EventType.EVENT_TYPE_ACTIVITY_TASK_SCHEDULED: - t.deepEqual( - event.activityTaskScheduledEventAttributes?.priority?.priorityKey, 5 + const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; + if (!failure) { + t.fail(); + return; + } + t.is(failure.message, 'Unhandled Promise rejection: Error: unhandled rejection'); + t.true(failure.stackTrace?.includes(`Error: unhandled rejection`)); + t.is(failure.cause?.cause?.message, 'root failure'); + }, + { minTimeout: 300, factor: 1, retries: 100 } + ) + ); + await handle.terminate(); +}); + +export async function throwObject(): Promise { + throw { plainObject: true }; +} + +test('throwObject includes message with our recommendation', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(throwObject); + await worker.runUntil( + asyncRetry( + async () => { + const history = await handle.fetchHistory(); + const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); + if (wftFailedEvent === undefined) { + throw new Error('No WFT failed event'); + } + const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; + if (!failure) { + t.fail(); + return; + } + t.is( + failure.message, + '{"plainObject":true} [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' ); - break; + }, + { minTimeout: 300, factor: 1, retries: 100 } + ) + ); + await handle.terminate(); +}); + +export async function throwBigInt(): Promise { + throw 42n; +} + +test('throwBigInt includes message with our recommendation', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(throwBigInt); + await worker.runUntil( + asyncRetry( + async () => { + const history = await handle.fetchHistory(); + const wftFailedEvent = history.events?.find((ev) => ev.workflowTaskFailedEventAttributes); + if (wftFailedEvent === undefined) { + throw new Error('No WFT failed event'); + } + const failure = wftFailedEvent.workflowTaskFailedEventAttributes?.failure; + if (!failure) { + t.fail(); + return; + } + t.is( + failure.message, + '42 [A non-Error value was thrown from your code. We recommend throwing Error objects so that we can provide a stack trace]' + ); + }, + { minTimeout: 300, factor: 1, retries: 100 } + ) + ); + await handle.terminate(); +}); + +test('Workflow RetryPolicy kicks in with retryable failure', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(workflows.throwAsync, { + args: ['retryable'], + retry: { + initialInterval: 1, + maximumInterval: 1, + maximumAttempts: 2, + }, + }); + await worker.runUntil(async () => { + await t.throwsAsync(handle.result()); + // Verify retry happened + const { runId } = await handle.describe(); + t.not(runId, handle.firstExecutionRunId); + }); +}); + +test('Workflow RetryPolicy ignored with nonRetryable failure', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(workflows.throwAsync, { + args: ['nonRetryable'], + retry: { + initialInterval: 1, + maximumInterval: 1, + maximumAttempts: 2, + }, + }); + await worker.runUntil(async () => { + await t.throwsAsync(handle.result()); + const res = await handle.describe(); + t.is( + res.raw.workflowExecutionInfo?.status, + iface.temporal.api.enums.v1.WorkflowExecutionStatus.WORKFLOW_EXECUTION_STATUS_FAILED + ); + // Verify retry did not happen + const { runId } = await handle.describe(); + t.is(runId, handle.firstExecutionRunId); + }); +}); + +test('WorkflowClient.start fails with WorkflowExecutionAlreadyStartedError', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + const handle = await startWorkflow(workflows.sleeper, { + args: [10000000], + }); + try { + await worker.runUntil( + t.throwsAsync( + client.workflow.start(workflows.sleeper, { + taskQueue, + workflowId: handle.workflowId, + }), + { + instanceOf: WorkflowExecutionAlreadyStartedError, + message: 'Workflow execution already started', + } + ) + ); + } finally { + await handle.terminate(); + } +}); + +test( + 'WorkflowClient.signalWithStart fails with WorkflowExecutionAlreadyStartedError', + configMacro, + async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + const handle = await startWorkflow(workflows.sleeper); + await worker.runUntil(async () => { + await handle.result(); + await t.throwsAsync( + client.workflow.signalWithStart(workflows.sleeper, { + taskQueue: 'test', + workflowId: handle.workflowId, + signal: workflows.interruptSignal, + signalArgs: ['interrupted from signalWithStart'], + workflowIdReusePolicy: 'REJECT_DUPLICATE', + }), + { + instanceOf: WorkflowExecutionAlreadyStartedError, + message: 'Workflow execution already started', + } + ); + }); + } +); + +test('Handle from WorkflowClient.start follows only own execution chain', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); + const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); + await worker.runUntil(async () => { + await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); + const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { + taskQueue: 'test', + workflowId: handleFromThrowerStart.workflowId, + args: [1_000_000], + }); + try { + await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); + } finally { + await handleFromSleeperStart.terminate(); } + }); +}); - const handle = await startWorkflow(workflows.priorityWorkflow, { - args: [false], - }) - await handle.result() +test('Handle from WorkflowClient.signalWithStart follows only own execution chain', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + const handleFromThrowerStart = await client.workflow.signalWithStart(workflows.throwAsync, { + taskQueue, + workflowId: uuid4(), + signal: 'unblock', + }); + const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId); + await worker.runUntil(async () => { + await t.throwsAsync(handleFromGet.result(), { message: /.*/ }); + const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { + taskQueue, + workflowId: handleFromThrowerStart.workflowId, + args: [1_000_000], + }); + try { + await t.throwsAsync(handleFromThrowerStart.result(), { message: 'Workflow execution failed' }); + } finally { + await handleFromSleeperStart.terminate(); + } + }); +}); + +test('Handle from WorkflowClient.getHandle follows only own execution chain', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow, taskQueue } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + const handleFromThrowerStart = await startWorkflow(workflows.throwAsync); + const handleFromGet = client.workflow.getHandle(handleFromThrowerStart.workflowId, undefined, { + firstExecutionRunId: handleFromThrowerStart.firstExecutionRunId, + }); + await worker.runUntil(async () => { + await t.throwsAsync(handleFromThrowerStart.result(), { message: /.*/ }); + const handleFromSleeperStart = await client.workflow.start(workflows.sleeper, { + taskQueue, + workflowId: handleFromThrowerStart.workflowId, + args: [1_000_000], + }); + try { + await t.throwsAsync(handleFromGet.result(), { message: 'Workflow execution failed' }); + } finally { + await handleFromSleeperStart.terminate(); + } + }); +}); + +test('Handle from WorkflowClient.start terminates run after continue as new', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { + args: [1_000_000], + }); + const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId, { + followRuns: false, + }); + await worker.runUntil(async () => { + await t.throwsAsync(handleFromGet.result(), { instanceOf: WorkflowContinuedAsNewError }); + await handleFromStart.terminate(); + await t.throwsAsync(handleFromStart.result(), { message: 'Workflow execution terminated' }); + }); +}); + +test( + 'Handle from WorkflowClient.getHandle does not terminate run after continue as new if given runId', + configMacro, + async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const client = env.client; + const handleFromStart = await startWorkflow(workflows.continueAsNewToDifferentWorkflow, { + args: [1_000_000], + followRuns: false, + }); + const handleFromGet = client.workflow.getHandle(handleFromStart.workflowId, handleFromStart.firstExecutionRunId); + await worker.runUntil(async () => { + await t.throwsAsync(handleFromStart.result(), { instanceOf: WorkflowContinuedAsNewError }); + try { + await t.throwsAsync(handleFromGet.terminate(), { + instanceOf: WorkflowNotFoundError, + message: 'workflow execution already completed', + }); + } finally { + await client.workflow.getHandle(handleFromStart.workflowId).terminate(); + } + }); + } +); + +test( + 'Runtime does not issue cancellations for activities and timers that throw during validation', + configMacro, + async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { executeWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + await worker.runUntil(executeWorkflow(workflows.cancelScopeOnFailedValidation)); + t.pass(); + } +); + +const mutateWorkflowStateQuery = defineQuery('mutateWorkflowState'); +export async function queryAndCondition(): Promise { + let mutated = false; + // Not a valid query, used to verify that condition isn't triggered for query jobs + setHandler(mutateWorkflowStateQuery, () => void (mutated = true)); + await condition(() => mutated); +} + +test('Query does not cause condition to be triggered', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t); + const handle = await startWorkflow(queryAndCondition); + await worker.runUntil(handle.query(mutateWorkflowStateQuery)); + await handle.terminate(); + // Worker did not crash + t.pass(); +}); + +const completeSignal = defineSignal('complete'); +const definedQuery = defineQuery('query-handler-type'); + +interface QueryNameAndArgs { + name: string; + queryName?: string; + args: any[]; +} + +export async function workflowWithMaybeDefinedQuery(useDefinedQuery: boolean): Promise { + let complete = false; + setHandler(completeSignal, () => { + complete = true; + }); + setDefaultQueryHandler((queryName: string, ...args: any[]) => { + return { name: 'default', queryName, args }; + }); + if (useDefinedQuery) { + setHandler(definedQuery, (...args: any[]) => { + return { name: definedQuery.name, args }; + }); } - // TODO: - // # Verify a workflow started without priorities sees None for the key - // handle = await client.start_workflow( - // WorkflowUsingPriorities.run, - // args=[None, True], - // id=f"workflow-{uuid.uuid4()}", - // task_queue=worker.task_queue, - // ) - // await handle.result() -}) + + await condition(() => complete); +} + +test('default query handler is used if requested query does not exist', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t, { activities }); + const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { + args: [false], + }); + await worker.runUntil(async () => { + const args = ['test', 'args']; + const result = await handle.query(definedQuery, ...args); + t.deepEqual(result, { name: 'default', queryName: definedQuery.name, args }); + }); +}); + +test('default query handler is not used if requested query exists', configMacro, async (t, config) => { + const { env, createWorkerWithDefaults } = config; + const { startWorkflow } = configurableHelpers(t, t.context.workflowBundle, env); + const worker = await createWorkerWithDefaults(t, { activities }); + const handle = await startWorkflow(workflowWithMaybeDefinedQuery, { + args: [true], + }); + await worker.runUntil(async () => { + const args = ['test', 'args']; + const result = await handle.query('query-handler-type', ...args); + t.deepEqual(result, { name: definedQuery.name, args }); + }); +}); diff --git a/packages/test/src/workflows/priority.ts b/packages/test/src/workflows/priority.ts index da36e4323..a48c76643 100644 --- a/packages/test/src/workflows/priority.ts +++ b/packages/test/src/workflows/priority.ts @@ -1,31 +1,24 @@ -// @@@SNIPSTART typescript-priority-workflow import { executeChild, proxyActivities, startChild, workflowInfo } from '@temporalio/workflow'; -import { temporal } from '@temporalio/proto'; import { Priority } from '@temporalio/common'; import type * as activities from '../activities'; +const { echo } = proxyActivities({ startToCloseTimeout: '5s', priority: { priorityKey: 5 } }); -const { echo } = proxyActivities({ startToCloseTimeout: '5s', priority: new Priority(5) }); - -export async function priorityWorkflow(stopAfterCheck: boolean, expectedPriority?: number): Promise { +export async function priorityWorkflow(stopAfterCheck: boolean, expectedPriority: number | undefined): Promise { const info = workflowInfo(); - console.log("info.priority", info.priority) - console.log("priorityKey", info.priority?.priorityKey) - console.log("expectedPriority", expectedPriority) if (info.priority?.priorityKey !== expectedPriority) { - throw new Error('workflow priority doesn\'t match expected priority'); + throw new Error( + `workflow priority ${info.priority?.priorityKey} doesn't match expected priority ${expectedPriority}` + ); } if (stopAfterCheck) { return; } - await executeChild(priorityWorkflow, {args: [true, 4]}); + await executeChild(priorityWorkflow, { args: [true, 4], priority: { priorityKey: 4 } }); - const child = await startChild(priorityWorkflow, {args: [true, 2],}); + const child = await startChild(priorityWorkflow, { args: [true, 2], priority: { priorityKey: 2 } }); await child.result(); - await echo("hi") - return; + await echo('hi'); } -// @@@SNIPEND - diff --git a/packages/worker/src/worker.ts b/packages/worker/src/worker.ts index 4e617914d..a47a22a36 100644 --- a/packages/worker/src/worker.ts +++ b/packages/worker/src/worker.ts @@ -32,7 +32,7 @@ import { ApplicationFailure, ensureApplicationFailure, TypedSearchAttributes, - Priority, + decodePriority, } from '@temporalio/common'; import { decodeArrayFromPayloads, @@ -1269,7 +1269,7 @@ export class Worker { cronSchedule, workflowExecutionExpirationTime, cronScheduleToScheduleInterval, - priority + priority, } = initWorkflowJob; // Note that we can't do payload convertion here, as there's no guarantee that converted payloads would be safe to @@ -1306,7 +1306,7 @@ export class Worker { now: () => Date.now(), // re-set in initRuntime isReplaying: activation.isReplaying, }, - priority: Priority.fromProto(initWorkflowJob.priority), + priority: decodePriority(priority), }; const logAttributes = workflowLogAttributes(workflowInfo); this.logger.trace('Creating workflow', logAttributes); @@ -1901,7 +1901,7 @@ async function extractActivityInfo( start.currentAttemptScheduledTime, 'currentAttemptScheduledTime' ), - priority: Priority.fromProto(start.priority), + priority: decodePriority(start.priority), }; } diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 956acab06..936e282d0 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -16,7 +16,7 @@ import { } from '@temporalio/common'; import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers'; import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow/enums-helpers'; -import { coresdk } from '@temporalio/proto'; +import type { coresdk } from '@temporalio/proto'; /** * Workflow Execution information diff --git a/packages/workflow/src/workflow.ts b/packages/workflow/src/workflow.ts index c5c6717bb..b5e6500d2 100644 --- a/packages/workflow/src/workflow.ts +++ b/packages/workflow/src/workflow.ts @@ -22,6 +22,7 @@ import { WorkflowReturnType, WorkflowUpdateValidatorType, SearchAttributeUpdatePair, + compilePriority, } from '@temporalio/common'; import { encodeUnifiedSearchAttributes, @@ -193,6 +194,7 @@ function scheduleActivityNextHandler({ options, args, headers, seq, activityType cancellationType: encodeActivityCancellationType(options.cancellationType), doNotEagerlyExecute: !(options.allowEagerDispatch ?? true), versioningIntent: versioningIntentToProto(options.versioningIntent), + priority: options.priority ? compilePriority(options.priority) : undefined, }, }); activator.completions.activity.set(seq, { @@ -393,7 +395,7 @@ function startChildWorkflowExecutionNextHandler({ : undefined, memo: options.memo && mapToPayloads(activator.payloadConverter, options.memo), versioningIntent: versioningIntentToProto(options.versioningIntent), - priority: options.priority, + priority: options.priority ? compilePriority(options.priority) : undefined, }, }); activator.completions.childWorkflowStart.set(seq, { From 7c1c5af6997e0fcd023545aeba4cb8c841438a08 Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Mon, 7 Apr 2025 13:38:00 -0700 Subject: [PATCH 5/9] clean up code --- packages/activity/src/index.ts | 2 +- packages/test/package.json | 4 +--- packages/test/src/helpers.ts | 1 - packages/test/src/test-integration-split-one.ts | 1 + packages/test/src/test-sinks.ts | 1 + packages/workflow/src/interfaces.ts | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/activity/src/index.ts b/packages/activity/src/index.ts index 2c00ecded..b6a7ea295 100644 --- a/packages/activity/src/index.ts +++ b/packages/activity/src/index.ts @@ -199,7 +199,7 @@ export interface Info { */ readonly taskQueue: string; /** - * Priority for Activity + * Priority of an Activity */ readonly priority?: Priority; } diff --git a/packages/test/package.json b/packages/test/package.json index 332e801e6..044a46c7e 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -8,9 +8,7 @@ "build:ts": "tsc --build", "build:protos": "node ./scripts/compile-proto.js", "test": "ava ./lib/test-*.js", - "test.watch": "ava --watch ./lib/test-*.js", - "test-priority": "ava ./lib/test-integration-split-three.js", - "test-priority-one": "ava ./lib/test-integration-split-one.js" + "test.watch": "ava --watch ./lib/test-*.js" }, "ava": { "timeout": "60s", diff --git a/packages/test/src/helpers.ts b/packages/test/src/helpers.ts index c21ce2b68..b8d2484bb 100644 --- a/packages/test/src/helpers.ts +++ b/packages/test/src/helpers.ts @@ -38,7 +38,6 @@ export const REUSE_V8_CONTEXT = inWorkflowContext() || isSet(process.env.REUSE_V export const RUN_TIME_SKIPPING_TESTS = inWorkflowContext() || !(process.platform === 'linux' && process.arch === 'arm64'); -// export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : process.env.TESTS_CLI_VERSION; // TODO: Remove after next CLI release export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : 'v1.3.1-priority.0'; diff --git a/packages/test/src/test-integration-split-one.ts b/packages/test/src/test-integration-split-one.ts index 1348870ed..5cdf53128 100644 --- a/packages/test/src/test-integration-split-one.ts +++ b/packages/test/src/test-integration-split-one.ts @@ -736,6 +736,7 @@ test('Workflow can read WorkflowInfo', configMacro, async (t, config) => { currentBuildId: res.currentBuildId, // unsafe.now is a function, so doesn't make it through serialization, but .now is required, so we need to cast unsafe: { isReplaying: false } as UnsafeWorkflowInfo, + priority: undefined, }); }); diff --git a/packages/test/src/test-sinks.ts b/packages/test/src/test-sinks.ts index dc6402f1b..f537698a9 100644 --- a/packages/test/src/test-sinks.ts +++ b/packages/test/src/test-sinks.ts @@ -133,6 +133,7 @@ if (RUN_INTEGRATION_TESTS) { unsafe: { isReplaying: false, } as UnsafeWorkflowInfo, + priority: undefined, }; t.deepEqual(recordedCalls, [ diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 936e282d0..6e65fc49e 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -186,7 +186,7 @@ export interface WorkflowInfo { readonly unsafe: UnsafeWorkflowInfo; /** - * Priority for a workflow + * Priority of a workflow */ readonly priority?: Priority; } From 5b0bfa3ae187c4a2fc9aa75f50389aa4ac29bda2 Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Mon, 7 Apr 2025 14:23:05 -0700 Subject: [PATCH 6/9] fix tests --- packages/common/src/priority.ts | 4 ++-- packages/test/src/test-integration-split-one.ts | 2 +- packages/test/src/test-sinks.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/common/src/priority.ts b/packages/common/src/priority.ts index 5e6cc8f92..9b2aeeab1 100644 --- a/packages/common/src/priority.ts +++ b/packages/common/src/priority.ts @@ -31,8 +31,8 @@ export interface Priority { /** * Turn a proto compatible Priority into a TS Priority */ -export function decodePriority(proto: temporal.api.common.v1.IPriority | null | undefined): Priority { - return { priorityKey: proto?.priorityKey ?? undefined }; +export function decodePriority(priority?: temporal.api.common.v1.IPriority | null): Priority { + return { priorityKey: priority?.priorityKey ?? undefined }; } /** diff --git a/packages/test/src/test-integration-split-one.ts b/packages/test/src/test-integration-split-one.ts index 5cdf53128..d8540f97f 100644 --- a/packages/test/src/test-integration-split-one.ts +++ b/packages/test/src/test-integration-split-one.ts @@ -736,7 +736,7 @@ test('Workflow can read WorkflowInfo', configMacro, async (t, config) => { currentBuildId: res.currentBuildId, // unsafe.now is a function, so doesn't make it through serialization, but .now is required, so we need to cast unsafe: { isReplaying: false } as UnsafeWorkflowInfo, - priority: undefined, + priority: {}, }); }); diff --git a/packages/test/src/test-sinks.ts b/packages/test/src/test-sinks.ts index f537698a9..a560be25f 100644 --- a/packages/test/src/test-sinks.ts +++ b/packages/test/src/test-sinks.ts @@ -133,7 +133,7 @@ if (RUN_INTEGRATION_TESTS) { unsafe: { isReplaying: false, } as UnsafeWorkflowInfo, - priority: undefined, + priority: {}, }; t.deepEqual(recordedCalls, [ From 36c04f07c161d87c26938415e08568c737666a5b Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Mon, 7 Apr 2025 14:32:48 -0700 Subject: [PATCH 7/9] fix tests and add clarity --- packages/test/src/test-sinks.ts | 590 ++++++++++++------------ packages/test/src/workflows/priority.ts | 3 + 2 files changed, 299 insertions(+), 294 deletions(-) diff --git a/packages/test/src/test-sinks.ts b/packages/test/src/test-sinks.ts index a560be25f..487ed2498 100644 --- a/packages/test/src/test-sinks.ts +++ b/packages/test/src/test-sinks.ts @@ -19,7 +19,7 @@ class DependencyError extends Error { } } -if (RUN_INTEGRATION_TESTS) { +// if (RUN_INTEGRATION_TESTS) { const recordedLogs: { [workflowId: string]: LogEntry[] } = {}; test.before(async (_) => { @@ -133,7 +133,9 @@ if (RUN_INTEGRATION_TESTS) { unsafe: { isReplaying: false, } as UnsafeWorkflowInfo, - priority: {}, + priority: { + priorityKey: undefined, + }, }; t.deepEqual(recordedCalls, [ @@ -176,295 +178,295 @@ if (RUN_INTEGRATION_TESTS) { ); }); - test('Sink functions are not called during replay if callDuringReplay is unset', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - async fn(info, message) { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - }, - }, - }; - - const client = new WorkflowClient(); - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - maxCachedWorkflows: 0, - maxConcurrentWorkflowTaskExecutions: 2, - }); - await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); - - t.deepEqual(recordedMessages, [ - { - message: 'Workflow execution started, replaying: false, hl: 3', - historyLength: 3, - isReplaying: false, - }, - { - message: 'Workflow execution completed, replaying: false, hl: 8', - historyLength: 8, - isReplaying: false, - }, - ]); - }); - - test('Sink functions are called during replay if callDuringReplay is set', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - callDuringReplay: true, - }, - }, - }; - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - maxCachedWorkflows: 0, - maxConcurrentWorkflowTaskExecutions: 2, - }); - const client = new WorkflowClient(); - await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); - - // Note that task may be replayed more than once and record the first messages multiple times. - t.deepEqual(recordedMessages.slice(0, 2), [ - { - message: 'Workflow execution started, replaying: false, hl: 3', - historyLength: 3, - isReplaying: false, - }, - { - message: 'Workflow execution started, replaying: true, hl: 3', - historyLength: 3, - isReplaying: true, - }, - ]); - t.deepEqual(recordedMessages[recordedMessages.length - 1], { - message: 'Workflow execution completed, replaying: false, hl: 8', - historyLength: 8, - isReplaying: false, - }); - }); - - test('Sink functions are not called in runReplayHistories if callDuringReplay is unset', async (t) => { - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - }, - }, - }; - - const client = new WorkflowClient(); - const taskQueue = `${__filename}-${t.title}`; - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - const workflowId = uuid4(); - await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId })); - const history = await client.getHandle(workflowId).fetchHistory(); - - // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted - history.events = history!.events!.slice(0, -3); - - recordedMessages.length = 0; - await Worker.runReplayHistory( - { - ...defaultOptions, - sinks, - }, - history, - workflowId - ); - - t.deepEqual(recordedMessages, []); - }); - - test('Sink functions are called in runReplayHistories if callDuringReplay is set', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - callDuringReplay: true, - }, - }, - }; - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - const client = new WorkflowClient(); - const workflowId = uuid4(); - await worker.runUntil(async () => { - await client.execute(workflows.logSinkTester, { taskQueue, workflowId }); - }); - const history = await client.getHandle(workflowId).fetchHistory(); - - // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted - history.events = history!.events!.slice(0, -3); - - recordedMessages.length = 0; - await Worker.runReplayHistory( - { - ...defaultOptions, - sinks, - }, - history, - workflowId - ); - - t.deepEqual(recordedMessages.slice(0, 2), [ - { - message: 'Workflow execution started, replaying: true, hl: 3', - isReplaying: true, - historyLength: 3, - }, - { - message: 'Workflow execution completed, replaying: false, hl: 7', - isReplaying: false, - historyLength: 7, - }, - ]); - }); - - test('Sink functions contains upserted search attributes', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; searchAttributes: SearchAttributes }>(); // eslint-disable-line deprecation/deprecation - const sinks = asSdkLoggerSink(async (info, message, _attrs) => { - recordedMessages.push({ - message, - searchAttributes: info.searchAttributes, // eslint-disable-line deprecation/deprecation - }); - }); - - const client = new WorkflowClient(); - const date = new Date(); - - const worker = await Worker.create({ - ...defaultOptions, - taskQueue, - sinks, - }); - await worker.runUntil( - client.execute(workflows.upsertAndReadSearchAttributes, { - taskQueue, - workflowId: uuid4(), - args: [date.getTime()], - }) - ); - - t.deepEqual(recordedMessages, [ - { - message: 'Workflow started', - searchAttributes: {}, - }, - { - message: 'Workflow completed', - searchAttributes: { - CustomBoolField: [true], - CustomKeywordField: ['durable code'], - CustomTextField: ['is useful'], - CustomDatetimeField: [date], - CustomDoubleField: [3.14], - }, - }, - ]); - }); - - test('Core issue 589', async (t) => { - const taskQueue = `${__filename}-${t.title}`; - - const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); - const sinks: InjectedSinks = { - customLogger: { - info: { - fn: async (info, message) => { - recordedMessages.push({ - message, - historyLength: info.historyLength, - isReplaying: info.unsafe.isReplaying, - }); - }, - callDuringReplay: true, - }, - }, - }; - - const client = new WorkflowClient(); - const handle = await client.start(workflows.coreIssue589, { taskQueue, workflowId: uuid4() }); - - const workerOptions: WorkerOptions = { - ...defaultOptions, - taskQueue, - sinks, - maxCachedWorkflows: 2, - maxConcurrentWorkflowTaskExecutions: 2, - - // Cut down on execution time - stickyQueueScheduleToStartTimeout: 1, - }; - - // Start the first worker and wait for the first task to complete before shutdown that worker - await (await Worker.create(workerOptions)).runUntil(handle.query('q')); - - // Start the second worker - await ( - await Worker.create(workerOptions) - ).runUntil(async () => { - await handle.query('q'); - await handle.signal(workflows.unblockSignal); - await handle.result(); - }); - - const checkpointEntries = recordedMessages.filter((m) => m.message.startsWith('Checkpoint')); - t.deepEqual(checkpointEntries, [ - { - message: 'Checkpoint, replaying: false, hl: 8', - historyLength: 8, - isReplaying: false, - }, - ]); - }); -} +// test('Sink functions are not called during replay if callDuringReplay is unset', async (t) => { +// const taskQueue = `${__filename}-${t.title}`; +// +// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); +// const sinks: InjectedSinks = { +// customLogger: { +// info: { +// async fn(info, message) { +// recordedMessages.push({ +// message, +// historyLength: info.historyLength, +// isReplaying: info.unsafe.isReplaying, +// }); +// }, +// }, +// }, +// }; +// +// const client = new WorkflowClient(); +// const worker = await Worker.create({ +// ...defaultOptions, +// taskQueue, +// sinks, +// maxCachedWorkflows: 0, +// maxConcurrentWorkflowTaskExecutions: 2, +// }); +// await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); +// +// t.deepEqual(recordedMessages, [ +// { +// message: 'Workflow execution started, replaying: false, hl: 3', +// historyLength: 3, +// isReplaying: false, +// }, +// { +// message: 'Workflow execution completed, replaying: false, hl: 8', +// historyLength: 8, +// isReplaying: false, +// }, +// ]); +// }); +// +// test('Sink functions are called during replay if callDuringReplay is set', async (t) => { +// const taskQueue = `${__filename}-${t.title}`; +// +// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); +// const sinks: InjectedSinks = { +// customLogger: { +// info: { +// fn: async (info, message) => { +// recordedMessages.push({ +// message, +// historyLength: info.historyLength, +// isReplaying: info.unsafe.isReplaying, +// }); +// }, +// callDuringReplay: true, +// }, +// }, +// }; +// +// const worker = await Worker.create({ +// ...defaultOptions, +// taskQueue, +// sinks, +// maxCachedWorkflows: 0, +// maxConcurrentWorkflowTaskExecutions: 2, +// }); +// const client = new WorkflowClient(); +// await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); +// +// // Note that task may be replayed more than once and record the first messages multiple times. +// t.deepEqual(recordedMessages.slice(0, 2), [ +// { +// message: 'Workflow execution started, replaying: false, hl: 3', +// historyLength: 3, +// isReplaying: false, +// }, +// { +// message: 'Workflow execution started, replaying: true, hl: 3', +// historyLength: 3, +// isReplaying: true, +// }, +// ]); +// t.deepEqual(recordedMessages[recordedMessages.length - 1], { +// message: 'Workflow execution completed, replaying: false, hl: 8', +// historyLength: 8, +// isReplaying: false, +// }); +// }); +// +// test('Sink functions are not called in runReplayHistories if callDuringReplay is unset', async (t) => { +// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); +// const sinks: InjectedSinks = { +// customLogger: { +// info: { +// fn: async (info, message) => { +// recordedMessages.push({ +// message, +// historyLength: info.historyLength, +// isReplaying: info.unsafe.isReplaying, +// }); +// }, +// }, +// }, +// }; +// +// const client = new WorkflowClient(); +// const taskQueue = `${__filename}-${t.title}`; +// const worker = await Worker.create({ +// ...defaultOptions, +// taskQueue, +// sinks, +// }); +// const workflowId = uuid4(); +// await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId })); +// const history = await client.getHandle(workflowId).fetchHistory(); +// +// // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted +// history.events = history!.events!.slice(0, -3); +// +// recordedMessages.length = 0; +// await Worker.runReplayHistory( +// { +// ...defaultOptions, +// sinks, +// }, +// history, +// workflowId +// ); +// +// t.deepEqual(recordedMessages, []); +// }); +// +// test('Sink functions are called in runReplayHistories if callDuringReplay is set', async (t) => { +// const taskQueue = `${__filename}-${t.title}`; +// +// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); +// const sinks: InjectedSinks = { +// customLogger: { +// info: { +// fn: async (info, message) => { +// recordedMessages.push({ +// message, +// historyLength: info.historyLength, +// isReplaying: info.unsafe.isReplaying, +// }); +// }, +// callDuringReplay: true, +// }, +// }, +// }; +// +// const worker = await Worker.create({ +// ...defaultOptions, +// taskQueue, +// sinks, +// }); +// const client = new WorkflowClient(); +// const workflowId = uuid4(); +// await worker.runUntil(async () => { +// await client.execute(workflows.logSinkTester, { taskQueue, workflowId }); +// }); +// const history = await client.getHandle(workflowId).fetchHistory(); +// +// // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted +// history.events = history!.events!.slice(0, -3); +// +// recordedMessages.length = 0; +// await Worker.runReplayHistory( +// { +// ...defaultOptions, +// sinks, +// }, +// history, +// workflowId +// ); +// +// t.deepEqual(recordedMessages.slice(0, 2), [ +// { +// message: 'Workflow execution started, replaying: true, hl: 3', +// isReplaying: true, +// historyLength: 3, +// }, +// { +// message: 'Workflow execution completed, replaying: false, hl: 7', +// isReplaying: false, +// historyLength: 7, +// }, +// ]); +// }); +// +// test('Sink functions contains upserted search attributes', async (t) => { +// const taskQueue = `${__filename}-${t.title}`; +// +// const recordedMessages = Array<{ message: string; searchAttributes: SearchAttributes }>(); // eslint-disable-line deprecation/deprecation +// const sinks = asSdkLoggerSink(async (info, message, _attrs) => { +// recordedMessages.push({ +// message, +// searchAttributes: info.searchAttributes, // eslint-disable-line deprecation/deprecation +// }); +// }); +// +// const client = new WorkflowClient(); +// const date = new Date(); +// +// const worker = await Worker.create({ +// ...defaultOptions, +// taskQueue, +// sinks, +// }); +// await worker.runUntil( +// client.execute(workflows.upsertAndReadSearchAttributes, { +// taskQueue, +// workflowId: uuid4(), +// args: [date.getTime()], +// }) +// ); +// +// t.deepEqual(recordedMessages, [ +// { +// message: 'Workflow started', +// searchAttributes: {}, +// }, +// { +// message: 'Workflow completed', +// searchAttributes: { +// CustomBoolField: [true], +// CustomKeywordField: ['durable code'], +// CustomTextField: ['is useful'], +// CustomDatetimeField: [date], +// CustomDoubleField: [3.14], +// }, +// }, +// ]); +// }); +// +// test('Core issue 589', async (t) => { +// const taskQueue = `${__filename}-${t.title}`; +// +// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); +// const sinks: InjectedSinks = { +// customLogger: { +// info: { +// fn: async (info, message) => { +// recordedMessages.push({ +// message, +// historyLength: info.historyLength, +// isReplaying: info.unsafe.isReplaying, +// }); +// }, +// callDuringReplay: true, +// }, +// }, +// }; +// +// const client = new WorkflowClient(); +// const handle = await client.start(workflows.coreIssue589, { taskQueue, workflowId: uuid4() }); +// +// const workerOptions: WorkerOptions = { +// ...defaultOptions, +// taskQueue, +// sinks, +// maxCachedWorkflows: 2, +// maxConcurrentWorkflowTaskExecutions: 2, +// +// // Cut down on execution time +// stickyQueueScheduleToStartTimeout: 1, +// }; +// +// // Start the first worker and wait for the first task to complete before shutdown that worker +// await (await Worker.create(workerOptions)).runUntil(handle.query('q')); +// +// // Start the second worker +// await ( +// await Worker.create(workerOptions) +// ).runUntil(async () => { +// await handle.query('q'); +// await handle.signal(workflows.unblockSignal); +// await handle.result(); +// }); +// +// const checkpointEntries = recordedMessages.filter((m) => m.message.startsWith('Checkpoint')); +// t.deepEqual(checkpointEntries, [ +// { +// message: 'Checkpoint, replaying: false, hl: 8', +// historyLength: 8, +// isReplaying: false, +// }, +// ]); +// }); +// } diff --git a/packages/test/src/workflows/priority.ts b/packages/test/src/workflows/priority.ts index a48c76643..b8299a2a4 100644 --- a/packages/test/src/workflows/priority.ts +++ b/packages/test/src/workflows/priority.ts @@ -6,6 +6,9 @@ const { echo } = proxyActivities({ startToCloseTimeout: '5s', export async function priorityWorkflow(stopAfterCheck: boolean, expectedPriority: number | undefined): Promise { const info = workflowInfo(); + if (!info.priority) { + throw new Error(`undefined priority`); + } if (info.priority?.priorityKey !== expectedPriority) { throw new Error( `workflow priority ${info.priority?.priorityKey} doesn't match expected priority ${expectedPriority}` From 2dfa4c0ca5c1bd412da86bb8c8e6c17f6098aa3e Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Mon, 7 Apr 2025 14:42:35 -0700 Subject: [PATCH 8/9] Allow for 0, fix lints --- packages/common/src/priority.ts | 2 +- packages/test/src/test-sinks.ts | 586 ++++++++++++------------ packages/test/src/workflows/priority.ts | 1 - 3 files changed, 294 insertions(+), 295 deletions(-) diff --git a/packages/common/src/priority.ts b/packages/common/src/priority.ts index 9b2aeeab1..a7bd220bc 100644 --- a/packages/common/src/priority.ts +++ b/packages/common/src/priority.ts @@ -43,7 +43,7 @@ export function compilePriority(priority: Priority): temporal.api.common.v1.IPri if (!Number.isInteger(priority.priorityKey)) { throw new TypeError('priorityKey must be an integer'); } - if (priority.priorityKey < 1) { + if (priority.priorityKey < 0) { throw new RangeError('priorityKey must be a positive integer'); } } diff --git a/packages/test/src/test-sinks.ts b/packages/test/src/test-sinks.ts index 487ed2498..f3991fccb 100644 --- a/packages/test/src/test-sinks.ts +++ b/packages/test/src/test-sinks.ts @@ -19,7 +19,7 @@ class DependencyError extends Error { } } -// if (RUN_INTEGRATION_TESTS) { +if (RUN_INTEGRATION_TESTS) { const recordedLogs: { [workflowId: string]: LogEntry[] } = {}; test.before(async (_) => { @@ -178,295 +178,295 @@ class DependencyError extends Error { ); }); -// test('Sink functions are not called during replay if callDuringReplay is unset', async (t) => { -// const taskQueue = `${__filename}-${t.title}`; -// -// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); -// const sinks: InjectedSinks = { -// customLogger: { -// info: { -// async fn(info, message) { -// recordedMessages.push({ -// message, -// historyLength: info.historyLength, -// isReplaying: info.unsafe.isReplaying, -// }); -// }, -// }, -// }, -// }; -// -// const client = new WorkflowClient(); -// const worker = await Worker.create({ -// ...defaultOptions, -// taskQueue, -// sinks, -// maxCachedWorkflows: 0, -// maxConcurrentWorkflowTaskExecutions: 2, -// }); -// await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); -// -// t.deepEqual(recordedMessages, [ -// { -// message: 'Workflow execution started, replaying: false, hl: 3', -// historyLength: 3, -// isReplaying: false, -// }, -// { -// message: 'Workflow execution completed, replaying: false, hl: 8', -// historyLength: 8, -// isReplaying: false, -// }, -// ]); -// }); -// -// test('Sink functions are called during replay if callDuringReplay is set', async (t) => { -// const taskQueue = `${__filename}-${t.title}`; -// -// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); -// const sinks: InjectedSinks = { -// customLogger: { -// info: { -// fn: async (info, message) => { -// recordedMessages.push({ -// message, -// historyLength: info.historyLength, -// isReplaying: info.unsafe.isReplaying, -// }); -// }, -// callDuringReplay: true, -// }, -// }, -// }; -// -// const worker = await Worker.create({ -// ...defaultOptions, -// taskQueue, -// sinks, -// maxCachedWorkflows: 0, -// maxConcurrentWorkflowTaskExecutions: 2, -// }); -// const client = new WorkflowClient(); -// await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); -// -// // Note that task may be replayed more than once and record the first messages multiple times. -// t.deepEqual(recordedMessages.slice(0, 2), [ -// { -// message: 'Workflow execution started, replaying: false, hl: 3', -// historyLength: 3, -// isReplaying: false, -// }, -// { -// message: 'Workflow execution started, replaying: true, hl: 3', -// historyLength: 3, -// isReplaying: true, -// }, -// ]); -// t.deepEqual(recordedMessages[recordedMessages.length - 1], { -// message: 'Workflow execution completed, replaying: false, hl: 8', -// historyLength: 8, -// isReplaying: false, -// }); -// }); -// -// test('Sink functions are not called in runReplayHistories if callDuringReplay is unset', async (t) => { -// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); -// const sinks: InjectedSinks = { -// customLogger: { -// info: { -// fn: async (info, message) => { -// recordedMessages.push({ -// message, -// historyLength: info.historyLength, -// isReplaying: info.unsafe.isReplaying, -// }); -// }, -// }, -// }, -// }; -// -// const client = new WorkflowClient(); -// const taskQueue = `${__filename}-${t.title}`; -// const worker = await Worker.create({ -// ...defaultOptions, -// taskQueue, -// sinks, -// }); -// const workflowId = uuid4(); -// await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId })); -// const history = await client.getHandle(workflowId).fetchHistory(); -// -// // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted -// history.events = history!.events!.slice(0, -3); -// -// recordedMessages.length = 0; -// await Worker.runReplayHistory( -// { -// ...defaultOptions, -// sinks, -// }, -// history, -// workflowId -// ); -// -// t.deepEqual(recordedMessages, []); -// }); -// -// test('Sink functions are called in runReplayHistories if callDuringReplay is set', async (t) => { -// const taskQueue = `${__filename}-${t.title}`; -// -// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); -// const sinks: InjectedSinks = { -// customLogger: { -// info: { -// fn: async (info, message) => { -// recordedMessages.push({ -// message, -// historyLength: info.historyLength, -// isReplaying: info.unsafe.isReplaying, -// }); -// }, -// callDuringReplay: true, -// }, -// }, -// }; -// -// const worker = await Worker.create({ -// ...defaultOptions, -// taskQueue, -// sinks, -// }); -// const client = new WorkflowClient(); -// const workflowId = uuid4(); -// await worker.runUntil(async () => { -// await client.execute(workflows.logSinkTester, { taskQueue, workflowId }); -// }); -// const history = await client.getHandle(workflowId).fetchHistory(); -// -// // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted -// history.events = history!.events!.slice(0, -3); -// -// recordedMessages.length = 0; -// await Worker.runReplayHistory( -// { -// ...defaultOptions, -// sinks, -// }, -// history, -// workflowId -// ); -// -// t.deepEqual(recordedMessages.slice(0, 2), [ -// { -// message: 'Workflow execution started, replaying: true, hl: 3', -// isReplaying: true, -// historyLength: 3, -// }, -// { -// message: 'Workflow execution completed, replaying: false, hl: 7', -// isReplaying: false, -// historyLength: 7, -// }, -// ]); -// }); -// -// test('Sink functions contains upserted search attributes', async (t) => { -// const taskQueue = `${__filename}-${t.title}`; -// -// const recordedMessages = Array<{ message: string; searchAttributes: SearchAttributes }>(); // eslint-disable-line deprecation/deprecation -// const sinks = asSdkLoggerSink(async (info, message, _attrs) => { -// recordedMessages.push({ -// message, -// searchAttributes: info.searchAttributes, // eslint-disable-line deprecation/deprecation -// }); -// }); -// -// const client = new WorkflowClient(); -// const date = new Date(); -// -// const worker = await Worker.create({ -// ...defaultOptions, -// taskQueue, -// sinks, -// }); -// await worker.runUntil( -// client.execute(workflows.upsertAndReadSearchAttributes, { -// taskQueue, -// workflowId: uuid4(), -// args: [date.getTime()], -// }) -// ); -// -// t.deepEqual(recordedMessages, [ -// { -// message: 'Workflow started', -// searchAttributes: {}, -// }, -// { -// message: 'Workflow completed', -// searchAttributes: { -// CustomBoolField: [true], -// CustomKeywordField: ['durable code'], -// CustomTextField: ['is useful'], -// CustomDatetimeField: [date], -// CustomDoubleField: [3.14], -// }, -// }, -// ]); -// }); -// -// test('Core issue 589', async (t) => { -// const taskQueue = `${__filename}-${t.title}`; -// -// const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); -// const sinks: InjectedSinks = { -// customLogger: { -// info: { -// fn: async (info, message) => { -// recordedMessages.push({ -// message, -// historyLength: info.historyLength, -// isReplaying: info.unsafe.isReplaying, -// }); -// }, -// callDuringReplay: true, -// }, -// }, -// }; -// -// const client = new WorkflowClient(); -// const handle = await client.start(workflows.coreIssue589, { taskQueue, workflowId: uuid4() }); -// -// const workerOptions: WorkerOptions = { -// ...defaultOptions, -// taskQueue, -// sinks, -// maxCachedWorkflows: 2, -// maxConcurrentWorkflowTaskExecutions: 2, -// -// // Cut down on execution time -// stickyQueueScheduleToStartTimeout: 1, -// }; -// -// // Start the first worker and wait for the first task to complete before shutdown that worker -// await (await Worker.create(workerOptions)).runUntil(handle.query('q')); -// -// // Start the second worker -// await ( -// await Worker.create(workerOptions) -// ).runUntil(async () => { -// await handle.query('q'); -// await handle.signal(workflows.unblockSignal); -// await handle.result(); -// }); -// -// const checkpointEntries = recordedMessages.filter((m) => m.message.startsWith('Checkpoint')); -// t.deepEqual(checkpointEntries, [ -// { -// message: 'Checkpoint, replaying: false, hl: 8', -// historyLength: 8, -// isReplaying: false, -// }, -// ]); -// }); -// } + test('Sink functions are not called during replay if callDuringReplay is unset', async (t) => { + const taskQueue = `${__filename}-${t.title}`; + + const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); + const sinks: InjectedSinks = { + customLogger: { + info: { + async fn(info, message) { + recordedMessages.push({ + message, + historyLength: info.historyLength, + isReplaying: info.unsafe.isReplaying, + }); + }, + }, + }, + }; + + const client = new WorkflowClient(); + const worker = await Worker.create({ + ...defaultOptions, + taskQueue, + sinks, + maxCachedWorkflows: 0, + maxConcurrentWorkflowTaskExecutions: 2, + }); + await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); + + t.deepEqual(recordedMessages, [ + { + message: 'Workflow execution started, replaying: false, hl: 3', + historyLength: 3, + isReplaying: false, + }, + { + message: 'Workflow execution completed, replaying: false, hl: 8', + historyLength: 8, + isReplaying: false, + }, + ]); + }); + + test('Sink functions are called during replay if callDuringReplay is set', async (t) => { + const taskQueue = `${__filename}-${t.title}`; + + const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); + const sinks: InjectedSinks = { + customLogger: { + info: { + fn: async (info, message) => { + recordedMessages.push({ + message, + historyLength: info.historyLength, + isReplaying: info.unsafe.isReplaying, + }); + }, + callDuringReplay: true, + }, + }, + }; + + const worker = await Worker.create({ + ...defaultOptions, + taskQueue, + sinks, + maxCachedWorkflows: 0, + maxConcurrentWorkflowTaskExecutions: 2, + }); + const client = new WorkflowClient(); + await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId: uuid4() })); + + // Note that task may be replayed more than once and record the first messages multiple times. + t.deepEqual(recordedMessages.slice(0, 2), [ + { + message: 'Workflow execution started, replaying: false, hl: 3', + historyLength: 3, + isReplaying: false, + }, + { + message: 'Workflow execution started, replaying: true, hl: 3', + historyLength: 3, + isReplaying: true, + }, + ]); + t.deepEqual(recordedMessages[recordedMessages.length - 1], { + message: 'Workflow execution completed, replaying: false, hl: 8', + historyLength: 8, + isReplaying: false, + }); + }); + + test('Sink functions are not called in runReplayHistories if callDuringReplay is unset', async (t) => { + const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); + const sinks: InjectedSinks = { + customLogger: { + info: { + fn: async (info, message) => { + recordedMessages.push({ + message, + historyLength: info.historyLength, + isReplaying: info.unsafe.isReplaying, + }); + }, + }, + }, + }; + + const client = new WorkflowClient(); + const taskQueue = `${__filename}-${t.title}`; + const worker = await Worker.create({ + ...defaultOptions, + taskQueue, + sinks, + }); + const workflowId = uuid4(); + await worker.runUntil(client.execute(workflows.logSinkTester, { taskQueue, workflowId })); + const history = await client.getHandle(workflowId).fetchHistory(); + + // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted + history.events = history!.events!.slice(0, -3); + + recordedMessages.length = 0; + await Worker.runReplayHistory( + { + ...defaultOptions, + sinks, + }, + history, + workflowId + ); + + t.deepEqual(recordedMessages, []); + }); + + test('Sink functions are called in runReplayHistories if callDuringReplay is set', async (t) => { + const taskQueue = `${__filename}-${t.title}`; + + const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); + const sinks: InjectedSinks = { + customLogger: { + info: { + fn: async (info, message) => { + recordedMessages.push({ + message, + historyLength: info.historyLength, + isReplaying: info.unsafe.isReplaying, + }); + }, + callDuringReplay: true, + }, + }, + }; + + const worker = await Worker.create({ + ...defaultOptions, + taskQueue, + sinks, + }); + const client = new WorkflowClient(); + const workflowId = uuid4(); + await worker.runUntil(async () => { + await client.execute(workflows.logSinkTester, { taskQueue, workflowId }); + }); + const history = await client.getHandle(workflowId).fetchHistory(); + + // Last 3 events are WorkflowExecutionStarted, WorkflowTaskCompleted and WorkflowExecutionCompleted + history.events = history!.events!.slice(0, -3); + + recordedMessages.length = 0; + await Worker.runReplayHistory( + { + ...defaultOptions, + sinks, + }, + history, + workflowId + ); + + t.deepEqual(recordedMessages.slice(0, 2), [ + { + message: 'Workflow execution started, replaying: true, hl: 3', + isReplaying: true, + historyLength: 3, + }, + { + message: 'Workflow execution completed, replaying: false, hl: 7', + isReplaying: false, + historyLength: 7, + }, + ]); + }); + + test('Sink functions contains upserted search attributes', async (t) => { + const taskQueue = `${__filename}-${t.title}`; + + const recordedMessages = Array<{ message: string; searchAttributes: SearchAttributes }>(); // eslint-disable-line deprecation/deprecation + const sinks = asSdkLoggerSink(async (info, message, _attrs) => { + recordedMessages.push({ + message, + searchAttributes: info.searchAttributes, // eslint-disable-line deprecation/deprecation + }); + }); + + const client = new WorkflowClient(); + const date = new Date(); + + const worker = await Worker.create({ + ...defaultOptions, + taskQueue, + sinks, + }); + await worker.runUntil( + client.execute(workflows.upsertAndReadSearchAttributes, { + taskQueue, + workflowId: uuid4(), + args: [date.getTime()], + }) + ); + + t.deepEqual(recordedMessages, [ + { + message: 'Workflow started', + searchAttributes: {}, + }, + { + message: 'Workflow completed', + searchAttributes: { + CustomBoolField: [true], + CustomKeywordField: ['durable code'], + CustomTextField: ['is useful'], + CustomDatetimeField: [date], + CustomDoubleField: [3.14], + }, + }, + ]); + }); + + test('Core issue 589', async (t) => { + const taskQueue = `${__filename}-${t.title}`; + + const recordedMessages = Array<{ message: string; historyLength: number; isReplaying: boolean }>(); + const sinks: InjectedSinks = { + customLogger: { + info: { + fn: async (info, message) => { + recordedMessages.push({ + message, + historyLength: info.historyLength, + isReplaying: info.unsafe.isReplaying, + }); + }, + callDuringReplay: true, + }, + }, + }; + + const client = new WorkflowClient(); + const handle = await client.start(workflows.coreIssue589, { taskQueue, workflowId: uuid4() }); + + const workerOptions: WorkerOptions = { + ...defaultOptions, + taskQueue, + sinks, + maxCachedWorkflows: 2, + maxConcurrentWorkflowTaskExecutions: 2, + + // Cut down on execution time + stickyQueueScheduleToStartTimeout: 1, + }; + + // Start the first worker and wait for the first task to complete before shutdown that worker + await (await Worker.create(workerOptions)).runUntil(handle.query('q')); + + // Start the second worker + await ( + await Worker.create(workerOptions) + ).runUntil(async () => { + await handle.query('q'); + await handle.signal(workflows.unblockSignal); + await handle.result(); + }); + + const checkpointEntries = recordedMessages.filter((m) => m.message.startsWith('Checkpoint')); + t.deepEqual(checkpointEntries, [ + { + message: 'Checkpoint, replaying: false, hl: 8', + historyLength: 8, + isReplaying: false, + }, + ]); + }); +} diff --git a/packages/test/src/workflows/priority.ts b/packages/test/src/workflows/priority.ts index b8299a2a4..3e99685a8 100644 --- a/packages/test/src/workflows/priority.ts +++ b/packages/test/src/workflows/priority.ts @@ -1,5 +1,4 @@ import { executeChild, proxyActivities, startChild, workflowInfo } from '@temporalio/workflow'; -import { Priority } from '@temporalio/common'; import type * as activities from '../activities'; const { echo } = proxyActivities({ startToCloseTimeout: '5s', priority: { priorityKey: 5 } }); From f7311b2d19476277dd43666a1acb230b84873821 Mon Sep 17 00:00:00 2001 From: Andrew Yuan Date: Fri, 18 Apr 2025 13:13:18 -0700 Subject: [PATCH 9/9] Move TEST_CLI_VERSION override to YAML files, change an to this --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 1 + packages/activity/src/index.ts | 2 +- packages/common/src/activity-options.ts | 2 +- packages/test/src/helpers.ts | 3 +-- packages/workflow/src/interfaces.ts | 2 +- 6 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 450e15940..523049675 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,6 +19,7 @@ env: # Use these variables to force specific version of CLI/Time Skipping Server for SDK tests # TESTS_CLI_VERSION: 'v0.13.2' + TESTS_CLI_VERSION: 'v1.3.1-persistence-fix.0' # TESTS_TIME_SKIPPING_SERVER_VERSION: 'v1.24.1' jobs: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8f88755fc..13e791d3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,7 @@ env: # Use these variables to force specific version of CLI/Time Skipping Server for SDK tests # TESTS_CLI_VERSION: 'v0.13.2' + TESTS_CLI_VERSION: 'v1.3.1-persistence-fix.0' # TESTS_TIME_SKIPPING_SERVER_VERSION: 'v1.24.1' jobs: diff --git a/packages/activity/src/index.ts b/packages/activity/src/index.ts index b6a7ea295..3a82ba5e8 100644 --- a/packages/activity/src/index.ts +++ b/packages/activity/src/index.ts @@ -199,7 +199,7 @@ export interface Info { */ readonly taskQueue: string; /** - * Priority of an Activity + * Priority of this activity */ readonly priority?: Priority; } diff --git a/packages/common/src/activity-options.ts b/packages/common/src/activity-options.ts index 3f19f505a..1d0bc31cc 100644 --- a/packages/common/src/activity-options.ts +++ b/packages/common/src/activity-options.ts @@ -125,7 +125,7 @@ export interface ActivityOptions { versioningIntent?: VersioningIntent; /** - * Priority of an activity + * Priority of this activity */ priority?: Priority; } diff --git a/packages/test/src/helpers.ts b/packages/test/src/helpers.ts index b8d2484bb..0190ecd54 100644 --- a/packages/test/src/helpers.ts +++ b/packages/test/src/helpers.ts @@ -38,8 +38,7 @@ export const REUSE_V8_CONTEXT = inWorkflowContext() || isSet(process.env.REUSE_V export const RUN_TIME_SKIPPING_TESTS = inWorkflowContext() || !(process.platform === 'linux' && process.arch === 'arm64'); -// TODO: Remove after next CLI release -export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : 'v1.3.1-priority.0'; +export const TESTS_CLI_VERSION = inWorkflowContext() ? '' : process.env.TESTS_CLI_VERSION; export const TESTS_TIME_SKIPPING_SERVER_VERSION = inWorkflowContext() ? '' diff --git a/packages/workflow/src/interfaces.ts b/packages/workflow/src/interfaces.ts index 6e65fc49e..25fb2218a 100644 --- a/packages/workflow/src/interfaces.ts +++ b/packages/workflow/src/interfaces.ts @@ -186,7 +186,7 @@ export interface WorkflowInfo { readonly unsafe: UnsafeWorkflowInfo; /** - * Priority of a workflow + * Priority of this workflow */ readonly priority?: Priority; }