Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d743cdf

Browse files
committed
feat(workflow): Implement continueAsNew
1 parent 4ba0b86 commit d743cdf

13 files changed

+289
-42
lines changed

packages/core-bridge/Cargo.lock

Lines changed: 9 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core-bridge/sdk-core

Submodule sdk-core updated 44 files

packages/meta/README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ Learn how to use Temporal on the [docs site](https://docs.temporal.io/docs/node/
1818

1919
#### Features
2020

21-
Partial implementation of all components that make up a Temporal SDK - Worker, Client, Workflows, and Activities
21+
Mostly complete implementation of all components that make up a Temporal SDK - Worker, Client, Workflows, and Activities
2222

23+
- General
24+
- Interceptors
2325
- Workflows
2426
- Scheduling timers
2527
- Scheduling (non-local) Activities
2628
- Cancelling timers and Activities
2729
- Signals
30+
- Queries
2831
- Activities
2932
- Heartbeats
3033
- Cancellation
@@ -34,19 +37,17 @@ Partial implementation of all components that make up a Temporal SDK - Worker, C
3437
- Service client (for administration)
3538
- Worker
3639
- Basic logging and tracing capabilities
37-
- Polling on a single non-sticky task queue
3840

3941
Notably these features are missing:
4042

4143
- WF History pagination (only short Workflows are supported ATM)
42-
- Sticky queues (meaning Workflows are not cached and are replayed from the beginning each time a new event comes in)
4344
- Telemetry
4445
- Workflow versioning
4546
- Workflow cancellation
46-
- Query support
4747
- Local activities
4848
- Side effects
4949
- Windows support
50+
- Search attributes
5051

5152
> NOTE: The API is considered unstable and may change at any time.
5253
> While in alpha we are gathering feedback from developers about the usability and ergonomics of the API.

packages/test/src/test-integration.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { tsToMs } from '@temporalio/workflow/lib/time';
77
import { Worker, DefaultLogger } from '@temporalio/worker';
88
import * as iface from '@temporalio/proto';
99
import {
10+
WorkflowExecutionContinuedAsNewError,
1011
WorkflowExecutionFailedError,
1112
WorkflowExecutionTerminatedError,
1213
WorkflowExecutionTimedOutError,
@@ -22,6 +23,7 @@ import {
2223
AsyncFailable,
2324
Failable,
2425
CancellableHTTPRequest,
26+
ContinueAsNewFromMainAndSignal,
2527
} from './interfaces';
2628
import { httpGet } from './activities';
2729
import { u8, RUN_INTEGRATION_TESTS } from './helpers';
@@ -294,5 +296,44 @@ if (RUN_INTEGRATION_TESTS) {
294296
await t.throwsAsync(workflow.result(), { instanceOf: WorkflowExecutionTerminatedError, message: 'check 1 2' });
295297
});
296298

297-
test.todo('untilComplete throws if continued as new');
299+
test('untilComplete throws if continued as new', async (t) => {
300+
const client = new WorkflowClient();
301+
let workflow = client.stub<ContinueAsNewFromMainAndSignal>('continue-as-new-same-workflow', {
302+
taskQueue: 'test',
303+
});
304+
let err = await t.throwsAsync(workflow.execute(), { instanceOf: WorkflowExecutionContinuedAsNewError });
305+
if (!(err instanceof WorkflowExecutionContinuedAsNewError)) return; // Type assertion
306+
workflow = client.stub<ContinueAsNewFromMainAndSignal>(workflow.workflowId, err.newExecutionRunId);
307+
308+
await workflow.signal.continueAsNew();
309+
err = await t.throwsAsync(workflow.result(), {
310+
instanceOf: WorkflowExecutionContinuedAsNewError,
311+
});
312+
if (!(err instanceof WorkflowExecutionContinuedAsNewError)) return; // Type assertion
313+
314+
workflow = client.stub<ContinueAsNewFromMainAndSignal>(workflow.workflowId, err.newExecutionRunId);
315+
await workflow.result();
316+
});
317+
318+
test('continue-as-new-to-different-workflow', async (t) => {
319+
const client = new WorkflowClient();
320+
let workflow = client.stub<Empty>('continue-as-new-to-different-workflow', {
321+
taskQueue: 'test',
322+
});
323+
const err = await t.throwsAsync(workflow.execute(), { instanceOf: WorkflowExecutionContinuedAsNewError });
324+
if (!(err instanceof WorkflowExecutionContinuedAsNewError)) return; // Type assertion
325+
workflow = client.stub<Sleeper>(workflow.workflowId, err.newExecutionRunId);
326+
await workflow.result();
327+
const info = await workflow.describe();
328+
t.is(info.workflowExecutionInfo?.type?.name, 'sleep');
329+
const { history } = await client.service.getWorkflowExecutionHistory({
330+
namespace,
331+
execution: { workflowId: workflow.workflowId, runId: err.newExecutionRunId },
332+
});
333+
const timeSlept = defaultDataConverter.fromPayloads(
334+
0,
335+
history?.events?.[0].workflowExecutionStartedEventAttributes?.input?.payloads
336+
);
337+
t.is(timeSlept, 1);
338+
});
298339
}

packages/test/src/test-interceptors.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Connection, WorkflowClient, WorkflowStub } from '@temporalio/client';
1212
import * as errors from '@temporalio/workflow/lib/errors';
1313
import { defaultDataConverter } from '@temporalio/workflow';
1414
import { defaultOptions } from './mock-native-worker';
15-
import { Sleeper } from './interfaces';
15+
import { Sleeper, Empty } from './interfaces';
1616
import { RUN_INTEGRATION_TESTS } from './helpers';
1717

1818
if (RUN_INTEGRATION_TESTS) {
@@ -131,4 +131,28 @@ if (RUN_INTEGRATION_TESTS) {
131131
}
132132
}
133133
});
134+
135+
test.serial('Workflow continueAsNew can be intercepted', async (t) => {
136+
const taskQueue = 'test-continue-as-new-interceptor';
137+
const worker = await Worker.create({
138+
...defaultOptions,
139+
taskQueue,
140+
logger: new DefaultLogger('DEBUG'),
141+
interceptors: {
142+
// Includes an interceptor for ContinueAsNew that will throw an error when used with the workflow below
143+
workflowModules: ['interceptor-example'],
144+
},
145+
});
146+
const client = new WorkflowClient();
147+
const workerDrained = worker.run();
148+
const workflow = client.stub<Empty>('continue-as-new-to-different-workflow', {
149+
taskQueue,
150+
});
151+
await t.throwsAsync(workflow.execute(), {
152+
instanceOf: errors.WorkflowExecutionFailedError,
153+
message: 'Expected anything other than 1',
154+
});
155+
worker.shutdown();
156+
await workerDrained;
157+
});
134158
}

packages/test/src/test-workflows.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1518,3 +1518,25 @@ test('log-before-timing-out', async (t) => {
15181518
await t.throwsAsync(activate(t, makeStartWorkflow(script)), { message: 'Script execution timed out.' });
15191519
t.deepEqual(logs, ['logging before getting stuck']);
15201520
});
1521+
1522+
test('continue-as-new-same-workflow', async (t) => {
1523+
const { script } = t.context;
1524+
{
1525+
const req = await activate(t, makeStartWorkflow(script));
1526+
compareCompletion(
1527+
t,
1528+
req,
1529+
makeSuccess([
1530+
{
1531+
continueAsNewWorkflowExecution: {
1532+
workflowType: script,
1533+
taskQueue: 'test',
1534+
arguments: defaultDataConverter.toPayloads('signal'),
1535+
},
1536+
},
1537+
])
1538+
);
1539+
}
1540+
});
1541+
1542+
test.todo('no-commands-can-be-issued-once-workflow-completes');
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* Tests continueAsNew for the same Workflow from main and signal handler
3+
* @module
4+
*/
5+
import { Context, CancellationScope } from '@temporalio/workflow';
6+
import { ContinueAsNewFromMainAndSignal } from '../interfaces';
7+
8+
const signals = {
9+
async continueAsNew(): Promise<void> {
10+
await Context.continueAsNew<typeof main>('none');
11+
},
12+
};
13+
14+
async function main(continueFrom: 'main' | 'signal' | 'none' = 'main'): Promise<void> {
15+
if (continueFrom === 'none') {
16+
return;
17+
}
18+
if (continueFrom === 'main') {
19+
await Context.continueAsNew<typeof main>('signal');
20+
}
21+
await CancellationScope.current().cancelRequested;
22+
}
23+
24+
export const workflow: ContinueAsNewFromMainAndSignal = { main, signals };
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* Tests continueAsNew to another Workflow
3+
* @module
4+
*/
5+
import { Context } from '@temporalio/workflow';
6+
import { Empty, Sleeper } from '../interfaces';
7+
8+
async function main(): Promise<void> {
9+
const continueAsNew = Context.makeContinueAsNewFunc<Sleeper['main']>({ workflowType: 'sleep' });
10+
await continueAsNew(1);
11+
}
12+
13+
export const workflow: Empty = { main };

packages/test/src/workflows/interceptor-example.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ export const interceptors: WorkflowInterceptors = {
7070
}
7171
return next(input);
7272
},
73+
async continueAsNew(input, next) {
74+
// Used to test interception of continue-as-new-to-different-workflow
75+
if (input.args[0] === 1) {
76+
throw new InvalidTimerDurationError('Expected anything other than 1');
77+
}
78+
return next(input);
79+
},
7380
},
7481
],
7582
};

packages/workflow/src/interceptors.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import { coresdk } from '@temporalio/proto';
10-
import { ActivityOptions } from './interfaces';
10+
import { ActivityOptions, ContinueAsNewOptions } from './interfaces';
1111
import { AnyFunc, OmitLastParam } from './type-helpers';
1212

1313
/**
@@ -96,6 +96,13 @@ export interface TimerInput {
9696
readonly seq: number;
9797
}
9898

99+
/** Input for WorkflowOutboundCallsInterceptor.continueAsNew */
100+
export interface ContinueAsNewInput {
101+
args: unknown[];
102+
headers: Headers;
103+
options: ContinueAsNewOptions;
104+
}
105+
99106
/**
100107
* Implement any of these methods to intercept Workflow code calls to the Temporal APIs, like scheduling an activity and starting a timer
101108
*/
@@ -105,15 +112,16 @@ export interface WorkflowOutboundCallsInterceptor {
105112
*
106113
* @return result of the activity execution
107114
*/
108-
scheduleActivity?: (
109-
input: ActivityInput,
110-
next: Next<WorkflowOutboundCallsInterceptor, 'scheduleActivity'>
111-
) => Promise<unknown>;
115+
scheduleActivity?: (input: ActivityInput, next: Next<this, 'scheduleActivity'>) => Promise<unknown>;
112116

113117
/**
114118
* Called when Workflow starts a timer
115119
*/
116-
startTimer?: (input: TimerInput, next: Next<WorkflowOutboundCallsInterceptor, 'startTimer'>) => Promise<void>;
120+
startTimer?: (input: TimerInput, next: Next<this, 'startTimer'>) => Promise<void>;
121+
/**
122+
* Called when Workflow calls continueAsNew
123+
*/
124+
continueAsNew?: (input: ContinueAsNewInput, next: Next<this, 'continueAsNew'>) => Promise<never>;
117125
}
118126

119127
/**

packages/workflow/src/interfaces.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { coresdk } from '@temporalio/proto';
12
export * from './dependencies';
23

34
/**
@@ -164,3 +165,46 @@ export interface WorkflowInfo {
164165
*/
165166
isReplaying: boolean;
166167
}
168+
169+
/**
170+
* Not an actual error, used by the Workflow runtime to abort execution when {@link Context.continueAsNew} is called
171+
*/
172+
export class ContinueAsNew extends Error {
173+
public readonly type = 'ContinueAsNew';
174+
175+
constructor(public readonly command: coresdk.workflow_commands.IContinueAsNewWorkflowExecution) {
176+
super();
177+
}
178+
}
179+
180+
/**
181+
* Options for continuing a Workflow as new
182+
*/
183+
export interface ContinueAsNewOptions {
184+
/**
185+
* A string representing the Workflow type name, e.g. the filename in the Node.js SDK or class name in Java
186+
*/
187+
workflowType?: string;
188+
/**
189+
* Task queue to continue the Workflow in
190+
*/
191+
taskQueue?: string;
192+
/**
193+
* Timeout for the entire Workflow run
194+
* @format {@link https://www.npmjs.com/package/ms | ms} formatted string
195+
*/
196+
workflowRunTimeout?: string;
197+
/**
198+
* Timeout for a single Workflow task
199+
* @format {@link https://www.npmjs.com/package/ms | ms} formatted string
200+
*/
201+
workflowTaskTimeout?: string;
202+
/**
203+
* TODO: document
204+
*/
205+
memo?: Record<string, any>;
206+
/**
207+
* TODO: document
208+
*/
209+
searchAttributes?: Record<string, any>;
210+
}

0 commit comments

Comments
 (0)