From 7391595a6796f7754fbc4a0a83abc5567be2dbb9 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Wed, 11 Jan 2023 16:25:19 -0600 Subject: [PATCH 01/14] feat: repalce sleep with time and duration --- apps/test-app-runtime/src/slack-bot.ts | 4 +- apps/tests/aws-runtime/test/test-service.ts | 12 ++-- .../@eventual/compiler/test-files/workflow.ts | 2 +- .../__snapshots__/esbuild-plugin.test.ts.snap | 2 +- packages/@eventual/core/src/await-time.ts | 56 +++++++++++++++++++ .../core/src/calls/await-time-call.ts | 50 +++++++++++++++++ packages/@eventual/core/src/calls/index.ts | 3 +- .../@eventual/core/src/calls/sleep-call.ts | 44 --------------- packages/@eventual/core/src/command.ts | 29 +++++----- packages/@eventual/core/src/eventual.ts | 22 ++++---- packages/@eventual/core/src/index.ts | 2 +- packages/@eventual/core/src/interpret.ts | 18 +++--- .../core/src/runtime/command-executor.ts | 43 +++++++++++--- packages/@eventual/core/src/signals.ts | 8 +-- packages/@eventual/core/src/sleep.ts | 34 ----------- packages/@eventual/core/test/command-util.ts | 23 ++++---- .../core/test/commend-executor.test.ts | 13 +++-- .../@eventual/core/test/interpret.test.ts | 56 +++++++++---------- packages/@eventual/testing/test/workflow.ts | 12 ++-- 19 files changed, 249 insertions(+), 184 deletions(-) create mode 100644 packages/@eventual/core/src/await-time.ts create mode 100644 packages/@eventual/core/src/calls/await-time-call.ts delete mode 100644 packages/@eventual/core/src/calls/sleep-call.ts delete mode 100644 packages/@eventual/core/src/sleep.ts diff --git a/apps/test-app-runtime/src/slack-bot.ts b/apps/test-app-runtime/src/slack-bot.ts index 865b34e65..7133527b4 100644 --- a/apps/test-app-runtime/src/slack-bot.ts +++ b/apps/test-app-runtime/src/slack-bot.ts @@ -1,10 +1,10 @@ import { Slack, SlackCredentials } from "@eventual/integrations-slack"; import { AWSSecret } from "@eventual/aws-client"; import { + duration, expectSignal, JsonSecret, sendSignal, - sleepFor, workflow, } from "@eventual/core"; import ms from "ms"; @@ -47,7 +47,7 @@ const remindMe = workflow( message: string; waitSeconds: number; }) => { - await sleepFor(request.waitSeconds); + await duration(request.waitSeconds); await slack.client.chat.postMessage({ channel: request.channel, diff --git a/apps/tests/aws-runtime/test/test-service.ts b/apps/tests/aws-runtime/test/test-service.ts index ce21823e1..3f7b0feb7 100644 --- a/apps/tests/aws-runtime/test/test-service.ts +++ b/apps/tests/aws-runtime/test/test-service.ts @@ -5,13 +5,13 @@ import { expectSignal, asyncResult, sendSignal, - sleepFor, - sleepUntil, + time, workflow, sendActivityHeartbeat, HeartbeatTimeout, EventualError, signal, + duration, } from "@eventual/core"; import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; import { AsyncWriterTestEvent } from "./async-writer-handler.js"; @@ -60,8 +60,8 @@ export const workflow2 = workflow("my-parent-workflow", async () => { }); export const workflow3 = workflow("sleepy", async () => { - await sleepFor(2); - await sleepUntil(new Date(new Date().getTime() + 1000 * 2)); + await duration(2); + await time(new Date(new Date().getTime() + 1000 * 2)); return `done!`; }); @@ -79,7 +79,7 @@ export const workflow4 = workflow("parallel", async () => { return Promise.allSettled([greetings, greetings2, greetings3, any, race]); async function sayHelloInSeconds(seconds: number) { - await sleepFor(seconds); + await duration(seconds); return await hello("sam"); } }); @@ -156,7 +156,7 @@ const slowActivity = activity( ); const slowWf = workflow("slowWorkflow", { timeoutSeconds: 5 }, () => - sleepFor(10) + duration(10) ); export const timedOutWorkflow = workflow( diff --git a/packages/@eventual/compiler/test-files/workflow.ts b/packages/@eventual/compiler/test-files/workflow.ts index f395b541d..2d70b2ad0 100644 --- a/packages/@eventual/compiler/test-files/workflow.ts +++ b/packages/@eventual/compiler/test-files/workflow.ts @@ -69,6 +69,6 @@ export const workflow3 = workflow("timeoutFlow", async () => { await callMe(); async function callMe() { - await sleepFor(20); + await duration(20, "seconds"); } }); diff --git a/packages/@eventual/compiler/test/__snapshots__/esbuild-plugin.test.ts.snap b/packages/@eventual/compiler/test/__snapshots__/esbuild-plugin.test.ts.snap index a1d91ee7f..07ac1d673 100644 --- a/packages/@eventual/compiler/test/__snapshots__/esbuild-plugin.test.ts.snap +++ b/packages/@eventual/compiler/test/__snapshots__/esbuild-plugin.test.ts.snap @@ -117,7 +117,7 @@ exports[`esbuild-plugin ts workflow 1`] = ` }); var workflow3 = workflow("timeoutFlow", function* () { const callMe = $eventual(function* () { - yield sleepFor(20); + yield duration(20, "seconds"); }); yield callMe(); }); diff --git a/packages/@eventual/core/src/await-time.ts b/packages/@eventual/core/src/await-time.ts new file mode 100644 index 000000000..c0abdb69a --- /dev/null +++ b/packages/@eventual/core/src/await-time.ts @@ -0,0 +1,56 @@ +import { + AwaitDurationCall, + AwaitTimeCall, + createAwaitDurationCall, + createAwaitTimeCall, +} from "./calls/await-time-call.js"; +import { isOrchestratorWorker } from "./runtime/flags.js"; + +export type DurationUnit = `${"second" | "minute" | "hour" | "day" | "year"}${ + | "s" + | ""}`; + +// TODO revisit these interfaces +export type TimeReference = Pick; +export type DurationReference = Pick; + +/** + * ```ts + * eventual(async () => { + * await duration(10, "minutes"); // sleep for 10 minutes + * return "DONE!"; + * }) + * ``` + */ +export function duration( + dur: number, + unit: DurationUnit = "seconds" +): Promise & DurationReference { + if (!isOrchestratorWorker()) { + // TODO: remove this limit + throw new Error("duration is only valid in a workflow"); + } + + // register a sleep command and return it (to be yielded) + return createAwaitDurationCall(dur, unit) as any; +} + +/** + * ```ts + * eventual(async () => { + * await time("2024-01-03T12:00:00Z"); // wait until this date + * return "DONE!"; + * }) + * ``` + */ +export function time(isoDate: string): Promise & TimeReference; +export function time(date: Date): Promise & TimeReference; +export function time(date: Date | string): Promise & TimeReference { + if (!isOrchestratorWorker()) { + throw new Error("time is only valid in a workflow"); + } + + const d = new Date(date); + // register a sleep command and return it (to be yielded) + return createAwaitTimeCall(d.toISOString()) as any; +} diff --git a/packages/@eventual/core/src/calls/await-time-call.ts b/packages/@eventual/core/src/calls/await-time-call.ts new file mode 100644 index 000000000..5830db7dd --- /dev/null +++ b/packages/@eventual/core/src/calls/await-time-call.ts @@ -0,0 +1,50 @@ +import { DurationUnit } from "../await-time.js"; +import { + EventualKind, + EventualBase, + isEventualOfKind, + createEventual, +} from "../eventual.js"; +import { registerEventual } from "../global.js"; +import { Resolved } from "../result.js"; + +export function isAwaitDurationCall(a: any): a is AwaitDurationCall { + return isEventualOfKind(EventualKind.AwaitDurationCall, a); +} + +export function isAwaitTimeCall(a: any): a is AwaitTimeCall { + return isEventualOfKind(EventualKind.AwaitTimeCall, a); +} + +export interface AwaitDurationCall + extends EventualBase> { + seq?: number; + dur: number; + unit: DurationUnit; +} + +export interface AwaitTimeCall + extends EventualBase> { + seq?: number; + isoDate: string; +} + +export function createAwaitDurationCall( + dur: number, + unit: DurationUnit +): AwaitDurationCall { + return registerEventual( + createEventual(EventualKind.AwaitDurationCall, { + dur, + unit, + }) + ); +} + +export function createAwaitTimeCall(isoDate: string): AwaitTimeCall { + return registerEventual( + createEventual(EventualKind.AwaitTimeCall, { + isoDate, + }) + ); +} diff --git a/packages/@eventual/core/src/calls/index.ts b/packages/@eventual/core/src/calls/index.ts index 8da9398f4..876a7995f 100644 --- a/packages/@eventual/core/src/calls/index.ts +++ b/packages/@eventual/core/src/calls/index.ts @@ -1,3 +1,4 @@ export * from "./activity-call.js"; -export * from "./sleep-call.js"; +export * from "./condition-call.js"; +export * from "./await-time-call.js"; export * from "./expect-signal-call.js"; diff --git a/packages/@eventual/core/src/calls/sleep-call.ts b/packages/@eventual/core/src/calls/sleep-call.ts deleted file mode 100644 index 19d649565..000000000 --- a/packages/@eventual/core/src/calls/sleep-call.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - EventualKind, - EventualBase, - isEventualOfKind, - createEventual, -} from "../eventual.js"; -import { registerEventual } from "../global.js"; -import { Resolved } from "../result.js"; - -export function isSleepForCall(a: any): a is SleepForCall { - return isEventualOfKind(EventualKind.SleepForCall, a); -} - -export function isSleepUntilCall(a: any): a is SleepUntilCall { - return isEventualOfKind(EventualKind.SleepUntilCall, a); -} - -export interface SleepForCall - extends EventualBase> { - seq?: number; - durationSeconds: number; -} - -export interface SleepUntilCall - extends EventualBase> { - seq?: number; - isoDate: string; -} - -export function createSleepForCall(durationSeconds: number): SleepForCall { - return registerEventual( - createEventual(EventualKind.SleepForCall, { - durationSeconds, - }) - ); -} - -export function createSleepUntilCall(isoDate: string): SleepUntilCall { - return registerEventual( - createEventual(EventualKind.SleepUntilCall, { - isoDate, - }) - ); -} diff --git a/packages/@eventual/core/src/command.ts b/packages/@eventual/core/src/command.ts index 99e2c9957..fbb94d61e 100644 --- a/packages/@eventual/core/src/command.ts +++ b/packages/@eventual/core/src/command.ts @@ -1,3 +1,4 @@ +import { DurationUnit } from "./await-time.js"; import { EventEnvelope } from "./event.js"; import { SignalTarget } from "./signals.js"; import { WorkflowOptions } from "./workflow.js"; @@ -8,8 +9,8 @@ export type Command = | ScheduleWorkflowCommand | PublishEventsCommand | SendSignalCommand - | SleepForCommand - | SleepUntilCommand + | AwaitDurationCommand + | AwaitTimeCommand | StartConditionCommand; interface CommandBase { @@ -18,11 +19,11 @@ interface CommandBase { } export enum CommandType { + AwaitDuration = "AwaitDuration", + AwaitTime = "AwaitTime", ExpectSignal = "ExpectSignal", PublishEvents = "PublishEvents", SendSignal = "SendSignal", - SleepFor = "SleepFor", - SleepUntil = "SleepUntil", StartActivity = "StartActivity", StartCondition = "StartCondition", StartWorkflow = "StartWorkflow", @@ -62,30 +63,32 @@ export function isScheduleWorkflowCommand( return a.kind === CommandType.StartWorkflow; } -export interface SleepUntilCommand extends CommandBase { +export interface AwaitTimeCommand extends CommandBase { /** * Minimum time (in ISO 8601) where the machine should wake up. */ untilTime: string; } -export function isSleepUntilCommand( +export function isAwaitTimeCommand( command: Command -): command is SleepUntilCommand { - return command.kind === CommandType.SleepUntil; +): command is AwaitTimeCommand { + return command.kind === CommandType.AwaitTime; } -export interface SleepForCommand extends CommandBase { +export interface AwaitDurationCommand + extends CommandBase { /** * Number of seconds from the time the command is executed until the machine should wake up. */ - durationSeconds: number; + dur: number; + unit: DurationUnit; } -export function isSleepForCommand( +export function isAwaitDurationCommand( command: Command -): command is SleepForCommand { - return command.kind === CommandType.SleepFor; +): command is AwaitDurationCommand { + return command.kind === CommandType.AwaitDuration; } export interface ExpectSignalCommand diff --git a/packages/@eventual/core/src/eventual.ts b/packages/@eventual/core/src/eventual.ts index b7dec1bde..d86099fd3 100644 --- a/packages/@eventual/core/src/eventual.ts +++ b/packages/@eventual/core/src/eventual.ts @@ -4,11 +4,11 @@ import { chain, Chain } from "./chain.js"; import type { Program } from "./interpret.js"; import { Result } from "./result.js"; import { - isSleepForCall, - isSleepUntilCall, - SleepForCall, - SleepUntilCall, -} from "./calls/sleep-call.js"; + isAwaitDurationCall, + isAwaitTimeCall, + AwaitDurationCall, + AwaitTimeCall, +} from "./calls/await-time-call.js"; import { isExpectSignalCall, ExpectSignalCall, @@ -49,6 +49,8 @@ export enum EventualKind { AwaitAll = 0, AwaitAllSettled = 12, AwaitAny = 10, + AwaitDurationCall = 3, + AwaitTimeCall = 4, Chain = 2, ConditionCall = 9, ExpectSignalCall = 6, @@ -56,8 +58,6 @@ export enum EventualKind { Race = 11, RegisterSignalHandlerCall = 7, SendSignalCall = 8, - SleepForCall = 3, - SleepUntilCall = 4, WorkflowCall = 5, } @@ -98,8 +98,8 @@ export type CommandCall = | RegisterSignalHandlerCall | PublishEventsCall | SendSignalCall - | SleepForCall - | SleepUntilCall + | AwaitDurationCall + | AwaitTimeCall | WorkflowCall; export function isCommandCall(call: Eventual): call is CommandCall { @@ -110,8 +110,8 @@ export function isCommandCall(call: Eventual): call is CommandCall { isPublishEventsCall(call) || isRegisterSignalHandlerCall(call) || isSendSignalCall(call) || - isSleepForCall(call) || - isSleepUntilCall(call) || + isAwaitDurationCall(call) || + isAwaitTimeCall(call) || isWorkflowCall(call) ); } diff --git a/packages/@eventual/core/src/index.ts b/packages/@eventual/core/src/index.ts index 975f50b83..0e0105bdf 100644 --- a/packages/@eventual/core/src/index.ts +++ b/packages/@eventual/core/src/index.ts @@ -19,7 +19,7 @@ export * from "./secret.js"; export * from "./service-client.js"; export * from "./service-type.js"; export * from "./signals.js"; -export * from "./sleep.js"; +export * from "./await-time.js"; export * from "./tasks.js"; export * from "./util.js"; export * from "./workflow-events.js"; diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index 60ab4d3bd..41d50e568 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -50,7 +50,10 @@ import { import { createChain, isChain, Chain } from "./chain.js"; import { assertNever, or } from "./util.js"; import { Command, CommandType } from "./command.js"; -import { isSleepForCall, isSleepUntilCall } from "./calls/sleep-call.js"; +import { + isAwaitDurationCall, + isAwaitTimeCall, +} from "./calls/await-time-call.js"; import { isExpectSignalCall, ExpectSignalCall, @@ -202,17 +205,18 @@ export function interpret( heartbeatSeconds: call.heartbeatSeconds, seq: call.seq!, }; - } else if (isSleepUntilCall(call)) { + } else if (isAwaitTimeCall(call)) { return { - kind: CommandType.SleepUntil, + kind: CommandType.AwaitTime, seq: call.seq!, untilTime: call.isoDate, }; - } else if (isSleepForCall(call)) { + } else if (isAwaitDurationCall(call)) { return { - kind: CommandType.SleepFor, + kind: CommandType.AwaitDuration, seq: call.seq!, - durationSeconds: call.durationSeconds, + dur: call.dur, + unit: call.unit, }; } else if (isWorkflowCall(call)) { return { @@ -557,7 +561,7 @@ function isCorresponding(event: ScheduledEvent, call: CommandCall) { } else if (isChildWorkflowScheduled(event)) { return isWorkflowCall(call) && call.name === event.name; } else if (isSleepScheduled(event)) { - return isSleepUntilCall(call) || isSleepForCall(call); + return isAwaitTimeCall(call) || isAwaitDurationCall(call); } else if (isExpectSignalStarted(event)) { return isExpectSignalCall(call) && event.signalId === call.signalId; } else if (isSignalSent(event)) { diff --git a/packages/@eventual/core/src/runtime/command-executor.ts b/packages/@eventual/core/src/runtime/command-executor.ts index 57d916564..c7b515f2d 100644 --- a/packages/@eventual/core/src/runtime/command-executor.ts +++ b/packages/@eventual/core/src/runtime/command-executor.ts @@ -6,15 +6,15 @@ import { isScheduleActivityCommand, isScheduleWorkflowCommand, isSendSignalCommand, - isSleepForCommand, - isSleepUntilCommand, + isAwaitDurationCommand, + isAwaitTimeCommand, isStartConditionCommand, PublishEventsCommand, ScheduleActivityCommand, ScheduleWorkflowCommand, SendSignalCommand, - SleepForCommand, - SleepUntilCommand, + AwaitDurationCommand, + AwaitTimeCommand, StartConditionCommand, } from "../command.js"; import { @@ -32,7 +32,11 @@ import { ConditionTimedOut, SignalSent, } from "../workflow-events.js"; -import { EventsPublished, isChildExecutionTarget } from "../index.js"; +import { + DurationUnit, + EventsPublished, + isChildExecutionTarget, +} from "../index.js"; import { assertNever } from "../util.js"; import { Workflow } from "../workflow.js"; import { formatChildExecutionName, formatExecutionId } from "./execution-id.js"; @@ -73,7 +77,7 @@ export class CommandExecutor { ); } else if (isScheduleWorkflowCommand(command)) { return this.scheduleChildWorkflow(executionId, command, baseTime); - } else if (isSleepForCommand(command) || isSleepUntilCommand(command)) { + } else if (isAwaitDurationCommand(command) || isAwaitTimeCommand(command)) { // all sleep times are computed using the start time of the WorkflowTaskStarted return this.scheduleSleep(executionId, command, baseTime); } else if (isExpectSignalCommand(command)) { @@ -158,13 +162,13 @@ export class CommandExecutor { private async scheduleSleep( executionId: string, - command: SleepForCommand | SleepUntilCommand, + command: AwaitDurationCommand | AwaitTimeCommand, baseTime: Date ): Promise { // TODO validate - const untilTime = isSleepUntilCommand(command) + const untilTime = isAwaitTimeCommand(command) ? new Date(command.untilTime) - : new Date(baseTime.getTime() + command.durationSeconds * 1000); + : computeDurationDate(baseTime, command.dur, command.unit); const untilTimeIso = untilTime.toISOString(); await this.props.timerClient.scheduleEvent({ @@ -283,3 +287,24 @@ export class CommandExecutor { ); } } + +export function computeDurationDate( + now: Date, + dur: number, + unit: DurationUnit +) { + const milliseconds = + unit === "seconds" || unit === "second" + ? dur * 1000 + : unit === "minutes" || unit === "minute" + ? dur * 1000 * 60 + : unit === "hours" || unit === "hour" + ? dur * 1000 * 60 * 60 + : unit === "days" || unit === "day" + ? dur * 1000 * 60 * 60 * 24 + : unit === "years" || unit === "year" + ? dur * 1000 * 60 * 60 * 24 * 365.25 + : assertNever(unit); + + return new Date(now.getTime() + milliseconds); +} diff --git a/packages/@eventual/core/src/signals.ts b/packages/@eventual/core/src/signals.ts index 882aacf7f..7be877bc4 100644 --- a/packages/@eventual/core/src/signals.ts +++ b/packages/@eventual/core/src/signals.ts @@ -40,7 +40,7 @@ export class Signal { * workflow("wf", () => { * let done = false; * mySignal.onSignal(async () => { - * await sleepFor(10); + * await duration(10, "seconds"); * done = true; * }); * @@ -53,7 +53,7 @@ export class Signal { * ```ts * const handler = mySignal.onSignal(() => {}); * - * await sleepFor(10); + * await duration(10, "seconds"); * * handler.dispose(); * ``` @@ -168,7 +168,7 @@ export function expectSignal( * workflow("wf", () => { * let done = false; * onSignal("MySignal", async () => { - * await sleepFor(10); + * await duration(10, "seconds"); * done = true; * }); * @@ -181,7 +181,7 @@ export function expectSignal( * ```ts * const handler = onSignal("MySignal", () => {}); * - * await sleepFor(10); + * await duration(10, "seconds"); * * handler.dispose(); * ``` diff --git a/packages/@eventual/core/src/sleep.ts b/packages/@eventual/core/src/sleep.ts deleted file mode 100644 index f1a6af14d..000000000 --- a/packages/@eventual/core/src/sleep.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { - createSleepForCall, - createSleepUntilCall, -} from "./calls/sleep-call.js"; -import { isOrchestratorWorker } from "./runtime/flags.js"; - -/** - * ```ts - * eventual(async () => { - * await sleepFor(10 * 60); // sleep for 10 minutes - * return "DONE!"; - * }) - * ``` - */ -export function sleepFor(seconds: number): Promise { - if (!isOrchestratorWorker()) { - throw new Error("sleepFor is only valid in a workflow"); - } - - // register a sleep command and return it (to be yielded) - return createSleepForCall(seconds) as any; -} - -export function sleepUntil(isoDate: string): Promise; -export function sleepUntil(date: Date): Promise; -export function sleepUntil(date: Date | string): Promise { - if (!isOrchestratorWorker()) { - throw new Error("sleepUntil is only valid in a workflow"); - } - - const d = new Date(date); - // register a sleep command and return it (to be yielded) - return createSleepUntilCall(d.toISOString()) as any; -} diff --git a/packages/@eventual/core/test/command-util.ts b/packages/@eventual/core/test/command-util.ts index 3a3fc65a6..3f7bfc5dd 100644 --- a/packages/@eventual/core/test/command-util.ts +++ b/packages/@eventual/core/test/command-util.ts @@ -1,13 +1,13 @@ import { ulid } from "ulidx"; import { + AwaitDurationCommand, + AwaitTimeCommand, CommandType, ExpectSignalCommand, PublishEventsCommand, ScheduleActivityCommand, ScheduleWorkflowCommand, SendSignalCommand, - SleepForCommand, - SleepUntilCommand, StartConditionCommand, } from "../src/command.js"; import { EventEnvelope } from "../src/event.js"; @@ -33,25 +33,28 @@ import { ActivityHeartbeatTimedOut, } from "../src/workflow-events.js"; import { SignalTarget } from "../src/signals.js"; +import { DurationUnit } from "../src/await-time.js"; -export function createSleepUntilCommand( +export function createAwaitTimeCommand( untilTime: string, seq: number -): SleepUntilCommand { +): AwaitTimeCommand { return { - kind: CommandType.SleepUntil, + kind: CommandType.AwaitTime, untilTime, seq, }; } -export function createSleepForCommand( - durationSeconds: number, +export function createAwaitDurationCommand( + dur: number, + unit: DurationUnit, seq: number -): SleepForCommand { +): AwaitDurationCommand { return { - kind: CommandType.SleepFor, - durationSeconds, + kind: CommandType.AwaitDuration, + dur, + unit, seq, }; } diff --git a/packages/@eventual/core/test/commend-executor.test.ts b/packages/@eventual/core/test/commend-executor.test.ts index 7dc0f0620..af294e83f 100644 --- a/packages/@eventual/core/test/commend-executor.test.ts +++ b/packages/@eventual/core/test/commend-executor.test.ts @@ -65,14 +65,15 @@ afterEach(() => { jest.resetAllMocks(); }); -describe("sleep", () => { - test("sleep for", async () => { +describe("await times", () => { + test("await duration", async () => { const event = await testExecutor.executeCommand( workflow, executionId, { - kind: CommandType.SleepFor, - durationSeconds: 10, + kind: CommandType.AwaitDuration, + dur: 10, + unit: "seconds", seq: 0, }, baseTime @@ -99,12 +100,12 @@ describe("sleep", () => { }); }); - test("sleep until", async () => { + test("await time", async () => { const event = await testExecutor.executeCommand( workflow, executionId, { - kind: CommandType.SleepUntil, + kind: CommandType.AwaitTime, untilTime: baseTime.toISOString(), seq: 0, }, diff --git a/packages/@eventual/core/test/interpret.test.ts b/packages/@eventual/core/test/interpret.test.ts index ca16687f4..805c08605 100644 --- a/packages/@eventual/core/test/interpret.test.ts +++ b/packages/@eventual/core/test/interpret.test.ts @@ -5,6 +5,7 @@ import { EventualError, HeartbeatTimeout, Timeout } from "../src/error.js"; import { Context, createAwaitAll, + duration, Eventual, interpret, Program, @@ -13,14 +14,13 @@ import { SERVICE_TYPE_FLAG, signal, SignalTargetType, - sleepFor, - sleepUntil, + time, Workflow, workflow as _workflow, WorkflowHandler, WorkflowResult, } from "../src/index.js"; -import { createSleepUntilCall } from "../src/calls/sleep-call.js"; +import { createAwaitTimeCall } from "../src/calls/await-time-call.js"; import { activitySucceeded, activityFailed, @@ -35,8 +35,8 @@ import { createScheduledActivityCommand, createScheduledWorkflowCommand, createSendSignalCommand, - createSleepForCommand, - createSleepUntilCommand, + createAwaitDurationCommand, + createAwaitTimeCommand, createStartConditionCommand, eventsPublished, scheduledSleep, @@ -72,7 +72,7 @@ function* myWorkflow(event: any): Program { createActivityCall("my-activity-0", [event]); const all = yield Eventual.all([ - createSleepUntilCall("then"), + createAwaitTimeCall("then"), createActivityCall("my-activity-2", [event]), ]) as any; return [a, all]; @@ -119,7 +119,7 @@ test("should continue with result of completed Activity", () => { ).toMatchObject({ commands: [ createScheduledActivityCommand("my-activity-0", [event], 1), - createSleepUntilCommand("then", 2), + createAwaitTimeCommand("then", 2), createScheduledActivityCommand("my-activity-2", [event], 3), ], }); @@ -209,7 +209,7 @@ test("should handle missing blocks", () => { commands: [ createScheduledActivityCommand("my-activity", [event], 0), createScheduledActivityCommand("my-activity-0", [event], 1), - createSleepUntilCommand("then", 2), + createAwaitTimeCommand("then", 2), createScheduledActivityCommand("my-activity-2", [event], 3), ], }); @@ -224,7 +224,7 @@ test("should handle partial blocks", () => { ]) ).toMatchObject({ commands: [ - createSleepUntilCommand("then", 2), + createAwaitTimeCommand("then", 2), createScheduledActivityCommand("my-activity-2", [event], 3), ], }); @@ -240,7 +240,7 @@ test("should handle partial blocks with partial completes", () => { ]) ).toMatchObject({ commands: [ - createSleepUntilCommand("then", 2), + createAwaitTimeCommand("then", 2), createScheduledActivityCommand("my-activity-2", [event], 3), ], }); @@ -426,17 +426,17 @@ test("should return result of inner function", () => { test("should schedule sleep for", () => { function* workflow() { - yield sleepFor(10); + yield duration(10); } expect(interpret(workflow() as any, [])).toMatchObject({ - commands: [createSleepForCommand(10, 0)], + commands: [createAwaitDurationCommand(10, "seconds", 0)], }); }); test("should not re-schedule sleep for", () => { function* workflow() { - yield sleepFor(10); + yield duration(10); } expect( @@ -448,7 +448,7 @@ test("should not re-schedule sleep for", () => { test("should complete sleep for", () => { function* workflow() { - yield sleepFor(10); + yield duration(10); return "done"; } @@ -467,11 +467,11 @@ test("should schedule sleep until", () => { const now = new Date(); function* workflow() { - yield sleepUntil(now); + yield time(now); } expect(interpret(workflow() as any, [])).toMatchObject({ - commands: [createSleepUntilCommand(now.toISOString(), 0)], + commands: [createAwaitTimeCommand(now.toISOString(), 0)], }); }); @@ -479,7 +479,7 @@ test("should not re-schedule sleep until", () => { const now = new Date(); function* workflow() { - yield sleepUntil(now); + yield time(now); } expect( @@ -493,7 +493,7 @@ test("should complete sleep until", () => { const now = new Date(); function* workflow() { - yield sleepUntil(now); + yield time(now); return "done"; } @@ -530,7 +530,7 @@ describe("temple of doom", () => { let jump = false; const startTrap = chain(function* () { - yield createSleepUntilCall("X"); + yield createAwaitTimeCall("X"); trapDown = true; }); const waitForJump = chain(function* () { @@ -560,7 +560,7 @@ describe("temple of doom", () => { test("run until blocked", () => { expect(interpret(workflow() as any, [])).toMatchObject({ commands: [ - createSleepUntilCommand("X", 0), + createAwaitTimeCommand("X", 0), createScheduledActivityCommand("jump", [], 1), createScheduledActivityCommand("run", [], 2), ], @@ -1582,12 +1582,12 @@ describe("signals", () => { } ); - yield createSleepUntilCall(""); + yield createAwaitTimeCall(""); mySignalHandler.dispose(); myOtherSignalHandler.dispose(); - yield createSleepUntilCall(""); + yield createAwaitTimeCall(""); return { mySignalHappened, @@ -1600,7 +1600,7 @@ describe("signals", () => { expect(interpret(wf.definition(undefined, context), [])).toMatchObject(< WorkflowResult >{ - commands: [createSleepUntilCommand("", 0)], + commands: [createAwaitTimeCommand("", 0)], }); }); @@ -1610,7 +1610,7 @@ describe("signals", () => { signalReceived("MySignal"), ]) ).toMatchObject({ - commands: [createSleepUntilCommand("", 0)], + commands: [createAwaitTimeCommand("", 0)], }); }); @@ -1682,7 +1682,7 @@ describe("signals", () => { ]) ).toMatchObject({ commands: [ - createSleepUntilCommand("", 0), + createAwaitTimeCommand("", 0), createScheduledActivityCommand("act1", ["hi"], 1), ], }); @@ -1696,7 +1696,7 @@ describe("signals", () => { ]) ).toMatchObject({ commands: [ - createSleepUntilCommand("", 0), + createAwaitTimeCommand("", 0), createScheduledActivityCommand("act1", ["hi"], 1), createScheduledActivityCommand("act1", ["hi2"], 2), ], @@ -2070,7 +2070,7 @@ test("nestedChains", () => { const wf = workflow(function* () { const funcs = { a: chain(function* () { - yield createSleepUntilCall(""); + yield createAwaitTimeCall(""); }), }; @@ -2088,7 +2088,7 @@ test("nestedChains", () => { expect( interpret(wf.definition(undefined, context), []) ).toMatchObject({ - commands: [createSleepUntilCommand("", 0)], + commands: [createAwaitTimeCommand("", 0)], }); }); diff --git a/packages/@eventual/testing/test/workflow.ts b/packages/@eventual/testing/test/workflow.ts index d8e3d833b..e5a96a22c 100644 --- a/packages/@eventual/testing/test/workflow.ts +++ b/packages/@eventual/testing/test/workflow.ts @@ -1,11 +1,11 @@ import { activity, asyncResult, + duration, event, sendSignal, signal, - sleepFor, - sleepUntil, + time, workflow, } from "@eventual/core"; import { SendMessageCommand, SQSClient } from "@aws-sdk/client-sqs"; @@ -24,9 +24,9 @@ export const sleepWorkflow = workflow( "sleepWorkflow", async (relative: boolean) => { if (relative) { - await sleepFor(10); + await duration(10); } else { - await sleepUntil("2022-01-02T12:00:00Z"); + await time("2022-01-02T12:00:00Z"); } return "hello"; } @@ -140,7 +140,7 @@ export const orchestrateWorkflow = workflow( await throwWorkflow(undefined); } const execution = signalWorkflow(undefined); - await sleepFor(1); + await duration(1); await execution.sendSignal(dataSignal, "hello from a workflow"); await execution.sendSignal(dataDoneSignal); await execution.sendSignal(continueSignal); @@ -195,7 +195,7 @@ export const longRunningWorkflow = workflow("longRunningWf", async () => { const result = Promise.race([ act, (async () => { - await sleepFor(60 * 60); + await duration(60 * 60); return "sleep"; })(), ]); From 1ebb62578824c32400bf5ea306977c6c26fe7023 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Wed, 11 Jan 2023 23:15:43 -0600 Subject: [PATCH 02/14] activity timeout --- apps/tests/aws-runtime/test/test-service.ts | 4 +- packages/@eventual/core/src/activity.ts | 15 +- packages/@eventual/core/src/await-time.ts | 21 +- .../@eventual/core/src/calls/activity-call.ts | 9 +- packages/@eventual/core/src/command.ts | 1 - packages/@eventual/core/src/interpret.ts | 44 +++- .../core/src/runtime/command-executor.ts | 55 +--- .../core/src/runtime/handlers/orchestrator.ts | 4 +- packages/@eventual/core/src/util.ts | 26 ++ .../@eventual/core/src/workflow-events.ts | 49 ++-- packages/@eventual/core/src/workflow.ts | 26 +- packages/@eventual/core/test/command-util.ts | 21 +- .../core/test/commend-executor.test.ts | 56 +--- .../@eventual/core/test/interpret.test.ts | 244 ++++++++++++++---- packages/@eventual/testing/test/workflow.ts | 2 +- 15 files changed, 345 insertions(+), 232 deletions(-) diff --git a/apps/tests/aws-runtime/test/test-service.ts b/apps/tests/aws-runtime/test/test-service.ts index ee3e34f5e..1ceee22da 100644 --- a/apps/tests/aws-runtime/test/test-service.ts +++ b/apps/tests/aws-runtime/test/test-service.ts @@ -153,7 +153,7 @@ export const childWorkflow = workflow( const slowActivity = activity( "slowAct", - { timeoutSeconds: 5 }, + { timeout: duration(5, "seconds") }, () => new Promise((resolve) => setTimeout(resolve, 10 * 1000)) ); @@ -211,7 +211,7 @@ export const asyncWorkflow = workflow( const activityWithHeartbeat = activity( "activityWithHeartbeat", - { heartbeatSeconds: 2 }, + { heartbeatTimeout: duration(2, "seconds") }, async (n: number, type: "success" | "no-heartbeat" | "some-heartbeat") => { const delay = (s: number) => new Promise((resolve) => { diff --git a/packages/@eventual/core/src/activity.ts b/packages/@eventual/core/src/activity.ts index 61c9234ed..e1625e940 100644 --- a/packages/@eventual/core/src/activity.ts +++ b/packages/@eventual/core/src/activity.ts @@ -1,9 +1,11 @@ +import { DurationReference } from "./await-time.js"; import { createActivityCall } from "./calls/activity-call.js"; import { callableActivities, getActivityContext, getServiceClient, } from "./global.js"; +import { computeDurationSeconds } from "./index.js"; import { isActivityWorker, isOrchestratorWorker } from "./runtime/flags.js"; import { EventualServiceClient, @@ -19,7 +21,7 @@ export interface ActivityOptions { * * @default - workflow will run forever. */ - timeoutSeconds?: number; + timeout?: DurationReference; /** * For long running activities, it is suggested that they report back that they * are still in progress to avoid waiting forever or until a long timeout when @@ -30,7 +32,7 @@ export interface ActivityOptions { * * If it fails to do so, the workflow will cancel the activity and throw an error. */ - heartbeatSeconds?: number; + heartbeatTimeout?: DurationReference; } export interface ActivityFunction { @@ -225,8 +227,13 @@ export function activity( return createActivityCall( activityID, args, - opts?.timeoutSeconds, - opts?.heartbeatSeconds + opts?.timeout, + opts?.heartbeatTimeout + ? computeDurationSeconds( + opts.heartbeatTimeout.dur, + opts.heartbeatTimeout.unit + ) / 1000 + : undefined ) as any; } else { // calling the activity from outside the orchestrator just calls the handler diff --git a/packages/@eventual/core/src/await-time.ts b/packages/@eventual/core/src/await-time.ts index c0abdb69a..a18755009 100644 --- a/packages/@eventual/core/src/await-time.ts +++ b/packages/@eventual/core/src/await-time.ts @@ -4,6 +4,7 @@ import { createAwaitDurationCall, createAwaitTimeCall, } from "./calls/await-time-call.js"; +import { createEventual, EventualKind } from "./eventual.js"; import { isOrchestratorWorker } from "./runtime/flags.js"; export type DurationUnit = `${"second" | "minute" | "hour" | "day" | "year"}${ @@ -16,7 +17,7 @@ export type DurationReference = Pick; /** * ```ts - * eventual(async () => { + * workflow(async () => { * await duration(10, "minutes"); // sleep for 10 minutes * return "DONE!"; * }) @@ -37,7 +38,7 @@ export function duration( /** * ```ts - * eventual(async () => { + * workflow(async () => { * await time("2024-01-03T12:00:00Z"); // wait until this date * return "DONE!"; * }) @@ -54,3 +55,19 @@ export function time(date: Date | string): Promise & TimeReference { // register a sleep command and return it (to be yielded) return createAwaitTimeCall(d.toISOString()) as any; } + +export function createTimeReference(iso: string): TimeReference { + return createEventual(EventualKind.AwaitTimeCall, { + isoDate: iso, + }); +} + +export function createDurationReference( + dur: number, + unit: DurationUnit +): DurationReference { + return createEventual(EventualKind.AwaitDurationCall, { + dur, + unit, + }); +} diff --git a/packages/@eventual/core/src/calls/activity-call.ts b/packages/@eventual/core/src/calls/activity-call.ts index 841ed3434..f5f708b2d 100644 --- a/packages/@eventual/core/src/calls/activity-call.ts +++ b/packages/@eventual/core/src/calls/activity-call.ts @@ -17,20 +17,23 @@ export interface ActivityCall name: string; args: any[]; heartbeatSeconds?: number; - timeoutSeconds?: number; + /** + * Timeout can be any Eventual (promise). When the promise resolves, the activity is considered to be timed out. + */ + timeout?: any; } export function createActivityCall( name: string, args: any[], - timeoutSeconds?: number, + timeout?: any, heartbeatSeconds?: number ): ActivityCall { return registerEventual( createEventual(EventualKind.ActivityCall, { name, args, - timeoutSeconds, + timeout, heartbeatSeconds, }) ); diff --git a/packages/@eventual/core/src/command.ts b/packages/@eventual/core/src/command.ts index fbb94d61e..af5c4229f 100644 --- a/packages/@eventual/core/src/command.ts +++ b/packages/@eventual/core/src/command.ts @@ -39,7 +39,6 @@ export interface ScheduleActivityCommand extends CommandBase { name: string; args: any[]; - timeoutSeconds?: number; heartbeatSeconds?: number; } diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index 6b8d2bc2f..23ef7b486 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -25,8 +25,8 @@ import { isSignalReceived, isFailedEvent, isScheduledEvent, - isSleepCompleted, - isSleepScheduled, + isAlarmCompleted, + isAlarmScheduled, isExpectSignalStarted, isExpectSignalTimedOut, ScheduledEvent, @@ -34,7 +34,6 @@ import { isConditionStarted, isConditionTimedOut, isWorkflowTimedOut, - isActivityTimedOut, isActivityHeartbeatTimedOut, isEventsPublished, WorkflowEvent, @@ -65,7 +64,11 @@ import { } from "./calls/signal-handler-call.js"; import { isSendSignalCall } from "./calls/send-signal-call.js"; import { isWorkflowCall } from "./calls/workflow-call.js"; -import { clearEventualCollector, setEventualCollector } from "./global.js"; +import { + clearEventualCollector, + registerEventual, + setEventualCollector, +} from "./global.js"; import { isConditionCall } from "./calls/condition-call.js"; import { isAwaitAllSettled } from "./await-all-settled.js"; import { isAwaitAny } from "./await-any.js"; @@ -214,7 +217,6 @@ export function interpret( kind: CommandType.StartActivity, args: call.args, name: call.name, - timeoutSeconds: call.timeoutSeconds, heartbeatSeconds: call.heartbeatSeconds, seq: call.seq!, }; @@ -307,6 +309,24 @@ export function interpret( subscribeToSignal(activity.signalId, activity); // signal handler does not emit a call/command. It is only internal. return activity; + } else if (isActivityCall(activity)) { + if (activity?.timeout) { + if (isEventual(activity?.timeout)) { + // if the eventual is not started yet, start it + if ( + !("seq" in activity.timeout) || + activity.timeout === undefined + ) { + registerEventual(activity.timeout); + } + } else { + activity.result = Result.failed( + new Timeout( + "Activity immediately timed out, timeout was not awaitable." + ) + ); + } + } } activity.seq = nextSeq(); callTable[activity.seq!] = activity; @@ -468,6 +488,14 @@ export function interpret( } else if (predicateResult) { return Result.resolved(true); } + } else if (isActivityCall(activity)) { + if (activity.timeout) { + const timeoutResult = tryResolveResult(activity.timeout); + if (isResolved(timeoutResult) || isFailed(timeoutResult)) { + return Result.failed(new Timeout("Activity Timed Out")); + } + } + return undefined; } else if (isChain(activity) || isCommandCall(activity)) { // chain and most commands will be resolved elsewhere (ex: commitCompletionEvent or commitSignal) return undefined; @@ -551,15 +579,13 @@ export function interpret( } call.result = isSucceededEvent(event) ? Result.resolved(event.result) - : isSleepCompleted(event) + : isAlarmCompleted(event) ? Result.resolved(undefined) : isExpectSignalTimedOut(event) ? Result.failed(new Timeout("Expect Signal Timed Out")) : isConditionTimedOut(event) ? // a timed out condition returns false Result.resolved(false) - : isActivityTimedOut(event) - ? Result.failed(new Timeout("Activity Timed Out")) : isActivityHeartbeatTimedOut(event) ? Result.failed(new HeartbeatTimeout("Activity Heartbeat TimedOut")) : Result.failed(new EventualError(event.error, event.message)); @@ -573,7 +599,7 @@ function isCorresponding(event: ScheduledEvent, call: CommandCall) { return isActivityCall(call) && call.name === event.name; } else if (isChildWorkflowScheduled(event)) { return isWorkflowCall(call) && call.name === event.name; - } else if (isSleepScheduled(event)) { + } else if (isAlarmScheduled(event)) { return isAwaitTimeCall(call) || isAwaitDurationCall(call); } else if (isExpectSignalStarted(event)) { return isExpectSignalCall(call) && event.signalId === call.signalId; diff --git a/packages/@eventual/core/src/runtime/command-executor.ts b/packages/@eventual/core/src/runtime/command-executor.ts index 67bffb6a8..e744f20e9 100644 --- a/packages/@eventual/core/src/runtime/command-executor.ts +++ b/packages/@eventual/core/src/runtime/command-executor.ts @@ -18,13 +18,12 @@ import { StartConditionCommand, } from "../command.js"; import { - ActivityTimedOut, WorkflowEventType, createEvent, ActivityScheduled, ChildWorkflowScheduled, - SleepScheduled, - SleepCompleted, + AlarmScheduled, + AlarmCompleted, ExpectSignalStarted, ExpectSignalTimedOut, HistoryStateEvent, @@ -33,7 +32,7 @@ import { SignalSent, EventsPublished, } from "../workflow-events.js"; -import { assertNever } from "../util.js"; +import { assertNever, computeDurationDate } from "../util.js"; import { Workflow } from "../workflow.js"; import { formatChildExecutionName, formatExecutionId } from "./execution-id.js"; import { ActivityWorkerRequest } from "./handlers/activity-worker.js"; @@ -42,7 +41,6 @@ import { WorkflowRuntimeClient } from "./clients/workflow-runtime-client.js"; import { WorkflowClient } from "./clients/workflow-client.js"; import { EventClient } from "./clients/event-client.js"; import { isChildExecutionTarget } from "../signals.js"; -import { DurationUnit } from "../await-time.js"; interface CommandExecutorProps { workflowRuntimeClient: WorkflowRuntimeClient; @@ -103,21 +101,7 @@ export class CommandExecutor { retry: 0, }; - const timeoutStarter = command.timeoutSeconds - ? await this.props.timerClient.scheduleEvent({ - schedule: Schedule.relative(command.timeoutSeconds, baseTime), - event: { - type: WorkflowEventType.ActivityTimedOut, - seq: command.seq, - }, - executionId, - }) - : undefined; - - const activityStarter = - this.props.workflowRuntimeClient.startActivity(request); - - await Promise.all([activityStarter, timeoutStarter]); + await this.props.workflowRuntimeClient.startActivity(request); return createEvent( { @@ -159,25 +143,25 @@ export class CommandExecutor { command: AwaitDurationCommand | AwaitTimeCommand, baseTime: Date - ): Promise { + ): Promise { // TODO validate const untilTime = isAwaitTimeCommand(command) ? new Date(command.untilTime) : computeDurationDate(baseTime, command.dur, command.unit); const untilTimeIso = untilTime.toISOString(); - await this.props.timerClient.scheduleEvent({ + await this.props.timerClient.scheduleEvent({ event: { - type: WorkflowEventType.SleepCompleted, + type: WorkflowEventType.AlarmCompleted, seq: command.seq, }, schedule: Schedule.absolute(untilTimeIso), executionId, }); - return createEvent( + return createEvent( { - type: WorkflowEventType.SleepScheduled, + type: WorkflowEventType.AlarmScheduled, seq: command.seq, untilTime: untilTime.toISOString(), }, @@ -282,24 +266,3 @@ export class CommandExecutor { ); } } - -export function computeDurationDate( - now: Date, - dur: number, - unit: DurationUnit -) { - const milliseconds = - unit === "seconds" || unit === "second" - ? dur * 1000 - : unit === "minutes" || unit === "minute" - ? dur * 1000 * 60 - : unit === "hours" || unit === "hour" - ? dur * 1000 * 60 * 60 - : unit === "days" || unit === "day" - ? dur * 1000 * 60 * 60 * 24 - : unit === "years" || unit === "year" - ? dur * 1000 * 60 * 60 * 24 * 365.25 - : assertNever(unit); - - return new Date(now.getTime() + milliseconds); -} diff --git a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts index f6c75be4f..df8a0455a 100644 --- a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts +++ b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts @@ -6,7 +6,7 @@ import { getEventId, HistoryStateEvent, isHistoryEvent, - isSleepCompleted, + isAlarmCompleted, isWorkflowSucceeded, isWorkflowFailed, isWorkflowStarted, @@ -717,7 +717,7 @@ function logEventMetrics( events: WorkflowEvent[], now: Date ) { - const sleepCompletedEvents = events.filter(isSleepCompleted); + const sleepCompletedEvents = events.filter(isAlarmCompleted); if (sleepCompletedEvents.length > 0) { const sleepCompletedVariance = sleepCompletedEvents.map( (s) => now.getTime() - new Date(s.timestamp).getTime() diff --git a/packages/@eventual/core/src/util.ts b/packages/@eventual/core/src/util.ts index 10340eb0b..8d0c5909e 100644 --- a/packages/@eventual/core/src/util.ts +++ b/packages/@eventual/core/src/util.ts @@ -1,3 +1,5 @@ +import { DurationUnit } from "./await-time.js"; + export function assertNever(never: never, msg?: string): never { throw new Error(msg ?? `reached unreachable code with value ${never}`); } @@ -78,3 +80,27 @@ export function iterator( } } } + +export function computeDurationDate( + now: Date, + dur: number, + unit: DurationUnit +) { + const milliseconds = computeDurationSeconds(dur, unit) * 1000; + + return new Date(now.getTime() + milliseconds); +} + +export function computeDurationSeconds(dur: number, unit: DurationUnit) { + return unit === "seconds" || unit === "second" + ? dur + : unit === "minutes" || unit === "minute" + ? dur * 60 + : unit === "hours" || unit === "hour" + ? dur * 60 * 60 + : unit === "days" || unit === "day" + ? dur * 60 * 60 * 24 + : unit === "years" || unit === "year" + ? dur * 60 * 60 * 24 * 365.25 + : assertNever(unit); +} diff --git a/packages/@eventual/core/src/workflow-events.ts b/packages/@eventual/core/src/workflow-events.ts index b23a690e6..95e1cd5e6 100644 --- a/packages/@eventual/core/src/workflow-events.ts +++ b/packages/@eventual/core/src/workflow-events.ts @@ -21,7 +21,8 @@ export enum WorkflowEventType { ActivityFailed = "ActivityFailed", ActivityHeartbeatTimedOut = "ActivityHeartbeatTimedOut", ActivityScheduled = "ActivityScheduled", - ActivityTimedOut = "ActivityTimedOut", + AlarmCompleted = "AlarmCompleted", + AlarmScheduled = "AlarmScheduled", ChildWorkflowSucceeded = "ChildWorkflowSucceeded", ChildWorkflowFailed = "ChildWorkflowFailed", ChildWorkflowScheduled = "ChildWorkflowScheduled", @@ -32,8 +33,6 @@ export enum WorkflowEventType { ExpectSignalTimedOut = "ExpectSignalTimedOut", SignalReceived = "SignalReceived", SignalSent = "SignalSent", - SleepCompleted = "SleepCompleted", - SleepScheduled = "SleepScheduled", WorkflowSucceeded = "WorkflowSucceeded", WorkflowFailed = "WorkflowFailed", WorkflowStarted = "WorkflowStarted", @@ -55,22 +54,21 @@ export type WorkflowEvent = export type ScheduledEvent = | ActivityScheduled + | AlarmScheduled | ChildWorkflowScheduled | ConditionStarted | EventsPublished | ExpectSignalStarted - | SignalSent - | SleepScheduled; + | SignalSent; export type SucceededEvent = | ActivitySucceeded - | ChildWorkflowSucceeded - | SleepCompleted; + | AlarmCompleted + | ChildWorkflowSucceeded; export type FailedEvent = | ActivityFailed | ActivityHeartbeatTimedOut - | ActivityTimedOut | ChildWorkflowFailed | ConditionTimedOut | ExpectSignalTimedOut; @@ -228,19 +226,19 @@ export function isActivityHeartbeatTimedOut( return event.type === WorkflowEventType.ActivityHeartbeatTimedOut; } -export interface SleepScheduled extends HistoryEventBase { - type: WorkflowEventType.SleepScheduled; +export interface AlarmScheduled extends HistoryEventBase { + type: WorkflowEventType.AlarmScheduled; untilTime: string; } -export function isSleepScheduled( +export function isAlarmScheduled( event: WorkflowEvent -): event is SleepScheduled { - return event.type === WorkflowEventType.SleepScheduled; +): event is AlarmScheduled { + return event.type === WorkflowEventType.AlarmScheduled; } -export interface SleepCompleted extends HistoryEventBase { - type: WorkflowEventType.SleepCompleted; +export interface AlarmCompleted extends HistoryEventBase { + type: WorkflowEventType.AlarmCompleted; result?: undefined; } @@ -278,10 +276,10 @@ export function isChildWorkflowFailed( return event.type === WorkflowEventType.ChildWorkflowFailed; } -export function isSleepCompleted( +export function isAlarmCompleted( event: WorkflowEvent -): event is SleepCompleted { - return event.type === WorkflowEventType.SleepCompleted; +): event is AlarmCompleted { + return event.type === WorkflowEventType.AlarmCompleted; } export const isWorkflowCompletedEvent = or( @@ -366,20 +364,10 @@ export function isConditionTimedOut( return event.type === WorkflowEventType.ConditionTimedOut; } -export interface ActivityTimedOut extends HistoryEventBase { - type: WorkflowEventType.ActivityTimedOut; -} - export interface WorkflowTimedOut extends BaseEvent { type: WorkflowEventType.WorkflowTimedOut; } -export function isActivityTimedOut( - event: WorkflowEvent -): event is ActivityTimedOut { - return event.type === WorkflowEventType.ActivityTimedOut; -} - export function isWorkflowTimedOut( event: WorkflowEvent ): event is WorkflowTimedOut { @@ -393,18 +381,17 @@ export const isScheduledEvent = or( isEventsPublished, isExpectSignalStarted, isSignalSent, - isSleepScheduled + isAlarmScheduled ); export const isSucceededEvent = or( isActivitySucceeded, isChildWorkflowSucceeded, - isSleepCompleted + isAlarmCompleted ); export const isFailedEvent = or( isActivityFailed, - isActivityTimedOut, isActivityHeartbeatTimedOut, isChildWorkflowFailed, isConditionTimedOut, diff --git a/packages/@eventual/core/src/workflow.ts b/packages/@eventual/core/src/workflow.ts index be95222bb..649baecdf 100644 --- a/packages/@eventual/core/src/workflow.ts +++ b/packages/@eventual/core/src/workflow.ts @@ -3,10 +3,10 @@ import type { Program } from "./interpret.js"; import type { Context } from "./context.js"; import { HistoryStateEvent, - isSleepCompleted, - isSleepScheduled, - SleepCompleted, - SleepScheduled, + isAlarmCompleted, + isAlarmScheduled, + AlarmCompleted, + AlarmScheduled, WorkflowEventType, } from "./workflow-events.js"; import { createWorkflowCall } from "./calls/workflow-call.js"; @@ -175,28 +175,28 @@ export function runWorkflowDefinition( } /** - * Generates synthetic events, for example, {@link SleepCompleted} events when the time has passed, but a real completed event has not come in yet. + * Generates synthetic events, for example, {@link AlarmCompleted} events when the time has passed, but a real completed event has not come in yet. */ export function generateSyntheticEvents( events: HistoryStateEvent[], baseTime: Date -): SleepCompleted[] { - const unresolvedSleep: Record = {}; +): AlarmCompleted[] { + const unresolvedSleep: Record = {}; const sleepEvents = events.filter( - (event): event is SleepScheduled | SleepCompleted => - isSleepScheduled(event) || isSleepCompleted(event) + (event): event is AlarmScheduled | AlarmCompleted => + isAlarmScheduled(event) || isAlarmCompleted(event) ); for (const event of sleepEvents) { - if (isSleepScheduled(event)) { + if (isAlarmScheduled(event)) { unresolvedSleep[event.seq] = event; } else { delete unresolvedSleep[event.seq]; } } - const syntheticSleepComplete: SleepCompleted[] = Object.values( + const syntheticSleepComplete: AlarmCompleted[] = Object.values( unresolvedSleep ) .filter( @@ -205,10 +205,10 @@ export function generateSyntheticEvents( .map( (e) => ({ - type: WorkflowEventType.SleepCompleted, + type: WorkflowEventType.AlarmCompleted, seq: e.seq, timestamp: baseTime.toISOString(), - } satisfies SleepCompleted) + } satisfies AlarmCompleted) ); return syntheticSleepComplete; diff --git a/packages/@eventual/core/test/command-util.ts b/packages/@eventual/core/test/command-util.ts index 3f7bfc5dd..2ca561092 100644 --- a/packages/@eventual/core/test/command-util.ts +++ b/packages/@eventual/core/test/command-util.ts @@ -15,7 +15,6 @@ import { ActivitySucceeded, ActivityFailed, ActivityScheduled, - ActivityTimedOut, ChildWorkflowSucceeded, ChildWorkflowFailed, ChildWorkflowScheduled, @@ -26,8 +25,8 @@ import { ExpectSignalTimedOut, SignalReceived, SignalSent, - SleepCompleted, - SleepScheduled, + AlarmCompleted, + AlarmScheduled, WorkflowEventType, WorkflowTimedOut, ActivityHeartbeatTimedOut, @@ -164,14 +163,6 @@ export function activityFailed(error: any, seq: number): ActivityFailed { }; } -export function activityTimedOut(seq: number): ActivityTimedOut { - return { - type: WorkflowEventType.ActivityTimedOut, - seq, - timestamp: new Date(0).toISOString(), - }; -} - export function workflowFailed(error: any, seq: number): ChildWorkflowFailed { return { type: WorkflowEventType.ChildWorkflowFailed, @@ -227,18 +218,18 @@ export function workflowScheduled( }; } -export function scheduledSleep(untilTime: string, seq: number): SleepScheduled { +export function scheduledAlarm(untilTime: string, seq: number): AlarmScheduled { return { - type: WorkflowEventType.SleepScheduled, + type: WorkflowEventType.AlarmScheduled, untilTime, seq, timestamp: new Date(0).toISOString(), }; } -export function completedSleep(seq: number): SleepCompleted { +export function completedAlarm(seq: number): AlarmCompleted { return { - type: WorkflowEventType.SleepCompleted, + type: WorkflowEventType.AlarmCompleted, seq, timestamp: new Date(0).toISOString(), }; diff --git a/packages/@eventual/core/test/commend-executor.test.ts b/packages/@eventual/core/test/commend-executor.test.ts index af294e83f..eb74779e9 100644 --- a/packages/@eventual/core/test/commend-executor.test.ts +++ b/packages/@eventual/core/test/commend-executor.test.ts @@ -2,7 +2,6 @@ import { jest } from "@jest/globals"; import { CommandType } from "../src/command.js"; import { ActivityScheduled, - ActivityTimedOut, ChildWorkflowScheduled, ConditionStarted, ConditionTimedOut, @@ -10,8 +9,8 @@ import { ExpectSignalStarted, ExpectSignalTimedOut, SignalSent, - SleepCompleted, - SleepScheduled, + AlarmCompleted, + AlarmScheduled, WorkflowEventType, } from "../src/workflow-events.js"; import { @@ -82,20 +81,20 @@ describe("await times", () => { const untilTime = new Date(baseTime.getTime() + 10 * 1000).toISOString(); expect(mockTimerClient.scheduleEvent).toHaveBeenCalledWith< - [ScheduleEventRequest] + [ScheduleEventRequest] >({ event: { - type: WorkflowEventType.SleepCompleted, + type: WorkflowEventType.AlarmCompleted, seq: 0, }, schedule: Schedule.absolute(untilTime), executionId, }); - expect(event).toMatchObject({ + expect(event).toMatchObject({ seq: 0, timestamp: expect.stringContaining("Z"), - type: WorkflowEventType.SleepScheduled, + type: WorkflowEventType.AlarmScheduled, untilTime, }); }); @@ -113,20 +112,20 @@ describe("await times", () => { ); expect(mockTimerClient.scheduleEvent).toHaveBeenCalledWith< - [ScheduleEventRequest] + [ScheduleEventRequest] >({ event: { - type: WorkflowEventType.SleepCompleted, + type: WorkflowEventType.AlarmCompleted, seq: 0, }, schedule: Schedule.absolute(baseTime.toISOString()), executionId, }); - expect(event).toMatchObject({ + expect(event).toMatchObject({ seq: 0, timestamp: expect.stringContaining("Z"), - type: WorkflowEventType.SleepScheduled, + type: WorkflowEventType.AlarmScheduled, untilTime: baseTime.toISOString(), }); }); @@ -157,41 +156,6 @@ describe("activity", () => { name: "activity", }); }); - - test("start with timeout", async () => { - const event = await testExecutor.executeCommand( - workflow, - executionId, - { - kind: CommandType.StartActivity, - args: [], - name: "activity", - seq: 0, - timeoutSeconds: 100, - }, - baseTime - ); - - expect(mockTimerClient.scheduleEvent).toHaveBeenCalledWith< - [ScheduleEventRequest] - >({ - event: { - type: WorkflowEventType.ActivityTimedOut, - seq: 0, - }, - schedule: Schedule.relative(100, baseTime), - executionId, - }); - - expect(mockWorkflowRuntimeClient.startActivity).toHaveBeenCalledTimes(1); - - expect(event).toMatchObject({ - seq: 0, - timestamp: expect.stringContaining("Z"), - type: WorkflowEventType.ActivityScheduled, - name: "activity", - }); - }); }); describe("workflow", () => { diff --git a/packages/@eventual/core/test/interpret.test.ts b/packages/@eventual/core/test/interpret.test.ts index 805c08605..6eccb6a3a 100644 --- a/packages/@eventual/core/test/interpret.test.ts +++ b/packages/@eventual/core/test/interpret.test.ts @@ -5,6 +5,7 @@ import { EventualError, HeartbeatTimeout, Timeout } from "../src/error.js"; import { Context, createAwaitAll, + createTimeReference, duration, Eventual, interpret, @@ -26,8 +27,7 @@ import { activityFailed, activityHeartbeatTimedOut, activityScheduled, - activityTimedOut, - completedSleep, + completedAlarm, conditionStarted, conditionTimedOut, createExpectSignalCommand, @@ -39,7 +39,7 @@ import { createAwaitTimeCommand, createStartConditionCommand, eventsPublished, - scheduledSleep, + scheduledAlarm, signalReceived, signalSent, startedExpectSignal, @@ -55,6 +55,7 @@ import { createWorkflowCall } from "../src/calls/workflow-call.js"; import { createSendSignalCall } from "../src/calls/send-signal-call.js"; import { createConditionCall } from "../src/calls/condition-call.js"; import { createPublishEventsCall } from "../src/calls/send-events-call.js"; +import { createAwaitAllSettled } from "../src/await-all-settled.js"; beforeAll(() => { process.env[SERVICE_TYPE_FLAG] = ServiceType.OrchestratorWorker; @@ -168,32 +169,161 @@ test("should catch error of failed Activity", () => { }); test("should catch error of timing out Activity", () => { + function* myWorkflow(event: any): Program { + try { + const a: any = yield createActivityCall( + "my-activity", + [event], + createAwaitTimeCall("") + ); + + return a; + } catch (err) { + yield createActivityCall("handle-error", [err]); + return []; + } + } + expect( interpret(myWorkflow(event), [ - activityScheduled("my-activity", 0), - activityTimedOut(0), + scheduledAlarm("", 0), + completedAlarm(0), + activityScheduled("my-activity", 1), ]) ).toMatchObject({ commands: [ createScheduledActivityCommand( "handle-error", [new Timeout("Activity Timed Out")], - 1 + 2 + ), + ], + }); +}); + +test("should schedule timeout alarm when not started", () => { + function* myWorkflow(event: any): Program { + try { + const a: any = yield createActivityCall( + "my-activity", + [event], + createTimeReference("") + ); + + return a; + } catch (err) { + yield createActivityCall("handle-error", [err]); + return []; + } + } + + expect( + interpret(myWorkflow(event), [ + scheduledAlarm("", 0), + activityScheduled("my-activity", 1), + completedAlarm(0), + ]) + ).toMatchObject({ + commands: [ + createScheduledActivityCommand( + "handle-error", + [new Timeout("Activity Timed Out")], + 2 ), ], }); }); +test("immediately abort activity on invalid timeout", () => { + function* myWorkflow(event: any): Program { + return createActivityCall("my-activity", [event], "not an awaitable"); + } + + expect( + interpret(myWorkflow(event), [activityScheduled("my-activity", 0)]) + ).toMatchObject({ + result: Result.failed( + new Timeout("Activity immediately timed out, timeout was not awaitable.") + ), + }); +}); + +test("timeout multiple activities at once", () => { + function* myWorkflow(event: any): Program { + const time = createAwaitTimeCall(""); + const a = createActivityCall("my-activity", [event], time); + const b = createActivityCall("my-activity", [event], time); + + return yield createAwaitAllSettled([a, b]); + } + + expect( + interpret(myWorkflow(event), [ + scheduledAlarm("", 0), + activityScheduled("my-activity", 1), + activityScheduled("my-activity", 2), + completedAlarm(0), + ]) + ).toMatchObject({ + result: Result.resolved([ + { + status: "rejected", + reason: new Timeout("Activity Timed Out").toJSON(), + }, + { + status: "rejected", + reason: new Timeout("Activity Timed Out").toJSON(), + }, + ]), + commands: [], + }); +}); + +test("activity times out activity", () => { + function* myWorkflow(event: any): Program { + const z = createActivityCall("my-activity", [event]); + const a = createActivityCall("my-activity", [event], z); + const b = createActivityCall("my-activity", [event], a); + + return yield createAwaitAllSettled([z, a, b]); + } + + expect( + interpret(myWorkflow(event), [ + activityScheduled("my-activity", 0), + activityScheduled("my-activity", 1), + activityScheduled("my-activity", 2), + activitySucceeded("woo", 0), + ]) + ).toMatchObject({ + result: Result.resolved([ + { + status: "fulfilled", + value: "woo", + }, + { + status: "rejected", + reason: new Timeout("Activity Timed Out").toJSON(), + }, + { + status: "rejected", + reason: new Timeout("Activity Timed Out").toJSON(), + }, + ]), + commands: [], + }); +}); + test("should return final result", () => { expect( interpret(myWorkflow(event), [ activityScheduled("my-activity", 0), activitySucceeded("result", 0), activityScheduled("my-activity-0", 1), - scheduledSleep("then", 2), + scheduledAlarm("then", 2), activityScheduled("my-activity-2", 3), activitySucceeded("result-0", 1), - completedSleep(2), + completedAlarm(2), activitySucceeded("result-2", 3), ]) ).toMatchObject({ @@ -322,7 +452,7 @@ describe("activity", () => { test("should throw when scheduled does not correspond to call", () => { expect( - interpret(myWorkflow(event), [scheduledSleep("result", 0)]) + interpret(myWorkflow(event), [scheduledAlarm("result", 0)]) ).toMatchObject({ result: Result.failed({ name: "DeterminismError" }), commands: [], @@ -400,10 +530,10 @@ test("should wait if partial results", () => { activityScheduled("my-activity", 0), activitySucceeded("result", 0), activityScheduled("my-activity-0", 1), - scheduledSleep("then", 2), + scheduledAlarm("then", 2), activityScheduled("my-activity-2", 3), activitySucceeded("result-0", 1), - completedSleep(2), + completedAlarm(2), ]) ).toMatchObject({ commands: [], @@ -440,7 +570,7 @@ test("should not re-schedule sleep for", () => { } expect( - interpret(workflow() as any, [scheduledSleep("anything", 0)]) + interpret(workflow() as any, [scheduledAlarm("anything", 0)]) ).toMatchObject({ commands: [], }); @@ -454,8 +584,8 @@ test("should complete sleep for", () => { expect( interpret(workflow() as any, [ - scheduledSleep("anything", 0), - completedSleep(0), + scheduledAlarm("anything", 0), + completedAlarm(0), ]) ).toMatchObject({ result: Result.resolved("done"), @@ -483,7 +613,7 @@ test("should not re-schedule sleep until", () => { } expect( - interpret(workflow() as any, [scheduledSleep("anything", 0)]) + interpret(workflow() as any, [scheduledAlarm("anything", 0)]) ).toMatchObject({ commands: [], }); @@ -499,8 +629,8 @@ test("should complete sleep until", () => { expect( interpret(workflow() as any, [ - scheduledSleep("anything", 0), - completedSleep(0), + scheduledAlarm("anything", 0), + completedAlarm(0), ]) ).toMatchObject({ result: Result.resolved("done"), @@ -570,7 +700,7 @@ describe("temple of doom", () => { test("waiting", () => { expect( interpret(workflow() as any, [ - scheduledSleep("X", 0), + scheduledAlarm("X", 0), activityScheduled("jump", 1), activityScheduled("run", 2), ]) @@ -583,10 +713,10 @@ describe("temple of doom", () => { // complete sleep, nothing happens expect( interpret(workflow() as any, [ - scheduledSleep("X", 0), + scheduledAlarm("X", 0), activityScheduled("jump", 1), activityScheduled("run", 2), - completedSleep(0), + completedAlarm(0), ]) ).toMatchObject({ commands: [], @@ -597,10 +727,10 @@ describe("temple of doom", () => { // complete sleep, turn on, release the player, dead expect( interpret(workflow() as any, [ - scheduledSleep("X", 0), + scheduledAlarm("X", 0), activityScheduled("jump", 1), activityScheduled("run", 2), - completedSleep(0), + completedAlarm(0), activitySucceeded("anything", 2), ]) ).toMatchObject({ @@ -613,9 +743,9 @@ describe("temple of doom", () => { // complete sleep, turn on, release the player, dead expect( interpret(workflow() as any, [ - completedSleep(0), + completedAlarm(0), activitySucceeded("anything", 2), - scheduledSleep("X", 0), + scheduledAlarm("X", 0), activityScheduled("jump", 1), activityScheduled("run", 2), ]) @@ -629,7 +759,7 @@ describe("temple of doom", () => { // release the player, not on, alive expect( interpret(workflow() as any, [ - scheduledSleep("X", 0), + scheduledAlarm("X", 0), activityScheduled("jump", 1), activityScheduled("run", 2), activitySucceeded("anything", 2), @@ -644,7 +774,7 @@ describe("temple of doom", () => { // release the player, not on, alive expect( interpret(workflow() as any, [ - scheduledSleep("X", 0), + scheduledAlarm("X", 0), activitySucceeded("anything", 2), activityScheduled("jump", 1), activityScheduled("run", 2), @@ -660,7 +790,7 @@ describe("temple of doom", () => { expect( interpret(workflow() as any, [ activitySucceeded("anything", 2), - scheduledSleep("X", 0), + scheduledAlarm("X", 0), activityScheduled("jump", 1), activityScheduled("run", 2), ]) @@ -673,11 +803,11 @@ describe("temple of doom", () => { test("release the player before the trap triggers, player lives", () => { expect( interpret(workflow() as any, [ - scheduledSleep("X", 0), + scheduledAlarm("X", 0), activityScheduled("jump", 1), activityScheduled("run", 2), activitySucceeded("anything", 2), - completedSleep(0), + completedAlarm(0), ]) ).toMatchObject({ result: Result.resolved("alive"), @@ -1618,10 +1748,10 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ signalReceived("MySignal"), - scheduledSleep("", 0), - completedSleep(0), - scheduledSleep("", 1), - completedSleep(1), + scheduledAlarm("", 0), + completedAlarm(0), + scheduledAlarm("", 1), + completedAlarm(1), ]) ).toMatchObject({ result: Result.resolved({ @@ -1639,10 +1769,10 @@ describe("signals", () => { signalReceived("MySignal"), signalReceived("MySignal"), signalReceived("MySignal"), - scheduledSleep("", 0), - completedSleep(0), - scheduledSleep("", 1), - completedSleep(1), + scheduledAlarm("", 0), + completedAlarm(0), + scheduledAlarm("", 1), + completedAlarm(1), ]) ).toMatchObject({ result: Result.resolved({ @@ -1657,13 +1787,13 @@ describe("signals", () => { test("send signal after dispose", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledSleep("", 0), - completedSleep(0), + scheduledAlarm("", 0), + completedAlarm(0), signalReceived("MySignal"), signalReceived("MySignal"), signalReceived("MySignal"), - scheduledSleep("", 1), - completedSleep(1), + scheduledAlarm("", 1), + completedAlarm(1), ]) ).toMatchObject({ result: Result.resolved({ @@ -1707,11 +1837,11 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ signalReceived("MyOtherSignal", "hi"), - scheduledSleep("", 0), - completedSleep(0), + scheduledAlarm("", 0), + completedAlarm(0), activityScheduled("act1", 1), - scheduledSleep("", 2), - completedSleep(2), + scheduledAlarm("", 2), + completedAlarm(2), ]) ).toMatchObject({ result: Result.resolved({ @@ -1727,12 +1857,12 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ signalReceived("MyOtherSignal", "hi"), - scheduledSleep("", 0), + scheduledAlarm("", 0), activityScheduled("act1", 1), activitySucceeded("act1", 1), - completedSleep(0), - scheduledSleep("", 2), - completedSleep(2), + completedAlarm(0), + scheduledAlarm("", 2), + completedAlarm(2), ]) ).toMatchObject({ result: Result.resolved({ @@ -1748,12 +1878,12 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ signalReceived("MyOtherSignal", "hi"), - scheduledSleep("", 0), - completedSleep(0), + scheduledAlarm("", 0), + completedAlarm(0), activityScheduled("act1", 1), activitySucceeded("act1", 1), - scheduledSleep("", 2), - completedSleep(2), + scheduledAlarm("", 2), + completedAlarm(2), ]) ).toMatchObject({ result: Result.resolved({ @@ -1768,11 +1898,11 @@ describe("signals", () => { test("send other signal after dispose", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledSleep("", 0), - completedSleep(0), + scheduledAlarm("", 0), + completedAlarm(0), signalReceived("MyOtherSignal", "hi"), - scheduledSleep("", 1), - completedSleep(1), + scheduledAlarm("", 1), + completedAlarm(1), ]) ).toMatchObject({ result: Result.resolved({ diff --git a/packages/@eventual/testing/test/workflow.ts b/packages/@eventual/testing/test/workflow.ts index e5a96a22c..b13efcdc5 100644 --- a/packages/@eventual/testing/test/workflow.ts +++ b/packages/@eventual/testing/test/workflow.ts @@ -151,7 +151,7 @@ export const orchestrateWorkflow = workflow( export const actWithTimeout = activity( "actWithTimeout", - { timeoutSeconds: 30 }, + { timeout: duration(30, "seconds") }, async () => { return "hi"; } From 713e4ae70f9871702985c3520dd23dd02a1ab711 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Thu, 12 Jan 2023 00:58:57 -0600 Subject: [PATCH 03/14] workflow timeout, cli, api, and remove service type env --- apps/tests/aws-runtime/test/test-service.ts | 12 +- .../@eventual/aws-cdk/src/service-function.ts | 3 +- .../src/clients/workflow-client.ts | 11 +- .../src/handlers/api/executions/new.ts | 39 +- packages/@eventual/cli/src/commands/start.ts | 15 +- .../client/src/http-service-client.ts | 3 +- packages/@eventual/core/src/activity.ts | 11 +- packages/@eventual/core/src/await-time.ts | 68 ++-- .../@eventual/core/src/calls/activity-call.ts | 3 +- packages/@eventual/core/src/interpret.ts | 24 +- packages/@eventual/core/src/runtime/flags.ts | 14 + .../src/runtime/handlers/activity-worker.ts | 353 +++++++++--------- .../core/src/runtime/handlers/api-handler.ts | 20 +- .../src/runtime/handlers/event-handler.ts | 20 +- .../core/src/runtime/handlers/orchestrator.ts | 101 ++--- packages/@eventual/core/src/workflow.ts | 5 +- .../@eventual/core/test/interpret.test.ts | 40 +- .../testing/src/clients/event-client.ts | 6 +- .../testing/src/clients/workflow-client.ts | 9 +- .../src/clients/workflow-runtime-client.ts | 14 +- packages/@eventual/testing/src/environment.ts | 5 +- packages/@eventual/testing/src/utils.ts | 15 - packages/@eventual/testing/test/workflow.ts | 4 +- 23 files changed, 398 insertions(+), 397 deletions(-) delete mode 100644 packages/@eventual/testing/src/utils.ts diff --git a/apps/tests/aws-runtime/test/test-service.ts b/apps/tests/aws-runtime/test/test-service.ts index 1ceee22da..41f187ec5 100644 --- a/apps/tests/aws-runtime/test/test-service.ts +++ b/apps/tests/aws-runtime/test/test-service.ts @@ -157,13 +157,15 @@ const slowActivity = activity( () => new Promise((resolve) => setTimeout(resolve, 10 * 1000)) ); -const slowWf = workflow("slowWorkflow", { timeoutSeconds: 5 }, () => - duration(10) +const slowWf = workflow( + "slowWorkflow", + { timeout: duration(5, "seconds") }, + () => duration(10) ); export const timedOutWorkflow = workflow( "timedOut", - { timeoutSeconds: 100 }, + { timeout: duration(100, "seconds") }, async () => { // chains to be able to run in parallel. const timedOutFunctions = { @@ -196,7 +198,7 @@ export const timedOutWorkflow = workflow( export const asyncWorkflow = workflow( "asyncWorkflow", - { timeoutSeconds: 100 }, // timeout eventually + { timeout: duration(100, "seconds") }, // timeout eventually async () => { const result = await asyncActivity("complete"); @@ -234,7 +236,7 @@ const activityWithHeartbeat = activity( export const heartbeatWorkflow = workflow( "heartbeatWorkflow", - { timeoutSeconds: 100 }, // timeout eventually + { timeout: duration(100, "seconds") }, // timeout eventually async (n: number) => { return await Promise.allSettled([ activityWithHeartbeat(n, "success"), diff --git a/packages/@eventual/aws-cdk/src/service-function.ts b/packages/@eventual/aws-cdk/src/service-function.ts index e0ba86516..d0fe9759a 100644 --- a/packages/@eventual/aws-cdk/src/service-function.ts +++ b/packages/@eventual/aws-cdk/src/service-function.ts @@ -1,4 +1,4 @@ -import { ServiceType, SERVICE_TYPE_FLAG } from "@eventual/core"; +import { ServiceType } from "@eventual/core"; import { Architecture, Code, @@ -28,7 +28,6 @@ export class ServiceFunction extends Function { environment: { ...props.environment, NODE_OPTIONS: "--enable-source-maps", - [SERVICE_TYPE_FLAG]: props.serviceType, }, }); } diff --git a/packages/@eventual/aws-runtime/src/clients/workflow-client.ts b/packages/@eventual/aws-runtime/src/clients/workflow-client.ts index 33ed90fbc..308ad4397 100644 --- a/packages/@eventual/aws-runtime/src/clients/workflow-client.ts +++ b/packages/@eventual/aws-runtime/src/clients/workflow-client.ts @@ -25,6 +25,7 @@ import { lookupWorkflow, SortOrder, isExecutionStatus, + computeDurationDate, } from "@eventual/core"; import { ulid } from "ulidx"; import { inspect } from "util"; @@ -56,7 +57,7 @@ export class AWSWorkflowClient extends WorkflowClient { executionName = ulid(), workflow, input, - timeoutSeconds, + timeout, ...request }: StartExecutionRequest | StartChildExecutionRequest) { if (typeof workflow === "string" && !lookupWorkflow(workflow)) { @@ -107,9 +108,11 @@ export class AWSWorkflowClient extends WorkflowClient { workflowName, // generate the time for the workflow to timeout based on when it was started. // the timer will be started by the orchestrator so the client does not need to have access to the timer client. - timeoutTime: timeoutSeconds - ? new Date( - new Date().getTime() + timeoutSeconds * 1000 + timeoutTime: timeout + ? computeDurationDate( + new Date(), + timeout.dur, + timeout.unit ).toISOString() : undefined, context: { diff --git a/packages/@eventual/aws-runtime/src/handlers/api/executions/new.ts b/packages/@eventual/aws-runtime/src/handlers/api/executions/new.ts index 16d8e03f5..87d57e3e9 100644 --- a/packages/@eventual/aws-runtime/src/handlers/api/executions/new.ts +++ b/packages/@eventual/aws-runtime/src/handlers/api/executions/new.ts @@ -1,7 +1,12 @@ // startWorkflow uses the global workflows() to validate the workflow name. import "@eventual/entry/injected"; -import type { StartExecutionResponse } from "@eventual/core"; +import { + DurationUnit, + DURATION_UNITS, + isDurationUnit, + StartExecutionResponse, +} from "@eventual/core"; import type { APIGatewayProxyEventV2, APIGatewayProxyHandlerV2, @@ -20,21 +25,30 @@ const workflowClient = createWorkflowClient({ * * workflowName - name of the workflow to start * * Query Parameters: - * * timeoutSeconds - Number of seconds the workflow should run before it times out. Default: use the configured timeout or no timeout. + * * timeout - Number of `timeoutUnit` (default seconds) the workflow should run before it times out. Default: use the configured timeout or no timeout. + * * timeoutUnit - "seconds" | "minutes" | "hours" | "days" | "years". Units to use for the timeout, default: "seconds". * * executionName - name to give the workflow. Default: auto generated UUID. */ export const handler: APIGatewayProxyHandlerV2 = withErrorMiddleware(async (event: APIGatewayProxyEventV2) => { - const { timeoutSeconds: timeoutSecondsString, executionName } = - event.queryStringParameters ?? {}; + const { + timeout: timeoutString, + timeoutUnit, + executionName, + } = event.queryStringParameters ?? {}; + + const timeout = timeoutString ? parseInt(timeoutString) : undefined; - const timeoutSeconds = timeoutSecondsString - ? parseInt(timeoutSecondsString) - : undefined; + if (timeout !== undefined && isNaN(timeout)) { + throw new Error( + "Expected optional parameter timeout to be a valid number" + ); + } - if (timeoutSeconds !== undefined && isNaN(timeoutSeconds)) { + if (timeoutUnit && !isDurationUnit(timeoutUnit)) { throw new Error( - "Expected optional parameter timeoutSeconds to be a valid number" + "Expected optional parameter timeoutUnit to be one of: " + + DURATION_UNITS.join() ); } @@ -47,6 +61,11 @@ export const handler: APIGatewayProxyHandlerV2 = workflow: workflowName, input: event.body && JSON.parse(event.body), executionName, - timeoutSeconds, + timeout: timeout + ? { + dur: timeout, + unit: (timeoutUnit as DurationUnit) ?? "seconds", + } + : undefined, }); }); diff --git a/packages/@eventual/cli/src/commands/start.ts b/packages/@eventual/cli/src/commands/start.ts index 33e364131..4c854376c 100644 --- a/packages/@eventual/cli/src/commands/start.ts +++ b/packages/@eventual/cli/src/commands/start.ts @@ -4,6 +4,8 @@ import { isWorkflowSucceeded, isWorkflowFailed, ExecutionEventsResponse, + DURATION_UNITS, + DurationUnit, } from "@eventual/core"; import { Argv } from "yargs"; import { serviceAction, setServiceOptions } from "../service-action.js"; @@ -49,6 +51,12 @@ export const start = (yargs: Argv) => type: "number", defaultDescription: "Configured on the workflow definition or no timeout.", + }) + .option("timeoutUnit", { + describe: "Number of seconds until the execution times out.", + type: "string", + choices: DURATION_UNITS, + default: "seconds", }), (args) => { return serviceAction( @@ -94,7 +102,12 @@ export const start = (yargs: Argv) => workflow: args.workflow, input: inputJSON, executionName: args.name, - timeoutSeconds: args.timeout, + timeout: args.timeout + ? { + dur: args.timeout, + unit: args.timeoutUnit as DurationUnit, + } + : undefined, }); } } diff --git a/packages/@eventual/client/src/http-service-client.ts b/packages/@eventual/client/src/http-service-client.ts index bf54e62b5..3a0db90fd 100644 --- a/packages/@eventual/client/src/http-service-client.ts +++ b/packages/@eventual/client/src/http-service-client.ts @@ -75,7 +75,8 @@ export class HttpServiceClient implements EventualServiceClient { : request.workflow.workflowName; const queryString = formatQueryString({ - timeoutSeconds: request.timeoutSeconds, + timeout: request.timeout?.dur, + timeoutUnit: request.timeout?.unit, executionName: request.executionName, }); diff --git a/packages/@eventual/core/src/activity.ts b/packages/@eventual/core/src/activity.ts index e1625e940..530b737f5 100644 --- a/packages/@eventual/core/src/activity.ts +++ b/packages/@eventual/core/src/activity.ts @@ -1,5 +1,6 @@ -import { DurationReference } from "./await-time.js"; +import { DurationSpec } from "./await-time.js"; import { createActivityCall } from "./calls/activity-call.js"; +import { createAwaitDurationCall } from "./calls/await-time-call.js"; import { callableActivities, getActivityContext, @@ -21,7 +22,7 @@ export interface ActivityOptions { * * @default - workflow will run forever. */ - timeout?: DurationReference; + timeout?: DurationSpec; /** * For long running activities, it is suggested that they report back that they * are still in progress to avoid waiting forever or until a long timeout when @@ -32,7 +33,7 @@ export interface ActivityOptions { * * If it fails to do so, the workflow will cancel the activity and throw an error. */ - heartbeatTimeout?: DurationReference; + heartbeatTimeout?: DurationSpec; } export interface ActivityFunction { @@ -227,7 +228,9 @@ export function activity( return createActivityCall( activityID, args, - opts?.timeout, + opts?.timeout + ? createAwaitDurationCall(opts.timeout.dur, opts.timeout.unit) + : undefined, opts?.heartbeatTimeout ? computeDurationSeconds( opts.heartbeatTimeout.dur, diff --git a/packages/@eventual/core/src/await-time.ts b/packages/@eventual/core/src/await-time.ts index a18755009..e841dfe23 100644 --- a/packages/@eventual/core/src/await-time.ts +++ b/packages/@eventual/core/src/await-time.ts @@ -1,19 +1,34 @@ import { - AwaitDurationCall, - AwaitTimeCall, createAwaitDurationCall, createAwaitTimeCall, } from "./calls/await-time-call.js"; -import { createEventual, EventualKind } from "./eventual.js"; import { isOrchestratorWorker } from "./runtime/flags.js"; -export type DurationUnit = `${"second" | "minute" | "hour" | "day" | "year"}${ - | "s" - | ""}`; +export const DURATION_UNITS = [ + "second", + "seconds", + "minute", + "minutes", + "hour", + "hours", + "day", + "days", + "year", + "years", +] as const; +export type DurationUnit = typeof DURATION_UNITS[number]; -// TODO revisit these interfaces -export type TimeReference = Pick; -export type DurationReference = Pick; +export function isDurationUnit(u: string): u is DurationUnit { + return DURATION_UNITS.includes(u as any); +} + +export interface TimeSpec { + isoDate: string; +} +export interface DurationSpec { + dur: number; + unit: DurationUnit; +} /** * ```ts @@ -26,10 +41,9 @@ export type DurationReference = Pick; export function duration( dur: number, unit: DurationUnit = "seconds" -): Promise & DurationReference { +): Promise & DurationSpec { if (!isOrchestratorWorker()) { - // TODO: remove this limit - throw new Error("duration is only valid in a workflow"); + return { dur, unit } as Promise & DurationSpec; } // register a sleep command and return it (to be yielded) @@ -44,30 +58,16 @@ export function duration( * }) * ``` */ -export function time(isoDate: string): Promise & TimeReference; -export function time(date: Date): Promise & TimeReference; -export function time(date: Date | string): Promise & TimeReference { +export function time(isoDate: string): Promise & TimeSpec; +export function time(date: Date): Promise & TimeSpec; +export function time(date: Date | string): Promise & TimeSpec { + const d = new Date(date); + const iso = d.toISOString(); + if (!isOrchestratorWorker()) { - throw new Error("time is only valid in a workflow"); + return { isoDate: iso } as Promise & TimeSpec; } - const d = new Date(date); // register a sleep command and return it (to be yielded) - return createAwaitTimeCall(d.toISOString()) as any; -} - -export function createTimeReference(iso: string): TimeReference { - return createEventual(EventualKind.AwaitTimeCall, { - isoDate: iso, - }); -} - -export function createDurationReference( - dur: number, - unit: DurationUnit -): DurationReference { - return createEventual(EventualKind.AwaitDurationCall, { - dur, - unit, - }); + return createAwaitTimeCall(iso) as any; } diff --git a/packages/@eventual/core/src/calls/activity-call.ts b/packages/@eventual/core/src/calls/activity-call.ts index f5f708b2d..5f1d618f9 100644 --- a/packages/@eventual/core/src/calls/activity-call.ts +++ b/packages/@eventual/core/src/calls/activity-call.ts @@ -3,6 +3,7 @@ import { EventualBase, isEventualOfKind, createEventual, + Eventual, } from "../eventual.js"; import { registerEventual } from "../global.js"; import { Resolved, Failed } from "../result.js"; @@ -26,7 +27,7 @@ export interface ActivityCall export function createActivityCall( name: string, args: any[], - timeout?: any, + timeout?: Eventual, heartbeatSeconds?: number ): ActivityCall { return registerEventual( diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index 23ef7b486..77db1443b 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -64,11 +64,7 @@ import { } from "./calls/signal-handler-call.js"; import { isSendSignalCall } from "./calls/send-signal-call.js"; import { isWorkflowCall } from "./calls/workflow-call.js"; -import { - clearEventualCollector, - registerEventual, - setEventualCollector, -} from "./global.js"; +import { clearEventualCollector, setEventualCollector } from "./global.js"; import { isConditionCall } from "./calls/condition-call.js"; import { isAwaitAllSettled } from "./await-all-settled.js"; import { isAwaitAny } from "./await-any.js"; @@ -309,24 +305,6 @@ export function interpret( subscribeToSignal(activity.signalId, activity); // signal handler does not emit a call/command. It is only internal. return activity; - } else if (isActivityCall(activity)) { - if (activity?.timeout) { - if (isEventual(activity?.timeout)) { - // if the eventual is not started yet, start it - if ( - !("seq" in activity.timeout) || - activity.timeout === undefined - ) { - registerEventual(activity.timeout); - } - } else { - activity.result = Result.failed( - new Timeout( - "Activity immediately timed out, timeout was not awaitable." - ) - ); - } - } } activity.seq = nextSeq(); callTable[activity.seq!] = activity; diff --git a/packages/@eventual/core/src/runtime/flags.ts b/packages/@eventual/core/src/runtime/flags.ts index 225e735f7..2c780ba64 100644 --- a/packages/@eventual/core/src/runtime/flags.ts +++ b/packages/@eventual/core/src/runtime/flags.ts @@ -17,3 +17,17 @@ export function isOrchestratorWorker() { export function isEventHandler() { return process.env[SERVICE_TYPE_FLAG] === ServiceType.EventHandler; } + +export async function serviceTypeScope( + serviceType: ServiceType, + handler: () => Output +): Promise> { + const back = process.env[SERVICE_TYPE_FLAG]; + try { + process.env[SERVICE_TYPE_FLAG] = serviceType; + // await before return so that the promise is completed before the finally call. + return await handler(); + } finally { + process.env[SERVICE_TYPE_FLAG] = back; + } +} diff --git a/packages/@eventual/core/src/runtime/handlers/activity-worker.ts b/packages/@eventual/core/src/runtime/handlers/activity-worker.ts index cb3bcc91c..0f6edc6a0 100644 --- a/packages/@eventual/core/src/runtime/handlers/activity-worker.ts +++ b/packages/@eventual/core/src/runtime/handlers/activity-worker.ts @@ -35,6 +35,8 @@ import { LogLevel, } from "../log-agent.js"; import { EventClient } from "../clients/event-client.js"; +import { serviceTypeScope } from "../flags.js"; +import { ServiceType } from "../../service-type.js"; export interface CreateActivityWorkerProps { activityRuntimeClient: ActivityRuntimeClient; @@ -92,197 +94,204 @@ export function createActivityWorker({ request: ActivityWorkerRequest, baseTime: Date = new Date(), getEndTime = () => new Date() - ) => { - const activityHandle = `${request.command.seq} for execution ${request.executionId} on retry ${request.retry}`; - metrics.resetDimensions(false); - metrics.setNamespace(MetricsCommon.EventualNamespace); - metrics.putDimensions({ - ActivityName: request.command.name, - WorkflowName: request.workflowName, - }); - // the time from the workflow emitting the activity scheduled command - // to the request being seen. - const activityLogContext: ActivityLogContext = { - type: LogContextType.Activity, - activityName: request.command.name, - executionId: request.executionId, - seq: request.command.seq, - }; - const start = baseTime; - const recordAge = - start.getTime() - new Date(request.scheduledTime).getTime(); - metrics.putMetric( - ActivityMetrics.ActivityRequestAge, - recordAge, - Unit.Milliseconds - ); - if ( - !(await timed(metrics, ActivityMetrics.ClaimDuration, () => - activityRuntimeClient.claimActivity( + ) => + await serviceTypeScope(ServiceType.ActivityWorker, async () => { + const activityHandle = `${request.command.seq} for execution ${request.executionId} on retry ${request.retry}`; + metrics.resetDimensions(false); + metrics.setNamespace(MetricsCommon.EventualNamespace); + metrics.putDimensions({ + ActivityName: request.command.name, + WorkflowName: request.workflowName, + }); + // the time from the workflow emitting the activity scheduled command + // to the request being seen. + const activityLogContext: ActivityLogContext = { + type: LogContextType.Activity, + activityName: request.command.name, + executionId: request.executionId, + seq: request.command.seq, + }; + const start = baseTime; + const recordAge = + start.getTime() - new Date(request.scheduledTime).getTime(); + metrics.putMetric( + ActivityMetrics.ActivityRequestAge, + recordAge, + Unit.Milliseconds + ); + if ( + !(await timed(metrics, ActivityMetrics.ClaimDuration, () => + activityRuntimeClient.claimActivity( + request.executionId, + request.command.seq, + request.retry + ) + )) + ) { + metrics.putMetric(ActivityMetrics.ClaimRejected, 1, Unit.Count); + console.info(`Activity ${activityHandle} already claimed.`); + return; + } + if (request.command.heartbeatSeconds) { + await timerClient.startTimer({ + activitySeq: request.command.seq, + type: TimerRequestType.ActivityHeartbeatMonitor, + executionId: request.executionId, + heartbeatSeconds: request.command.heartbeatSeconds, + schedule: Schedule.relative(request.command.heartbeatSeconds), + }); + } + setActivityContext({ + activityToken: createActivityToken( request.executionId, - request.command.seq, - request.retry - ) - )) - ) { - metrics.putMetric(ActivityMetrics.ClaimRejected, 1, Unit.Count); - console.info(`Activity ${activityHandle} already claimed.`); - return; - } - if (request.command.heartbeatSeconds) { - await timerClient.startTimer({ - activitySeq: request.command.seq, - type: TimerRequestType.ActivityHeartbeatMonitor, + request.command.seq + ), executionId: request.executionId, - heartbeatSeconds: request.command.heartbeatSeconds, - schedule: Schedule.relative(request.command.heartbeatSeconds), + scheduledTime: request.scheduledTime, + workflowName: request.workflowName, }); - } - setActivityContext({ - activityToken: createActivityToken( - request.executionId, - request.command.seq - ), - executionId: request.executionId, - scheduledTime: request.scheduledTime, - workflowName: request.workflowName, - }); - metrics.putMetric(ActivityMetrics.ClaimRejected, 0, Unit.Count); + metrics.putMetric(ActivityMetrics.ClaimRejected, 0, Unit.Count); + + console.info(`Processing ${activityHandle}.`); - console.info(`Processing ${activityHandle}.`); + const activity = activityProvider.getActivityHandler( + request.command.name + ); + try { + if (!activity) { + metrics.putMetric(ActivityMetrics.NotFoundError, 1, Unit.Count); + throw new ActivityNotFoundError( + request.command.name, + activityProvider.getActivityIds() + ); + } - const activity = activityProvider.getActivityHandler( - request.command.name - ); - try { - if (!activity) { - metrics.putMetric(ActivityMetrics.NotFoundError, 1, Unit.Count); - throw new ActivityNotFoundError( - request.command.name, - activityProvider.getActivityIds() + const result = await logAgent.logContextScope( + activityLogContext, + async () => { + return await timed( + metrics, + ActivityMetrics.OperationDuration, + () => activity(...request.command.args) + ); + } ); - } - const result = await logAgent.logContextScope( - activityLogContext, - async () => { - return await timed( + if (isAsyncResult(result)) { + metrics.setProperty(ActivityMetrics.HasResult, 0); + metrics.setProperty(ActivityMetrics.AsyncResult, 1); + + // TODO: Send heartbeat on sync activity completion. + + /** + * The activity has declared that it is async, other than logging, there is nothing left to do here. + * The activity should call {@link WorkflowClient.sendActivitySuccess} or {@link WorkflowClient.sendActivityFailure} when it is done. + */ + return timed( metrics, - ActivityMetrics.OperationDuration, - () => activity(...request.command.args) + ActivityMetrics.ActivityLogWriteDuration, + () => logAgent.flush() + ); + } else if (result) { + metrics.setProperty(ActivityMetrics.HasResult, 1); + metrics.setProperty(ActivityMetrics.AsyncResult, 0); + metrics.putMetric( + ActivityMetrics.ResultBytes, + JSON.stringify(result).length, + Unit.Bytes ); + } else { + metrics.setProperty(ActivityMetrics.HasResult, 0); + metrics.setProperty(ActivityMetrics.AsyncResult, 0); } - ); - - if (isAsyncResult(result)) { - metrics.setProperty(ActivityMetrics.HasResult, 0); - metrics.setProperty(ActivityMetrics.AsyncResult, 1); - // TODO: Send heartbeat on sync activity completion. - - /** - * The activity has declared that it is async, other than logging, there is nothing left to do here. - * The activity should call {@link WorkflowClient.sendActivitySuccess} or {@link WorkflowClient.sendActivityFailure} when it is done. - */ - return timed( - metrics, - ActivityMetrics.ActivityLogWriteDuration, - () => logAgent.flush() + logAgent.logWithContext( + activityLogContext, + LogLevel.INFO, + `Activity ${activityHandle} succeeded, reporting back to execution.` ); - } else if (result) { - metrics.setProperty(ActivityMetrics.HasResult, 1); - metrics.setProperty(ActivityMetrics.AsyncResult, 0); - metrics.putMetric( - ActivityMetrics.ResultBytes, - JSON.stringify(result).length, - Unit.Bytes - ); - } else { - metrics.setProperty(ActivityMetrics.HasResult, 0); - metrics.setProperty(ActivityMetrics.AsyncResult, 0); - } - - logAgent.logWithContext( - activityLogContext, - LogLevel.INFO, - `Activity ${activityHandle} succeeded, reporting back to execution.` - ); - const endTime = getEndTime(start); - const event = createEvent( - { - type: WorkflowEventType.ActivitySucceeded, - seq: request.command.seq, - result, - }, - endTime - ); + const endTime = getEndTime(start); + const event = createEvent( + { + type: WorkflowEventType.ActivitySucceeded, + seq: request.command.seq, + result, + }, + endTime + ); - await finishActivity( - event, - recordAge + (endTime.getTime() - start.getTime()) - ); - } catch (err) { - const [error, message] = extendsError(err) - ? [err.name, err.message] - : ["Error", JSON.stringify(err)]; + await finishActivity( + event, + recordAge + (endTime.getTime() - start.getTime()) + ); + } catch (err) { + const [error, message] = extendsError(err) + ? [err.name, err.message] + : ["Error", JSON.stringify(err)]; - logAgent.logWithContext( - activityLogContext, - LogLevel.DEBUG, - `Activity ${activityHandle} failed, reporting failure back to execution: ${error}: ${message}` - ); + logAgent.logWithContext( + activityLogContext, + LogLevel.DEBUG, + `Activity ${activityHandle} failed, reporting failure back to execution: ${error}: ${message}` + ); - const endTime = getEndTime(start); - const event = createEvent( - { - type: WorkflowEventType.ActivityFailed, - seq: request.command.seq, - error, - message, - }, - endTime - ); + const endTime = getEndTime(start); + const event = createEvent( + { + type: WorkflowEventType.ActivityFailed, + seq: request.command.seq, + error, + message, + }, + endTime + ); - await finishActivity( - event, - recordAge + (endTime.getTime() - start.getTime()) - ); - } finally { - clearActivityContext(); - } + await finishActivity( + event, + recordAge + (endTime.getTime() - start.getTime()) + ); + } finally { + clearActivityContext(); + } - function logActivityCompleteMetrics(failed: boolean, duration: number) { - metrics.putMetric( - ActivityMetrics.ActivityFailed, - failed ? 1 : 0, - Unit.Count - ); - metrics.putMetric( - ActivityMetrics.ActivitySucceeded, - failed ? 0 : 1, - Unit.Count - ); - // The total time from the activity being scheduled until it's result is send to the workflow. - metrics.putMetric(ActivityMetrics.TotalDuration, duration); - } + function logActivityCompleteMetrics( + failed: boolean, + duration: number + ) { + metrics.putMetric( + ActivityMetrics.ActivityFailed, + failed ? 1 : 0, + Unit.Count + ); + metrics.putMetric( + ActivityMetrics.ActivitySucceeded, + failed ? 0 : 1, + Unit.Count + ); + // The total time from the activity being scheduled until it's result is send to the workflow. + metrics.putMetric(ActivityMetrics.TotalDuration, duration); + } - async function finishActivity( - event: ActivitySucceeded | ActivityFailed, - duration: number - ) { - const logFlush = timed( - metrics, - ActivityMetrics.ActivityLogWriteDuration, - () => logAgent.flush() - ); - await timed(metrics, ActivityMetrics.SubmitWorkflowTaskDuration, () => - workflowClient.submitWorkflowTask(request.executionId, event) - ); - await logFlush; + async function finishActivity( + event: ActivitySucceeded | ActivityFailed, + duration: number + ) { + const logFlush = timed( + metrics, + ActivityMetrics.ActivityLogWriteDuration, + () => logAgent.flush() + ); + await timed( + metrics, + ActivityMetrics.SubmitWorkflowTaskDuration, + () => + workflowClient.submitWorkflowTask(request.executionId, event) + ); + await logFlush; - logActivityCompleteMetrics(isWorkflowFailed(event), duration); - } - } + logActivityCompleteMetrics(isWorkflowFailed(event), duration); + } + }) ); } diff --git a/packages/@eventual/core/src/runtime/handlers/api-handler.ts b/packages/@eventual/core/src/runtime/handlers/api-handler.ts index a19a05458..51cc910b7 100644 --- a/packages/@eventual/core/src/runtime/handlers/api-handler.ts +++ b/packages/@eventual/core/src/runtime/handlers/api-handler.ts @@ -1,6 +1,8 @@ import { api } from "../../api.js"; import { registerServiceClient } from "../../global.js"; import { EventualServiceClient } from "../../service-client.js"; +import { ServiceType } from "../../service-type.js"; +import { serviceTypeScope } from "../flags.js"; export interface ApiHandlerDependencies { serviceClient: EventualServiceClient; @@ -25,13 +27,15 @@ export function createApiHandler({ serviceClient }: ApiHandlerDependencies) { * then handles the request. */ return async function processRequest(request: Request): Promise { - try { - return api.handle(request); - } catch (err) { - console.error(err); - return new Response("Internal Server Error", { - status: 500, - }); - } + return await serviceTypeScope(ServiceType.ApiHandler, async () => { + try { + return api.handle(request); + } catch (err) { + console.error(err); + return new Response("Internal Server Error", { + status: 500, + }); + } + }); }; } diff --git a/packages/@eventual/core/src/runtime/handlers/event-handler.ts b/packages/@eventual/core/src/runtime/handlers/event-handler.ts index 700bdbf68..d16feff91 100644 --- a/packages/@eventual/core/src/runtime/handlers/event-handler.ts +++ b/packages/@eventual/core/src/runtime/handlers/event-handler.ts @@ -2,6 +2,8 @@ import { registerServiceClient } from "../../global.js"; import type { EventEnvelope } from "../../event.js"; import { EventHandlerProvider } from "../providers/event-handler-provider.js"; import { EventualServiceClient } from "../../service-client.js"; +import { serviceTypeScope } from "../flags.js"; +import { ServiceType } from "../../service-type.js"; /** * The dependencies of {@link createEventHandlerWorker}. @@ -38,14 +40,16 @@ export function createEventHandlerWorker({ } return async function (events) { - await Promise.allSettled( - events.map((event) => - Promise.allSettled( - eventHandlerProvider - .getEventHandlersForEvent(event.name) - .map((handler) => handler(event.event)) + return await serviceTypeScope(ServiceType.EventHandler, async () => { + await Promise.allSettled( + events.map((event) => + Promise.allSettled( + eventHandlerProvider + .getEventHandlersForEvent(event.name) + .map((handler) => handler(event.event)) + ) ) - ) - ); + ); + }); }; } diff --git a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts index df8a0455a..1f065b5db 100644 --- a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts +++ b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts @@ -60,6 +60,8 @@ import { WorkflowRuntimeClient } from "../clients/workflow-runtime-client.js"; import { WorkflowClient } from "../clients/workflow-client.js"; import { MetricsClient } from "../clients/metrics-client.js"; import { EventClient } from "../clients/event-client.js"; +import { serviceTypeScope } from "../flags.js"; +import { ServiceType } from "../../service-type.js"; /** * The Orchestrator's client dependencies. @@ -107,62 +109,63 @@ export function createOrchestrator({ eventClient, }); - return async (workflowTasks, baseTime = new Date()) => { - const tasksByExecutionId = groupBy( - workflowTasks, - (task) => task.executionId - ); + return async (workflowTasks, baseTime = new Date()) => + await serviceTypeScope(ServiceType.OrchestratorWorker, async () => { + const tasksByExecutionId = groupBy( + workflowTasks, + (task) => task.executionId + ); - const eventsByExecutionId = Object.fromEntries( - Object.entries(tasksByExecutionId).map(([executionId, records]) => [ - executionId, - records.flatMap((e) => e.events), - ]) - ); + const eventsByExecutionId = Object.fromEntries( + Object.entries(tasksByExecutionId).map(([executionId, records]) => [ + executionId, + records.flatMap((e) => e.events), + ]) + ); - console.info( - "Found execution ids: " + Object.keys(eventsByExecutionId).join(", ") - ); + console.info( + "Found execution ids: " + Object.keys(eventsByExecutionId).join(", ") + ); - // for each execution id - const results = await promiseAllSettledPartitioned( - Object.entries(eventsByExecutionId), - async ([executionId, records]) => { - if (!isExecutionId(executionId)) { - throw new Error(`invalid ExecutionID: '${executionId}'`); - } - const workflowName = parseWorkflowName(executionId); - if (workflowName === undefined) { - throw new Error(`execution ID '${executionId}' does not exist`); + // for each execution id + const results = await promiseAllSettledPartitioned( + Object.entries(eventsByExecutionId), + async ([executionId, records]) => { + if (!isExecutionId(executionId)) { + throw new Error(`invalid ExecutionID: '${executionId}'`); + } + const workflowName = parseWorkflowName(executionId); + if (workflowName === undefined) { + throw new Error(`execution ID '${executionId}' does not exist`); + } + // TODO: get workflow from execution id + return orchestrateExecution( + workflowName, + executionId, + records, + baseTime + ); } - // TODO: get workflow from execution id - return orchestrateExecution( - workflowName, - executionId, - records, - baseTime - ); - } - ); - - console.debug( - "Executions succeeded: " + - results.fulfilled.map(([[executionId]]) => executionId).join(",") - ); + ); - if (results.rejected.length > 0) { - console.error( - "Executions failed: \n" + - results.rejected - .map(([[executionId], error]) => `${executionId}: ${error}`) - .join("\n") + console.debug( + "Executions succeeded: " + + results.fulfilled.map(([[executionId]]) => executionId).join(",") ); - } - return { - failedExecutionIds: results.rejected.map((rejected) => rejected[0][0]), - }; - }; + if (results.rejected.length > 0) { + console.error( + "Executions failed: \n" + + results.rejected + .map(([[executionId], error]) => `${executionId}: ${error}`) + .join("\n") + ); + } + + return { + failedExecutionIds: results.rejected.map((rejected) => rejected[0][0]), + }; + }); async function orchestrateExecution( workflowName: string, diff --git a/packages/@eventual/core/src/workflow.ts b/packages/@eventual/core/src/workflow.ts index 649baecdf..17fe6c35b 100644 --- a/packages/@eventual/core/src/workflow.ts +++ b/packages/@eventual/core/src/workflow.ts @@ -15,6 +15,7 @@ import { isOrchestratorWorker } from "./runtime/flags.js"; import { isChain } from "./chain.js"; import { ChildExecution, ExecutionHandle } from "./execution.js"; import { StartExecutionRequest } from "./service-client.js"; +import { DurationSpec } from "./await-time.js"; export type WorkflowHandler = ( input: Input, @@ -32,7 +33,7 @@ export interface WorkflowOptions { * * @default - workflow will never timeout. */ - timeoutSeconds?: number; + timeout?: DurationSpec; } export type WorkflowOutput = W extends Workflow< @@ -150,7 +151,7 @@ export function workflow( workflow: name, executionName: input.executionName, input: input.input, - timeoutSeconds: input.timeoutSeconds, + timeout: input.timeout, ...opts, }); }; diff --git a/packages/@eventual/core/test/interpret.test.ts b/packages/@eventual/core/test/interpret.test.ts index 6eccb6a3a..20f684b6b 100644 --- a/packages/@eventual/core/test/interpret.test.ts +++ b/packages/@eventual/core/test/interpret.test.ts @@ -5,7 +5,6 @@ import { EventualError, HeartbeatTimeout, Timeout } from "../src/error.js"; import { Context, createAwaitAll, - createTimeReference, duration, Eventual, interpret, @@ -201,42 +200,13 @@ test("should catch error of timing out Activity", () => { }); }); -test("should schedule timeout alarm when not started", () => { - function* myWorkflow(event: any): Program { - try { - const a: any = yield createActivityCall( - "my-activity", - [event], - createTimeReference("") - ); - - return a; - } catch (err) { - yield createActivityCall("handle-error", [err]); - return []; - } - } - - expect( - interpret(myWorkflow(event), [ - scheduledAlarm("", 0), - activityScheduled("my-activity", 1), - completedAlarm(0), - ]) - ).toMatchObject({ - commands: [ - createScheduledActivityCommand( - "handle-error", - [new Timeout("Activity Timed Out")], - 2 - ), - ], - }); -}); - test("immediately abort activity on invalid timeout", () => { function* myWorkflow(event: any): Program { - return createActivityCall("my-activity", [event], "not an awaitable"); + return createActivityCall( + "my-activity", + [event], + "not an awaitable" as any + ); } expect( diff --git a/packages/@eventual/testing/src/clients/event-client.ts b/packages/@eventual/testing/src/clients/event-client.ts index 8ecd732f8..7aabfe55e 100644 --- a/packages/@eventual/testing/src/clients/event-client.ts +++ b/packages/@eventual/testing/src/clients/event-client.ts @@ -3,9 +3,7 @@ import { EventEnvelope, EventHandlerWorker, EventPayload, - ServiceType, } from "@eventual/core"; -import { serviceTypeScope } from "../utils.js"; export class TestEventClient implements EventClient { constructor(private eventHandlerWorker: EventHandlerWorker) {} @@ -13,8 +11,6 @@ export class TestEventClient implements EventClient { public async publishEvents( ...event: EventEnvelope[] ): Promise { - return serviceTypeScope(ServiceType.EventHandler, async () => { - await this.eventHandlerWorker(event); - }); + return await this.eventHandlerWorker(event); } } diff --git a/packages/@eventual/testing/src/clients/workflow-client.ts b/packages/@eventual/testing/src/clients/workflow-client.ts index e470f6cbc..d4fbed975 100644 --- a/packages/@eventual/testing/src/clients/workflow-client.ts +++ b/packages/@eventual/testing/src/clients/workflow-client.ts @@ -1,5 +1,6 @@ import { ActivityRuntimeClient, + computeDurationDate, createEvent, Execution, ExecutionStatus, @@ -72,9 +73,11 @@ export class TestWorkflowClient extends WorkflowClient { }, workflowName, input: request.input, - timeoutTime: request.timeoutSeconds - ? new Date( - baseTime.getTime() + request.timeoutSeconds * 1000 + timeoutTime: request.timeout + ? computeDurationDate( + baseTime, + request.timeout.dur, + request.timeout.unit ).toISOString() : undefined, }, diff --git a/packages/@eventual/testing/src/clients/workflow-runtime-client.ts b/packages/@eventual/testing/src/clients/workflow-runtime-client.ts index 52cff5f3f..b2086c03f 100644 --- a/packages/@eventual/testing/src/clients/workflow-runtime-client.ts +++ b/packages/@eventual/testing/src/clients/workflow-runtime-client.ts @@ -7,12 +7,10 @@ import { FailedExecution, FailExecutionRequest, HistoryStateEvent, - ServiceType, UpdateHistoryRequest, WorkflowClient, WorkflowRuntimeClient, } from "@eventual/core"; -import { serviceTypeScope } from "../utils.js"; import { TimeConnector } from "../environment.js"; import { ExecutionStore } from "../execution-store.js"; @@ -77,13 +75,11 @@ export class TestWorkflowRuntimeClient extends WorkflowRuntimeClient { } public async startActivity(request: ActivityWorkerRequest): Promise { - return serviceTypeScope(ServiceType.ActivityWorker, () => - this.activityWorker( - request, - this.timeConnector.getTime(), - // end time is the start time plus one second - (start) => new Date(start.getTime() + 1000) - ) + return this.activityWorker( + request, + this.timeConnector.getTime(), + // end time is the start time plus one second + (start) => new Date(start.getTime() + 1000) ); } } diff --git a/packages/@eventual/testing/src/environment.ts b/packages/@eventual/testing/src/environment.ts index 3e4a1a18c..72ff1e339 100644 --- a/packages/@eventual/testing/src/environment.ts +++ b/packages/@eventual/testing/src/environment.ts @@ -43,7 +43,6 @@ import { TestActivityRuntimeClient } from "./clients/activity-runtime-client.js" import { TestTimerClient } from "./clients/timer-client.js"; import { TimeController } from "./time-controller.js"; import { ExecutionStore } from "./execution-store.js"; -import { serviceTypeScope } from "./utils.js"; import { MockableActivityProvider, MockActivity, @@ -436,9 +435,7 @@ export class TestEnvironment extends RuntimeServiceClient { throw new Error("Unknown event types in the TimerController."); } - await serviceTypeScope(ServiceType.OrchestratorWorker, () => - this.orchestrator(events, this.time) - ); + await this.orchestrator(events, this.time); } } diff --git a/packages/@eventual/testing/src/utils.ts b/packages/@eventual/testing/src/utils.ts deleted file mode 100644 index e3fc636c8..000000000 --- a/packages/@eventual/testing/src/utils.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ServiceType, SERVICE_TYPE_FLAG } from "@eventual/core"; - -export async function serviceTypeScope( - serviceType: ServiceType, - handler: () => Output -): Promise> { - const back = process.env[SERVICE_TYPE_FLAG]; - try { - process.env[SERVICE_TYPE_FLAG] = serviceType; - // await before return so that the promise is completed before the finally call. - return await handler(); - } finally { - process.env[SERVICE_TYPE_FLAG] = back; - } -} diff --git a/packages/@eventual/testing/test/workflow.ts b/packages/@eventual/testing/test/workflow.ts index b13efcdc5..c44a27843 100644 --- a/packages/@eventual/testing/test/workflow.ts +++ b/packages/@eventual/testing/test/workflow.ts @@ -159,12 +159,12 @@ export const actWithTimeout = activity( export const workflow2WithTimeouts = workflow( "wf2", - { timeoutSeconds: 50 }, + { timeout: duration(50, "seconds") }, async () => actWithTimeout() ); export const workflowWithTimeouts = workflow( "wf1", - { timeoutSeconds: 100 }, + { timeout: duration(100, "seconds") }, async () => { return Promise.allSettled([ actWithTimeout(), From 1b8a861d815cd7de4e62fcf050e76cad172160d6 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Thu, 12 Jan 2023 03:15:11 -0600 Subject: [PATCH 04/14] tests pass and no more timeoutSEconds --- apps/tests/aws-runtime/test/test-service.ts | 16 +- .../core/src/calls/condition-call.ts | 7 +- .../core/src/calls/expect-signal-call.ts | 7 +- packages/@eventual/core/src/command.ts | 6 +- packages/@eventual/core/src/condition.ts | 10 +- packages/@eventual/core/src/interpret.ts | 28 ++-- .../core/src/runtime/command-executor.ts | 38 +---- packages/@eventual/core/src/signals.ts | 42 +++++- .../@eventual/core/src/workflow-events.ts | 28 +--- packages/@eventual/core/test/command-util.ts | 34 +---- .../core/test/commend-executor.test.ts | 28 ---- .../@eventual/core/test/interpret.test.ts | 142 ++++++++++++------ packages/@eventual/testing/test/workflow.ts | 2 +- 13 files changed, 187 insertions(+), 201 deletions(-) diff --git a/apps/tests/aws-runtime/test/test-service.ts b/apps/tests/aws-runtime/test/test-service.ts index 41f187ec5..53cb95940 100644 --- a/apps/tests/aws-runtime/test/test-service.ts +++ b/apps/tests/aws-runtime/test/test-service.ts @@ -95,7 +95,7 @@ const doneSignal = signal("done"); export const parentWorkflow = workflow("parentWorkflow", async () => { const child = childWorkflow({ name: "child" }); while (true) { - const n = await mySignal.expectSignal({ timeoutSeconds: 10 }); + const n = await mySignal.expectSignal({ timeout: duration(10, "seconds") }); console.log(n); @@ -142,7 +142,9 @@ export const childWorkflow = workflow( while (!done) { sendSignal(parentId, mySignal, last + 1); block = true; - if (!(await condition({ timeoutSeconds: 10 }, () => !block))) { + if ( + !(await condition({ timeout: duration(10, "seconds") }, () => !block)) + ) { throw new Error("timed out!"); } } @@ -170,12 +172,14 @@ export const timedOutWorkflow = workflow( // chains to be able to run in parallel. const timedOutFunctions = { condition: async () => { - if (!(await condition({ timeoutSeconds: 2 }, () => false))) { + if ( + !(await condition({ timeout: duration(2, "seconds") }, () => false)) + ) { throw new Error("Timed Out!"); } }, signal: async () => { - await mySignal.expectSignal({ timeoutSeconds: 2 }); + await mySignal.expectSignal({ timeout: duration(2, "seconds") }); }, activity: slowActivity, workflow: () => slowWf(undefined), @@ -293,13 +297,13 @@ export const eventDrivenWorkflow = workflow( // wait for the event to come back around and wake this workflow const { value } = await expectSignal("start", { - timeoutSeconds: 30, + timeout: duration(30, "seconds"), }); await sendFinishEvent(ctx.execution.id); await expectSignal("finish", { - timeoutSeconds: 30, + timeout: duration(30, "seconds"), }); return value; diff --git a/packages/@eventual/core/src/calls/condition-call.ts b/packages/@eventual/core/src/calls/condition-call.ts index 5a8c9fe07..ea5a1aa72 100644 --- a/packages/@eventual/core/src/calls/condition-call.ts +++ b/packages/@eventual/core/src/calls/condition-call.ts @@ -1,6 +1,7 @@ import { ConditionPredicate } from "../condition.js"; import { createEventual, + Eventual, EventualBase, EventualKind, isEventualOfKind, @@ -16,17 +17,17 @@ export interface ConditionCall extends EventualBase | Failed> { seq?: number; predicate: ConditionPredicate; - timeoutSeconds?: number; + timeout?: Eventual; } export function createConditionCall( predicate: ConditionPredicate, - timeoutSeconds?: number + timeout?: Eventual ) { return registerEventual( createEventual(EventualKind.ConditionCall, { predicate, - timeoutSeconds, + timeout, }) ); } diff --git a/packages/@eventual/core/src/calls/expect-signal-call.ts b/packages/@eventual/core/src/calls/expect-signal-call.ts index f15a0a661..0f63d8239 100644 --- a/packages/@eventual/core/src/calls/expect-signal-call.ts +++ b/packages/@eventual/core/src/calls/expect-signal-call.ts @@ -3,6 +3,7 @@ import { EventualBase, isEventualOfKind, createEventual, + Eventual, } from "../eventual.js"; import { registerEventual } from "../global.js"; import { Failed, Resolved } from "../result.js"; @@ -15,16 +16,16 @@ export interface ExpectSignalCall extends EventualBase | Failed> { seq?: number; signalId: string; - timeoutSeconds?: number; + timeout?: Eventual; } export function createExpectSignalCall( signalId: string, - timeoutSeconds?: number + timeout?: Eventual ): ExpectSignalCall { return registerEventual( createEventual(EventualKind.ExpectSignalCall, { - timeoutSeconds, + timeout, signalId, }) ); diff --git a/packages/@eventual/core/src/command.ts b/packages/@eventual/core/src/command.ts index af5c4229f..b4d17317a 100644 --- a/packages/@eventual/core/src/command.ts +++ b/packages/@eventual/core/src/command.ts @@ -93,7 +93,6 @@ export function isAwaitDurationCommand( export interface ExpectSignalCommand extends CommandBase { signalId: string; - timeoutSeconds?: number; } export function isExpectSignalCommand( @@ -114,10 +113,7 @@ export function isSendSignalCommand( return command.kind === CommandType.SendSignal; } -export interface StartConditionCommand - extends CommandBase { - timeoutSeconds?: number; -} +export type StartConditionCommand = CommandBase; export function isStartConditionCommand( command: Command diff --git a/packages/@eventual/core/src/condition.ts b/packages/@eventual/core/src/condition.ts index c16159a83..c3d87831f 100644 --- a/packages/@eventual/core/src/condition.ts +++ b/packages/@eventual/core/src/condition.ts @@ -1,10 +1,11 @@ import { createConditionCall } from "./calls/condition-call.js"; +import { isEventual } from "./eventual.js"; import { isOrchestratorWorker } from "./runtime/flags.js"; export type ConditionPredicate = () => boolean; export interface ConditionOptions { - timeoutSeconds?: number; + timeout?: Promise; } /** @@ -55,5 +56,10 @@ export function condition( } const [opts, predicate] = args.length === 1 ? [undefined, args[0]] : args; - return createConditionCall(predicate, opts?.timeoutSeconds) as any; + const timeout = opts?.timeout; + if (timeout && !isEventual(timeout)) { + throw new Error("Timeout promise must be an Eventual."); + } + + return createConditionCall(predicate, timeout) as any; } diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index 77db1443b..9c49450e5 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -28,11 +28,9 @@ import { isAlarmCompleted, isAlarmScheduled, isExpectSignalStarted, - isExpectSignalTimedOut, ScheduledEvent, isSignalSent, isConditionStarted, - isConditionTimedOut, isWorkflowTimedOut, isActivityHeartbeatTimedOut, isEventsPublished, @@ -242,7 +240,6 @@ export function interpret( kind: CommandType.ExpectSignal, signalId: call.signalId, seq: call.seq!, - timeoutSeconds: call.timeoutSeconds, }; } else if (isSendSignalCall(call)) { return { @@ -256,7 +253,6 @@ export function interpret( return { kind: CommandType.StartCondition, seq: call.seq!, - timeoutSeconds: call.timeoutSeconds, }; } else if (isRegisterSignalHandlerCall(call)) { return []; @@ -455,6 +451,13 @@ export function interpret( */ function resolveResult(activity: Eventual & { result: undefined }) { if (isConditionCall(activity)) { + // first check the state of the condition's timeout + if (activity.timeout) { + const timeoutResult = tryResolveResult(activity.timeout); + if (isResolved(timeoutResult) || isFailed(timeoutResult)) { + return Result.resolved(false); + } + } // try to evaluate the condition's result. const predicateResult = activity.predicate(); if (isGenerator(predicateResult)) { @@ -466,11 +469,19 @@ export function interpret( } else if (predicateResult) { return Result.resolved(true); } - } else if (isActivityCall(activity)) { + } else if (isActivityCall(activity) || isExpectSignalCall(activity)) { if (activity.timeout) { const timeoutResult = tryResolveResult(activity.timeout); if (isResolved(timeoutResult) || isFailed(timeoutResult)) { - return Result.failed(new Timeout("Activity Timed Out")); + return Result.failed( + new Timeout( + isActivityCall(activity) + ? "Activity Timed Out" + : isExpectSignalCall(activity) + ? "Expect Signal Timed Out" + : assertNever(activity) + ) + ); } } return undefined; @@ -559,11 +570,6 @@ export function interpret( ? Result.resolved(event.result) : isAlarmCompleted(event) ? Result.resolved(undefined) - : isExpectSignalTimedOut(event) - ? Result.failed(new Timeout("Expect Signal Timed Out")) - : isConditionTimedOut(event) - ? // a timed out condition returns false - Result.resolved(false) : isActivityHeartbeatTimedOut(event) ? Result.failed(new HeartbeatTimeout("Activity Heartbeat TimedOut")) : Result.failed(new EventualError(event.error, event.message)); diff --git a/packages/@eventual/core/src/runtime/command-executor.ts b/packages/@eventual/core/src/runtime/command-executor.ts index e744f20e9..b33458d4d 100644 --- a/packages/@eventual/core/src/runtime/command-executor.ts +++ b/packages/@eventual/core/src/runtime/command-executor.ts @@ -25,10 +25,8 @@ import { AlarmScheduled, AlarmCompleted, ExpectSignalStarted, - ExpectSignalTimedOut, HistoryStateEvent, ConditionStarted, - ConditionTimedOut, SignalSent, EventsPublished, } from "../workflow-events.js"; @@ -75,11 +73,11 @@ export class CommandExecutor { return this.scheduleSleep(executionId, command, baseTime); } else if (isExpectSignalCommand(command)) { // should the timeout command be generic (ex: StartTimeout) or specific (ex: ExpectSignal)? - return this.executeExpectSignal(executionId, command, baseTime); + return this.executeExpectSignal(command, baseTime); } else if (isSendSignalCommand(command)) { return this.sendSignal(executionId, command, baseTime); } else if (isStartConditionCommand(command)) { - return this.startCondition(executionId, command, baseTime); + return this.startCondition(command, baseTime); } else if (isPublishEventsCommand(command)) { return this.publishEvents(command, baseTime); } else { @@ -170,29 +168,14 @@ export class CommandExecutor { } private async executeExpectSignal( - executionId: string, - command: ExpectSignalCommand, baseTime: Date ): Promise { - if (command.timeoutSeconds) { - await this.props.timerClient.scheduleEvent({ - event: { - signalId: command.signalId, - seq: command.seq, - type: WorkflowEventType.ExpectSignalTimedOut, - }, - schedule: Schedule.relative(command.timeoutSeconds, baseTime), - executionId, - }); - } - return createEvent( { signalId: command.signalId, seq: command.seq, type: WorkflowEventType.ExpectSignalStarted, - timeoutSeconds: command.timeoutSeconds, }, baseTime ); @@ -229,22 +212,7 @@ export class CommandExecutor { ); } - private async startCondition( - executionId: string, - command: StartConditionCommand, - baseTime: Date - ) { - if (command.timeoutSeconds) { - await this.props.timerClient.scheduleEvent({ - event: { - type: WorkflowEventType.ConditionTimedOut, - seq: command.seq, - }, - executionId, - schedule: Schedule.relative(command.timeoutSeconds, baseTime), - }); - } - + private async startCondition(command: StartConditionCommand, baseTime: Date) { return createEvent( { type: WorkflowEventType.ConditionStarted, diff --git a/packages/@eventual/core/src/signals.ts b/packages/@eventual/core/src/signals.ts index 7be877bc4..4850e6bc6 100644 --- a/packages/@eventual/core/src/signals.ts +++ b/packages/@eventual/core/src/signals.ts @@ -4,6 +4,7 @@ import { createExpectSignalCall } from "./calls/expect-signal-call.js"; import { isOrchestratorWorker } from "./runtime/flags.js"; import { getServiceClient } from "./global.js"; import { ulid } from "ulidx"; +import { isEventual } from "./eventual.js"; /** * A reference to a created signal handler. @@ -120,11 +121,24 @@ export type SignalPayload> = E extends Signal export interface ExpectSignalOptions { /** - * Optional. Seconds to wait for the signal to be received. + * Optional. A promise that determines when to timeout a signal. * - * After the provided seconds, the promise will reject. + * Can be used together with {@link time} or {@link duration} or any other promise. + * + * ```ts + * await expectSignal(signal, { timeout: duration(10, "seconds") }) + * ``` + * + * After the provided promise resolves or rejects, the {@link expectSignal} will reject. + * + * You can also chain an expect signal with other promises. + * + * ```ts + * const abortSignal = expectSignal(abortSignal); + * expectSignal(signal, { timeout: abortSignal }); + * ``` */ - timeoutSeconds: number; + timeout: Promise; } /** @@ -140,8 +154,19 @@ export interface ExpectSignalOptions { * }); * ``` * - * Use `opts.timeoutSeconds` to stop waiting after the provided time. The Promise will reject - * when the provided time has elapsed. + * Use `opts.timeout` to stop waiting after some condition. The Promise will reject + * when the provided promise resolves. + * + * ```ts + * // timeout after 10 seconds + * await expectSignal(signal, { timeout: duration(10, "seconds") }) + * ``` + * + * ```ts + * // timeout after receiving a signal + * const abortSignal = expectSignal(abortSignal); + * await expectSignal(signal, { timeout: abortSignal }); + * ``` */ export function expectSignal( signal: Signal | string, @@ -151,9 +176,14 @@ export function expectSignal( throw new Error("expectSignal is only valid in a workflow"); } + const timeout = opts?.timeout; + if (timeout && !isEventual(timeout)) { + throw new Error("Timeout promise must be an Eventual."); + } + return createExpectSignalCall( typeof signal === "string" ? signal : signal.id, - opts?.timeoutSeconds + timeout ) as any; } diff --git a/packages/@eventual/core/src/workflow-events.ts b/packages/@eventual/core/src/workflow-events.ts index 95e1cd5e6..dbc6f6b68 100644 --- a/packages/@eventual/core/src/workflow-events.ts +++ b/packages/@eventual/core/src/workflow-events.ts @@ -69,9 +69,7 @@ export type SucceededEvent = export type FailedEvent = | ActivityFailed | ActivityHeartbeatTimedOut - | ChildWorkflowFailed - | ConditionTimedOut - | ExpectSignalTimedOut; + | ChildWorkflowFailed; /** * Events used by the workflow to replay an execution. @@ -290,12 +288,6 @@ export const isWorkflowCompletedEvent = or( export interface ExpectSignalStarted extends HistoryEventBase { type: WorkflowEventType.ExpectSignalStarted; signalId: string; - timeoutSeconds?: number; -} - -export interface ExpectSignalTimedOut extends HistoryEventBase { - type: WorkflowEventType.ExpectSignalTimedOut; - signalId: string; } export interface SignalReceived extends BaseEvent { @@ -310,12 +302,6 @@ export function isExpectSignalStarted( return event.type === WorkflowEventType.ExpectSignalStarted; } -export function isExpectSignalTimedOut( - event: WorkflowEvent -): event is ExpectSignalTimedOut { - return event.type === WorkflowEventType.ExpectSignalTimedOut; -} - export function isSignalReceived( event: WorkflowEvent ): event is SignalReceived { @@ -354,16 +340,6 @@ export function isConditionStarted( return event.type === WorkflowEventType.ConditionStarted; } -export interface ConditionTimedOut extends HistoryEventBase { - type: WorkflowEventType.ConditionTimedOut; -} - -export function isConditionTimedOut( - event: WorkflowEvent -): event is ConditionTimedOut { - return event.type === WorkflowEventType.ConditionTimedOut; -} - export interface WorkflowTimedOut extends BaseEvent { type: WorkflowEventType.WorkflowTimedOut; } @@ -394,8 +370,6 @@ export const isFailedEvent = or( isActivityFailed, isActivityHeartbeatTimedOut, isChildWorkflowFailed, - isConditionTimedOut, - isExpectSignalTimedOut, isWorkflowTimedOut ); diff --git a/packages/@eventual/core/test/command-util.ts b/packages/@eventual/core/test/command-util.ts index 2ca561092..7d089ae95 100644 --- a/packages/@eventual/core/test/command-util.ts +++ b/packages/@eventual/core/test/command-util.ts @@ -19,10 +19,8 @@ import { ChildWorkflowFailed, ChildWorkflowScheduled, ConditionStarted, - ConditionTimedOut, EventsPublished, ExpectSignalStarted, - ExpectSignalTimedOut, SignalReceived, SignalSent, AlarmCompleted, @@ -86,14 +84,12 @@ export function createScheduledWorkflowCommand( export function createExpectSignalCommand( signalId: string, - seq: number, - timeoutSeconds?: number + seq: number ): ExpectSignalCommand { return { kind: CommandType.ExpectSignal, signalId, seq, - timeoutSeconds, }; } @@ -122,13 +118,11 @@ export function createPublishEventCommand( } export function createStartConditionCommand( - seq: number, - timeoutSeconds?: number + seq: number ): StartConditionCommand { return { kind: CommandType.StartCondition, seq, - timeoutSeconds, }; } @@ -235,29 +229,15 @@ export function completedAlarm(seq: number): AlarmCompleted { }; } -export function timedOutExpectSignal( - signalId: string, - seq: number -): ExpectSignalTimedOut { - return { - type: WorkflowEventType.ExpectSignalTimedOut, - timestamp: new Date().toISOString(), - seq, - signalId, - }; -} - export function startedExpectSignal( signalId: string, - seq: number, - timeoutSeconds?: number + seq: number ): ExpectSignalStarted { return { type: WorkflowEventType.ExpectSignalStarted, signalId, timestamp: new Date().toISOString(), seq, - timeoutSeconds, }; } @@ -298,14 +278,6 @@ export function conditionStarted(seq: number): ConditionStarted { }; } -export function conditionTimedOut(seq: number): ConditionTimedOut { - return { - type: WorkflowEventType.ConditionTimedOut, - timestamp: new Date().toISOString(), - seq, - }; -} - export function eventsPublished( events: EventEnvelope[], seq: number diff --git a/packages/@eventual/core/test/commend-executor.test.ts b/packages/@eventual/core/test/commend-executor.test.ts index eb74779e9..2e42c9fda 100644 --- a/packages/@eventual/core/test/commend-executor.test.ts +++ b/packages/@eventual/core/test/commend-executor.test.ts @@ -4,10 +4,8 @@ import { ActivityScheduled, ChildWorkflowScheduled, ConditionStarted, - ConditionTimedOut, EventsPublished, ExpectSignalStarted, - ExpectSignalTimedOut, SignalSent, AlarmCompleted, AlarmScheduled, @@ -223,29 +221,15 @@ describe("expect signal", () => { kind: CommandType.ExpectSignal, signalId: "signal", seq: 0, - timeoutSeconds: 100, }, baseTime ); - expect(mockTimerClient.scheduleEvent).toHaveBeenCalledWith< - [ScheduleEventRequest] - >({ - event: { - signalId: "signal", - seq: 0, - type: WorkflowEventType.ExpectSignalTimedOut, - }, - schedule: Schedule.relative(100, baseTime), - executionId, - }); - expect(event).toMatchObject({ seq: 0, timestamp: expect.stringContaining("Z"), type: WorkflowEventType.ExpectSignalStarted, signalId: "signal", - timeoutSeconds: 100, }); }); }); @@ -346,22 +330,10 @@ describe("condition", () => { { kind: CommandType.StartCondition, seq: 0, - timeoutSeconds: 100, }, baseTime ); - expect(mockTimerClient.scheduleEvent).toHaveBeenCalledWith< - [ScheduleEventRequest] - >({ - event: { - type: WorkflowEventType.ConditionTimedOut, - seq: 0, - }, - executionId, - schedule: Schedule.relative(100, baseTime), - }); - expect(event).toMatchObject({ seq: 0, type: WorkflowEventType.ConditionStarted, diff --git a/packages/@eventual/core/test/interpret.test.ts b/packages/@eventual/core/test/interpret.test.ts index 20f684b6b..95eb7a2a2 100644 --- a/packages/@eventual/core/test/interpret.test.ts +++ b/packages/@eventual/core/test/interpret.test.ts @@ -20,7 +20,10 @@ import { WorkflowHandler, WorkflowResult, } from "../src/index.js"; -import { createAwaitTimeCall } from "../src/calls/await-time-call.js"; +import { + createAwaitDurationCall, + createAwaitTimeCall, +} from "../src/calls/await-time-call.js"; import { activitySucceeded, activityFailed, @@ -28,7 +31,6 @@ import { activityScheduled, completedAlarm, conditionStarted, - conditionTimedOut, createExpectSignalCommand, createPublishEventCommand, createScheduledActivityCommand, @@ -42,7 +44,6 @@ import { signalReceived, signalSent, startedExpectSignal, - timedOutExpectSignal, workflowSucceeded, workflowFailed, workflowScheduled, @@ -212,9 +213,7 @@ test("immediately abort activity on invalid timeout", () => { expect( interpret(myWorkflow(event), [activityScheduled("my-activity", 0)]) ).toMatchObject({ - result: Result.failed( - new Timeout("Activity immediately timed out, timeout was not awaitable.") - ), + result: Result.failed(new Timeout("Activity Timed Out")), }); }); @@ -1507,7 +1506,10 @@ test("workflow calling other workflow", () => { describe("signals", () => { describe("expect signal", () => { const wf = workflow(function* (): any { - const result = yield createExpectSignalCall("MySignal", 100 * 1000); + const result = yield createExpectSignalCall( + "MySignal", + createAwaitDurationCall(100 * 1000, "seconds") + ); return result ?? "done"; }); @@ -1516,14 +1518,18 @@ describe("signals", () => { expect(interpret(wf.definition(undefined, context), [])).toMatchObject(< WorkflowResult >{ - commands: [createExpectSignalCommand("MySignal", 0, 100 * 1000)], + commands: [ + createAwaitDurationCommand(100 * 1000, "seconds", 0), + createExpectSignalCommand("MySignal", 1), + ], }); }); test("no signal", () => { expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), ]) ).toMatchObject({ commands: [], @@ -1533,7 +1539,8 @@ describe("signals", () => { test("match signal", () => { expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), signalReceived("MySignal"), ]) ).toMatchObject({ @@ -1545,7 +1552,8 @@ describe("signals", () => { test("match signal with payload", () => { expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), signalReceived("MySignal", { done: true }), ]) ).toMatchObject({ @@ -1557,8 +1565,9 @@ describe("signals", () => { test("timed out", () => { expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), - timedOutExpectSignal("MySignal", 0), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), + completedAlarm(0), ]) ).toMatchObject({ result: Result.failed(new Timeout("Expect Signal Timed Out")), @@ -1569,8 +1578,9 @@ describe("signals", () => { test("timed out then signal", () => { expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), - timedOutExpectSignal("MySignal", 0), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), + completedAlarm(0), signalReceived("MySignal", { done: true }), ]) ).toMatchObject({ @@ -1582,9 +1592,10 @@ describe("signals", () => { test("match signal then timeout", () => { expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), signalReceived("MySignal"), - timedOutExpectSignal("MySignal", 0), + completedAlarm(0), ]) ).toMatchObject({ result: Result.resolved("done"), @@ -1595,7 +1606,8 @@ describe("signals", () => { test("match signal twice", () => { expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), signalReceived("MySignal"), signalReceived("MySignal"), ]) @@ -1607,16 +1619,24 @@ describe("signals", () => { test("multiple of the same signal", () => { const wf = workflow(function* () { - const wait1 = createExpectSignalCall("MySignal", 100 * 1000); - const wait2 = createExpectSignalCall("MySignal", 100 * 1000); + const wait1 = createExpectSignalCall( + "MySignal", + createAwaitDurationCall(100 * 1000, "seconds") + ); + const wait2 = createExpectSignalCall( + "MySignal", + createAwaitDurationCall(100 * 1000, "seconds") + ); return Eventual.all([wait1, wait2]); }); expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), - startedExpectSignal("MySignal", 1, 100 * 1000), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), + scheduledAlarm("", 2), + startedExpectSignal("MySignal", 3), signalReceived("MySignal", "done!!!"), ]) ).toMatchObject({ @@ -1627,14 +1647,21 @@ describe("signals", () => { test("expect then timeout", () => { const wf = workflow(function* (): any { - yield createExpectSignalCall("MySignal", 100 * 1000); - yield createExpectSignalCall("MySignal", 100 * 1000); + yield createExpectSignalCall( + "MySignal", + createAwaitDurationCall(100 * 1000, "seconds") + ); + yield createExpectSignalCall( + "MySignal", + createAwaitDurationCall(100 * 1000, "seconds") + ); }); expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), - timedOutExpectSignal("MySignal", 0), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), + completedAlarm(0), ]) ).toMatchObject({ result: Result.failed({ name: "Timeout" }), @@ -1644,15 +1671,22 @@ describe("signals", () => { test("expect random signal then timeout", () => { const wf = workflow(function* (): any { - yield createExpectSignalCall("MySignal", 100 * 1000); - yield createExpectSignalCall("MySignal", 100 * 1000); + yield createExpectSignalCall( + "MySignal", + createAwaitDurationCall(100 * 1000, "seconds") + ); + yield createExpectSignalCall( + "MySignal", + createAwaitDurationCall(100 * 1000, "seconds") + ); }); expect( interpret(wf.definition(undefined, context), [ - startedExpectSignal("MySignal", 0, 100 * 1000), + scheduledAlarm("", 0), + startedExpectSignal("MySignal", 1), signalReceived("SomethingElse"), - timedOutExpectSignal("MySignal", 0), + completedAlarm(0), ]) ).toMatchObject({ result: Result.failed({ name: "Timeout" }), @@ -2030,23 +2064,35 @@ describe("condition", () => { test("false condition emits events with timeout", () => { const wf = workflow(function* (): any { - yield createConditionCall(() => false, 100); + yield createConditionCall( + () => false, + createAwaitDurationCall(100, "seconds") + ); }); expect( interpret(wf.definition(undefined, context), []) ).toMatchObject({ - commands: [createStartConditionCommand(0, 100)], + commands: [ + createAwaitDurationCommand(100, "seconds", 0), + createStartConditionCommand(1), + ], }); }); test("false condition does not re-emit", () => { const wf = workflow(function* (): any { - yield createConditionCall(() => false, 100); + yield createConditionCall( + () => false, + createAwaitDurationCall(100, "seconds") + ); }); expect( - interpret(wf.definition(undefined, context), [conditionStarted(0)]) + interpret(wf.definition(undefined, context), [ + scheduledAlarm("", 0), + conditionStarted(1), + ]) ).toMatchObject({ commands: [], }); @@ -2057,7 +2103,12 @@ describe("condition", () => { createRegisterSignalHandlerCall("Yes", () => { yes = true; }); - if (!(yield createConditionCall(() => yes) as any)) { + if ( + !(yield createConditionCall( + () => yes, + createAwaitDurationCall(100, "seconds") + ) as any) + ) { return "timed out"; } return "done"; @@ -2066,7 +2117,8 @@ describe("condition", () => { test("trigger success", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - conditionStarted(0), + scheduledAlarm("", 0), + conditionStarted(1), signalReceived("Yes"), ]) ).toMatchObject({ @@ -2078,7 +2130,8 @@ describe("condition", () => { test("trigger success eventually", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - conditionStarted(0), + scheduledAlarm("", 0), + conditionStarted(1), signalReceived("No"), signalReceived("No"), signalReceived("No"), @@ -2117,8 +2170,9 @@ describe("condition", () => { test("trigger timeout", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - conditionStarted(0), - conditionTimedOut(0), + scheduledAlarm("", 0), + conditionStarted(1), + completedAlarm(0), ]) ).toMatchObject({ result: Result.resolved("timed out"), @@ -2129,9 +2183,10 @@ describe("condition", () => { test("trigger success before timeout", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - conditionStarted(0), + scheduledAlarm("", 0), + conditionStarted(1), signalReceived("Yes"), - conditionTimedOut(0), + completedAlarm(0), ]) ).toMatchObject({ result: Result.resolved("done"), @@ -2142,8 +2197,9 @@ describe("condition", () => { test("trigger timeout before success", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - conditionStarted(0), - conditionTimedOut(0), + scheduledAlarm("", 0), + conditionStarted(1), + completedAlarm(0), signalReceived("Yes"), ]) ).toMatchObject({ diff --git a/packages/@eventual/testing/test/workflow.ts b/packages/@eventual/testing/test/workflow.ts index c44a27843..47b50b820 100644 --- a/packages/@eventual/testing/test/workflow.ts +++ b/packages/@eventual/testing/test/workflow.ts @@ -169,7 +169,7 @@ export const workflowWithTimeouts = workflow( return Promise.allSettled([ actWithTimeout(), workflow2WithTimeouts(undefined), - dataSignal.expectSignal({ timeoutSeconds: 30 }), + dataSignal.expectSignal({ timeout: duration(30, "seconds") }), ]); } ); From 228765e8fc884475bc1194d89fde31969cefb799 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Thu, 12 Jan 2023 03:56:50 -0600 Subject: [PATCH 05/14] signal handler doesn't generate commands --- packages/@eventual/core/src/eventual.ts | 10 +++------- packages/@eventual/core/src/interpret.ts | 15 ++++++++++----- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/@eventual/core/src/eventual.ts b/packages/@eventual/core/src/eventual.ts index d86099fd3..fea77126c 100644 --- a/packages/@eventual/core/src/eventual.ts +++ b/packages/@eventual/core/src/eventual.ts @@ -13,10 +13,7 @@ import { isExpectSignalCall, ExpectSignalCall, } from "./calls/expect-signal-call.js"; -import { - isRegisterSignalHandlerCall, - RegisterSignalHandlerCall, -} from "./calls/signal-handler-call.js"; +import { RegisterSignalHandlerCall } from "./calls/signal-handler-call.js"; import { isSendSignalCall, SendSignalCall } from "./calls/send-signal-call.js"; import { isWorkflowCall, WorkflowCall } from "./calls/workflow-call.js"; import { ConditionCall, isConditionCall } from "./calls/condition-call.js"; @@ -86,7 +83,8 @@ export type Eventual = | AwaitAny | Chain | CommandCall - | Race; + | Race + | RegisterSignalHandlerCall; /** * Calls which emit commands. @@ -95,7 +93,6 @@ export type CommandCall = | ActivityCall | ConditionCall | ExpectSignalCall - | RegisterSignalHandlerCall | PublishEventsCall | SendSignalCall | AwaitDurationCall @@ -108,7 +105,6 @@ export function isCommandCall(call: Eventual): call is CommandCall { isConditionCall(call) || isExpectSignalCall(call) || isPublishEventsCall(call) || - isRegisterSignalHandlerCall(call) || isSendSignalCall(call) || isAwaitDurationCall(call) || isAwaitTimeCall(call) || diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index 9c49450e5..c62011a53 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -297,10 +297,6 @@ export function interpret( if (result) { return activity; } - } else if (isRegisterSignalHandlerCall(activity)) { - subscribeToSignal(activity.signalId, activity); - // signal handler does not emit a call/command. It is only internal. - return activity; } activity.seq = nextSeq(); callTable[activity.seq!] = activity; @@ -317,7 +313,12 @@ export function interpret( isRace(activity) ) { return activity; + } else if (isRegisterSignalHandlerCall(activity)) { + subscribeToSignal(activity.signalId, activity); + // signal handler does not emit a call/command. It is only internal. + return activity; } + return assertNever(activity); }, }; @@ -485,7 +486,11 @@ export function interpret( } } return undefined; - } else if (isChain(activity) || isCommandCall(activity)) { + } else if ( + isChain(activity) || + isCommandCall(activity) || + isRegisterSignalHandlerCall(activity) + ) { // chain and most commands will be resolved elsewhere (ex: commitCompletionEvent or commitSignal) return undefined; } else if (isAwaitAll(activity)) { From dd77318d467e2e2f5a10bc42b7db2ae5b1d54997 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Thu, 12 Jan 2023 08:34:27 -0600 Subject: [PATCH 06/14] remove unessecary events and commands --- packages/@eventual/core/src/command.ts | 29 +------ packages/@eventual/core/src/eventual.ts | 17 ++-- packages/@eventual/core/src/interpret.ts | 30 +------ .../core/src/runtime/command-executor.ts | 35 -------- .../@eventual/core/src/workflow-events.ts | 29 ------- packages/@eventual/core/test/command-util.ts | 44 ---------- .../core/test/commend-executor.test.ts | 86 ------------------- .../@eventual/core/test/interpret.test.ts | 46 ++-------- 8 files changed, 20 insertions(+), 296 deletions(-) diff --git a/packages/@eventual/core/src/command.ts b/packages/@eventual/core/src/command.ts index b4d17317a..b6547f10b 100644 --- a/packages/@eventual/core/src/command.ts +++ b/packages/@eventual/core/src/command.ts @@ -4,14 +4,12 @@ import { SignalTarget } from "./signals.js"; import { WorkflowOptions } from "./workflow.js"; export type Command = - | ExpectSignalCommand + | AwaitDurationCommand + | AwaitTimeCommand | ScheduleActivityCommand | ScheduleWorkflowCommand | PublishEventsCommand - | SendSignalCommand - | AwaitDurationCommand - | AwaitTimeCommand - | StartConditionCommand; + | SendSignalCommand; interface CommandBase { kind: T; @@ -21,11 +19,9 @@ interface CommandBase { export enum CommandType { AwaitDuration = "AwaitDuration", AwaitTime = "AwaitTime", - ExpectSignal = "ExpectSignal", PublishEvents = "PublishEvents", SendSignal = "SendSignal", StartActivity = "StartActivity", - StartCondition = "StartCondition", StartWorkflow = "StartWorkflow", } @@ -90,17 +86,6 @@ export function isAwaitDurationCommand( return command.kind === CommandType.AwaitDuration; } -export interface ExpectSignalCommand - extends CommandBase { - signalId: string; -} - -export function isExpectSignalCommand( - command: Command -): command is ExpectSignalCommand { - return command.kind === CommandType.ExpectSignal; -} - export interface SendSignalCommand extends CommandBase { signalId: string; target: SignalTarget; @@ -113,14 +98,6 @@ export function isSendSignalCommand( return command.kind === CommandType.SendSignal; } -export type StartConditionCommand = CommandBase; - -export function isStartConditionCommand( - command: Command -): command is StartConditionCommand { - return command.kind === CommandType.StartCondition; -} - export interface PublishEventsCommand extends CommandBase { events: EventEnvelope[]; diff --git a/packages/@eventual/core/src/eventual.ts b/packages/@eventual/core/src/eventual.ts index fea77126c..69aafe491 100644 --- a/packages/@eventual/core/src/eventual.ts +++ b/packages/@eventual/core/src/eventual.ts @@ -9,14 +9,10 @@ import { AwaitDurationCall, AwaitTimeCall, } from "./calls/await-time-call.js"; -import { - isExpectSignalCall, - ExpectSignalCall, -} from "./calls/expect-signal-call.js"; import { RegisterSignalHandlerCall } from "./calls/signal-handler-call.js"; import { isSendSignalCall, SendSignalCall } from "./calls/send-signal-call.js"; import { isWorkflowCall, WorkflowCall } from "./calls/workflow-call.js"; -import { ConditionCall, isConditionCall } from "./calls/condition-call.js"; +import { ConditionCall } from "./calls/condition-call.js"; import { isOrchestratorWorker } from "./runtime/flags.js"; import { AwaitAny, createAwaitAny } from "./await-any.js"; import { AwaitAllSettled, createAwaitAllSettled } from "./await-all-settled.js"; @@ -25,6 +21,7 @@ import { isPublishEventsCall, PublishEventsCall, } from "./calls/send-events-call.js"; +import { ExpectSignalCall } from "./calls/expect-signal-call.js"; export type AwaitedEventual = T extends Promise ? Awaited @@ -83,6 +80,8 @@ export type Eventual = | AwaitAny | Chain | CommandCall + | ConditionCall + | ExpectSignalCall | Race | RegisterSignalHandlerCall; @@ -91,19 +90,15 @@ export type Eventual = */ export type CommandCall = | ActivityCall - | ConditionCall - | ExpectSignalCall - | PublishEventsCall - | SendSignalCall | AwaitDurationCall | AwaitTimeCall + | PublishEventsCall + | SendSignalCall | WorkflowCall; export function isCommandCall(call: Eventual): call is CommandCall { return ( isActivityCall(call) || - isConditionCall(call) || - isExpectSignalCall(call) || isPublishEventsCall(call) || isSendSignalCall(call) || isAwaitDurationCall(call) || diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index c62011a53..fbeb7897c 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -27,10 +27,8 @@ import { isScheduledEvent, isAlarmCompleted, isAlarmScheduled, - isExpectSignalStarted, ScheduledEvent, isSignalSent, - isConditionStarted, isWorkflowTimedOut, isActivityHeartbeatTimedOut, isEventsPublished, @@ -235,12 +233,6 @@ export function interpret( name: call.name, opts: call.opts, }; - } else if (isExpectSignalCall(call)) { - return { - kind: CommandType.ExpectSignal, - signalId: call.signalId, - seq: call.seq!, - }; } else if (isSendSignalCall(call)) { return { kind: CommandType.SendSignal, @@ -249,11 +241,6 @@ export function interpret( seq: call.seq!, payload: call.payload, }; - } else if (isConditionCall(call)) { - return { - kind: CommandType.StartCondition, - seq: call.seq!, - }; } else if (isRegisterSignalHandlerCall(call)) { return []; } else if (isPublishEventsCall(call)) { @@ -289,15 +276,6 @@ export function interpret( */ pushEventual(activity) { if (isCommandCall(activity)) { - if (isExpectSignalCall(activity)) { - subscribeToSignal(activity.signalId, activity); - } else if (isConditionCall(activity)) { - // if the condition is resolvable, don't add it to the calls. - const result = tryResolveResult(activity); - if (result) { - return activity; - } - } activity.seq = nextSeq(); callTable[activity.seq!] = activity; calls.push(activity); @@ -310,6 +288,7 @@ export function interpret( isAwaitAll(activity) || isAwaitAllSettled(activity) || isAwaitAny(activity) || + isConditionCall(activity) || isRace(activity) ) { return activity; @@ -317,6 +296,9 @@ export function interpret( subscribeToSignal(activity.signalId, activity); // signal handler does not emit a call/command. It is only internal. return activity; + } else if (isExpectSignalCall(activity)) { + subscribeToSignal(activity.signalId, activity); + return activity; } return assertNever(activity); @@ -590,12 +572,8 @@ function isCorresponding(event: ScheduledEvent, call: CommandCall) { return isWorkflowCall(call) && call.name === event.name; } else if (isAlarmScheduled(event)) { return isAwaitTimeCall(call) || isAwaitDurationCall(call); - } else if (isExpectSignalStarted(event)) { - return isExpectSignalCall(call) && event.signalId === call.signalId; } else if (isSignalSent(event)) { return isSendSignalCall(call) && event.signalId === call.signalId; - } else if (isConditionStarted(event)) { - return isConditionCall(call); } else if (isEventsPublished(event)) { return isPublishEventsCall(call); } diff --git a/packages/@eventual/core/src/runtime/command-executor.ts b/packages/@eventual/core/src/runtime/command-executor.ts index b33458d4d..67fb2b33d 100644 --- a/packages/@eventual/core/src/runtime/command-executor.ts +++ b/packages/@eventual/core/src/runtime/command-executor.ts @@ -1,21 +1,17 @@ import { Command, - ExpectSignalCommand, - isExpectSignalCommand, isPublishEventsCommand, isScheduleActivityCommand, isScheduleWorkflowCommand, isSendSignalCommand, isAwaitDurationCommand, isAwaitTimeCommand, - isStartConditionCommand, PublishEventsCommand, ScheduleActivityCommand, ScheduleWorkflowCommand, SendSignalCommand, AwaitDurationCommand, AwaitTimeCommand, - StartConditionCommand, } from "../command.js"; import { WorkflowEventType, @@ -24,9 +20,7 @@ import { ChildWorkflowScheduled, AlarmScheduled, AlarmCompleted, - ExpectSignalStarted, HistoryStateEvent, - ConditionStarted, SignalSent, EventsPublished, } from "../workflow-events.js"; @@ -71,13 +65,8 @@ export class CommandExecutor { } else if (isAwaitDurationCommand(command) || isAwaitTimeCommand(command)) { // all sleep times are computed using the start time of the WorkflowTaskStarted return this.scheduleSleep(executionId, command, baseTime); - } else if (isExpectSignalCommand(command)) { - // should the timeout command be generic (ex: StartTimeout) or specific (ex: ExpectSignal)? - return this.executeExpectSignal(command, baseTime); } else if (isSendSignalCommand(command)) { return this.sendSignal(executionId, command, baseTime); - } else if (isStartConditionCommand(command)) { - return this.startCondition(command, baseTime); } else if (isPublishEventsCommand(command)) { return this.publishEvents(command, baseTime); } else { @@ -167,20 +156,6 @@ export class CommandExecutor { ); } - private async executeExpectSignal( - command: ExpectSignalCommand, - baseTime: Date - ): Promise { - return createEvent( - { - signalId: command.signalId, - seq: command.seq, - type: WorkflowEventType.ExpectSignalStarted, - }, - baseTime - ); - } - private async sendSignal( executionId: string, command: SendSignalCommand, @@ -212,16 +187,6 @@ export class CommandExecutor { ); } - private async startCondition(command: StartConditionCommand, baseTime: Date) { - return createEvent( - { - type: WorkflowEventType.ConditionStarted, - seq: command.seq!, - }, - baseTime - ); - } - private async publishEvents(command: PublishEventsCommand, baseTime: Date) { await this.props.eventClient.publishEvents(...command.events); return createEvent( diff --git a/packages/@eventual/core/src/workflow-events.ts b/packages/@eventual/core/src/workflow-events.ts index dbc6f6b68..35f17f146 100644 --- a/packages/@eventual/core/src/workflow-events.ts +++ b/packages/@eventual/core/src/workflow-events.ts @@ -26,11 +26,7 @@ export enum WorkflowEventType { ChildWorkflowSucceeded = "ChildWorkflowSucceeded", ChildWorkflowFailed = "ChildWorkflowFailed", ChildWorkflowScheduled = "ChildWorkflowScheduled", - ConditionStarted = "ConditionStarted", - ConditionTimedOut = "ConditionTimedOut", EventsPublished = "EventsPublished", - ExpectSignalStarted = "ExpectSignalStarted", - ExpectSignalTimedOut = "ExpectSignalTimedOut", SignalReceived = "SignalReceived", SignalSent = "SignalSent", WorkflowSucceeded = "WorkflowSucceeded", @@ -56,9 +52,7 @@ export type ScheduledEvent = | ActivityScheduled | AlarmScheduled | ChildWorkflowScheduled - | ConditionStarted | EventsPublished - | ExpectSignalStarted | SignalSent; export type SucceededEvent = @@ -285,23 +279,12 @@ export const isWorkflowCompletedEvent = or( isWorkflowSucceeded ); -export interface ExpectSignalStarted extends HistoryEventBase { - type: WorkflowEventType.ExpectSignalStarted; - signalId: string; -} - export interface SignalReceived extends BaseEvent { type: WorkflowEventType.SignalReceived; signalId: string; payload?: Payload; } -export function isExpectSignalStarted( - event: WorkflowEvent -): event is ExpectSignalStarted { - return event.type === WorkflowEventType.ExpectSignalStarted; -} - export function isSignalReceived( event: WorkflowEvent ): event is SignalReceived { @@ -330,16 +313,6 @@ export function isEventsPublished( return event.type === WorkflowEventType.EventsPublished; } -export interface ConditionStarted extends HistoryEventBase { - type: WorkflowEventType.ConditionStarted; -} - -export function isConditionStarted( - event: WorkflowEvent -): event is ConditionStarted { - return event.type === WorkflowEventType.ConditionStarted; -} - export interface WorkflowTimedOut extends BaseEvent { type: WorkflowEventType.WorkflowTimedOut; } @@ -353,9 +326,7 @@ export function isWorkflowTimedOut( export const isScheduledEvent = or( isActivityScheduled, isChildWorkflowScheduled, - isConditionStarted, isEventsPublished, - isExpectSignalStarted, isSignalSent, isAlarmScheduled ); diff --git a/packages/@eventual/core/test/command-util.ts b/packages/@eventual/core/test/command-util.ts index 7d089ae95..4fd8ce4f8 100644 --- a/packages/@eventual/core/test/command-util.ts +++ b/packages/@eventual/core/test/command-util.ts @@ -3,12 +3,10 @@ import { AwaitDurationCommand, AwaitTimeCommand, CommandType, - ExpectSignalCommand, PublishEventsCommand, ScheduleActivityCommand, ScheduleWorkflowCommand, SendSignalCommand, - StartConditionCommand, } from "../src/command.js"; import { EventEnvelope } from "../src/event.js"; import { @@ -18,9 +16,7 @@ import { ChildWorkflowSucceeded, ChildWorkflowFailed, ChildWorkflowScheduled, - ConditionStarted, EventsPublished, - ExpectSignalStarted, SignalReceived, SignalSent, AlarmCompleted, @@ -82,17 +78,6 @@ export function createScheduledWorkflowCommand( }; } -export function createExpectSignalCommand( - signalId: string, - seq: number -): ExpectSignalCommand { - return { - kind: CommandType.ExpectSignal, - signalId, - seq, - }; -} - export function createSendSignalCommand( target: SignalTarget, signalId: string, @@ -117,15 +102,6 @@ export function createPublishEventCommand( }; } -export function createStartConditionCommand( - seq: number -): StartConditionCommand { - return { - kind: CommandType.StartCondition, - seq, - }; -} - export function activitySucceeded(result: any, seq: number): ActivitySucceeded { return { type: WorkflowEventType.ActivitySucceeded, @@ -229,18 +205,6 @@ export function completedAlarm(seq: number): AlarmCompleted { }; } -export function startedExpectSignal( - signalId: string, - seq: number -): ExpectSignalStarted { - return { - type: WorkflowEventType.ExpectSignalStarted, - signalId, - timestamp: new Date().toISOString(), - seq, - }; -} - export function signalReceived( signalId: string, payload?: any @@ -270,14 +234,6 @@ export function signalSent( }; } -export function conditionStarted(seq: number): ConditionStarted { - return { - type: WorkflowEventType.ConditionStarted, - seq, - timestamp: new Date().toISOString(), - }; -} - export function eventsPublished( events: EventEnvelope[], seq: number diff --git a/packages/@eventual/core/test/commend-executor.test.ts b/packages/@eventual/core/test/commend-executor.test.ts index 2e42c9fda..fb6bbbdd4 100644 --- a/packages/@eventual/core/test/commend-executor.test.ts +++ b/packages/@eventual/core/test/commend-executor.test.ts @@ -3,9 +3,7 @@ import { CommandType } from "../src/command.js"; import { ActivityScheduled, ChildWorkflowScheduled, - ConditionStarted, EventsPublished, - ExpectSignalStarted, SignalSent, AlarmCompleted, AlarmScheduled, @@ -190,50 +188,6 @@ describe("workflow", () => { }); }); -describe("expect signal", () => { - test("start", async () => { - const event = await testExecutor.executeCommand( - workflow, - executionId, - { - kind: CommandType.ExpectSignal, - signalId: "signal", - seq: 0, - }, - baseTime - ); - - expect(mockTimerClient.scheduleEvent).not.toHaveBeenCalled(); - - expect(event).toMatchObject({ - seq: 0, - timestamp: expect.stringContaining("Z"), - type: WorkflowEventType.ExpectSignalStarted, - signalId: "signal", - }); - }); - - test("start", async () => { - const event = await testExecutor.executeCommand( - workflow, - executionId, - { - kind: CommandType.ExpectSignal, - signalId: "signal", - seq: 0, - }, - baseTime - ); - - expect(event).toMatchObject({ - seq: 0, - timestamp: expect.stringContaining("Z"), - type: WorkflowEventType.ExpectSignalStarted, - signalId: "signal", - }); - }); -}); - describe("send signal", () => { test("send", async () => { const event = await testExecutor.executeCommand( @@ -302,46 +256,6 @@ describe("send signal", () => { }); }); -describe("condition", () => { - test("send", async () => { - const event = await testExecutor.executeCommand( - workflow, - executionId, - { - kind: CommandType.StartCondition, - seq: 0, - }, - baseTime - ); - - expect(mockTimerClient.scheduleEvent).not.toHaveBeenCalled(); - - expect(event).toMatchObject({ - seq: 0, - type: WorkflowEventType.ConditionStarted, - timestamp: expect.stringContaining("Z"), - }); - }); - - test("send with timeout", async () => { - const event = await testExecutor.executeCommand( - workflow, - executionId, - { - kind: CommandType.StartCondition, - seq: 0, - }, - baseTime - ); - - expect(event).toMatchObject({ - seq: 0, - type: WorkflowEventType.ConditionStarted, - timestamp: expect.stringContaining("Z"), - }); - }); -}); - describe("public events", () => { test("send", async () => { const event = await testExecutor.executeCommand( diff --git a/packages/@eventual/core/test/interpret.test.ts b/packages/@eventual/core/test/interpret.test.ts index 95eb7a2a2..e02717a30 100644 --- a/packages/@eventual/core/test/interpret.test.ts +++ b/packages/@eventual/core/test/interpret.test.ts @@ -30,20 +30,16 @@ import { activityHeartbeatTimedOut, activityScheduled, completedAlarm, - conditionStarted, - createExpectSignalCommand, createPublishEventCommand, createScheduledActivityCommand, createScheduledWorkflowCommand, createSendSignalCommand, createAwaitDurationCommand, createAwaitTimeCommand, - createStartConditionCommand, eventsPublished, scheduledAlarm, signalReceived, signalSent, - startedExpectSignal, workflowSucceeded, workflowFailed, workflowScheduled, @@ -1518,19 +1514,13 @@ describe("signals", () => { expect(interpret(wf.definition(undefined, context), [])).toMatchObject(< WorkflowResult >{ - commands: [ - createAwaitDurationCommand(100 * 1000, "seconds", 0), - createExpectSignalCommand("MySignal", 1), - ], + commands: [createAwaitDurationCommand(100 * 1000, "seconds", 0)], }); }); test("no signal", () => { expect( - interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), - ]) + interpret(wf.definition(undefined, context), [scheduledAlarm("", 0)]) ).toMatchObject({ commands: [], }); @@ -1540,7 +1530,6 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), signalReceived("MySignal"), ]) ).toMatchObject({ @@ -1553,7 +1542,6 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), signalReceived("MySignal", { done: true }), ]) ).toMatchObject({ @@ -1566,7 +1554,6 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), completedAlarm(0), ]) ).toMatchObject({ @@ -1579,7 +1566,6 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), completedAlarm(0), signalReceived("MySignal", { done: true }), ]) @@ -1593,7 +1579,6 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), signalReceived("MySignal"), completedAlarm(0), ]) @@ -1607,7 +1592,6 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), signalReceived("MySignal"), signalReceived("MySignal"), ]) @@ -1634,9 +1618,7 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), - scheduledAlarm("", 2), - startedExpectSignal("MySignal", 3), + scheduledAlarm("", 1), signalReceived("MySignal", "done!!!"), ]) ).toMatchObject({ @@ -1660,7 +1642,6 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), completedAlarm(0), ]) ).toMatchObject({ @@ -1684,7 +1665,6 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ scheduledAlarm("", 0), - startedExpectSignal("MySignal", 1), signalReceived("SomethingElse"), completedAlarm(0), ]) @@ -2058,7 +2038,7 @@ describe("condition", () => { expect( interpret(wf.definition(undefined, context), []) ).toMatchObject({ - commands: [createStartConditionCommand(0)], + commands: [], }); }); @@ -2073,10 +2053,7 @@ describe("condition", () => { expect( interpret(wf.definition(undefined, context), []) ).toMatchObject({ - commands: [ - createAwaitDurationCommand(100, "seconds", 0), - createStartConditionCommand(1), - ], + commands: [createAwaitDurationCommand(100, "seconds", 0)], }); }); @@ -2089,10 +2066,7 @@ describe("condition", () => { }); expect( - interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), - conditionStarted(1), - ]) + interpret(wf.definition(undefined, context), [scheduledAlarm("", 0)]) ).toMatchObject({ commands: [], }); @@ -2118,7 +2092,6 @@ describe("condition", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ scheduledAlarm("", 0), - conditionStarted(1), signalReceived("Yes"), ]) ).toMatchObject({ @@ -2131,7 +2104,6 @@ describe("condition", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ scheduledAlarm("", 0), - conditionStarted(1), signalReceived("No"), signalReceived("No"), signalReceived("No"), @@ -2159,7 +2131,6 @@ describe("condition", () => { expect( interpret(signalConditionOnAndOffFlow.definition(undefined, context), [ - conditionStarted(0), signalReceived("Yes"), ]) ).toMatchObject({ @@ -2171,7 +2142,6 @@ describe("condition", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ scheduledAlarm("", 0), - conditionStarted(1), completedAlarm(0), ]) ).toMatchObject({ @@ -2184,7 +2154,6 @@ describe("condition", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ scheduledAlarm("", 0), - conditionStarted(1), signalReceived("Yes"), completedAlarm(0), ]) @@ -2198,7 +2167,6 @@ describe("condition", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ scheduledAlarm("", 0), - conditionStarted(1), completedAlarm(0), signalReceived("Yes"), ]) @@ -2217,7 +2185,7 @@ describe("condition", () => { expect( interpret(wf.definition(undefined, context), []) ).toMatchObject({ - commands: [createStartConditionCommand(0)], + commands: [], }); }); }); From ef1b839a264652c462432cf72280eb20a76eeb65 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Thu, 12 Jan 2023 13:04:41 -0600 Subject: [PATCH 07/14] feedback --- packages/@eventual/core/src/await-time.ts | 59 +++++++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/packages/@eventual/core/src/await-time.ts b/packages/@eventual/core/src/await-time.ts index e841dfe23..a9270dded 100644 --- a/packages/@eventual/core/src/await-time.ts +++ b/packages/@eventual/core/src/await-time.ts @@ -31,12 +31,48 @@ export interface DurationSpec { } /** + * Represents a time duration. + * + * Within a workflow, awaiting a duration can be used to resume in relative period of time. + * * ```ts - * workflow(async () => { + * workflow("myWorkflow", async () => { * await duration(10, "minutes"); // sleep for 10 minutes * return "DONE!"; * }) * ``` + * + * It behaves like any other promises, able to be aggregated with other promises. + * + * ```ts + * workflow("myWorkflow", async () => { + * const minTime = duration(10, "minutes"); // sleep for 10 minutes + * // wait for 10 minutes OR the duration of myActivity, whichever is longer. + * await Promise.all([minTime, await myActivity()]); + * return "DONE"; + * }) + * ``` + * + * A `duration` can be used to configure relative timeouts within a workflow or outside of it. + * + * ```ts + * // workflow that will timeout after an hour + * workflow("myWorkflow", { timeout: duration(1, "hour") }, async () => { + * // if the signal is not received within 30 minutes, the line will throw a Timeout error. + * await expectSignal("mySignal", { timeout: duration(30, "minutes"); }); + * }); + * ``` + * + * Durations are computing using a simple computation of the number of standard milliseconds in a + * period of time, not relative to the point in time, added to the milliseconds of the current execution time. + * + * duration(dur, unit): + * + * second(s) - dur * 1000 + * minute(s) - dur * 1000 * 60 + * hour(s) - dur * 1000 * 60 * 60 + * day(s) - dur * 1000 * 60 * 60 * 24 + * year(s) - dur * 1000 * 60 * 60 * 24 * 365.25 */ export function duration( dur: number, @@ -46,17 +82,32 @@ export function duration( return { dur, unit } as Promise & DurationSpec; } - // register a sleep command and return it (to be yielded) + // register an await duration command and return it (to be yielded) return createAwaitDurationCall(dur, unit) as any; } /** + * Represents a point in time. + * + * Awaiting a duration can be used to resume at a point in time. + * * ```ts - * workflow(async () => { + * workflow("myWorkflow", async () => { * await time("2024-01-03T12:00:00Z"); // wait until this date * return "DONE!"; * }) * ``` + * + * It behaves like any other promises, able to be aggregated with other promises. + * + * ```ts + * workflow("myWorkflow", async ({ endTime }) => { + * const goalTime = time(endTime); // sleep for 10 minutes + * // wait until the given time or until the activity is completed. + * await Promise.race([goalTime, await myActivity()]); + * return "DONE"; + * }) + * ``` */ export function time(isoDate: string): Promise & TimeSpec; export function time(date: Date): Promise & TimeSpec; @@ -68,6 +119,6 @@ export function time(date: Date | string): Promise & TimeSpec { return { isoDate: iso } as Promise & TimeSpec; } - // register a sleep command and return it (to be yielded) + // register an await time command and return it (to be yielded) return createAwaitTimeCall(iso) as any; } From 645c7d1f1cb6aefa88bb48b0582c9c7e51aab557 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Fri, 13 Jan 2023 11:47:46 -0600 Subject: [PATCH 08/14] fixed heartbeat computation bug --- packages/@eventual/core/src/activity.ts | 2 +- .../@eventual/core/src/runtime/handlers/orchestrator.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/@eventual/core/src/activity.ts b/packages/@eventual/core/src/activity.ts index 530b737f5..1766e34bc 100644 --- a/packages/@eventual/core/src/activity.ts +++ b/packages/@eventual/core/src/activity.ts @@ -235,7 +235,7 @@ export function activity( ? computeDurationSeconds( opts.heartbeatTimeout.dur, opts.heartbeatTimeout.unit - ) / 1000 + ) : undefined ) as any; } else { diff --git a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts index 1f065b5db..f4caa2be4 100644 --- a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts +++ b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts @@ -372,12 +372,16 @@ export function createOrchestrator({ logAgent.logWithContext( executionLogContext, LogLevel.DEBUG, - "Workflow terminated with: " + JSON.stringify(result) + result + ? "Workflow returned a result with: " + JSON.stringify(result) + : "Workflow did not return a result." ); logAgent.logWithContext( executionLogContext, LogLevel.DEBUG, - `Found ${newCommands.length} new commands.` + `Found ${newCommands.length} new commands. ${JSON.stringify( + newCommands + )}` ); yield* await timed( From 670354db0daf4fb44c21c203d1c80e61f49eaae0 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Fri, 13 Jan 2023 12:07:55 -0600 Subject: [PATCH 09/14] update more references --- packages/@eventual/compiler/test-files/workflow.ts | 2 +- .../compiler/test/__snapshots__/esbuild-plugin.test.ts.snap | 2 +- packages/@eventual/core/src/condition.ts | 2 +- packages/@eventual/core/src/error.ts | 2 +- packages/@eventual/core/src/heartbeat.ts | 2 +- packages/@eventual/core/src/signals.ts | 4 ++-- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/@eventual/compiler/test-files/workflow.ts b/packages/@eventual/compiler/test-files/workflow.ts index 2d70b2ad0..f882e963d 100644 --- a/packages/@eventual/compiler/test-files/workflow.ts +++ b/packages/@eventual/compiler/test-files/workflow.ts @@ -59,7 +59,7 @@ export default workflow("workflow", async (input) => { export const workflow2 = workflow( "timeoutFlow", - { timeoutSeconds: 100 }, + { timeout: duration(100, "seconds") }, async () => { await doWork("something"); } diff --git a/packages/@eventual/compiler/test/__snapshots__/esbuild-plugin.test.ts.snap b/packages/@eventual/compiler/test/__snapshots__/esbuild-plugin.test.ts.snap index 07ac1d673..7188a34e5 100644 --- a/packages/@eventual/compiler/test/__snapshots__/esbuild-plugin.test.ts.snap +++ b/packages/@eventual/compiler/test/__snapshots__/esbuild-plugin.test.ts.snap @@ -111,7 +111,7 @@ exports[`esbuild-plugin ts workflow 1`] = ` yield func2(); }); var workflow2 = workflow("timeoutFlow", { - timeoutSeconds: 100 + timeout: duration(100, "seconds") }, function* () { yield doWork("something"); }); diff --git a/packages/@eventual/core/src/condition.ts b/packages/@eventual/core/src/condition.ts index c3d87831f..94d1db98f 100644 --- a/packages/@eventual/core/src/condition.ts +++ b/packages/@eventual/core/src/condition.ts @@ -33,7 +33,7 @@ export interface ConditionOptions { * onSignal("mySignal", () => { n++ }); * * // after 5 mySignals, this promise will be resolved. - * if(!(await condition({ timeoutSeconds: 5 * 60 }, () => n === 5))) { + * if(!(await condition({ timeout: duration(5, "minutes") }, () => n === 5))) { * return "did not get 5 in 5 minutes." * } * diff --git a/packages/@eventual/core/src/error.ts b/packages/@eventual/core/src/error.ts index 749305e63..f32820d71 100644 --- a/packages/@eventual/core/src/error.ts +++ b/packages/@eventual/core/src/error.ts @@ -23,7 +23,7 @@ export class DeterminismError extends EventualError { * Thrown from within a workflow when any set timeout expires. * * ```ts - * const myAct = new activity("myAct", {timeoutSeconds: 100}, async () => { ... }); + * const myAct = new activity("myAct", {timeout: duration(100, "seconds") }, async () => { ... }); * workflow("myWorkflow", async () => { * try { * await myAct(); diff --git a/packages/@eventual/core/src/heartbeat.ts b/packages/@eventual/core/src/heartbeat.ts index d1be8710c..b5d939aa0 100644 --- a/packages/@eventual/core/src/heartbeat.ts +++ b/packages/@eventual/core/src/heartbeat.ts @@ -7,7 +7,7 @@ import { SendActivityHeartbeatResponse } from "./service-client.js"; * * If called from outside of an {@link activity}, the activity token must be provided. * - * If the activity has a heartbeatTimeout set and the workflow has not received a heartbeat in heartbeatTimeoutSeconds, + * If the activity has a heartbeatTimeout set and the workflow has not received a heartbeat within the set duration, * the workflow will throw a {@link HeartbeatTimeout} and cancel the activity. * * @returns {@link HeartbeatResponse} which has response.cancelled if the activity was cancelled for any reason (ex: workflow succeeded, failed, or the activity timed out). diff --git a/packages/@eventual/core/src/signals.ts b/packages/@eventual/core/src/signals.ts index 4850e6bc6..f3c6b4cb8 100644 --- a/packages/@eventual/core/src/signals.ts +++ b/packages/@eventual/core/src/signals.ts @@ -77,14 +77,14 @@ export class Signal { * }); * ``` * - * Use `opts.timeoutSeconds` to stop waiting after the provided time. The Promise will reject + * Use `opts.timeout` to stop waiting after the provided time. The Promise will reject * when the provided time has elapsed. * * ```ts * const mySignal = signal("MySignal"); * workflow("wf", async () => { * try { - * const payload = await mySignal.expectSignal({ timeoutSecond: 10 * 60 }); + * const payload = await mySignal.expectSignal({ timeout: duration(10, "minutes) }); * * return payload; * } catch { From ba0a92e84c62ff8c8bd375369f9936df0672fb6c Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Fri, 13 Jan 2023 12:13:53 -0600 Subject: [PATCH 10/14] update replay to set the service type flag later --- packages/@eventual/cli/src/commands/replay.ts | 3 --- .../@eventual/cli/src/replay/orchestrator.ts | 24 +++++++++++-------- packages/@eventual/core/src/runtime/flags.ts | 14 +++++++++++ 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/@eventual/cli/src/commands/replay.ts b/packages/@eventual/cli/src/commands/replay.ts index 8a6955707..cab081cd1 100644 --- a/packages/@eventual/cli/src/commands/replay.ts +++ b/packages/@eventual/cli/src/commands/replay.ts @@ -2,8 +2,6 @@ import { encodeExecutionId, ExecutionID, parseWorkflowName, - ServiceType, - SERVICE_TYPE_FLAG, workflows, } from "@eventual/core"; import { Argv } from "yargs"; @@ -30,7 +28,6 @@ export const replay = (yargs: Argv) => }), serviceAction( async (spinner, serviceClient, { entry, service, execution }) => { - process.env[SERVICE_TYPE_FLAG] = ServiceType.OrchestratorWorker; spinner.start("Constructing replay..."); const [, { events }] = await Promise.all([ loadService(service, encodeExecutionId(execution), entry), diff --git a/packages/@eventual/cli/src/replay/orchestrator.ts b/packages/@eventual/cli/src/replay/orchestrator.ts index 9417d7f7e..1e0432db0 100644 --- a/packages/@eventual/cli/src/replay/orchestrator.ts +++ b/packages/@eventual/cli/src/replay/orchestrator.ts @@ -5,6 +5,8 @@ import { isWorkflowStarted, isHistoryEvent, runWorkflowDefinition, + ServiceType, + serviceTypeScopeSync, } from "@eventual/core"; export type Orchestrator = typeof orchestrator; @@ -24,15 +26,17 @@ export function orchestrator( throw new Error("Missing start event"); } const interpretEvents = historyEvents.filter(isHistoryEvent); - return interpret( - runWorkflowDefinition(workflow, startEvent.input, { - workflow: { name: workflow.name }, - execution: { - ...startEvent.context, - startTime: startEvent.timestamp, - id: executionId, - }, - }), - interpretEvents + return serviceTypeScopeSync(ServiceType.OrchestratorWorker, () => + interpret( + runWorkflowDefinition(workflow, startEvent.input, { + workflow: { name: workflow.name }, + execution: { + ...startEvent.context, + startTime: startEvent.timestamp, + id: executionId, + }, + }), + interpretEvents + ) ); } diff --git a/packages/@eventual/core/src/runtime/flags.ts b/packages/@eventual/core/src/runtime/flags.ts index 2c780ba64..9bbdb3c05 100644 --- a/packages/@eventual/core/src/runtime/flags.ts +++ b/packages/@eventual/core/src/runtime/flags.ts @@ -31,3 +31,17 @@ export async function serviceTypeScope( process.env[SERVICE_TYPE_FLAG] = back; } } + +export function serviceTypeScopeSync( + serviceType: ServiceType, + handler: () => Output +): Output { + const back = process.env[SERVICE_TYPE_FLAG]; + try { + process.env[SERVICE_TYPE_FLAG] = serviceType; + // await before return so that the promise is completed before the finally call. + return handler(); + } finally { + process.env[SERVICE_TYPE_FLAG] = back; + } +} From 21fd61e08e70f02d5e3a1bf0f4b3f37969c555ed Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Fri, 13 Jan 2023 15:35:29 -0600 Subject: [PATCH 11/14] only one timer command, refactor tings --- packages/@eventual/aws-cdk/src/scheduler.ts | 4 +- packages/@eventual/aws-cdk/src/service.ts | 4 +- packages/@eventual/aws-cdk/src/workflows.ts | 2 +- .../aws-runtime/src/clients/create.ts | 2 +- .../aws-runtime/src/clients/timer-client.ts | 36 +-- packages/@eventual/core/src/command.ts | 33 +-- packages/@eventual/core/src/interpret.ts | 32 +-- .../core/src/runtime/clients/timer-client.ts | 35 ++- .../core/src/runtime/command-executor.ts | 47 ++-- .../core/src/runtime/handlers/orchestrator.ts | 16 +- .../core/src/runtime/metrics/constants.ts | 4 +- .../@eventual/core/src/workflow-events.ts | 32 +-- packages/@eventual/core/src/workflow.ts | 38 +-- packages/@eventual/core/test/command-util.ts | 48 ++-- .../core/test/commend-executor.test.ts | 50 +--- .../@eventual/core/test/interpret.test.ts | 244 +++++++++--------- .../testing/src/clients/timer-client.ts | 11 +- 17 files changed, 284 insertions(+), 354 deletions(-) diff --git a/packages/@eventual/aws-cdk/src/scheduler.ts b/packages/@eventual/aws-cdk/src/scheduler.ts index 9b9096f87..5531fde9e 100644 --- a/packages/@eventual/aws-cdk/src/scheduler.ts +++ b/packages/@eventual/aws-cdk/src/scheduler.ts @@ -39,7 +39,7 @@ export interface SchedulerProps { } /** - * Subsystem that orchestrates long running timers. Used to orchestrate timeouts, sleep + * Subsystem that orchestrates long running timers. Used to orchestrate timeouts, timers * and heartbeats. */ export class Scheduler extends Construct implements IScheduler, IGrantable { @@ -48,7 +48,7 @@ export class Scheduler extends Construct implements IScheduler, IGrantable { */ public readonly schedulerRole: IRole; /** - * Timer (standard) queue which helps orchestrate scheduled things like sleep and dynamic retries. + * Timer (standard) queue which helps orchestrate scheduled things like timers, heartbeat, and dynamic retries. * * Worths in tandem with the {@link CfnSchedulerGroup} to create millisecond latency, long running timers. */ diff --git a/packages/@eventual/aws-cdk/src/service.ts b/packages/@eventual/aws-cdk/src/service.ts index 582c3f116..bb59e2901 100644 --- a/packages/@eventual/aws-cdk/src/service.ts +++ b/packages/@eventual/aws-cdk/src/service.ts @@ -122,11 +122,11 @@ export class Service extends Construct implements IGrantable { */ public readonly workflows: Workflows; /** - * The subsystem for schedules and sleep timers. + * The subsystem for schedules and timers. */ public readonly scheduler: Scheduler; /** - * The Resources for schedules and sleep timers. + * The Resources for schedules and timers. */ public readonly cliRole: Role; /** diff --git a/packages/@eventual/aws-cdk/src/workflows.ts b/packages/@eventual/aws-cdk/src/workflows.ts index ff2ca0af2..c3ec40311 100644 --- a/packages/@eventual/aws-cdk/src/workflows.ts +++ b/packages/@eventual/aws-cdk/src/workflows.ts @@ -200,7 +200,7 @@ export class Workflows extends Construct implements IWorkflows, IGrantable { this.configureRecordHistory(this.orchestrator); // allows the orchestrator to directly invoke the activity worker lambda function (async) this.props.activities.configureScheduleActivity(this.orchestrator); - // allows allows the orchestrator to start timeout and sleep timers + // allows allows the orchestrator to start timeout and timers this.props.scheduler.configureScheduleTimer(this.orchestrator); // allows the orchestrator to send events to the workflow queue, // write events to the execution table, and start other workflows diff --git a/packages/@eventual/aws-runtime/src/clients/create.ts b/packages/@eventual/aws-runtime/src/clients/create.ts index 95ce80b68..62ccb720d 100644 --- a/packages/@eventual/aws-runtime/src/clients/create.ts +++ b/packages/@eventual/aws-runtime/src/clients/create.ts @@ -117,7 +117,7 @@ export const createTimerClient = /* @__PURE__ */ memoize( schedulerRoleArn: props.schedulerRoleArn ?? env.schedulerRoleArn(), schedulerDlqArn: props.schedulerDlqArn ?? env.schedulerDlqArn(), schedulerGroup: props.schedulerGroup ?? env.schedulerGroup(), - sleepQueueThresholdSeconds: props.sleepQueueThresholdSeconds ?? 15 * 60, + timerQueueThresholdSeconds: props.timerQueueThresholdSeconds ?? 15 * 60, sqs: props.sqs ?? sqs(), timerQueueUrl: props.timerQueueUrl ?? env.timerQueueUrl(), scheduleForwarderArn: diff --git a/packages/@eventual/aws-runtime/src/clients/timer-client.ts b/packages/@eventual/aws-runtime/src/clients/timer-client.ts index 220f84dc9..2d94e8142 100644 --- a/packages/@eventual/aws-runtime/src/clients/timer-client.ts +++ b/packages/@eventual/aws-runtime/src/clients/timer-client.ts @@ -15,7 +15,7 @@ import { ScheduleForwarderRequest, TimerRequest, isActivityHeartbeatMonitorRequest, - computeUntilTime, + computeScheduleDate, } from "@eventual/core"; import { ulid } from "ulidx"; @@ -25,10 +25,10 @@ export interface AWSTimerClientProps { readonly schedulerDlqArn: string; readonly schedulerGroup: string; /** - * If a sleep has a longer duration (in seconds) than this threshold, + * If a timer has a longer duration (in seconds) than this threshold, * create an Event Bus Scheduler before sending it to the TimerQueue */ - readonly sleepQueueThresholdSeconds: number; + readonly timerQueueThresholdSeconds: number; readonly timerQueueUrl: string; readonly sqs: SQSClient; readonly scheduleForwarderArn: string; @@ -36,7 +36,7 @@ export interface AWSTimerClientProps { export class AWSTimerClient extends TimerClient { constructor(private props: AWSTimerClientProps) { - super(); + super(() => new Date()); } /** @@ -74,8 +74,8 @@ export class AWSTimerClient extends TimerClient { /** * Starts a timer of any (positive) length. * - * If the timer is longer than 15 minutes (configurable via `props.sleepQueueThresholdMillis`), - * the timer will create a EventBridge schedule until the untilTime - props.sleepQueueThresholdMillis + * If the timer is longer than 15 minutes (configurable via `props.timerQueueThresholdMillis`), + * the timer will create a EventBridge schedule until the untilTime - props.timerQueueThresholdMillis * when the timer will be moved to the SQS queue. * * The SQS Queue will delay for floor(untilTime - currentTime) seconds until the timer handler can pick up the message. @@ -84,22 +84,24 @@ export class AWSTimerClient extends TimerClient { * the {@link TimerRequest} provided. */ public async startTimer(timerRequest: TimerRequest) { - const untilTimeIso = computeUntilTime(timerRequest.schedule); - const untilTime = new Date(untilTimeIso); - const sleepDuration = computeTimerSeconds(timerRequest.schedule); + const untilTime = computeScheduleDate( + timerRequest.schedule, + this.baseTime() + ); + const timerDuration = computeTimerSeconds(timerRequest.schedule); /** - * If the sleep is longer than 15 minutes, create an EventBridge schedule first. + * If the timer is longer than 15 minutes, create an EventBridge schedule first. * The Schedule will trigger a lambda which will re-compute the delay time and * create a message in the timerQueue. * - * The timerQueue ultimately will pick up the event and forward the {@link SleepComplete} to the workflow queue. + * The timerQueue ultimately will pick up the event and forward the {@link TimerComplete} to the workflow queue. */ - if (sleepDuration > this.props.sleepQueueThresholdSeconds) { - // wait for utilTime - sleepQueueThresholdMillis and then forward the event to + if (timerDuration > this.props.timerQueueThresholdSeconds) { + // wait for utilTime - timerQueueThresholdMillis and then forward the event to // the timerQueue const scheduleTime = - untilTime.getTime() - this.props.sleepQueueThresholdSeconds; + untilTime.getTime() - this.props.timerQueueThresholdSeconds; // EventBridge Scheduler only supports HH:MM:SS, strip off the milliseconds and `Z`. const formattedSchedulerTime = new Date(scheduleTime) .toISOString() @@ -112,7 +114,7 @@ export class AWSTimerClient extends TimerClient { scheduleName, timerRequest, forwardTime: "", - untilTime: untilTimeIso, + untilTime: untilTime.toISOString(), }; try { @@ -145,7 +147,7 @@ export class AWSTimerClient extends TimerClient { } } else { /** - * When the sleep is less than 15 minutes, send the timer directly to the + * When the timer is less than 15 minutes, send the timer directly to the * timer queue. The timer queue will pass the event on to the workflow queue * once delaySeconds have passed. */ @@ -159,7 +161,7 @@ export class AWSTimerClient extends TimerClient { * Use this method to clean the schedule. * * The provided schedule-forwarder function will call this method in Eventual when - * the timer is transferred from EventBridge to SQS at `props.sleepQueueThresholdMillis`. + * the timer is transferred from EventBridge to SQS at `props.timerQueueThresholdMillis`. */ public async clearSchedule(scheduleName: string) { try { diff --git a/packages/@eventual/core/src/command.ts b/packages/@eventual/core/src/command.ts index b6547f10b..89ee20431 100644 --- a/packages/@eventual/core/src/command.ts +++ b/packages/@eventual/core/src/command.ts @@ -1,11 +1,10 @@ -import { DurationUnit } from "./await-time.js"; import { EventEnvelope } from "./event.js"; +import { Schedule } from "./index.js"; import { SignalTarget } from "./signals.js"; import { WorkflowOptions } from "./workflow.js"; export type Command = - | AwaitDurationCommand - | AwaitTimeCommand + | StartTimerCommand | ScheduleActivityCommand | ScheduleWorkflowCommand | PublishEventsCommand @@ -17,11 +16,10 @@ interface CommandBase { } export enum CommandType { - AwaitDuration = "AwaitDuration", - AwaitTime = "AwaitTime", PublishEvents = "PublishEvents", SendSignal = "SendSignal", StartActivity = "StartActivity", + StartTimer = "StartTimer", StartWorkflow = "StartWorkflow", } @@ -58,32 +56,17 @@ export function isScheduleWorkflowCommand( return a.kind === CommandType.StartWorkflow; } -export interface AwaitTimeCommand extends CommandBase { +export interface StartTimerCommand extends CommandBase { /** * Minimum time (in ISO 8601) where the machine should wake up. */ - untilTime: string; + schedule: Schedule; } -export function isAwaitTimeCommand( +export function isStartTimerCommand( command: Command -): command is AwaitTimeCommand { - return command.kind === CommandType.AwaitTime; -} - -export interface AwaitDurationCommand - extends CommandBase { - /** - * Number of seconds from the time the command is executed until the machine should wake up. - */ - dur: number; - unit: DurationUnit; -} - -export function isAwaitDurationCommand( - command: Command -): command is AwaitDurationCommand { - return command.kind === CommandType.AwaitDuration; +): command is StartTimerCommand { + return command.kind === CommandType.StartTimer; } export interface SendSignalCommand extends CommandBase { diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index fbeb7897c..0bea48b35 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -25,8 +25,8 @@ import { isSignalReceived, isFailedEvent, isScheduledEvent, - isAlarmCompleted, - isAlarmScheduled, + isTimerCompleted, + isTimerScheduled, ScheduledEvent, isSignalSent, isWorkflowTimedOut, @@ -44,7 +44,13 @@ import { isResolvedOrFailed, } from "./result.js"; import { createChain, isChain, Chain } from "./chain.js"; -import { assertNever, _Iterator, iterator, or } from "./util.js"; +import { + assertNever, + _Iterator, + iterator, + or, + computeDurationSeconds, +} from "./util.js"; import { Command, CommandType } from "./command.js"; import { isAwaitDurationCall, @@ -66,6 +72,7 @@ import { isAwaitAllSettled } from "./await-all-settled.js"; import { isAwaitAny } from "./await-any.js"; import { isRace } from "./race.js"; import { isPublishEventsCall } from "./calls/send-events-call.js"; +import { Schedule } from "./index.js"; export interface WorkflowResult { /** @@ -212,18 +219,13 @@ export function interpret( heartbeatSeconds: call.heartbeatSeconds, seq: call.seq!, }; - } else if (isAwaitTimeCall(call)) { - return { - kind: CommandType.AwaitTime, - seq: call.seq!, - untilTime: call.isoDate, - }; - } else if (isAwaitDurationCall(call)) { + } else if (isAwaitTimeCall(call) || isAwaitDurationCall(call)) { return { - kind: CommandType.AwaitDuration, + kind: CommandType.StartTimer, seq: call.seq!, - dur: call.dur, - unit: call.unit, + schedule: isAwaitTimeCall(call) + ? Schedule.absolute(call.isoDate) + : Schedule.relative(computeDurationSeconds(call.dur, call.unit)), }; } else if (isWorkflowCall(call)) { return { @@ -555,7 +557,7 @@ export function interpret( } call.result = isSucceededEvent(event) ? Result.resolved(event.result) - : isAlarmCompleted(event) + : isTimerCompleted(event) ? Result.resolved(undefined) : isActivityHeartbeatTimedOut(event) ? Result.failed(new HeartbeatTimeout("Activity Heartbeat TimedOut")) @@ -570,7 +572,7 @@ function isCorresponding(event: ScheduledEvent, call: CommandCall) { return isActivityCall(call) && call.name === event.name; } else if (isChildWorkflowScheduled(event)) { return isWorkflowCall(call) && call.name === event.name; - } else if (isAlarmScheduled(event)) { + } else if (isTimerScheduled(event)) { return isAwaitTimeCall(call) || isAwaitDurationCall(call); } else if (isSignalSent(event)) { return isSendSignalCall(call) && event.signalId === call.signalId; diff --git a/packages/@eventual/core/src/runtime/clients/timer-client.ts b/packages/@eventual/core/src/runtime/clients/timer-client.ts index 7d5edc002..fb2ae512b 100644 --- a/packages/@eventual/core/src/runtime/clients/timer-client.ts +++ b/packages/@eventual/core/src/runtime/clients/timer-client.ts @@ -1,6 +1,8 @@ import { HistoryStateEvent } from "../../workflow-events.js"; export abstract class TimerClient { + constructor(protected baseTime: () => Date) {} + /** * Starts a timer using SQS's message delay. * @@ -18,8 +20,8 @@ export abstract class TimerClient { /** * Starts a timer of any (positive) length. * - * If the timer is longer than 15 minutes (configurable via `props.sleepQueueThresholdMillis`), - * the timer will create a EventBridge schedule until the untilTime - props.sleepQueueThresholdMillis + * If the timer is longer than 15 minutes (configurable via `props.timerQueueThresholdMillis`), + * the timer will create a EventBridge schedule until the untilTime - props.timerQueueThresholdMillis * when the timer will be moved to the SQS queue. * * The SQS Queue will delay for floor(untilTime - currentTime) seconds until the timer handler can pick up the message. @@ -35,7 +37,7 @@ export abstract class TimerClient { * Use this method to clean the schedule. * * The provided schedule-forwarder function will call this method in Eventual when - * the timer is transferred from EventBridge to SQS at `props.sleepQueueThresholdMillis`. + * the timer is transferred from EventBridge to SQS at `props.timerQueueThresholdMillis`. */ public abstract clearSchedule(scheduleName: string): Promise; @@ -47,7 +49,10 @@ export abstract class TimerClient { public async scheduleEvent( request: ScheduleEventRequest ): Promise { - const untilTime = computeUntilTime(request.schedule); + const untilTime = computeScheduleDate( + request.schedule, + this.baseTime() + ).toISOString(); const event = { ...request.event, @@ -58,7 +63,7 @@ export abstract class TimerClient { event, executionId: request.executionId, type: TimerRequestType.ScheduleEvent, - schedule: Schedule.absolute(untilTime), + schedule: request.schedule, }); } } @@ -75,7 +80,6 @@ export enum TimerRequestType { export interface RelativeSchedule { type: "Relative"; timerSeconds: number; - baseTime: Date; } export interface AbsoluteSchedule { @@ -86,20 +90,17 @@ export interface AbsoluteSchedule { export type Schedule = RelativeSchedule | AbsoluteSchedule; export const Schedule = { - relative( - timerSeconds: number, - baseTime: Date = new Date() - ): RelativeSchedule { + relative(timerSeconds: number): RelativeSchedule { return { type: "Relative", timerSeconds, - baseTime, }; }, - absolute(untilTime: string): AbsoluteSchedule { + absolute(untilTime: string | Date): AbsoluteSchedule { return { type: "Absolute", - untilTime, + untilTime: + typeof untilTime === "string" ? untilTime : untilTime.toISOString(), }; }, }; @@ -156,10 +157,8 @@ export interface ScheduleEventRequest event: Omit; } -export function computeUntilTime(schedule: TimerRequest["schedule"]): string { +export function computeScheduleDate(schedule: Schedule, baseTime: Date): Date { return "untilTime" in schedule - ? schedule.untilTime - : new Date( - schedule.baseTime.getTime() + schedule.timerSeconds * 1000 - ).toISOString(); + ? new Date(schedule.untilTime) + : new Date(baseTime.getTime() + schedule.timerSeconds * 1000); } diff --git a/packages/@eventual/core/src/runtime/command-executor.ts b/packages/@eventual/core/src/runtime/command-executor.ts index 67fb2b33d..1c7455ffd 100644 --- a/packages/@eventual/core/src/runtime/command-executor.ts +++ b/packages/@eventual/core/src/runtime/command-executor.ts @@ -4,31 +4,29 @@ import { isScheduleActivityCommand, isScheduleWorkflowCommand, isSendSignalCommand, - isAwaitDurationCommand, - isAwaitTimeCommand, + isStartTimerCommand, PublishEventsCommand, ScheduleActivityCommand, ScheduleWorkflowCommand, SendSignalCommand, - AwaitDurationCommand, - AwaitTimeCommand, + StartTimerCommand, } from "../command.js"; import { WorkflowEventType, createEvent, ActivityScheduled, ChildWorkflowScheduled, - AlarmScheduled, - AlarmCompleted, + TimerScheduled, + TimerCompleted, HistoryStateEvent, SignalSent, EventsPublished, } from "../workflow-events.js"; -import { assertNever, computeDurationDate } from "../util.js"; +import { assertNever } from "../util.js"; import { Workflow } from "../workflow.js"; import { formatChildExecutionName, formatExecutionId } from "./execution-id.js"; import { ActivityWorkerRequest } from "./handlers/activity-worker.js"; -import { Schedule, TimerClient } from "./clients/timer-client.js"; +import { computeScheduleDate, TimerClient } from "./clients/timer-client.js"; import { WorkflowRuntimeClient } from "./clients/workflow-runtime-client.js"; import { WorkflowClient } from "./clients/workflow-client.js"; import { EventClient } from "./clients/event-client.js"; @@ -62,9 +60,9 @@ export class CommandExecutor { ); } else if (isScheduleWorkflowCommand(command)) { return this.scheduleChildWorkflow(executionId, command, baseTime); - } else if (isAwaitDurationCommand(command) || isAwaitTimeCommand(command)) { - // all sleep times are computed using the start time of the WorkflowTaskStarted - return this.scheduleSleep(executionId, command, baseTime); + } else if (isStartTimerCommand(command)) { + // all timers are computed using the start time of the WorkflowTaskStarted + return this.startTimer(executionId, command, baseTime); } else if (isSendSignalCommand(command)) { return this.sendSignal(executionId, command, baseTime); } else if (isPublishEventsCommand(command)) { @@ -125,32 +123,29 @@ export class CommandExecutor { ); } - private async scheduleSleep( + private async startTimer( executionId: string, - - command: AwaitDurationCommand | AwaitTimeCommand, + command: StartTimerCommand, baseTime: Date - ): Promise { + ): Promise { // TODO validate - const untilTime = isAwaitTimeCommand(command) - ? new Date(command.untilTime) - : computeDurationDate(baseTime, command.dur, command.unit); - const untilTimeIso = untilTime.toISOString(); - - await this.props.timerClient.scheduleEvent({ + await this.props.timerClient.scheduleEvent({ event: { - type: WorkflowEventType.AlarmCompleted, + type: WorkflowEventType.TimerCompleted, seq: command.seq, }, - schedule: Schedule.absolute(untilTimeIso), + schedule: command.schedule, executionId, }); - return createEvent( + return createEvent( { - type: WorkflowEventType.AlarmScheduled, + type: WorkflowEventType.TimerScheduled, seq: command.seq, - untilTime: untilTime.toISOString(), + untilTime: computeScheduleDate( + command.schedule, + baseTime + ).toISOString(), }, baseTime ); diff --git a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts index f4caa2be4..e5d9822a4 100644 --- a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts +++ b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts @@ -6,7 +6,7 @@ import { getEventId, HistoryStateEvent, isHistoryEvent, - isAlarmCompleted, + isTimerCompleted, isWorkflowSucceeded, isWorkflowFailed, isWorkflowStarted, @@ -460,7 +460,7 @@ export function createOrchestrator({ const inputEvents = [...historyEvents, ...uniqueTaskEvents]; - // Generates events that are time sensitive, like sleep completed events. + // Generates events that are time sensitive, like timer completed events. const syntheticEvents = generateSyntheticEvents(inputEvents, baseTime); const allEvents = [...inputEvents, ...syntheticEvents]; @@ -724,14 +724,14 @@ function logEventMetrics( events: WorkflowEvent[], now: Date ) { - const sleepCompletedEvents = events.filter(isAlarmCompleted); - if (sleepCompletedEvents.length > 0) { - const sleepCompletedVariance = sleepCompletedEvents.map( + const timerCompletedEvents = events.filter(isTimerCompleted); + if (timerCompletedEvents.length > 0) { + const timerCompletedVariance = timerCompletedEvents.map( (s) => now.getTime() - new Date(s.timestamp).getTime() ); const avg = - sleepCompletedVariance.reduce((t, n) => t + n, 0) / - sleepCompletedVariance.length; - metrics.setProperty(OrchestratorMetrics.SleepVarianceMillis, avg); + timerCompletedVariance.reduce((t, n) => t + n, 0) / + timerCompletedVariance.length; + metrics.setProperty(OrchestratorMetrics.TimerVarianceMillis, avg); } } diff --git a/packages/@eventual/core/src/runtime/metrics/constants.ts b/packages/@eventual/core/src/runtime/metrics/constants.ts index 9b05790ff..977e7e923 100644 --- a/packages/@eventual/core/src/runtime/metrics/constants.ts +++ b/packages/@eventual/core/src/runtime/metrics/constants.ts @@ -100,9 +100,9 @@ export namespace OrchestratorMetrics { */ export const ExecutionResultBytes = "ExecutionResultBytes"; /** - * Number of milliseconds between the expected sleep wakeup time and the actual incoming {@link SleepCompleted} event. + * Number of milliseconds between the expected timer wakeup time and the actual incoming {@link TimerCompleted} event. */ - export const SleepVarianceMillis = "SleepVarianceMillis"; + export const TimerVarianceMillis = "TimerVarianceMillis"; /** * Number of milliseconds it takes to send execution logs to where ever they are persisted. */ diff --git a/packages/@eventual/core/src/workflow-events.ts b/packages/@eventual/core/src/workflow-events.ts index 35f17f146..890fdd1e0 100644 --- a/packages/@eventual/core/src/workflow-events.ts +++ b/packages/@eventual/core/src/workflow-events.ts @@ -21,14 +21,14 @@ export enum WorkflowEventType { ActivityFailed = "ActivityFailed", ActivityHeartbeatTimedOut = "ActivityHeartbeatTimedOut", ActivityScheduled = "ActivityScheduled", - AlarmCompleted = "AlarmCompleted", - AlarmScheduled = "AlarmScheduled", ChildWorkflowSucceeded = "ChildWorkflowSucceeded", ChildWorkflowFailed = "ChildWorkflowFailed", ChildWorkflowScheduled = "ChildWorkflowScheduled", EventsPublished = "EventsPublished", SignalReceived = "SignalReceived", SignalSent = "SignalSent", + TimerCompleted = "TimerCompleted", + TimerScheduled = "TimerScheduled", WorkflowSucceeded = "WorkflowSucceeded", WorkflowFailed = "WorkflowFailed", WorkflowStarted = "WorkflowStarted", @@ -50,14 +50,14 @@ export type WorkflowEvent = export type ScheduledEvent = | ActivityScheduled - | AlarmScheduled + | TimerScheduled | ChildWorkflowScheduled | EventsPublished | SignalSent; export type SucceededEvent = | ActivitySucceeded - | AlarmCompleted + | TimerCompleted | ChildWorkflowSucceeded; export type FailedEvent = @@ -218,19 +218,19 @@ export function isActivityHeartbeatTimedOut( return event.type === WorkflowEventType.ActivityHeartbeatTimedOut; } -export interface AlarmScheduled extends HistoryEventBase { - type: WorkflowEventType.AlarmScheduled; +export interface TimerScheduled extends HistoryEventBase { + type: WorkflowEventType.TimerScheduled; untilTime: string; } -export function isAlarmScheduled( +export function isTimerScheduled( event: WorkflowEvent -): event is AlarmScheduled { - return event.type === WorkflowEventType.AlarmScheduled; +): event is TimerScheduled { + return event.type === WorkflowEventType.TimerScheduled; } -export interface AlarmCompleted extends HistoryEventBase { - type: WorkflowEventType.AlarmCompleted; +export interface TimerCompleted extends HistoryEventBase { + type: WorkflowEventType.TimerCompleted; result?: undefined; } @@ -268,10 +268,10 @@ export function isChildWorkflowFailed( return event.type === WorkflowEventType.ChildWorkflowFailed; } -export function isAlarmCompleted( +export function isTimerCompleted( event: WorkflowEvent -): event is AlarmCompleted { - return event.type === WorkflowEventType.AlarmCompleted; +): event is TimerCompleted { + return event.type === WorkflowEventType.TimerCompleted; } export const isWorkflowCompletedEvent = or( @@ -328,13 +328,13 @@ export const isScheduledEvent = or( isChildWorkflowScheduled, isEventsPublished, isSignalSent, - isAlarmScheduled + isTimerScheduled ); export const isSucceededEvent = or( isActivitySucceeded, isChildWorkflowSucceeded, - isAlarmCompleted + isTimerCompleted ); export const isFailedEvent = or( diff --git a/packages/@eventual/core/src/workflow.ts b/packages/@eventual/core/src/workflow.ts index 17fe6c35b..f8c6b3fe6 100644 --- a/packages/@eventual/core/src/workflow.ts +++ b/packages/@eventual/core/src/workflow.ts @@ -3,10 +3,10 @@ import type { Program } from "./interpret.js"; import type { Context } from "./context.js"; import { HistoryStateEvent, - isAlarmCompleted, - isAlarmScheduled, - AlarmCompleted, - AlarmScheduled, + isTimerCompleted, + isTimerScheduled, + TimerCompleted, + TimerScheduled, WorkflowEventType, } from "./workflow-events.js"; import { createWorkflowCall } from "./calls/workflow-call.js"; @@ -176,29 +176,29 @@ export function runWorkflowDefinition( } /** - * Generates synthetic events, for example, {@link AlarmCompleted} events when the time has passed, but a real completed event has not come in yet. + * Generates synthetic events, for example, {@link TimerCompleted} events when the time has passed, but a real completed event has not come in yet. */ export function generateSyntheticEvents( events: HistoryStateEvent[], baseTime: Date -): AlarmCompleted[] { - const unresolvedSleep: Record = {}; +): TimerCompleted[] { + const unresolvedTimers: Record = {}; - const sleepEvents = events.filter( - (event): event is AlarmScheduled | AlarmCompleted => - isAlarmScheduled(event) || isAlarmCompleted(event) + const timerEvents = events.filter( + (event): event is TimerScheduled | TimerCompleted => + isTimerScheduled(event) || isTimerCompleted(event) ); - for (const event of sleepEvents) { - if (isAlarmScheduled(event)) { - unresolvedSleep[event.seq] = event; + for (const event of timerEvents) { + if (isTimerScheduled(event)) { + unresolvedTimers[event.seq] = event; } else { - delete unresolvedSleep[event.seq]; + delete unresolvedTimers[event.seq]; } } - const syntheticSleepComplete: AlarmCompleted[] = Object.values( - unresolvedSleep + const syntheticTimerComplete: TimerCompleted[] = Object.values( + unresolvedTimers ) .filter( (event) => new Date(event.untilTime).getTime() <= baseTime.getTime() @@ -206,11 +206,11 @@ export function generateSyntheticEvents( .map( (e) => ({ - type: WorkflowEventType.AlarmCompleted, + type: WorkflowEventType.TimerCompleted, seq: e.seq, timestamp: baseTime.toISOString(), - } satisfies AlarmCompleted) + } satisfies TimerCompleted) ); - return syntheticSleepComplete; + return syntheticTimerComplete; } diff --git a/packages/@eventual/core/test/command-util.ts b/packages/@eventual/core/test/command-util.ts index 4fd8ce4f8..b5c4d7086 100644 --- a/packages/@eventual/core/test/command-util.ts +++ b/packages/@eventual/core/test/command-util.ts @@ -1,7 +1,6 @@ import { ulid } from "ulidx"; import { - AwaitDurationCommand, - AwaitTimeCommand, + StartTimerCommand, CommandType, PublishEventsCommand, ScheduleActivityCommand, @@ -19,35 +18,28 @@ import { EventsPublished, SignalReceived, SignalSent, - AlarmCompleted, - AlarmScheduled, WorkflowEventType, WorkflowTimedOut, ActivityHeartbeatTimedOut, + TimerCompleted, + TimerScheduled, } from "../src/workflow-events.js"; import { SignalTarget } from "../src/signals.js"; -import { DurationUnit } from "../src/await-time.js"; +import { Schedule } from "../src/index.js"; -export function createAwaitTimeCommand( - untilTime: string, +export function createStartTimerCommand( + schedule: Schedule, seq: number -): AwaitTimeCommand { +): StartTimerCommand; +export function createStartTimerCommand(seq: number): StartTimerCommand; +export function createStartTimerCommand( + ...args: [schedule: Schedule, seq: number] | [seq: number] +): StartTimerCommand { + const [schedule, seq] = + args.length === 1 ? [Schedule.absolute("then"), args[0]] : args; return { - kind: CommandType.AwaitTime, - untilTime, - seq, - }; -} - -export function createAwaitDurationCommand( - dur: number, - unit: DurationUnit, - seq: number -): AwaitDurationCommand { - return { - kind: CommandType.AwaitDuration, - dur, - unit, + kind: CommandType.StartTimer, + schedule, seq, }; } @@ -188,18 +180,18 @@ export function workflowScheduled( }; } -export function scheduledAlarm(untilTime: string, seq: number): AlarmScheduled { +export function timerScheduled(seq: number): TimerScheduled { return { - type: WorkflowEventType.AlarmScheduled, - untilTime, + type: WorkflowEventType.TimerScheduled, + untilTime: "", seq, timestamp: new Date(0).toISOString(), }; } -export function completedAlarm(seq: number): AlarmCompleted { +export function timerCompleted(seq: number): TimerCompleted { return { - type: WorkflowEventType.AlarmCompleted, + type: WorkflowEventType.TimerCompleted, seq, timestamp: new Date(0).toISOString(), }; diff --git a/packages/@eventual/core/test/commend-executor.test.ts b/packages/@eventual/core/test/commend-executor.test.ts index fb6bbbdd4..485f4af5a 100644 --- a/packages/@eventual/core/test/commend-executor.test.ts +++ b/packages/@eventual/core/test/commend-executor.test.ts @@ -5,8 +5,8 @@ import { ChildWorkflowScheduled, EventsPublished, SignalSent, - AlarmCompleted, - AlarmScheduled, + TimerCompleted, + TimerScheduled, WorkflowEventType, } from "../src/workflow-events.js"; import { @@ -61,67 +61,33 @@ afterEach(() => { }); describe("await times", () => { - test("await duration", async () => { - const event = await testExecutor.executeCommand( - workflow, - executionId, - { - kind: CommandType.AwaitDuration, - dur: 10, - unit: "seconds", - seq: 0, - }, - baseTime - ); - - const untilTime = new Date(baseTime.getTime() + 10 * 1000).toISOString(); - - expect(mockTimerClient.scheduleEvent).toHaveBeenCalledWith< - [ScheduleEventRequest] - >({ - event: { - type: WorkflowEventType.AlarmCompleted, - seq: 0, - }, - schedule: Schedule.absolute(untilTime), - executionId, - }); - - expect(event).toMatchObject({ - seq: 0, - timestamp: expect.stringContaining("Z"), - type: WorkflowEventType.AlarmScheduled, - untilTime, - }); - }); - test("await time", async () => { const event = await testExecutor.executeCommand( workflow, executionId, { - kind: CommandType.AwaitTime, - untilTime: baseTime.toISOString(), + kind: CommandType.StartTimer, + schedule: Schedule.absolute(baseTime), seq: 0, }, baseTime ); expect(mockTimerClient.scheduleEvent).toHaveBeenCalledWith< - [ScheduleEventRequest] + [ScheduleEventRequest] >({ event: { - type: WorkflowEventType.AlarmCompleted, + type: WorkflowEventType.TimerCompleted, seq: 0, }, schedule: Schedule.absolute(baseTime.toISOString()), executionId, }); - expect(event).toMatchObject({ + expect(event).toMatchObject({ seq: 0, timestamp: expect.stringContaining("Z"), - type: WorkflowEventType.AlarmScheduled, + type: WorkflowEventType.TimerScheduled, untilTime: baseTime.toISOString(), }); }); diff --git a/packages/@eventual/core/test/interpret.test.ts b/packages/@eventual/core/test/interpret.test.ts index e02717a30..81504ee7c 100644 --- a/packages/@eventual/core/test/interpret.test.ts +++ b/packages/@eventual/core/test/interpret.test.ts @@ -10,6 +10,7 @@ import { interpret, Program, Result, + Schedule, ServiceType, SERVICE_TYPE_FLAG, signal, @@ -29,17 +30,16 @@ import { activityFailed, activityHeartbeatTimedOut, activityScheduled, - completedAlarm, createPublishEventCommand, createScheduledActivityCommand, createScheduledWorkflowCommand, createSendSignalCommand, - createAwaitDurationCommand, - createAwaitTimeCommand, + createStartTimerCommand, eventsPublished, - scheduledAlarm, signalReceived, signalSent, + timerCompleted, + timerScheduled, workflowSucceeded, workflowFailed, workflowScheduled, @@ -116,7 +116,7 @@ test("should continue with result of completed Activity", () => { ).toMatchObject({ commands: [ createScheduledActivityCommand("my-activity-0", [event], 1), - createAwaitTimeCommand("then", 2), + createStartTimerCommand(2), createScheduledActivityCommand("my-activity-2", [event], 3), ], }); @@ -182,8 +182,8 @@ test("should catch error of timing out Activity", () => { expect( interpret(myWorkflow(event), [ - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), activityScheduled("my-activity", 1), ]) ).toMatchObject({ @@ -224,10 +224,10 @@ test("timeout multiple activities at once", () => { expect( interpret(myWorkflow(event), [ - scheduledAlarm("", 0), + timerScheduled(0), activityScheduled("my-activity", 1), activityScheduled("my-activity", 2), - completedAlarm(0), + timerCompleted(0), ]) ).toMatchObject({ result: Result.resolved([ @@ -285,10 +285,10 @@ test("should return final result", () => { activityScheduled("my-activity", 0), activitySucceeded("result", 0), activityScheduled("my-activity-0", 1), - scheduledAlarm("then", 2), + timerScheduled(2), activityScheduled("my-activity-2", 3), activitySucceeded("result-0", 1), - completedAlarm(2), + timerCompleted(2), activitySucceeded("result-2", 3), ]) ).toMatchObject({ @@ -304,7 +304,7 @@ test("should handle missing blocks", () => { commands: [ createScheduledActivityCommand("my-activity", [event], 0), createScheduledActivityCommand("my-activity-0", [event], 1), - createAwaitTimeCommand("then", 2), + createStartTimerCommand(2), createScheduledActivityCommand("my-activity-2", [event], 3), ], }); @@ -319,7 +319,7 @@ test("should handle partial blocks", () => { ]) ).toMatchObject({ commands: [ - createAwaitTimeCommand("then", 2), + createStartTimerCommand(2), createScheduledActivityCommand("my-activity-2", [event], 3), ], }); @@ -335,7 +335,7 @@ test("should handle partial blocks with partial completes", () => { ]) ).toMatchObject({ commands: [ - createAwaitTimeCommand("then", 2), + createStartTimerCommand(2), createScheduledActivityCommand("my-activity-2", [event], 3), ], }); @@ -417,7 +417,7 @@ describe("activity", () => { test("should throw when scheduled does not correspond to call", () => { expect( - interpret(myWorkflow(event), [scheduledAlarm("result", 0)]) + interpret(myWorkflow(event), [timerScheduled(0)]) ).toMatchObject({ result: Result.failed({ name: "DeterminismError" }), commands: [], @@ -495,10 +495,10 @@ test("should wait if partial results", () => { activityScheduled("my-activity", 0), activitySucceeded("result", 0), activityScheduled("my-activity-0", 1), - scheduledAlarm("then", 2), + timerScheduled(2), activityScheduled("my-activity-2", 3), activitySucceeded("result-0", 1), - completedAlarm(2), + timerCompleted(2), ]) ).toMatchObject({ commands: [], @@ -519,46 +519,43 @@ test("should return result of inner function", () => { }); }); -test("should schedule sleep for", () => { +test("should schedule duration", () => { function* workflow() { yield duration(10); } expect(interpret(workflow() as any, [])).toMatchObject({ - commands: [createAwaitDurationCommand(10, "seconds", 0)], + commands: [createStartTimerCommand(Schedule.relative(10), 0)], }); }); -test("should not re-schedule sleep for", () => { +test("should not re-schedule duration", () => { function* workflow() { yield duration(10); } - expect( - interpret(workflow() as any, [scheduledAlarm("anything", 0)]) - ).toMatchObject({ + expect(interpret(workflow() as any, [timerScheduled(0)])).toMatchObject(< + WorkflowResult + >{ commands: [], }); }); -test("should complete sleep for", () => { +test("should complete duration", () => { function* workflow() { yield duration(10); return "done"; } expect( - interpret(workflow() as any, [ - scheduledAlarm("anything", 0), - completedAlarm(0), - ]) + interpret(workflow() as any, [timerScheduled(0), timerCompleted(0)]) ).toMatchObject({ result: Result.resolved("done"), commands: [], }); }); -test("should schedule sleep until", () => { +test("should schedule time", () => { const now = new Date(); function* workflow() { @@ -566,25 +563,27 @@ test("should schedule sleep until", () => { } expect(interpret(workflow() as any, [])).toMatchObject({ - commands: [createAwaitTimeCommand(now.toISOString(), 0)], + commands: [ + createStartTimerCommand(Schedule.absolute(now.toISOString()), 0), + ], }); }); -test("should not re-schedule sleep until", () => { +test("should not re-schedule time", () => { const now = new Date(); function* workflow() { yield time(now); } - expect( - interpret(workflow() as any, [scheduledAlarm("anything", 0)]) - ).toMatchObject({ + expect(interpret(workflow() as any, [timerScheduled(0)])).toMatchObject(< + WorkflowResult + >{ commands: [], }); }); -test("should complete sleep until", () => { +test("should complete time", () => { const now = new Date(); function* workflow() { @@ -593,10 +592,7 @@ test("should complete sleep until", () => { } expect( - interpret(workflow() as any, [ - scheduledAlarm("anything", 0), - completedAlarm(0), - ]) + interpret(workflow() as any, [timerScheduled(0), timerCompleted(0)]) ).toMatchObject({ result: Result.resolved("done"), commands: [], @@ -607,13 +603,13 @@ describe("temple of doom", () => { /** * In our game, the player wants to get to the end of a hallway with traps. * The trap starts above the player and moves to a space in front of them - * after a sleepUntil("X"). + * after a time("X"). * * If the trap has moved (X time), the player may jump to avoid it. * If the player jumps when then trap has not moved, they will beheaded. * If the player runs when the trap has been triggered without jumping, they will have their legs cut off. * - * The trap is represented by a sleep command for X time. + * The trap is represented by a timer command for X time. * The player starts running by returning the "run" activity. * The player jumps be returning the "jump" activity. * (this would be better modeled with signals and conditions, but the effect is the same, wait, complete) @@ -625,7 +621,7 @@ describe("temple of doom", () => { let jump = false; const startTrap = chain(function* () { - yield createAwaitTimeCall("X"); + yield createAwaitTimeCall("then"); trapDown = true; }); const waitForJump = chain(function* () { @@ -655,7 +651,7 @@ describe("temple of doom", () => { test("run until blocked", () => { expect(interpret(workflow() as any, [])).toMatchObject({ commands: [ - createAwaitTimeCommand("X", 0), + createStartTimerCommand(0), createScheduledActivityCommand("jump", [], 1), createScheduledActivityCommand("run", [], 2), ], @@ -665,7 +661,7 @@ describe("temple of doom", () => { test("waiting", () => { expect( interpret(workflow() as any, [ - scheduledAlarm("X", 0), + timerScheduled(0), activityScheduled("jump", 1), activityScheduled("run", 2), ]) @@ -675,13 +671,13 @@ describe("temple of doom", () => { }); test("trap triggers, player has not started, nothing happens", () => { - // complete sleep, nothing happens + // complete timer, nothing happens expect( interpret(workflow() as any, [ - scheduledAlarm("X", 0), + timerScheduled(0), activityScheduled("jump", 1), activityScheduled("run", 2), - completedAlarm(0), + timerCompleted(0), ]) ).toMatchObject({ commands: [], @@ -689,13 +685,13 @@ describe("temple of doom", () => { }); test("trap triggers and then the player starts, player is dead", () => { - // complete sleep, turn on, release the player, dead + // complete timer, turn on, release the player, dead expect( interpret(workflow() as any, [ - scheduledAlarm("X", 0), + timerScheduled(0), activityScheduled("jump", 1), activityScheduled("run", 2), - completedAlarm(0), + timerCompleted(0), activitySucceeded("anything", 2), ]) ).toMatchObject({ @@ -705,12 +701,12 @@ describe("temple of doom", () => { }); test("trap triggers and then the player starts, player is dead, commands are out of order", () => { - // complete sleep, turn on, release the player, dead + // complete timer, turn on, release the player, dead expect( interpret(workflow() as any, [ - completedAlarm(0), + timerCompleted(0), activitySucceeded("anything", 2), - scheduledAlarm("X", 0), + timerScheduled(0), activityScheduled("jump", 1), activityScheduled("run", 2), ]) @@ -724,7 +720,7 @@ describe("temple of doom", () => { // release the player, not on, alive expect( interpret(workflow() as any, [ - scheduledAlarm("X", 0), + timerScheduled(0), activityScheduled("jump", 1), activityScheduled("run", 2), activitySucceeded("anything", 2), @@ -739,7 +735,7 @@ describe("temple of doom", () => { // release the player, not on, alive expect( interpret(workflow() as any, [ - scheduledAlarm("X", 0), + timerScheduled(0), activitySucceeded("anything", 2), activityScheduled("jump", 1), activityScheduled("run", 2), @@ -755,7 +751,7 @@ describe("temple of doom", () => { expect( interpret(workflow() as any, [ activitySucceeded("anything", 2), - scheduledAlarm("X", 0), + timerScheduled(0), activityScheduled("jump", 1), activityScheduled("run", 2), ]) @@ -768,11 +764,11 @@ describe("temple of doom", () => { test("release the player before the trap triggers, player lives", () => { expect( interpret(workflow() as any, [ - scheduledAlarm("X", 0), + timerScheduled(0), activityScheduled("jump", 1), activityScheduled("run", 2), activitySucceeded("anything", 2), - completedAlarm(0), + timerCompleted(0), ]) ).toMatchObject({ result: Result.resolved("alive"), @@ -1514,13 +1510,13 @@ describe("signals", () => { expect(interpret(wf.definition(undefined, context), [])).toMatchObject(< WorkflowResult >{ - commands: [createAwaitDurationCommand(100 * 1000, "seconds", 0)], + commands: [createStartTimerCommand(Schedule.relative(100 * 1000), 0)], }); }); test("no signal", () => { expect( - interpret(wf.definition(undefined, context), [scheduledAlarm("", 0)]) + interpret(wf.definition(undefined, context), [timerScheduled(0)]) ).toMatchObject({ commands: [], }); @@ -1529,7 +1525,7 @@ describe("signals", () => { test("match signal", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), + timerScheduled(0), signalReceived("MySignal"), ]) ).toMatchObject({ @@ -1541,7 +1537,7 @@ describe("signals", () => { test("match signal with payload", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), + timerScheduled(0), signalReceived("MySignal", { done: true }), ]) ).toMatchObject({ @@ -1553,8 +1549,8 @@ describe("signals", () => { test("timed out", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), ]) ).toMatchObject({ result: Result.failed(new Timeout("Expect Signal Timed Out")), @@ -1565,8 +1561,8 @@ describe("signals", () => { test("timed out then signal", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), signalReceived("MySignal", { done: true }), ]) ).toMatchObject({ @@ -1578,9 +1574,9 @@ describe("signals", () => { test("match signal then timeout", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), + timerScheduled(0), signalReceived("MySignal"), - completedAlarm(0), + timerCompleted(0), ]) ).toMatchObject({ result: Result.resolved("done"), @@ -1591,7 +1587,7 @@ describe("signals", () => { test("match signal twice", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), + timerScheduled(0), signalReceived("MySignal"), signalReceived("MySignal"), ]) @@ -1617,8 +1613,8 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), - scheduledAlarm("", 1), + timerScheduled(0), + timerScheduled(1), signalReceived("MySignal", "done!!!"), ]) ).toMatchObject({ @@ -1641,8 +1637,8 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), ]) ).toMatchObject({ result: Result.failed({ name: "Timeout" }), @@ -1664,9 +1660,9 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), + timerScheduled(0), signalReceived("SomethingElse"), - completedAlarm(0), + timerCompleted(0), ]) ).toMatchObject({ result: Result.failed({ name: "Timeout" }), @@ -1696,12 +1692,12 @@ describe("signals", () => { } ); - yield createAwaitTimeCall(""); + yield createAwaitTimeCall("then"); mySignalHandler.dispose(); myOtherSignalHandler.dispose(); - yield createAwaitTimeCall(""); + yield createAwaitTimeCall("then"); return { mySignalHappened, @@ -1714,7 +1710,7 @@ describe("signals", () => { expect(interpret(wf.definition(undefined, context), [])).toMatchObject(< WorkflowResult >{ - commands: [createAwaitTimeCommand("", 0)], + commands: [createStartTimerCommand(0)], }); }); @@ -1724,7 +1720,7 @@ describe("signals", () => { signalReceived("MySignal"), ]) ).toMatchObject({ - commands: [createAwaitTimeCommand("", 0)], + commands: [createStartTimerCommand(0)], }); }); @@ -1732,10 +1728,10 @@ describe("signals", () => { expect( interpret(wf.definition(undefined, context), [ signalReceived("MySignal"), - scheduledAlarm("", 0), - completedAlarm(0), - scheduledAlarm("", 1), - completedAlarm(1), + timerScheduled(0), + timerCompleted(0), + timerScheduled(1), + timerCompleted(1), ]) ).toMatchObject({ result: Result.resolved({ @@ -1753,10 +1749,10 @@ describe("signals", () => { signalReceived("MySignal"), signalReceived("MySignal"), signalReceived("MySignal"), - scheduledAlarm("", 0), - completedAlarm(0), - scheduledAlarm("", 1), - completedAlarm(1), + timerScheduled(0), + timerCompleted(0), + timerScheduled(1), + timerCompleted(1), ]) ).toMatchObject({ result: Result.resolved({ @@ -1771,13 +1767,13 @@ describe("signals", () => { test("send signal after dispose", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), signalReceived("MySignal"), signalReceived("MySignal"), signalReceived("MySignal"), - scheduledAlarm("", 1), - completedAlarm(1), + timerScheduled(1), + timerCompleted(1), ]) ).toMatchObject({ result: Result.resolved({ @@ -1796,7 +1792,7 @@ describe("signals", () => { ]) ).toMatchObject({ commands: [ - createAwaitTimeCommand("", 0), + createStartTimerCommand(0), createScheduledActivityCommand("act1", ["hi"], 1), ], }); @@ -1810,22 +1806,22 @@ describe("signals", () => { ]) ).toMatchObject({ commands: [ - createAwaitTimeCommand("", 0), + createStartTimerCommand(0), createScheduledActivityCommand("act1", ["hi"], 1), createScheduledActivityCommand("act1", ["hi2"], 2), ], }); }); - test("send other signal, wake sleep, with act scheduled", () => { + test("send other signal, wake timer, with act scheduled", () => { expect( interpret(wf.definition(undefined, context), [ signalReceived("MyOtherSignal", "hi"), - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), activityScheduled("act1", 1), - scheduledAlarm("", 2), - completedAlarm(2), + timerScheduled(2), + timerCompleted(2), ]) ).toMatchObject({ result: Result.resolved({ @@ -1837,16 +1833,16 @@ describe("signals", () => { }); }); - test("send other signal, wake sleep, complete activity", () => { + test("send other signal, wake timer, complete activity", () => { expect( interpret(wf.definition(undefined, context), [ signalReceived("MyOtherSignal", "hi"), - scheduledAlarm("", 0), + timerScheduled(0), activityScheduled("act1", 1), activitySucceeded("act1", 1), - completedAlarm(0), - scheduledAlarm("", 2), - completedAlarm(2), + timerCompleted(0), + timerScheduled(2), + timerCompleted(2), ]) ).toMatchObject({ result: Result.resolved({ @@ -1858,16 +1854,16 @@ describe("signals", () => { }); }); - test("send other signal, wake sleep, complete activity after dispose", () => { + test("send other signal, wake timer, complete activity after dispose", () => { expect( interpret(wf.definition(undefined, context), [ signalReceived("MyOtherSignal", "hi"), - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), activityScheduled("act1", 1), activitySucceeded("act1", 1), - scheduledAlarm("", 2), - completedAlarm(2), + timerScheduled(2), + timerCompleted(2), ]) ).toMatchObject({ result: Result.resolved({ @@ -1882,11 +1878,11 @@ describe("signals", () => { test("send other signal after dispose", () => { expect( interpret(wf.definition(undefined, context), [ - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), signalReceived("MyOtherSignal", "hi"), - scheduledAlarm("", 1), - completedAlarm(1), + timerScheduled(1), + timerCompleted(1), ]) ).toMatchObject({ result: Result.resolved({ @@ -2053,7 +2049,7 @@ describe("condition", () => { expect( interpret(wf.definition(undefined, context), []) ).toMatchObject({ - commands: [createAwaitDurationCommand(100, "seconds", 0)], + commands: [createStartTimerCommand(Schedule.relative(100), 0)], }); }); @@ -2066,7 +2062,7 @@ describe("condition", () => { }); expect( - interpret(wf.definition(undefined, context), [scheduledAlarm("", 0)]) + interpret(wf.definition(undefined, context), [timerScheduled(0)]) ).toMatchObject({ commands: [], }); @@ -2091,7 +2087,7 @@ describe("condition", () => { test("trigger success", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - scheduledAlarm("", 0), + timerScheduled(0), signalReceived("Yes"), ]) ).toMatchObject({ @@ -2103,7 +2099,7 @@ describe("condition", () => { test("trigger success eventually", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - scheduledAlarm("", 0), + timerScheduled(0), signalReceived("No"), signalReceived("No"), signalReceived("No"), @@ -2141,8 +2137,8 @@ describe("condition", () => { test("trigger timeout", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), ]) ).toMatchObject({ result: Result.resolved("timed out"), @@ -2153,9 +2149,9 @@ describe("condition", () => { test("trigger success before timeout", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - scheduledAlarm("", 0), + timerScheduled(0), signalReceived("Yes"), - completedAlarm(0), + timerCompleted(0), ]) ).toMatchObject({ result: Result.resolved("done"), @@ -2166,8 +2162,8 @@ describe("condition", () => { test("trigger timeout before success", () => { expect( interpret(signalConditionFlow.definition(undefined, context), [ - scheduledAlarm("", 0), - completedAlarm(0), + timerScheduled(0), + timerCompleted(0), signalReceived("Yes"), ]) ).toMatchObject({ @@ -2194,7 +2190,7 @@ test("nestedChains", () => { const wf = workflow(function* () { const funcs = { a: chain(function* () { - yield createAwaitTimeCall(""); + yield createAwaitTimeCall("then"); }), }; @@ -2212,7 +2208,7 @@ test("nestedChains", () => { expect( interpret(wf.definition(undefined, context), []) ).toMatchObject({ - commands: [createAwaitTimeCommand("", 0)], + commands: [createStartTimerCommand(0)], }); }); diff --git a/packages/@eventual/testing/src/clients/timer-client.ts b/packages/@eventual/testing/src/clients/timer-client.ts index 461f53905..b263be7f9 100644 --- a/packages/@eventual/testing/src/clients/timer-client.ts +++ b/packages/@eventual/testing/src/clients/timer-client.ts @@ -1,5 +1,6 @@ import { assertNever, + computeScheduleDate, isActivityHeartbeatMonitorRequest, isTimerScheduleEventRequest, TimerClient, @@ -9,17 +10,11 @@ import { TimeConnector } from "../environment.js"; export class TestTimerClient extends TimerClient { constructor(private timeConnector: TimeConnector) { - super(); + super(() => timeConnector.getTime()); } public async startShortTimer(timerRequest: TimerRequest): Promise { - const time = - timerRequest.schedule.type === "Absolute" - ? new Date(timerRequest.schedule.untilTime) - : new Date( - timerRequest.schedule.baseTime.getTime() + - timerRequest.schedule.timerSeconds * 1000 - ); + const time = computeScheduleDate(timerRequest.schedule, this.baseTime()); const seconds = (time.getTime() - this.timeConnector.getTime().getTime()) / 1000; From 0dab6d2b5ccaa824451293df03dac89839a39c89 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Fri, 13 Jan 2023 15:42:23 -0600 Subject: [PATCH 12/14] move schedule out of timer client --- packages/@eventual/core/src/command.ts | 2 +- packages/@eventual/core/src/index.ts | 1 + packages/@eventual/core/src/interpret.ts | 2 +- .../core/src/runtime/clients/timer-client.ts | 35 +------------------ .../core/src/runtime/command-executor.ts | 3 +- .../src/runtime/handlers/activity-worker.ts | 7 ++-- .../core/src/runtime/handlers/orchestrator.ts | 3 +- .../src/runtime/handlers/timer-handler.ts | 2 +- packages/@eventual/core/src/schedule.ts | 33 +++++++++++++++++ .../core/test/commend-executor.test.ts | 2 +- 10 files changed, 45 insertions(+), 45 deletions(-) create mode 100644 packages/@eventual/core/src/schedule.ts diff --git a/packages/@eventual/core/src/command.ts b/packages/@eventual/core/src/command.ts index 89ee20431..c8c22fe43 100644 --- a/packages/@eventual/core/src/command.ts +++ b/packages/@eventual/core/src/command.ts @@ -1,5 +1,5 @@ import { EventEnvelope } from "./event.js"; -import { Schedule } from "./index.js"; +import { Schedule } from "./schedule.js"; import { SignalTarget } from "./signals.js"; import { WorkflowOptions } from "./workflow.js"; diff --git a/packages/@eventual/core/src/index.ts b/packages/@eventual/core/src/index.ts index 0e0105bdf..955cd7ce8 100644 --- a/packages/@eventual/core/src/index.ts +++ b/packages/@eventual/core/src/index.ts @@ -15,6 +15,7 @@ export * from "./heartbeat.js"; export * from "./interpret.js"; export * from "./result.js"; export * from "./runtime/index.js"; +export * from "./schedule.js"; export * from "./secret.js"; export * from "./service-client.js"; export * from "./service-type.js"; diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index 0bea48b35..b7b73e507 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -72,7 +72,7 @@ import { isAwaitAllSettled } from "./await-all-settled.js"; import { isAwaitAny } from "./await-any.js"; import { isRace } from "./race.js"; import { isPublishEventsCall } from "./calls/send-events-call.js"; -import { Schedule } from "./index.js"; +import { Schedule } from "./schedule.js"; export interface WorkflowResult { /** diff --git a/packages/@eventual/core/src/runtime/clients/timer-client.ts b/packages/@eventual/core/src/runtime/clients/timer-client.ts index fb2ae512b..696eaf575 100644 --- a/packages/@eventual/core/src/runtime/clients/timer-client.ts +++ b/packages/@eventual/core/src/runtime/clients/timer-client.ts @@ -1,3 +1,4 @@ +import { computeScheduleDate, Schedule } from "../../schedule.js"; import { HistoryStateEvent } from "../../workflow-events.js"; export abstract class TimerClient { @@ -77,34 +78,6 @@ export enum TimerRequestType { ActivityHeartbeatMonitor = "CheckHeartbeat", } -export interface RelativeSchedule { - type: "Relative"; - timerSeconds: number; -} - -export interface AbsoluteSchedule { - type: "Absolute"; - untilTime: string; -} - -export type Schedule = RelativeSchedule | AbsoluteSchedule; - -export const Schedule = { - relative(timerSeconds: number): RelativeSchedule { - return { - type: "Relative", - timerSeconds, - }; - }, - absolute(untilTime: string | Date): AbsoluteSchedule { - return { - type: "Absolute", - untilTime: - typeof untilTime === "string" ? untilTime : untilTime.toISOString(), - }; - }, -}; - export type TimerRequestBase = { type: T; schedule: Schedule; @@ -156,9 +129,3 @@ export interface ScheduleEventRequest extends Omit { event: Omit; } - -export function computeScheduleDate(schedule: Schedule, baseTime: Date): Date { - return "untilTime" in schedule - ? new Date(schedule.untilTime) - : new Date(baseTime.getTime() + schedule.timerSeconds * 1000); -} diff --git a/packages/@eventual/core/src/runtime/command-executor.ts b/packages/@eventual/core/src/runtime/command-executor.ts index 1c7455ffd..8e0b46438 100644 --- a/packages/@eventual/core/src/runtime/command-executor.ts +++ b/packages/@eventual/core/src/runtime/command-executor.ts @@ -26,11 +26,12 @@ import { assertNever } from "../util.js"; import { Workflow } from "../workflow.js"; import { formatChildExecutionName, formatExecutionId } from "./execution-id.js"; import { ActivityWorkerRequest } from "./handlers/activity-worker.js"; -import { computeScheduleDate, TimerClient } from "./clients/timer-client.js"; +import { TimerClient } from "./clients/timer-client.js"; import { WorkflowRuntimeClient } from "./clients/workflow-runtime-client.js"; import { WorkflowClient } from "./clients/workflow-client.js"; import { EventClient } from "./clients/event-client.js"; import { isChildExecutionTarget } from "../signals.js"; +import { computeScheduleDate } from "../schedule.js"; interface CommandExecutorProps { workflowRuntimeClient: WorkflowRuntimeClient; diff --git a/packages/@eventual/core/src/runtime/handlers/activity-worker.ts b/packages/@eventual/core/src/runtime/handlers/activity-worker.ts index 0f6edc6a0..be294cc42 100644 --- a/packages/@eventual/core/src/runtime/handlers/activity-worker.ts +++ b/packages/@eventual/core/src/runtime/handlers/activity-worker.ts @@ -22,11 +22,7 @@ import { timed } from "../metrics/utils.js"; import { ActivityProvider } from "../providers/activity-provider.js"; import { ActivityNotFoundError } from "../../error.js"; import { extendsError } from "../../util.js"; -import { - Schedule, - TimerClient, - TimerRequestType, -} from "../clients/timer-client.js"; +import { TimerClient, TimerRequestType } from "../clients/timer-client.js"; import { RuntimeServiceClient } from "../clients/runtime-service-clients.js"; import { ActivityLogContext, @@ -37,6 +33,7 @@ import { import { EventClient } from "../clients/event-client.js"; import { serviceTypeScope } from "../flags.js"; import { ServiceType } from "../../service-type.js"; +import { Schedule } from "../../schedule.js"; export interface CreateActivityWorkerProps { activityRuntimeClient: ActivityRuntimeClient; diff --git a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts index e5d9822a4..c2578bbd5 100644 --- a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts +++ b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts @@ -55,13 +55,14 @@ import { interpret } from "../../interpret.js"; import { clearEventualCollector } from "../../global.js"; import { DeterminismError } from "../../error.js"; import { ExecutionHistoryClient } from "../clients/execution-history-client.js"; -import { Schedule, TimerClient } from "../clients/timer-client.js"; +import { TimerClient } from "../clients/timer-client.js"; import { WorkflowRuntimeClient } from "../clients/workflow-runtime-client.js"; import { WorkflowClient } from "../clients/workflow-client.js"; import { MetricsClient } from "../clients/metrics-client.js"; import { EventClient } from "../clients/event-client.js"; import { serviceTypeScope } from "../flags.js"; import { ServiceType } from "../../service-type.js"; +import { Schedule } from "../../schedule.js"; /** * The Orchestrator's client dependencies. diff --git a/packages/@eventual/core/src/runtime/handlers/timer-handler.ts b/packages/@eventual/core/src/runtime/handlers/timer-handler.ts index 4b56e36f9..f93b026ef 100644 --- a/packages/@eventual/core/src/runtime/handlers/timer-handler.ts +++ b/packages/@eventual/core/src/runtime/handlers/timer-handler.ts @@ -7,7 +7,6 @@ import { assertNever } from "../../util.js"; import { isActivityHeartbeatMonitorRequest, isTimerScheduleEventRequest, - Schedule, TimerClient, TimerRequest, TimerRequestType, @@ -15,6 +14,7 @@ import { import type { WorkflowClient } from "../clients/workflow-client.js"; import { ActivityRuntimeClient } from "../clients/activity-runtime-client.js"; import { LogAgent, LogContextType, LogLevel } from "../log-agent.js"; +import { Schedule } from "../../schedule.js"; interface TimerHandlerProps { workflowClient: WorkflowClient; diff --git a/packages/@eventual/core/src/schedule.ts b/packages/@eventual/core/src/schedule.ts new file mode 100644 index 000000000..41b04956b --- /dev/null +++ b/packages/@eventual/core/src/schedule.ts @@ -0,0 +1,33 @@ +export interface RelativeSchedule { + type: "Relative"; + timerSeconds: number; +} + +export interface AbsoluteSchedule { + type: "Absolute"; + untilTime: string; +} + +export type Schedule = RelativeSchedule | AbsoluteSchedule; + +export const Schedule = { + relative(timerSeconds: number): RelativeSchedule { + return { + type: "Relative", + timerSeconds, + }; + }, + absolute(untilTime: string | Date): AbsoluteSchedule { + return { + type: "Absolute", + untilTime: + typeof untilTime === "string" ? untilTime : untilTime.toISOString(), + }; + }, +}; + +export function computeScheduleDate(schedule: Schedule, baseTime: Date): Date { + return "untilTime" in schedule + ? new Date(schedule.untilTime) + : new Date(baseTime.getTime() + schedule.timerSeconds * 1000); +} diff --git a/packages/@eventual/core/test/commend-executor.test.ts b/packages/@eventual/core/test/commend-executor.test.ts index 485f4af5a..a46305eb3 100644 --- a/packages/@eventual/core/test/commend-executor.test.ts +++ b/packages/@eventual/core/test/commend-executor.test.ts @@ -15,13 +15,13 @@ import { formatChildExecutionName, formatExecutionId, INTERNAL_EXECUTION_ID_PREFIX, + Schedule, SendSignalRequest, SignalTargetType, WorkflowClient, WorkflowRuntimeClient, } from "../src/index.js"; import { - Schedule, ScheduleEventRequest, TimerClient, } from "../src/runtime/clients/timer-client.js"; From 73bafacdeaff90d0316650bf1869b059550086c3 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Fri, 13 Jan 2023 16:03:58 -0600 Subject: [PATCH 13/14] join schedule and duration spec concepts --- .../aws-runtime/src/clients/timer-client.ts | 21 +++-- .../src/clients/workflow-client.ts | 8 +- .../src/handlers/api/executions/new.ts | 6 +- packages/@eventual/cli/src/commands/start.ts | 6 +- packages/@eventual/core/src/activity.ts | 7 +- packages/@eventual/core/src/await-time.ts | 39 ++------- .../core/src/calls/await-time-call.ts | 2 +- packages/@eventual/core/src/interpret.ts | 12 +-- .../src/runtime/handlers/activity-worker.ts | 2 +- .../core/src/runtime/handlers/orchestrator.ts | 2 +- .../src/runtime/handlers/timer-handler.ts | 2 +- packages/@eventual/core/src/schedule.ts | 82 +++++++++++++++---- packages/@eventual/core/src/util.ts | 26 ------ packages/@eventual/core/src/workflow.ts | 4 +- packages/@eventual/core/test/command-util.ts | 2 +- .../core/test/commend-executor.test.ts | 4 +- .../@eventual/core/test/interpret.test.ts | 10 +-- .../testing/src/clients/workflow-client.ts | 8 +- 18 files changed, 114 insertions(+), 129 deletions(-) diff --git a/packages/@eventual/aws-runtime/src/clients/timer-client.ts b/packages/@eventual/aws-runtime/src/clients/timer-client.ts index 2d94e8142..1cc071308 100644 --- a/packages/@eventual/aws-runtime/src/clients/timer-client.ts +++ b/packages/@eventual/aws-runtime/src/clients/timer-client.ts @@ -16,6 +16,9 @@ import { TimerRequest, isActivityHeartbeatMonitorRequest, computeScheduleDate, + Schedule, + isTimeSchedule, + computeDurationSeconds, } from "@eventual/core"; import { ulid } from "ulidx"; @@ -52,7 +55,10 @@ export class AWSTimerClient extends TimerClient { * the {@link TimerRequest} provided. */ public async startShortTimer(timerRequest: TimerRequest) { - const delaySeconds = computeTimerSeconds(timerRequest.schedule); + const delaySeconds = computeTimerSeconds( + timerRequest.schedule, + this.baseTime() + ); if (delaySeconds > 15 * 60) { throw new Error( @@ -88,7 +94,10 @@ export class AWSTimerClient extends TimerClient { timerRequest.schedule, this.baseTime() ); - const timerDuration = computeTimerSeconds(timerRequest.schedule); + const timerDuration = computeTimerSeconds( + timerRequest.schedule, + this.baseTime() + ); /** * If the timer is longer than 15 minutes, create an EventBridge schedule first. @@ -205,16 +214,16 @@ function safeScheduleName(name: string) { return name.replaceAll(/[^0-9a-zA-Z-_.]/g, ""); } -export function computeTimerSeconds(schedule: TimerRequest["schedule"]) { - return "untilTime" in schedule +function computeTimerSeconds(schedule: Schedule, baseTime: Date) { + return isTimeSchedule(schedule) ? Math.max( // Compute the number of seconds (floored) // subtract 1 because the maxBatchWindow is set to 1s on the lambda event source. // this allows for more events to be sent at once while not adding extra latency Math.ceil( - (new Date(schedule.untilTime).getTime() - new Date().getTime()) / 1000 + (new Date(schedule.isoDate).getTime() - baseTime.getTime()) / 1000 ), 0 ) - : schedule.timerSeconds; + : computeDurationSeconds(schedule.dur, schedule.unit); } diff --git a/packages/@eventual/aws-runtime/src/clients/workflow-client.ts b/packages/@eventual/aws-runtime/src/clients/workflow-client.ts index 308ad4397..4a8dad199 100644 --- a/packages/@eventual/aws-runtime/src/clients/workflow-client.ts +++ b/packages/@eventual/aws-runtime/src/clients/workflow-client.ts @@ -25,7 +25,7 @@ import { lookupWorkflow, SortOrder, isExecutionStatus, - computeDurationDate, + computeScheduleDate, } from "@eventual/core"; import { ulid } from "ulidx"; import { inspect } from "util"; @@ -109,11 +109,7 @@ export class AWSWorkflowClient extends WorkflowClient { // generate the time for the workflow to timeout based on when it was started. // the timer will be started by the orchestrator so the client does not need to have access to the timer client. timeoutTime: timeout - ? computeDurationDate( - new Date(), - timeout.dur, - timeout.unit - ).toISOString() + ? computeScheduleDate(timeout, this.baseTime()).toISOString() : undefined, context: { name: executionName, diff --git a/packages/@eventual/aws-runtime/src/handlers/api/executions/new.ts b/packages/@eventual/aws-runtime/src/handlers/api/executions/new.ts index 87d57e3e9..fa13917c0 100644 --- a/packages/@eventual/aws-runtime/src/handlers/api/executions/new.ts +++ b/packages/@eventual/aws-runtime/src/handlers/api/executions/new.ts @@ -5,6 +5,7 @@ import { DurationUnit, DURATION_UNITS, isDurationUnit, + Schedule, StartExecutionResponse, } from "@eventual/core"; import type { @@ -62,10 +63,7 @@ export const handler: APIGatewayProxyHandlerV2 = input: event.body && JSON.parse(event.body), executionName, timeout: timeout - ? { - dur: timeout, - unit: (timeoutUnit as DurationUnit) ?? "seconds", - } + ? Schedule.duration(timeout, timeoutUnit as DurationUnit) : undefined, }); }); diff --git a/packages/@eventual/cli/src/commands/start.ts b/packages/@eventual/cli/src/commands/start.ts index 4c854376c..47ea0bf8a 100644 --- a/packages/@eventual/cli/src/commands/start.ts +++ b/packages/@eventual/cli/src/commands/start.ts @@ -6,6 +6,7 @@ import { ExecutionEventsResponse, DURATION_UNITS, DurationUnit, + Schedule, } from "@eventual/core"; import { Argv } from "yargs"; import { serviceAction, setServiceOptions } from "../service-action.js"; @@ -103,10 +104,7 @@ export const start = (yargs: Argv) => input: inputJSON, executionName: args.name, timeout: args.timeout - ? { - dur: args.timeout, - unit: args.timeoutUnit as DurationUnit, - } + ? Schedule.duration(args.timeout, args.timeoutUnit as DurationUnit) : undefined, }); } diff --git a/packages/@eventual/core/src/activity.ts b/packages/@eventual/core/src/activity.ts index 1766e34bc..4378c8df3 100644 --- a/packages/@eventual/core/src/activity.ts +++ b/packages/@eventual/core/src/activity.ts @@ -1,4 +1,3 @@ -import { DurationSpec } from "./await-time.js"; import { createActivityCall } from "./calls/activity-call.js"; import { createAwaitDurationCall } from "./calls/await-time-call.js"; import { @@ -6,7 +5,7 @@ import { getActivityContext, getServiceClient, } from "./global.js"; -import { computeDurationSeconds } from "./index.js"; +import { computeDurationSeconds, DurationSchedule } from "./index.js"; import { isActivityWorker, isOrchestratorWorker } from "./runtime/flags.js"; import { EventualServiceClient, @@ -22,7 +21,7 @@ export interface ActivityOptions { * * @default - workflow will run forever. */ - timeout?: DurationSpec; + timeout?: DurationSchedule; /** * For long running activities, it is suggested that they report back that they * are still in progress to avoid waiting forever or until a long timeout when @@ -33,7 +32,7 @@ export interface ActivityOptions { * * If it fails to do so, the workflow will cancel the activity and throw an error. */ - heartbeatTimeout?: DurationSpec; + heartbeatTimeout?: DurationSchedule; } export interface ActivityFunction { diff --git a/packages/@eventual/core/src/await-time.ts b/packages/@eventual/core/src/await-time.ts index a9270dded..eba875bc0 100644 --- a/packages/@eventual/core/src/await-time.ts +++ b/packages/@eventual/core/src/await-time.ts @@ -3,32 +3,7 @@ import { createAwaitTimeCall, } from "./calls/await-time-call.js"; import { isOrchestratorWorker } from "./runtime/flags.js"; - -export const DURATION_UNITS = [ - "second", - "seconds", - "minute", - "minutes", - "hour", - "hours", - "day", - "days", - "year", - "years", -] as const; -export type DurationUnit = typeof DURATION_UNITS[number]; - -export function isDurationUnit(u: string): u is DurationUnit { - return DURATION_UNITS.includes(u as any); -} - -export interface TimeSpec { - isoDate: string; -} -export interface DurationSpec { - dur: number; - unit: DurationUnit; -} +import { DurationSchedule, DurationUnit, TimeSchedule } from "./schedule.js"; /** * Represents a time duration. @@ -77,9 +52,9 @@ export interface DurationSpec { export function duration( dur: number, unit: DurationUnit = "seconds" -): Promise & DurationSpec { +): Promise & DurationSchedule { if (!isOrchestratorWorker()) { - return { dur, unit } as Promise & DurationSpec; + return { dur, unit } as Promise & DurationSchedule; } // register an await duration command and return it (to be yielded) @@ -109,14 +84,14 @@ export function duration( * }) * ``` */ -export function time(isoDate: string): Promise & TimeSpec; -export function time(date: Date): Promise & TimeSpec; -export function time(date: Date | string): Promise & TimeSpec { +export function time(isoDate: string): Promise & TimeSchedule; +export function time(date: Date): Promise & TimeSchedule; +export function time(date: Date | string): Promise & TimeSchedule { const d = new Date(date); const iso = d.toISOString(); if (!isOrchestratorWorker()) { - return { isoDate: iso } as Promise & TimeSpec; + return { isoDate: iso } as Promise & TimeSchedule; } // register an await time command and return it (to be yielded) diff --git a/packages/@eventual/core/src/calls/await-time-call.ts b/packages/@eventual/core/src/calls/await-time-call.ts index 5830db7dd..8e0cba955 100644 --- a/packages/@eventual/core/src/calls/await-time-call.ts +++ b/packages/@eventual/core/src/calls/await-time-call.ts @@ -1,4 +1,3 @@ -import { DurationUnit } from "../await-time.js"; import { EventualKind, EventualBase, @@ -7,6 +6,7 @@ import { } from "../eventual.js"; import { registerEventual } from "../global.js"; import { Resolved } from "../result.js"; +import { DurationUnit } from "../schedule.js"; export function isAwaitDurationCall(a: any): a is AwaitDurationCall { return isEventualOfKind(EventualKind.AwaitDurationCall, a); diff --git a/packages/@eventual/core/src/interpret.ts b/packages/@eventual/core/src/interpret.ts index b7b73e507..4fe961351 100644 --- a/packages/@eventual/core/src/interpret.ts +++ b/packages/@eventual/core/src/interpret.ts @@ -44,13 +44,7 @@ import { isResolvedOrFailed, } from "./result.js"; import { createChain, isChain, Chain } from "./chain.js"; -import { - assertNever, - _Iterator, - iterator, - or, - computeDurationSeconds, -} from "./util.js"; +import { assertNever, _Iterator, iterator, or } from "./util.js"; import { Command, CommandType } from "./command.js"; import { isAwaitDurationCall, @@ -224,8 +218,8 @@ export function interpret( kind: CommandType.StartTimer, seq: call.seq!, schedule: isAwaitTimeCall(call) - ? Schedule.absolute(call.isoDate) - : Schedule.relative(computeDurationSeconds(call.dur, call.unit)), + ? Schedule.time(call.isoDate) + : Schedule.duration(call.dur, call.unit), }; } else if (isWorkflowCall(call)) { return { diff --git a/packages/@eventual/core/src/runtime/handlers/activity-worker.ts b/packages/@eventual/core/src/runtime/handlers/activity-worker.ts index be294cc42..f273637c3 100644 --- a/packages/@eventual/core/src/runtime/handlers/activity-worker.ts +++ b/packages/@eventual/core/src/runtime/handlers/activity-worker.ts @@ -135,7 +135,7 @@ export function createActivityWorker({ type: TimerRequestType.ActivityHeartbeatMonitor, executionId: request.executionId, heartbeatSeconds: request.command.heartbeatSeconds, - schedule: Schedule.relative(request.command.heartbeatSeconds), + schedule: Schedule.duration(request.command.heartbeatSeconds), }); } setActivityContext({ diff --git a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts index c2578bbd5..b3d46be5e 100644 --- a/packages/@eventual/core/src/runtime/handlers/orchestrator.ts +++ b/packages/@eventual/core/src/runtime/handlers/orchestrator.ts @@ -294,7 +294,7 @@ export function createOrchestrator({ OrchestratorMetrics.TimeoutStartedDuration, () => timerClient.scheduleEvent({ - schedule: Schedule.absolute(newWorkflowStart.timeoutTime!), + schedule: Schedule.time(newWorkflowStart.timeoutTime!), event: createEvent( { type: WorkflowEventType.WorkflowTimedOut, diff --git a/packages/@eventual/core/src/runtime/handlers/timer-handler.ts b/packages/@eventual/core/src/runtime/handlers/timer-handler.ts index f93b026ef..f02ffc223 100644 --- a/packages/@eventual/core/src/runtime/handlers/timer-handler.ts +++ b/packages/@eventual/core/src/runtime/handlers/timer-handler.ts @@ -76,7 +76,7 @@ export function createTimerHandler({ activitySeq: request.activitySeq, executionId: request.executionId, heartbeatSeconds: request.heartbeatSeconds, - schedule: Schedule.relative(request.heartbeatSeconds), + schedule: Schedule.duration(request.heartbeatSeconds), }); } } else { diff --git a/packages/@eventual/core/src/schedule.ts b/packages/@eventual/core/src/schedule.ts index 41b04956b..4bffb9fc5 100644 --- a/packages/@eventual/core/src/schedule.ts +++ b/packages/@eventual/core/src/schedule.ts @@ -1,33 +1,81 @@ -export interface RelativeSchedule { - type: "Relative"; - timerSeconds: number; +import { assertNever } from "./util.js"; + +export const DURATION_UNITS = [ + "second", + "seconds", + "minute", + "minutes", + "hour", + "hours", + "day", + "days", + "year", + "years", +] as const; +export type DurationUnit = typeof DURATION_UNITS[number]; + +export function isDurationUnit(u: string): u is DurationUnit { + return DURATION_UNITS.includes(u as any); } -export interface AbsoluteSchedule { - type: "Absolute"; - untilTime: string; +export interface DurationSchedule { + type: "Duration"; + dur: number; + unit: DurationUnit; } -export type Schedule = RelativeSchedule | AbsoluteSchedule; +export interface TimeSchedule { + type: "Time"; + isoDate: string; +} + +export type Schedule = DurationSchedule | TimeSchedule; export const Schedule = { - relative(timerSeconds: number): RelativeSchedule { + duration(dur: number, unit: DurationUnit = "seconds"): DurationSchedule { return { - type: "Relative", - timerSeconds, + type: "Duration", + dur, + unit, }; }, - absolute(untilTime: string | Date): AbsoluteSchedule { + time(isoDate: string | Date): TimeSchedule { return { - type: "Absolute", - untilTime: - typeof untilTime === "string" ? untilTime : untilTime.toISOString(), + type: "Time", + isoDate: typeof isoDate === "string" ? isoDate : isoDate.toISOString(), }; }, }; +export function isDurationSchedule( + schedule: Schedule +): schedule is DurationSchedule { + return schedule.type === "Duration"; +} + +export function isTimeSchedule(schedule: Schedule): schedule is TimeSchedule { + return schedule.type === "Time"; +} + export function computeScheduleDate(schedule: Schedule, baseTime: Date): Date { - return "untilTime" in schedule - ? new Date(schedule.untilTime) - : new Date(baseTime.getTime() + schedule.timerSeconds * 1000); + return isTimeSchedule(schedule) + ? new Date(schedule.isoDate) + : new Date( + baseTime.getTime() + + computeDurationSeconds(schedule.dur, schedule.unit) * 1000 + ); +} + +export function computeDurationSeconds(dur: number, unit: DurationUnit) { + return unit === "seconds" || unit === "second" + ? dur + : unit === "minutes" || unit === "minute" + ? dur * 60 + : unit === "hours" || unit === "hour" + ? dur * 60 * 60 + : unit === "days" || unit === "day" + ? dur * 60 * 60 * 24 + : unit === "years" || unit === "year" + ? dur * 60 * 60 * 24 * 365.25 + : assertNever(unit); } diff --git a/packages/@eventual/core/src/util.ts b/packages/@eventual/core/src/util.ts index 8d0c5909e..10340eb0b 100644 --- a/packages/@eventual/core/src/util.ts +++ b/packages/@eventual/core/src/util.ts @@ -1,5 +1,3 @@ -import { DurationUnit } from "./await-time.js"; - export function assertNever(never: never, msg?: string): never { throw new Error(msg ?? `reached unreachable code with value ${never}`); } @@ -80,27 +78,3 @@ export function iterator( } } } - -export function computeDurationDate( - now: Date, - dur: number, - unit: DurationUnit -) { - const milliseconds = computeDurationSeconds(dur, unit) * 1000; - - return new Date(now.getTime() + milliseconds); -} - -export function computeDurationSeconds(dur: number, unit: DurationUnit) { - return unit === "seconds" || unit === "second" - ? dur - : unit === "minutes" || unit === "minute" - ? dur * 60 - : unit === "hours" || unit === "hour" - ? dur * 60 * 60 - : unit === "days" || unit === "day" - ? dur * 60 * 60 * 24 - : unit === "years" || unit === "year" - ? dur * 60 * 60 * 24 * 365.25 - : assertNever(unit); -} diff --git a/packages/@eventual/core/src/workflow.ts b/packages/@eventual/core/src/workflow.ts index f8c6b3fe6..e6de537d3 100644 --- a/packages/@eventual/core/src/workflow.ts +++ b/packages/@eventual/core/src/workflow.ts @@ -15,7 +15,7 @@ import { isOrchestratorWorker } from "./runtime/flags.js"; import { isChain } from "./chain.js"; import { ChildExecution, ExecutionHandle } from "./execution.js"; import { StartExecutionRequest } from "./service-client.js"; -import { DurationSpec } from "./await-time.js"; +import { DurationSchedule } from "./schedule.js"; export type WorkflowHandler = ( input: Input, @@ -33,7 +33,7 @@ export interface WorkflowOptions { * * @default - workflow will never timeout. */ - timeout?: DurationSpec; + timeout?: DurationSchedule; } export type WorkflowOutput = W extends Workflow< diff --git a/packages/@eventual/core/test/command-util.ts b/packages/@eventual/core/test/command-util.ts index b5c4d7086..0f40b6e8b 100644 --- a/packages/@eventual/core/test/command-util.ts +++ b/packages/@eventual/core/test/command-util.ts @@ -36,7 +36,7 @@ export function createStartTimerCommand( ...args: [schedule: Schedule, seq: number] | [seq: number] ): StartTimerCommand { const [schedule, seq] = - args.length === 1 ? [Schedule.absolute("then"), args[0]] : args; + args.length === 1 ? [Schedule.time("then"), args[0]] : args; return { kind: CommandType.StartTimer, schedule, diff --git a/packages/@eventual/core/test/commend-executor.test.ts b/packages/@eventual/core/test/commend-executor.test.ts index a46305eb3..530222231 100644 --- a/packages/@eventual/core/test/commend-executor.test.ts +++ b/packages/@eventual/core/test/commend-executor.test.ts @@ -67,7 +67,7 @@ describe("await times", () => { executionId, { kind: CommandType.StartTimer, - schedule: Schedule.absolute(baseTime), + schedule: Schedule.time(baseTime), seq: 0, }, baseTime @@ -80,7 +80,7 @@ describe("await times", () => { type: WorkflowEventType.TimerCompleted, seq: 0, }, - schedule: Schedule.absolute(baseTime.toISOString()), + schedule: Schedule.time(baseTime.toISOString()), executionId, }); diff --git a/packages/@eventual/core/test/interpret.test.ts b/packages/@eventual/core/test/interpret.test.ts index 81504ee7c..ca1024db9 100644 --- a/packages/@eventual/core/test/interpret.test.ts +++ b/packages/@eventual/core/test/interpret.test.ts @@ -525,7 +525,7 @@ test("should schedule duration", () => { } expect(interpret(workflow() as any, [])).toMatchObject({ - commands: [createStartTimerCommand(Schedule.relative(10), 0)], + commands: [createStartTimerCommand(Schedule.duration(10), 0)], }); }); @@ -563,9 +563,7 @@ test("should schedule time", () => { } expect(interpret(workflow() as any, [])).toMatchObject({ - commands: [ - createStartTimerCommand(Schedule.absolute(now.toISOString()), 0), - ], + commands: [createStartTimerCommand(Schedule.time(now.toISOString()), 0)], }); }); @@ -1510,7 +1508,7 @@ describe("signals", () => { expect(interpret(wf.definition(undefined, context), [])).toMatchObject(< WorkflowResult >{ - commands: [createStartTimerCommand(Schedule.relative(100 * 1000), 0)], + commands: [createStartTimerCommand(Schedule.duration(100 * 1000), 0)], }); }); @@ -2049,7 +2047,7 @@ describe("condition", () => { expect( interpret(wf.definition(undefined, context), []) ).toMatchObject({ - commands: [createStartTimerCommand(Schedule.relative(100), 0)], + commands: [createStartTimerCommand(Schedule.duration(100), 0)], }); }); diff --git a/packages/@eventual/testing/src/clients/workflow-client.ts b/packages/@eventual/testing/src/clients/workflow-client.ts index d4fbed975..73e2a04bb 100644 --- a/packages/@eventual/testing/src/clients/workflow-client.ts +++ b/packages/@eventual/testing/src/clients/workflow-client.ts @@ -1,6 +1,6 @@ import { ActivityRuntimeClient, - computeDurationDate, + computeScheduleDate, createEvent, Execution, ExecutionStatus, @@ -74,11 +74,7 @@ export class TestWorkflowClient extends WorkflowClient { workflowName, input: request.input, timeoutTime: request.timeout - ? computeDurationDate( - baseTime, - request.timeout.dur, - request.timeout.unit - ).toISOString() + ? computeScheduleDate(request.timeout, baseTime).toISOString() : undefined, }, baseTime From 06a5a34322d9edce928c00e1657554e355d9cef1 Mon Sep 17 00:00:00 2001 From: Sam Sussman Date: Fri, 13 Jan 2023 17:01:00 -0600 Subject: [PATCH 14/14] random things --- packages/@eventual/core/src/await-time.ts | 4 ++-- packages/@eventual/core/src/calls/activity-call.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/@eventual/core/src/await-time.ts b/packages/@eventual/core/src/await-time.ts index eba875bc0..2d05d6861 100644 --- a/packages/@eventual/core/src/await-time.ts +++ b/packages/@eventual/core/src/await-time.ts @@ -21,9 +21,9 @@ import { DurationSchedule, DurationUnit, TimeSchedule } from "./schedule.js"; * * ```ts * workflow("myWorkflow", async () => { - * const minTime = duration(10, "minutes"); // sleep for 10 minutes + * const minTime = duration(10, "minutes"); * // wait for 10 minutes OR the duration of myActivity, whichever is longer. - * await Promise.all([minTime, await myActivity()]); + * await Promise.all([minTime, myActivity()]); * return "DONE"; * }) * ``` diff --git a/packages/@eventual/core/src/calls/activity-call.ts b/packages/@eventual/core/src/calls/activity-call.ts index 5f1d618f9..55708af2e 100644 --- a/packages/@eventual/core/src/calls/activity-call.ts +++ b/packages/@eventual/core/src/calls/activity-call.ts @@ -21,7 +21,7 @@ export interface ActivityCall /** * Timeout can be any Eventual (promise). When the promise resolves, the activity is considered to be timed out. */ - timeout?: any; + timeout?: Eventual; } export function createActivityCall(