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: add support for scheduling functions #1527

Merged
merged 26 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
acec70c
feat: add support for scheduling functions
rtpascual May 16, 2024
a5d6533
update valid natural language and cron
rtpascual May 17, 2024
afbd6f3
Merge branch 'main' into schedule-functions
rtpascual May 17, 2024
f104c9a
Merge branch 'main' into schedule-functions
rtpascual Jun 11, 2024
4346570
pr feedback and add unit tests
rtpascual Jun 11, 2024
d7f26db
add test that throws when schedule will invoke function again before …
rtpascual Jun 14, 2024
562e275
update schedule rate validation
rtpascual Jun 18, 2024
255dfc5
Merge branch 'main' into schedule-functions
rtpascual Jul 9, 2024
46d8d08
Merge branch 'main' into schedule-functions
rtpascual Jul 10, 2024
3e0bccd
add tests
rtpascual Jul 10, 2024
4f797ec
update e2e test
rtpascual Jul 10, 2024
d86ea22
fix e2e test
rtpascual Jul 10, 2024
8e8063f
PR feedback
rtpascual Jul 15, 2024
760888a
Merge branch 'main' into schedule-functions
rtpascual Jul 15, 2024
79dfa9f
fix test
rtpascual Jul 15, 2024
2d11214
add more testing
rtpascual Jul 16, 2024
3c846fb
fix lint and e2e test
rtpascual Jul 16, 2024
f6821ba
try this
rtpascual Jul 16, 2024
f0311eb
try this
rtpascual Jul 16, 2024
0d7e55a
try that
rtpascual Jul 16, 2024
6b89ceb
try this
rtpascual Jul 16, 2024
46a2192
Merge branch 'main' into schedule-functions
rtpascual Jul 16, 2024
2950c72
Update packages/backend-function/src/factory.ts
rtpascual Jul 17, 2024
0fa4a84
Merge branch 'main' into schedule-functions
rtpascual Jul 17, 2024
28a6825
pr feedback
rtpascual Jul 17, 2024
ca2d0bd
pr feedback
rtpascual Jul 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/green-coins-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/backend-function': minor
---

add support for scheduling functions
1 change: 1 addition & 0 deletions .eslint_dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"commonjs",
"corepack",
"cors",
"cron",
"ctor",
"darwin",
"datastore",
Expand Down
10 changes: 10 additions & 0 deletions packages/backend-function/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export type AddEnvironmentFactory = {
addEnvironment: (key: string, value: string | BackendSecret) => void;
};

// @public (undocumented)
export type CronSchedule = `${string} ${string} ${string} ${string} ${string}` | `${string} ${string} ${string} ${string} ${string} ${string}`;

// @public
export const defineFunction: (props?: FunctionProps) => ConstructFactory<ResourceProvider<FunctionResources> & ResourceAccessAcceptorFactory & AddEnvironmentFactory>;

Expand All @@ -26,11 +29,18 @@ export type FunctionProps = {
memoryMB?: number;
environment?: Record<string, string | BackendSecret>;
runtime?: NodeVersion;
schedule?: FunctionSchedule | FunctionSchedule[];
};

// @public (undocumented)
export type FunctionSchedule = TimeInterval | CronSchedule;

// @public (undocumented)
export type NodeVersion = 16 | 18 | 20;

// @public (undocumented)
export type TimeInterval = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`;

// (No @packageDocumentation comment for this package)

```
71 changes: 71 additions & 0 deletions packages/backend-function/src/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,77 @@ void describe('AmplifyFunctionFactory', () => {
});
});

void describe('schedule property', () => {
void it('sets valid schedule - rate', () => {
const lambda = defineFunction({
entry: './test-assets/default-lambda/handler.ts',
schedule: 'every 5m',
}).getInstance(getInstanceProps);
const template = Template.fromStack(Stack.of(lambda.resources.lambda));

template.hasResourceProperties('AWS::Events::Rule', {
ScheduleExpression: 'cron(*/5 * * * ? *)',
Targets: [
{
Arn: {
// eslint-disable-next-line spellcheck/spell-checker
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
},
Id: 'Target0',
},
],
});
});

void it('sets valid schedule - cron', () => {
const lambda = defineFunction({
entry: './test-assets/default-lambda/handler.ts',
schedule: '0 1 * * ?',
}).getInstance(getInstanceProps);
const template = Template.fromStack(Stack.of(lambda.resources.lambda));

template.hasResourceProperties('AWS::Events::Rule', {
ScheduleExpression: 'cron(0 1 * * ? *)',
Targets: [
{
Arn: {
// eslint-disable-next-line spellcheck/spell-checker
'Fn::GetAtt': ['handlerlambdaE29D1580', 'Arn'],
},
Id: 'Target0',
},
],
});
});

void it('sets valid schedule array', () => {
const lambda = defineFunction({
entry: './test-assets/default-lambda/handler.ts',
schedule: ['0 1 * * ?', 'every 5m'],
}).getInstance(getInstanceProps);
const template = Template.fromStack(Stack.of(lambda.resources.lambda));

template.resourceCountIs('AWS::Events::Rule', 2);

template.hasResourceProperties('AWS::Events::Rule', {
ScheduleExpression: 'cron(0 1 * * ? *)',
});

template.hasResourceProperties('AWS::Events::Rule', {
ScheduleExpression: 'cron(*/5 * * * ? *)',
});
});

void it('defaults to no event rule created', () => {
const lambda = defineFunction({
entry: './test-assets/default-lambda/handler.ts',
}).getInstance(getInstanceProps);
const template = Template.fromStack(Stack.of(lambda.resources.lambda));

template.resourceCountIs('AWS::Events::Rule', 0);
});
});

void describe('resourceAccessAcceptor', () => {
void it('attaches policy to execution role and configures ssm environment context', () => {
const functionFactory = defineFunction({
Expand Down
73 changes: 72 additions & 1 deletion packages/backend-function/src/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,28 @@ import { FunctionEnvironmentTypeGenerator } from './function_env_type_generator.
import { AttributionMetadataStorage } from '@aws-amplify/backend-output-storage';
import { fileURLToPath } from 'node:url';
import { AmplifyUserError, TagName } from '@aws-amplify/platform-core';
import { convertFunctionSchedulesToRuleSchedules } from './schedule_parser.js';
import * as targets from 'aws-cdk-lib/aws-events-targets';
import { Rule } from 'aws-cdk-lib/aws-events';

const functionStackType = 'function-Lambda';

export type AddEnvironmentFactory = {
addEnvironment: (key: string, value: string | BackendSecret) => void;
};

export type CronSchedule =
| `${string} ${string} ${string} ${string} ${string}`
| `${string} ${string} ${string} ${string} ${string} ${string}`;
export type TimeInterval =
| `every ${number}m`
| `every ${number}h`
| `every day`
| `every week`
| `every month`
| `every year`;
export type FunctionSchedule = TimeInterval | CronSchedule;

/**
* Entry point for defining a function in the Amplify ecosystem
*/
Expand Down Expand Up @@ -93,6 +108,19 @@ export type FunctionProps = {
* Defaults to the oldest NodeJS LTS version. See https://nodejs.org/en/about/previous-releases
*/
runtime?: NodeVersion;

/**
* A time interval string to periodically run the function.
* This can be either a string of `"every <positive whole number><m (minute) or h (hour)>"`, `"every day|week|month|year"` or cron expression.
* Defaults to no scheduling for the function.
* @example
* schedule: "every 5m"
* @example
* schedule: "every week"
* @example
* schedule: "0 9 * * 2" // every Monday at 9am
*/
schedule?: FunctionSchedule | FunctionSchedule[];
};

/**
Expand Down Expand Up @@ -137,6 +165,7 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {
memoryMB: this.resolveMemory(),
environment: this.props.environment ?? {},
runtime: this.resolveRuntime(),
schedule: this.resolveSchedule(),
};
};

Expand Down Expand Up @@ -226,6 +255,18 @@ class FunctionFactory implements ConstructFactory<AmplifyFunction> {

return this.props.runtime;
};

private resolveSchedule = () => {
edwardfoyle marked this conversation as resolved.
Show resolved Hide resolved
if (!this.props.schedule) {
return [];
}

const schedules = Array.isArray(this.props.schedule)
awsluja marked this conversation as resolved.
Show resolved Hide resolved
? this.props.schedule
: [this.props.schedule];

return schedules;
};
}

type HydratedFunctionProps = Required<FunctionProps>;
Expand Down Expand Up @@ -305,7 +346,7 @@ class AmplifyFunction
// This will be overwritten with the typed file at the end of synthesis
functionEnvironmentTypeGenerator.generateProcessEnvShim();

let functionLambda;
let functionLambda: NodejsFunction;
try {
functionLambda = new NodejsFunction(scope, `${id}-lambda`, {
entry: props.entry,
Expand Down Expand Up @@ -335,6 +376,36 @@ class AmplifyFunction
);
}

const timeIntervals = Array.isArray(props.schedule)
? props.schedule
: [props.schedule];

try {
const schedules = convertFunctionSchedulesToRuleSchedules(
functionLambda,
timeIntervals
);
const lambdaTarget = new targets.LambdaFunction(functionLambda);

schedules.forEach((schedule, index) => {
// Lambda name will be prepended to rule id, so only using index here for uniqueness
const rule = new Rule(functionLambda, `schedule${index}`, {
schedule,
});

rule.addTarget(lambdaTarget);
});
} catch (error) {
throw new AmplifyUserError(
'FunctionScheduleInitializationError',
{
message: 'Failed to instantiate schedule for nodejs function',
resolution: 'See the underlying error message for more details.',
},
error as Error
);
}

Tags.of(functionLambda).add(TagName.FRIENDLY_NAME, id);

this.functionEnvironmentTranslator = new FunctionEnvironmentTranslator(
Expand Down
Loading
Loading