Skip to content

Commit 70864b6

Browse files
authored
Expose root execution info (#1662)
1 parent 8c7506b commit 70864b6

File tree

12 files changed

+140
-8
lines changed

12 files changed

+140
-8
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,3 +323,5 @@ jobs:
323323
secrets:
324324
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
325325
VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
326+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
327+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}

.github/workflows/docs.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ on:
1515
VERCEL_TOKEN:
1616
required: false
1717
description: The Vercel token. Required if 'publish_target' is set.
18+
VERCEL_ORG_ID:
19+
required: false
20+
description: The Vercel token. Required if 'publish_target' is set.
21+
VERCEL_PROJECT_ID:
22+
required: false
23+
description: The Vercel token. Required if 'publish_target' is set.
1824

1925
env:
2026
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -62,10 +68,11 @@ jobs:
6268

6369
- name: Publish docs
6470
if: ${{ inputs.publish_target }}
71+
env:
72+
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
73+
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
6574
run: |
6675
npx vercel deploy packages/docs/build \
6776
-t '${{ secrets.VERCEL_TOKEN }}' \
68-
--name typescript \
69-
--scope temporal \
7077
--yes \
7178
${{ inputs.publish_target == 'prod' && '--prod' || '' }}

packages/client/src/helpers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ export async function executionInfoFromRaw<T>(
7878
runId: raw.parentExecution.runId!,
7979
}
8080
: undefined,
81+
rootExecution: raw.rootExecution
82+
? {
83+
workflowId: raw.rootExecution.workflowId!,
84+
runId: raw.rootExecution.runId!,
85+
}
86+
: undefined,
8187
raw: rawDataToEmbed,
8288
priority: decodePriority(raw.priority),
8389
};

packages/client/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export interface WorkflowExecutionInfo {
5151
searchAttributes: SearchAttributes; // eslint-disable-line deprecation/deprecation
5252
typedSearchAttributes: TypedSearchAttributes;
5353
parentExecution?: Required<proto.temporal.api.common.v1.IWorkflowExecution>;
54+
rootExecution?: Required<proto.temporal.api.common.v1.IWorkflowExecution>;
5455
raw: RawWorkflowExecutionInfo;
5556
priority?: Priority;
5657
}

packages/test/src/test-integration-workflows.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { activityStartedSignal } from './workflows/definitions';
2424
import * as workflows from './workflows';
2525
import { Context, createLocalTestEnvironment, helpers, makeTestFunction } from './helpers-integration';
2626
import { overrideSdkInternalFlag } from './mock-internal-flags';
27-
import { asSdkLoggerSink, loadHistory, RUN_TIME_SKIPPING_TESTS } from './helpers';
27+
import { asSdkLoggerSink, loadHistory, RUN_TIME_SKIPPING_TESTS, waitUntil } from './helpers';
2828

2929
const test = makeTestFunction({
3030
workflowsPath: __filename,
@@ -1337,3 +1337,78 @@ test('can register search attributes to dev server', async (t) => {
13371337
t.deepEqual(desc.searchAttributes, { 'new-search-attr': [12] }); // eslint-disable-line deprecation/deprecation
13381338
await env.teardown();
13391339
});
1340+
1341+
export async function ChildWorkflowInfo(): Promise<workflow.RootWorkflowInfo | undefined> {
1342+
let blocked = true;
1343+
workflow.setHandler(unblockSignal, () => {
1344+
blocked = false;
1345+
});
1346+
await workflow.condition(() => !blocked);
1347+
return workflow.workflowInfo().root;
1348+
}
1349+
1350+
export async function WithChildWorkflow(childWfId: string): Promise<workflow.RootWorkflowInfo | undefined> {
1351+
return await workflow.executeChild(ChildWorkflowInfo, {
1352+
workflowId: childWfId,
1353+
});
1354+
}
1355+
1356+
test('root execution is exposed', async (t) => {
1357+
const { createWorker, startWorkflow } = helpers(t);
1358+
const worker = await createWorker();
1359+
1360+
await worker.runUntil(async () => {
1361+
const childWfId = 'child-wf-id';
1362+
const handle = await startWorkflow(WithChildWorkflow, {
1363+
args: [childWfId],
1364+
});
1365+
1366+
const childHandle = t.context.env.client.workflow.getHandle(childWfId);
1367+
const childStarted = async (): Promise<boolean> => {
1368+
try {
1369+
await childHandle.describe();
1370+
return true;
1371+
} catch (e) {
1372+
if (e instanceof workflow.WorkflowNotFoundError) {
1373+
return false;
1374+
} else {
1375+
throw e;
1376+
}
1377+
}
1378+
};
1379+
await waitUntil(childStarted, 5000);
1380+
const childDesc = await childHandle.describe();
1381+
const parentDesc = await handle.describe();
1382+
1383+
t.true(childDesc.rootExecution?.workflowId === parentDesc.workflowId);
1384+
t.true(childDesc.rootExecution?.runId === parentDesc.runId);
1385+
1386+
await childHandle.signal(unblockSignal);
1387+
const childWfInfoRoot = await handle.result();
1388+
t.true(childWfInfoRoot?.workflowId === parentDesc.workflowId);
1389+
t.true(childWfInfoRoot?.runId === parentDesc.runId);
1390+
});
1391+
});
1392+
1393+
export async function rootWorkflow(): Promise<string> {
1394+
let result = '';
1395+
if (!workflow.workflowInfo().root) {
1396+
result += 'empty';
1397+
} else {
1398+
result += workflow.workflowInfo().root!.workflowId;
1399+
}
1400+
if (!workflow.workflowInfo().parent) {
1401+
result += ' ';
1402+
result += await workflow.executeChild(rootWorkflow);
1403+
}
1404+
return result;
1405+
}
1406+
1407+
test('Workflow can return root workflow', async (t) => {
1408+
const { createWorker, executeWorkflow } = helpers(t);
1409+
const worker = await createWorker();
1410+
await worker.runUntil(async () => {
1411+
const result = await executeWorkflow(rootWorkflow, { workflowId: 'test-root-workflow-length' });
1412+
t.deepEqual(result, 'empty test-root-workflow-length');
1413+
});
1414+
});

packages/test/src/test-schedules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ if (RUN_INTEGRATION_TESTS) {
773773
const exists =
774774
desc.typedSearchAttributes.getAll().find((pair) => pair.key.name === attributeName) !== undefined;
775775
return exists === shouldExist;
776-
}, 300);
776+
}, 5000);
777777
return await handle.describe();
778778
};
779779

packages/test/src/test-sinks.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ if (RUN_INTEGRATION_TESTS) {
117117
lastResult: undefined,
118118
memo: {},
119119
parent: undefined,
120+
root: undefined,
120121
searchAttributes: {},
121122
// FIXME: consider rehydrating the class before passing to sink functions or
122123
// create a variant of WorkflowInfo that corresponds to what we actually get in sinks.

packages/test/src/test-typed-search-attributes.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ if (test?.serial?.before) {
135135
Object.keys(untypedKeys).every((key) => key in resp.customAttributes) &&
136136
Object.keys(typedKeys).every((key) => key in resp.customAttributes)
137137
);
138-
}, 300);
138+
}, 5000);
139139
});
140140
}
141141

packages/worker/src/utils.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { coresdk } from '@temporalio/proto';
2-
import { IllegalStateError, ParentWorkflowInfo } from '@temporalio/workflow';
1+
import type { coresdk, temporal } from '@temporalio/proto';
2+
import { IllegalStateError, ParentWorkflowInfo, RootWorkflowInfo } from '@temporalio/workflow';
33

44
export const MiB = 1024 ** 2;
55

@@ -28,3 +28,19 @@ export function convertToParentWorkflowType(
2828
namespace: parent.namespace,
2929
};
3030
}
31+
32+
export function convertToRootWorkflowType(
33+
root: temporal.api.common.v1.IWorkflowExecution | null | undefined
34+
): RootWorkflowInfo | undefined {
35+
if (root == null) {
36+
return undefined;
37+
}
38+
if (!root.workflowId || !root.runId) {
39+
throw new IllegalStateError('Root workflow execution is missing a field that should be defined');
40+
}
41+
42+
return {
43+
workflowId: root.workflowId,
44+
runId: root.runId,
45+
};
46+
}

packages/worker/src/worker.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ import {
7171
} from './replay';
7272
import { History, Runtime } from './runtime';
7373
import { CloseableGroupedObservable, closeableGroupBy, mapWithState, mergeMapWithState } from './rxutils';
74-
import { byteArrayToBuffer, convertToParentWorkflowType } from './utils';
74+
import { byteArrayToBuffer, convertToParentWorkflowType, convertToRootWorkflowType } from './utils';
7575
import {
7676
CompiledWorkerOptions,
7777
CompiledWorkerOptionsWithBuildId,
@@ -1259,6 +1259,7 @@ export class Worker {
12591259
randomnessSeed,
12601260
workflowType,
12611261
parentWorkflowInfo,
1262+
rootWorkflow,
12621263
workflowExecutionTimeout,
12631264
workflowRunTimeout,
12641265
workflowTaskTimeout,
@@ -1281,6 +1282,7 @@ export class Worker {
12811282
searchAttributes: {},
12821283
typedSearchAttributes: new TypedSearchAttributes(),
12831284
parent: convertToParentWorkflowType(parentWorkflowInfo),
1285+
root: convertToRootWorkflowType(rootWorkflow),
12841286
taskQueue: this.options.taskQueue,
12851287
namespace: this.options.namespace,
12861288
firstExecutionRunId,

packages/workflow/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ export {
9696
StackTraceFileSlice,
9797
ParentClosePolicy,
9898
ParentWorkflowInfo,
99+
RootWorkflowInfo,
99100
StackTraceSDKInfo,
100101
StackTrace,
101102
UnsafeWorkflowInfo,

packages/workflow/src/interfaces.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,22 @@ export interface WorkflowInfo {
6262
*/
6363
readonly parent?: ParentWorkflowInfo;
6464

65+
/**
66+
* The root workflow execution, defined as follows:
67+
* 1. A workflow without a parent workflow is its own root workflow.
68+
* 2. A workflow with a parent workflow has the same root workflow as
69+
* its parent.
70+
*
71+
* When there is no parent workflow, i.e., the workflow is its own root workflow,
72+
* this field is `undefined`.
73+
*
74+
* Note that Continue-as-New (or reset) propagates the workflow parentage relationship,
75+
* and therefore, whether the new workflow has the same root workflow as the original one
76+
* depends on whether it had a parent.
77+
*
78+
*/
79+
readonly root?: RootWorkflowInfo;
80+
6581
/**
6682
* Result from the previous Run (present if this is a Cron Workflow or was Continued As New).
6783
*
@@ -228,6 +244,11 @@ export interface ParentWorkflowInfo {
228244
namespace: string;
229245
}
230246

247+
export interface RootWorkflowInfo {
248+
workflowId: string;
249+
runId: string;
250+
}
251+
231252
/**
232253
* Not an actual error, used by the Workflow runtime to abort execution when {@link continueAsNew} is called
233254
*/

0 commit comments

Comments
 (0)