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

Skip to content

Commit 81eb0fb

Browse files
authored
fix(workflow): Throw if patches are used at Workflow top level (temporalio#369)
1 parent 53cffb4 commit 81eb0fb

File tree

8 files changed

+81
-53
lines changed

8 files changed

+81
-53
lines changed

packages/test/src/test-workflows.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,20 @@ async function createWorkflow(
7474
startTime: number,
7575
workflowCreator: IsolatedVMWorkflowCreator
7676
) {
77-
const workflow = (await workflowCreator.createWorkflow(
78-
{
77+
const workflow = (await workflowCreator.createWorkflow({
78+
info: {
7979
workflowType,
8080
runId,
8181
workflowId: 'test-workflowId',
8282
namespace: 'default',
8383
taskQueue: 'test',
8484
isReplaying: false,
8585
},
86-
[],
87-
Long.fromInt(1337).toBytes(),
88-
startTime
89-
)) as IsolatedVMWorkflow;
86+
interceptorModules: [],
87+
randomnessSeed: Long.fromInt(1337).toBytes(),
88+
now: startTime,
89+
patches: [],
90+
})) as IsolatedVMWorkflow;
9091
return workflow;
9192
}
9293

@@ -1634,6 +1635,13 @@ test('deprecatePatchWorkflow', async (t) => {
16341635
t.deepEqual(logs, [['has change']]);
16351636
});
16361637

1638+
test('patchedTopLevel', async (t) => {
1639+
const { workflowType, logs } = t.context;
1640+
const completion = await activate(t, makeStartWorkflow(workflowType));
1641+
compareCompletion(t, completion, makeSuccess());
1642+
t.deepEqual(logs, [[['Patches cannot be used before Workflow starts']]]);
1643+
});
1644+
16371645
test('tryToContinueAfterCompletion', async (t) => {
16381646
const { workflowType } = t.context;
16391647
{

packages/test/src/workflows/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export * from './http';
4747
export * from './dependencies';
4848
export * from './interrupt-signal';
4949
export * from './patched';
50+
export * from './patched-top-level';
5051
export * from './deprecate-patch';
5152
export * from './fail-signal';
5253
export * from './async-fail-signal';
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { patched } from '@temporalio/workflow';
2+
3+
const startupErrors: string[] = [];
4+
5+
try {
6+
patched('should-sleep');
7+
} catch (err: any) {
8+
startupErrors.push(err.message);
9+
}
10+
11+
export async function patchedTopLevel(): Promise<void> {
12+
console.log(startupErrors);
13+
}

packages/worker/src/worker.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -598,13 +598,29 @@ export class Worker {
598598
isReplaying: activation.isReplaying,
599599
};
600600

601+
const patchJobs = activation.jobs.filter(
602+
(
603+
j
604+
): j is NonNullableObject<
605+
Pick<coresdk.workflow_activation.IWFActivationJob, 'notifyHasPatch'>
606+
> => j.notifyHasPatch != null
607+
);
608+
const patches = patchJobs.map(({ notifyHasPatch }) => {
609+
const { patchId } = notifyHasPatch;
610+
if (!patchId) {
611+
throw new TypeError('Got a patch without a patchId');
612+
}
613+
return patchId;
614+
});
615+
601616
const workflow = await instrument(this.tracer, span, 'workflow.create', async () => {
602-
return await workflowCreator.createWorkflow(
603-
workflowInfo,
604-
this.options.interceptors?.workflowModules ?? [],
605-
randomnessSeed.toBytes(),
606-
tsToMs(activation.timestamp)
607-
);
617+
return await workflowCreator.createWorkflow({
618+
info: workflowInfo,
619+
interceptorModules: this.options.interceptors?.workflowModules ?? [],
620+
randomnessSeed: randomnessSeed.toBytes(),
621+
now: tsToMs(activation.timestamp),
622+
patches,
623+
});
608624
});
609625
state = { workflow, info: workflowInfo };
610626
} else {

packages/worker/src/workflow/interface.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { coresdk } from '@temporalio/proto';
2-
import { WorkflowInfo } from '@temporalio/workflow';
32
import { ExternalCall } from '@temporalio/workflow/lib/dependencies';
3+
import { WorkflowCreateOptions } from '@temporalio/workflow/lib/worker-interface';
4+
export { WorkflowCreateOptions };
45

56
export interface Workflow {
67
/**
@@ -32,12 +33,7 @@ export interface WorkflowCreator {
3233
/**
3334
* Create a Workflow for the Worker to activate
3435
*/
35-
createWorkflow(
36-
info: WorkflowInfo,
37-
interceptorModules: string[],
38-
randomnessSeed: number[],
39-
now: number
40-
): Promise<Workflow>;
36+
createWorkflow(options: WorkflowCreateOptions): Promise<Workflow>;
4137

4238
/**
4339
* Destroy and cleanup any resources

packages/worker/src/workflow/isolated-vm.ts

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { coresdk } from '@temporalio/proto';
44
import * as internals from '@temporalio/workflow/lib/worker-interface';
55
import { ExternalDependencyFunction, WorkflowInfo, ExternalCall } from '@temporalio/workflow';
66
import { partition } from '../utils';
7-
import { Workflow, WorkflowCreator } from './interface';
7+
import { Workflow, WorkflowCreateOptions, WorkflowCreator } from './interface';
88

99
/**
1010
* Controls how an external dependency function is executed.
@@ -153,22 +153,10 @@ export class IsolatedVMWorkflowCreator implements WorkflowCreator {
153153
);
154154
}
155155

156-
async createWorkflow(
157-
info: WorkflowInfo,
158-
interceptorModules: string[],
159-
randomnessSeed: number[],
160-
now: number
161-
): Promise<Workflow> {
156+
async createWorkflow(options: WorkflowCreateOptions): Promise<Workflow> {
162157
const context = await this.getContext();
163-
await this.injectConsole(context, info);
164-
return await IsolatedVMWorkflow.create(
165-
context,
166-
info,
167-
interceptorModules,
168-
randomnessSeed,
169-
now,
170-
this.isolateExecutionTimeoutMs
171-
);
158+
await this.injectConsole(context, options.info);
159+
return await IsolatedVMWorkflow.create(context, options, this.isolateExecutionTimeoutMs);
172160
}
173161

174162
async destroy(): Promise<void> {
@@ -192,10 +180,7 @@ export class IsolatedVMWorkflow implements Workflow {
192180

193181
public static async create(
194182
context: ivm.Context,
195-
info: WorkflowInfo,
196-
interceptorModules: string[],
197-
randomnessSeed: number[],
198-
now: number,
183+
options: WorkflowCreateOptions,
199184
isolateExecutionTimeoutMs: number
200185
): Promise<IsolatedVMWorkflow> {
201186
const [activate, concludeActivation, getAndResetExternalCalls, tryUnblockConditions, isolateExtension] =
@@ -210,14 +195,13 @@ export class IsolatedVMWorkflow implements Workflow {
210195
.concat(isolateExtensionModule.create(context))
211196
);
212197

213-
await context.evalClosure(
214-
'lib.initRuntime($0, $1, $2, $3, $4)',
215-
[info, interceptorModules, randomnessSeed, now, isolateExtension.derefInto()],
216-
{ arguments: { copy: true }, timeout: isolateExecutionTimeoutMs }
217-
);
198+
await context.evalClosure('lib.initRuntime($0, $1)', [options, isolateExtension.derefInto()], {
199+
arguments: { copy: true },
200+
timeout: isolateExecutionTimeoutMs,
201+
});
218202

219203
return new this(
220-
info,
204+
options.info,
221205
context,
222206
{
223207
activate,

packages/workflow/src/worker-interface.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ import { ExternalCall } from './dependencies';
2121
import { WorkflowInterceptorsFactory } from './interceptors';
2222
import { HookManager, IsolateExtension } from './promise-hooks';
2323

24+
export interface WorkflowCreateOptions {
25+
info: WorkflowInfo;
26+
interceptorModules: string[];
27+
randomnessSeed: number[];
28+
now: number;
29+
patches: string[];
30+
}
31+
2432
export function setRequireFunc(fn: Exclude<typeof state['require'], undefined>): void {
2533
state.require = fn;
2634
}
@@ -99,10 +107,7 @@ export function overrideGlobals(): void {
99107
* Sets required internal state and instantiates the workflow and interceptors.
100108
*/
101109
export async function initRuntime(
102-
info: WorkflowInfo,
103-
interceptorModules: string[],
104-
randomnessSeed: number[],
105-
now: number,
110+
{ info, interceptorModules, randomnessSeed, now, patches }: WorkflowCreateOptions,
106111
isolateExtension: IsolateExtension
107112
): Promise<void> {
108113
// Globals are overridden while building the isolate before loading user code.
@@ -114,6 +119,11 @@ export async function initRuntime(
114119
state.now = now;
115120
state.random = alea(randomnessSeed);
116121
HookManager.instance.setIsolateExtension(isolateExtension);
122+
if (info.isReplaying) {
123+
for (const patch of patches) {
124+
state.knownPresentPatches.add(patch);
125+
}
126+
}
117127

118128
const { require: req } = state;
119129
if (req === undefined) {

packages/workflow/src/workflow.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -814,13 +814,13 @@ export function deprecatePatch(patchId: string): void {
814814
}
815815

816816
function patchInternal(patchId: string, deprecated: boolean): boolean {
817-
if (state.info === undefined) {
818-
throw new IllegalStateError('Workflow info must be set when calling patch functions');
819-
}
820817
// Patch operation does not support interception at the moment, if it did,
821818
// this would be the place to start the interception chain
822819

823-
const { isReplaying } = state.info;
820+
const { isReplaying } = workflowInfo();
821+
if (state.workflow === undefined) {
822+
throw new IllegalStateError('Patches cannot be used before Workflow starts');
823+
}
824824
const usePatch = !isReplaying || state.knownPresentPatches.has(patchId);
825825
// Avoid sending commands for patches core already knows about.
826826
// This optimization enables development of automatic patching tools.

0 commit comments

Comments
 (0)