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 1 commit
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
3 changes: 2 additions & 1 deletion packages/aws-cdk-lib/aws-scheduler/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './scheduler.generated';
export * from './scheduler.generated';
export * from './schedule-expression';
kaizencc marked this conversation as resolved.
Show resolved Hide resolved
104 changes: 104 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,104 @@
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 {


kaizencc marked this conversation as resolved.
Show resolved Hide resolved
/**
* 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
}
165 changes: 165 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,165 @@
import { Duration, Stack, Lazy, TimeZone } from '../../core';
import { ScheduleExpression } from '../lib';


kaizencc marked this conversation as resolved.
Show resolved Hide resolved
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);
});
});