Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: local workflow replay #42

Merged
merged 38 commits into from
Dec 2, 2022
Merged

feat: local workflow replay #42

merged 38 commits into from
Dec 2, 2022

Conversation

cfraz89
Copy link
Contributor

@cfraz89 cfraz89 commented Nov 23, 2022

eventual replay <entry> <workflow> <execution>.

TODO

  • encode workflow entry file in the infra, so it doesn't need to be specified.
  • Ensure compiler tests still run

@sam-goodwin sam-goodwin changed the title feat: Local workflow replay feat: local workflow replay Nov 25, 2022
@sam-goodwin sam-goodwin marked this pull request as ready for review November 25, 2022 12:47
packages/@eventual/aws-cdk/src/service.ts Outdated Show resolved Hide resolved
packages/@eventual/aws-cdk/src/service.ts Show resolved Hide resolved
packages/@eventual/aws-cdk/src/service.ts Show resolved Hide resolved
Comment on lines 446 to 463
"/executions/{executionId}/history": [
{
methods: [HttpMethod.GET],
entry: "executions/history.js",
config: (fn) => {
this.table.grantReadData(fn);
},
},
],
"/executions/{executionId}/workflow-history": [
{
methods: [HttpMethod.GET],
entry: "executions/workflow-history.js",
config: (fn) => {
this.history.grantRead(fn);
},
},
],
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've been thinking that we should bundle all the handlers into a single lambda function:

  1. we should not over-create resources in the customer's account
  2. minimize number of cold starts
  3. minimize impact of account-wide concurrent executions

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for the API? Do we have a monolith lambda router we like to use?

How would a monolith lambda help with concurrent executions?

Comment on lines 9 to 21
async function workflowHistory(
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2<HistoryStateEvent[]>> {
const executionId = event.pathParameters?.executionId;
if (!executionId) {
return { statusCode: 400, body: `Missing executionId` };
}

const workflowClient = createWorkflowRuntimeClient(getService());
return workflowClient.getHistory(decodeExecutionId(executionId));
}

export const handler = middy(workflowHistory).use(errorMiddleware);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are starting to look redundant. See my earlier comment - should we move all of these into a single handler?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a wrapper?

export const handler = withMiddy(workflowHandler);

packages/@eventual/compiler/src/eventual-bundle.ts Outdated Show resolved Hide resolved
Comment on lines -147 to +164
runtime: Runtime.NODEJS_16_X,
architecture: Architecture.ARM_64,
bundling: {
// https://github.com/aws/aws-cdk/issues/21329#issuecomment-1212336356
// cannot output as .mjs file as ulid does not support it.
mainFields: ["module", "main"],
esbuildArgs: {
"--conditions": "module,import,require",
},
metafile: true,
},
...baseNodeFnProps,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make a BaseNodeFn construct instead of merging in props?

Comment on lines 446 to 463
"/executions/{executionId}/history": [
{
methods: [HttpMethod.GET],
entry: "executions/history.js",
config: (fn) => {
this.table.grantReadData(fn);
},
},
],
"/executions/{executionId}/workflow-history": [
{
methods: [HttpMethod.GET],
entry: "executions/workflow-history.js",
config: (fn) => {
this.history.grantRead(fn);
},
},
],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just for the API? Do we have a monolith lambda router we like to use?

How would a monolith lambda help with concurrent executions?

packages/@eventual/aws-cdk/src/service.ts Show resolved Hide resolved
Comment on lines 9 to 21
async function workflowHistory(
event: APIGatewayProxyEventV2
): Promise<APIGatewayProxyResultV2<HistoryStateEvent[]>> {
const executionId = event.pathParameters?.executionId;
if (!executionId) {
return { statusCode: 400, body: `Missing executionId` };
}

const workflowClient = createWorkflowRuntimeClient(getService());
return workflowClient.getHistory(decodeExecutionId(executionId));
}

export const handler = middy(workflowHistory).use(errorMiddleware);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about a wrapper?

export const handler = withMiddy(workflowHandler);

packages/@eventual/cli/src/commands/replay.ts Outdated Show resolved Hide resolved
packages/@eventual/cli/src/service-action.ts Outdated Show resolved Hide resolved
packages/@eventual/cli/src/commands/replay.ts Outdated Show resolved Hide resolved
packages/@eventual/compiler/src/eventual-bundle.ts Outdated Show resolved Hide resolved
.vscode/settings.json Outdated Show resolved Hide resolved
packages/@eventual/aws-env/package.json Outdated Show resolved Hide resolved
packages/@eventual/aws-runtime/package.json Outdated Show resolved Hide resolved
packages/@eventual/aws-runtime/src/execution-id.ts Outdated Show resolved Hide resolved
packages/@eventual/cli/package.json Outdated Show resolved Hide resolved
packages/@eventual/cli/src/api-ky.ts Outdated Show resolved Hide resolved
@@ -42,7 +45,7 @@ export async function apiKy(region?: string): Promise<KyInstance> {
path: url.pathname,
protocol: url.protocol,
method: req.method.toUpperCase(),
body: (await req.body?.getReader().read())?.value,
body: req.body && (await streamToString(req.body)),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not req.text() ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It consumes the body, leaving it unreadable for the actual request

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that a problem? Is it used twice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, once for signing, and one to actually send the request

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you overwrite the body in the new one with the signed body?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I think getReader.read() also consumes the stream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok figured it out. When doing req.text() on node 14, polyfilled with node-fetch: it runs this in the consumeBody method:
data[INTERNALS].disturbed = true;
Which should be fine, since we're constructing a new request anyway. The new request uses a copy constructor, which unfortunately copied the disturbed flag, and hence would cause the new request to fail too. This was a quirk specific to node-fetch, on node 18, without the polyfill, text() was working fine.

I've changed the new request construction to just copy the minimum parameters it needs to work, url, method, headers, and body, so it doesnt copy the consumed flag. Now it is working fine on node 14.

packages/@eventual/compiler/src/build.ts Outdated Show resolved Hide resolved
//We box our cache in case our fn returns undefined
let resMap = new Map<any, { value: R }>();
return (...args) => {
let key = options?.cacheKey ? options.cacheKey(...args) : args;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we making use of the cache key concept?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of the function's we're memoing are parameterized. Although they may only ever currently be invoked with a single set of arguments currently, I think we're kind of breaking the function's contract by ignoring the parameters when memoing, and might lead to a nasty surprise down the road, so figured better to account for them.

packages/@eventual/aws-cdk/package.json Outdated Show resolved Hide resolved
packages/@eventual/aws-cdk/src/service.ts Show resolved Hide resolved
packages/@eventual/aws-cdk/src/service.ts Show resolved Hide resolved
packages/@eventual/aws-cdk/src/service.ts Show resolved Hide resolved
@@ -42,7 +45,7 @@ export async function apiKy(region?: string): Promise<KyInstance> {
path: url.pathname,
protocol: url.protocol,
method: req.method.toUpperCase(),
body: (await req.body?.getReader().read())?.value,
body: req.body && (await streamToString(req.body)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you overwrite the body in the new one with the signed body?

@@ -42,7 +45,7 @@ export async function apiKy(region?: string): Promise<KyInstance> {
path: url.pathname,
protocol: url.protocol,
method: req.method.toUpperCase(),
body: (await req.body?.getReader().read())?.value,
body: req.body && (await streamToString(req.body)),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I think getReader.read() also consumes the stream.

packages/@eventual/cli/src/cli.ts Outdated Show resolved Hide resolved
packages/@eventual/cli/src/commands/replay.ts Outdated Show resolved Hide resolved
"race",
];

export class OuterVisitor extends Visitor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is new? merge issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just moved from esbuild-plugin.ts isn't it?

Comment on lines -4 to -6
"module": "esnext",
"target": "esnext",
"moduleResolution": "node",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to stay, may be why the bundle size grew too.

Copy link
Contributor Author

@cfraz89 cfraz89 Dec 1, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved these properites tsconfig-base.json a while ago, so they'll be consistent across all projects.
Also tweaked them a bit.
module - esnext - so esm is default.
target: es2019. so it''ll compile down to node 14 es support.
moduleResolution: nodenext. needed for esm
This file was identical between both commits in the bundle size comparison fwiw

packages/@eventual/aws-runtime/tsconfig.cjs.json Outdated Show resolved Hide resolved
@thantos thantos merged commit f632589 into main Dec 2, 2022
@thantos thantos deleted the feat/local-replay branch June 16, 2023 15:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants