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(scheduler): schedule expression construct #25422

Merged
merged 23 commits into from
May 17, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5a0843d
Add schedule expression implementation
filletofish May 3, 2023
ec40985
Update README.md for scheduler module
filletofish May 4, 2023
7f9b377
Apply suggestions from code review
kaizencc May 5, 2023
540a1ab
Update packages/aws-cdk-lib/aws-scheduler/README.md
filletofish May 6, 2023
337b877
Update packages/aws-cdk-lib/aws-scheduler/lib/schedule-expression.ts
filletofish May 6, 2023
1d56d3b
Update packages/aws-cdk-lib/aws-scheduler/lib/schedule-expression.ts
filletofish May 6, 2023
e07a4d2
Move new L2 constructs to @aws-cdk/scheduler-alpha
filletofish May 6, 2023
4c6abd7
Revert changes to files in aws-cdk-lib
filletofish May 6, 2023
98d227e
Reformat readme.md file
filletofish May 6, 2023
34968a2
Update packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-expression.ts
filletofish May 14, 2023
a6fabd5
Update packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-expression.ts
filletofish May 14, 2023
34cc3e7
Update packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-expression.ts
filletofish May 14, 2023
10d388f
Update packages/@aws-cdk/aws-scheduler-alpha/lib/schedule-expression.ts
filletofish May 14, 2023
6b05925
Merge branch 'aws:main' into feat/23394
filletofish May 14, 2023
5802931
Rename package name to "@aws-cdk/aws-scheduler-alpha"
filletofish May 15, 2023
bd7d7a1
Add rosetta default package
filletofish May 15, 2023
60dcfd1
Update readme and fixtures to fix rosetta failures
filletofish May 15, 2023
e071c26
Reformat the file
filletofish May 15, 2023
260372b
Turn back on eslint rules
filletofish May 15, 2023
30c8787
Remove sections for not yet implemented content
filletofish May 16, 2023
e874e34
Update @types/jest library version
filletofish May 16, 2023
2ab1af0
Update README.md
kaizencc May 17, 2023
54b328c
Merge branch 'main' into feat/23394
mergify[bot] May 17, 2023
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
67 changes: 52 additions & 15 deletions packages/aws-cdk-lib/aws-scheduler/README.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,64 @@
# AWS::Scheduler Construct Library
# Amazon Scheduler Construct Library
filletofish marked this conversation as resolved.
Show resolved Hide resolved

Amazon EventBridge Scheduler](https://aws.amazon.com/blogs/compute/introducing-amazon-eventbridge-scheduler/) is a feature from Amazon EventBridge that allows you to create, run, and manage scheduled tasks at scale. With EventBridge Scheduler, you can schedule one-time or recurrently tens of millions of tasks across many AWS services without provisioning or managing underlying infrastructure.
Copy link
Contributor

Choose a reason for hiding this comment

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

another comment on impending linter errors that i feel should have been enforced somewhere: these paragraphs have to be broken into separate lines with max 150 characters i believe

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated readme. So the rule on characters per line in README is not enforced anywhere? What would be a good linter to add this rule to?

Copy link
Contributor

Choose a reason for hiding this comment

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

it has been enforced in the past, i think our repo restructure may have wreaked havoc on our linter perhaps. i've been meaning to look into it


This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.
1. **Schedule**: A schedule is the main resource you create, configure, and manage using Amazon EventBridge Scheduler. Every schedule has a schedule expression that determines when, and with what frequency, the schedule runs. EventBridge Scheduler supports three types of schedules: rate, cron, and one-time schedules. When you create a schedule, you configure a target for the schedule to invoke.
2. **Targets**: A target is an API operation that EventBridge Scheduler calls on your behalf every time your schedule runs. EventBridge Scheduler supports two types of targets: templated targets and universal targets. Templated targets invoke common API operations across a core groups of services. For example, EventBridge Scheduler supports templated targets for invoking AWS Lambda Function or starting execution of Step Function state machine. For API operations that are not supported by templated targets you can use customizeable universal targets. Universal targets support calling more than 6,000 API operations across over 270 AWS services.
3. **Schedule Group**: A schedule group is an Amazon EventBridge Scheduler resource that you use to organize your schedules. Your AWS account comes with a default scheduler group. A new schedule will always be added to a scheduling group. If you do not provide a scheduling group to add to, it will be added to the default scheduling group. You can create up to 500 schedule groups in your AWS account. Groups can be used to organize the schedules logically, access the schedule metrics and manage permissions at group granularity (see details below). Scheduling groups support tagging: with EventBridge Scheduler, you apply tags to schedule groups, not to individual schedules to organize your resources.

```ts nofixture
import * as scheduler from 'aws-cdk-lib/aws-scheduler';
```
This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project.

<!--BEGIN CFNONLY DISCLAIMER-->
## Defining a schedule

There are no official hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. Here are some suggestions on how to proceed:
```ts
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we need a disclaimer here saying that targets doesn't work yet.

Copy link
Contributor

@Jacco Jacco May 6, 2023

Choose a reason for hiding this comment

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

My initial thoughts were to add the whole README the information about all upcoming PRs already in this first PR. We could add a notice that this is not all implemented yet but will gradually come in multiple PRs to keep the PR size smaller and reviewable. What do you think @kaizencc? Reworking the README every PR seems a but useless.

Copy link
Contributor

Choose a reason for hiding this comment

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

good with one big disclaimer :) commented that elsewhere but i just saw this

const target = new targets.LambdaInvoke(props.func, {
input: ScheduleTargetInput.fromObject({
payload: 'useful',
}),
});

const schedule = new Schedule(this, 'Schedule', {
scheduleExpression: ScheduleExpression.rate(Duration.minutes(10)),
target,
description: 'This is a test schedule that invokes lambda function every 10 minutes.',
});
```

- Search [Construct Hub for Scheduler construct libraries](https://constructs.dev/search?q=scheduler)
- Use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, in the same way you would use [the CloudFormation AWS::Scheduler resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Scheduler.html) directly.
### Schedule Expressions

You can choose from three schedule types when configuring your schedule: rate-based, cron-based, and one-time schedules.

<!--BEGIN CFNONLY DISCLAIMER-->
Both rate-based and cron-based schedules are recurring schedules. You can configure each recurring schedule type using a schedule expression. For cron-based schedule you can specify a time zone in which EventBridge Scheduler evaluates the expression.

There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet.
However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly.
```ts
const rateBasedSchedule = new Schedule(this, 'Schedule', {
scheduleExpression: ScheduleExpression.rate(Duration.minutes(10)),
target,
description: 'This is a test rate-based schedule',
});

For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::Scheduler](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_Scheduler.html).
const cronBasedSchedule = new Schedule(this, 'Schedule', {
scheduleExpression: ScheduleExpression.cron({
minute: '0',
hour: '23',
day: '20',
month: '11',
timeZone: TimeZone.AMERICA_NEW_YORK,
}),
target,
description: 'This is a test cron-based schedule that will run at 11:00 PM, on day 20 of the month, only in November in New York timezone',
});
```

(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and submit an RFC if you are interested in contributing to this construct library.)
A one-time schedule is a schedule that invokes a target only once. You configure a one-time schedule when by specifying the time of the day, date, and time zone in which EventBridge Scheduler evaluates the schedule.

<!--END CFNONLY DISCLAIMER-->
```ts
const oneTimeSchedule = new Schedule(this, 'Schedule', {
scheduleExpression: ScheduleExpression.at(
new Date(2022, 10, 20, 19, 20, 23),
TimeZone.AMERICA_NEW_YORK,
),
target,
description: 'This is a one-time schedule in New York timezone',
});
```
4 changes: 3 additions & 1 deletion packages/aws-cdk-lib/aws-scheduler/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from './scheduler.generated';
export * from './scheduler.generated';

export * from './schedule-expression';
102 changes: 102 additions & 0 deletions packages/aws-cdk-lib/aws-scheduler/lib/schedule-expression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { Construct } from 'constructs';
import * as events from '../../aws-events';
import { CronOptions } from '../../aws-events';
import { Duration, TimeZone } from '../../core';


/**
* ScheduleExpression for EventBridge Schedule
*
* You can choose from three schedule types when configuring your schedule: rate-based, cron-based, and one-time schedules.
* Both rate-based and cron-based schedules are recurring schedules.
*
* @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html
*/
export abstract class ScheduleExpression {
/**
* Construct a one-time schedule from a date.
*
* @param date The date and time to use. The millisecond part will be ignored.
* @param timeZone The time zone to use for interpreting the date. Default: - UTC
*/
public static at(date: Date, timeZone?: TimeZone): ScheduleExpression {
try {
const literal = date.toISOString().split('.')[0];
return new LiteralScheduleExpression(`at(${literal})`, timeZone ?? TimeZone.ETC_UTC);
filletofish marked this conversation as resolved.
Show resolved Hide resolved
} catch (e) {
if (e instanceof RangeError) {
throw new Error('Invalid date');
}
throw e;
}
}

/**
* Construct a schedule from a literal schedule expression
* @param expression The expression to use. Must be in a format that EventBridge will recognize
* @param timeZone The time zone to use for interpreting the expression. Default: - UTC
*/
public static expression(expression: string, timeZone?: TimeZone): ScheduleExpression {
return new LiteralScheduleExpression(expression, timeZone ?? TimeZone.ETC_UTC);
}

/**
* Construct a recurring schedule from an interval and a time unit
*
* Rates may be defined with any unit of time, but when converted into minutes, the duration must be a positive whole number of minutes.
*/
static rate(duration: Duration): ScheduleExpression {
const schedule = events.Schedule.rate(duration);
return new LiteralScheduleExpression(schedule.expressionString);
}

/**
* Create a srecurring chedule from a set of cron fields and time zone.
*/
static cron(options: CronOptionsWithTimezone): ScheduleExpression {
const schedule = events.Schedule.cron(options);
return new LiteralScheduleExpression(schedule.expressionString, options.timeZone ?? TimeZone.ETC_UTC);
filletofish marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Retrieve the expression for this schedule
*/
public abstract readonly expressionString: string;
/**
* Retrieve the expression for this schedule
*/
public abstract readonly timeZone?: TimeZone;

protected constructor() {};
/**
*
* @internal
*/
filletofish marked this conversation as resolved.
Show resolved Hide resolved
abstract _bind(scope: Construct): void;
filletofish marked this conversation as resolved.
Show resolved Hide resolved
filletofish marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Options to configure a cron expression
*
* All fields are strings so you can use complex expressions. Absence of
* a field implies '*' or '?', whichever one is appropriate.
*
* @see https://docs.aws.amazon.com/eventbridge/latest/userguide/scheduled-events.html#cron-expressions
*/
export interface CronOptionsWithTimezone extends CronOptions {
/**
* The timezone to run the schedule in
*
* @default - UTC
*/
readonly timeZone?: TimeZone;
}

class LiteralScheduleExpression extends ScheduleExpression {

constructor(public readonly expressionString: string, public readonly timeZone?: TimeZone) {
super();
}

public _bind() {}
filletofish marked this conversation as resolved.
Show resolved Hide resolved
}
164 changes: 164 additions & 0 deletions packages/aws-cdk-lib/aws-scheduler/test/schedule-expression.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { Duration, Stack, Lazy, TimeZone } from '../../core';
import { ScheduleExpression } from '../lib';

describe('schedule expression', () => {
test('cron expressions day and dow are mutex: given weekday', () => {
// Run every 10 minutes Monday through Friday
expect('cron(0/10 * ? * MON-FRI *)').toEqual(ScheduleExpression.cron({
minute: '0/10',
weekDay: 'MON-FRI',
}).expressionString);
});

test('cron expressions day and dow are mutex: given month day', () => {
// Run at 8:00 am (UTC) every 1st day of the month
expect('cron(0 8 1 * ? *)').toEqual(ScheduleExpression.cron({
minute: '0',
hour: '8',
day: '1',
}).expressionString);
});

test('cron expressions day and dow are mutex: given neither', () => {
// Run at 10:00 am (UTC) every day
expect('cron(0 10 * * ? *)').toEqual(ScheduleExpression.cron({
minute: '0',
hour: '10',
}).expressionString);
});

test('cron expressions saves timezone', () => {
expect(TimeZone.EUROPE_LONDON).toEqual(ScheduleExpression.cron(
{
minute: '0',
hour: '10',
timeZone: TimeZone.EUROPE_LONDON,
}).timeZone);
});

test('cron expressions timezone is UTC if not specified', () => {
expect(TimeZone.ETC_UTC).toEqual(ScheduleExpression.cron(
{
minute: '0',
hour: '10',
}).timeZone);
});

test('rate cannot be 0', () => {
expect(() => {
ScheduleExpression.rate(Duration.days(0));
}).toThrow(/Duration cannot be 0/);
});

test('rate cannot be negative', () => {
expect(() => {
ScheduleExpression.rate(Duration.minutes(-2));
}).toThrow(/Duration amounts cannot be negative/);
});

test('rate can be from a token', () => {
const stack = new Stack();
const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 }));
const rate = ScheduleExpression.rate(lazyDuration);
expect('rate(5 minutes)').toEqual(stack.resolve(rate).expressionString);
});

test('rate can be in minutes', () => {
expect('rate(10 minutes)').toEqual(
ScheduleExpression.rate(Duration.minutes(10))
.expressionString);
});

test('rate can be in days', () => {
expect('rate(10 days)').toEqual(
ScheduleExpression.rate(Duration.days(10))
.expressionString);
});

test('rate can be in hours', () => {
expect('rate(10 hours)').toEqual(
ScheduleExpression.rate(Duration.hours(10))
.expressionString);
});

test('rate can be in seconds', () => {
expect('rate(2 minutes)').toEqual(
ScheduleExpression.rate(Duration.seconds(120))
.expressionString);
});

test('rate must not be in seconds when specified as a token', () => {
expect(() => {
ScheduleExpression.rate(Duration.seconds(Lazy.number({ produce: () => 5 })));
}).toThrow(/Allowed units for scheduling/);
});

test('one-time expression string has expected date', () => {
const x = ScheduleExpression.at(new Date(2022, 10, 20, 19, 20, 23));
expect(x.expressionString).toEqual('at(2022-11-20T19:20:23)');
});

test('one-time expression time zone is UTC if not provided', () => {
const x = ScheduleExpression.at(new Date(2022, 10, 20, 19, 20, 23));
expect(x.timeZone).toEqual(TimeZone.ETC_UTC);
});

test('one-time expression has expected time zone if provided', () => {
const x = ScheduleExpression.at(new Date(2022, 10, 20, 19, 20, 23), TimeZone.EUROPE_LONDON);
expect(x.expressionString).toEqual('at(2022-11-20T19:20:23)');
expect(x.timeZone).toEqual(TimeZone.EUROPE_LONDON);
});

test('one-time expression milliseconds ignored', () => {
const x = ScheduleExpression.at(new Date(Date.UTC(2022, 10, 20, 19, 20, 23, 111)));
expect(x.expressionString).toEqual('at(2022-11-20T19:20:23)');
});

test('one-time expression with invalid date throws', () => {
expect(() => ScheduleExpression.at(new Date('13-20-1969'))).toThrowError('Invalid date');
});
});

describe('fractional minutes checks', () => {
test('rate cannot be a fractional amount of minutes (defined with seconds)', () => {
expect(() => {
ScheduleExpression.rate(Duration.seconds(150));
}).toThrow(/cannot be converted into a whole number of/);
});

test('rate cannot be a fractional amount of minutes (defined with minutes)', () => {
expect(()=> {
ScheduleExpression.rate(Duration.minutes(5/3));
}).toThrow(/must be a whole number of/);
});

test('rate cannot be a fractional amount of minutes (defined with hours)', () => {
expect(()=> {
ScheduleExpression.rate(Duration.hours(1.03));
}).toThrow(/cannot be converted into a whole number of/);
});

test('rate cannot be less than 1 minute (defined with seconds)', () => {
expect(() => {
ScheduleExpression.rate(Duration.seconds(30));
}).toThrow(/'30 seconds' cannot be converted into a whole number of minutes./);
});

test('rate cannot be less than 1 minute (defined with minutes as fractions)', () => {
expect(() => {
ScheduleExpression.rate(Duration.minutes(1/2));
}).toThrow(/must be a whole number of/);
});

test('rate cannot be less than 1 minute (defined with minutes as decimals)', () => {
expect(() => {
ScheduleExpression.rate(Duration.minutes(0.25));
}).toThrow(/must be a whole number of/);
});

test('rate can be in minutes', () => {
expect('rate(10 minutes)').toEqual(
ScheduleExpression.rate(Duration.minutes(10))
.expressionString);
});
});