Skip to content

Commit

Permalink
feat(scheduler): flexible time windows (#28098)
Browse files Browse the repository at this point in the history
This PR adds support for configuring flexible time windows.

## Description
Currently, users cannot configure the `flexibleTimeWindow` feature in the Scheduler construct.
This feature enhances flexibility and reliability, allowing tasks to be invoked within a defined time window. 
https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-flexible-time-windows.html

CloudFormation allows users to take advantage of this feature as follows.
With this template,  it will invokes the target within 10 minutes after the scheduled time.
```yaml
Resources:
  Schedule:
    Type: AWS::Scheduler::Schedule
    Properties: 
      FlexibleTimeWindow: 
        Mode: "FLEXIBLE" # or "OFF"
        MaximumWindowInMinutes: 10 # between 1 and 1440
      Name: "sample-schedule"
      ScheduleExpression: "cron(0 9 * * ? *)"
      State: "ENABLED"
      Target:
        Arn: hoge
        RoleArn: hoge
```

## Changes
### add Enum indicating flexible time window mode
Currently there are only two modes, FLEXIBLE and OFF, so there is no problem using boolean instead of enum.
But I think it's better to use Enum to prepare for future expansion.
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-scheduler-schedule-flexibletimewindow.html

### add property to `ScheduleProps` interface
`flexibleTimeWindowMode` property defaults to `OFF` to avoid a breaking change.
```ts
interface ScheduleProps {
  // ....
  /**
   * Determines whether the schedule is invoked within a flexible time window.
   *
   * @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-flexible-time-windows.html
   *
   * @default - FlexibleTimeWindowMode.OFF
   */
  readonly flexibleTimeWindowMode?: FlexibleTimeWindowMode;

  /**
   * The maximum time window during which the schedule can be invoked.
   *
   * @default - Required if flexibleTimeWindowMode is FLEXIBLE.
   */
  readonly maximumWindowInMinutes?: Duration;
}
```

### set the added property to `CfnSchedule` construct
Basically, just set the values as documented, but with the following validations.
- If `flexibleTimeWindowMode` is `FLEXIBLE`
  - `maximumWindowInMinutes` must be specified
  - `maximumWindowInMinutes` must be set from 1 to 1440 minutes

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-scheduler-schedule-flexibletimewindow.html

In addition, I added some unit tests and integ-tests.

### others
- fixed typo in README
  -  `customizeable` => `customizable`

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
sakurai-ryo committed Dec 15, 2023
1 parent 7c62d68 commit 6554e48
Show file tree
Hide file tree
Showing 9 changed files with 227 additions and 8 deletions.
21 changes: 19 additions & 2 deletions packages/@aws-cdk/aws-scheduler-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ of millions of tasks across many AWS services without provisioning or managing u
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
machine. For API operations that are not supported by templated targets you can use customizable 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
Expand Down Expand Up @@ -157,7 +157,7 @@ new Schedule(this, 'Schedule', {

The `@aws-cdk/aws-scheduler-targets-alpha` module includes classes that implement the `IScheduleTarget` interface for
various AWS services. EventBridge Scheduler supports two types of targets: templated targets invoke common API
operations across a core groups of services, and customizeable universal targets that you can use to call more
operations across a core groups of services, and customizable universal targets that you can use to call more
than 6,000 operations across over 270 services. A list of supported targets can be found at `@aws-cdk/aws-scheduler-targets-alpha`.

### Input
Expand Down Expand Up @@ -241,6 +241,23 @@ const schedule = new Schedule(this, 'Schedule', {

> Visit [Data protection in Amazon EventBridge Scheduler](https://docs.aws.amazon.com/scheduler/latest/UserGuide/data-protection.html) for more details.
## Configuring flexible time windows

You can configure flexible time windows by specifying the `timeWindow` property.
Flexible time windows is disabled by default.

```ts
declare const target: targets.LambdaInvoke;

const schedule = new Schedule(this, 'Schedule', {
schedule: ScheduleExpression.rate(Duration.hours(12)),
target,
timeWindow: TimeWindow.flexible(Duration.hours(10)),
});
```

> Visit [Configuring flexible time windows](https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-flexible-time-windows.html) for more details.
## Error-handling

You can configure how your schedule handles failures, when EventBridge Scheduler is unable to deliver an event
Expand Down
58 changes: 57 additions & 1 deletion packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,47 @@ export interface ScheduleTargetProps {
readonly retryAttempts?: number;
}

/**
* A time window during which EventBridge Scheduler invokes the schedule.
*/
export class TimeWindow {
/**
* TimeWindow is disabled.
*/
public static off(): TimeWindow {
return new TimeWindow('OFF');
}

/**
* TimeWindow is enabled.
*/
public static flexible(maxWindow: Duration): TimeWindow {
if (maxWindow.toMinutes() < 1 || maxWindow.toMinutes() > 1440) {
throw new Error(`The provided duration must be between 1 minute and 1440 minutes, got ${maxWindow.toMinutes()}`);
}
return new TimeWindow('FLEXIBLE', maxWindow);
}

/**
* Determines whether the schedule is invoked within a flexible time window.
*/
public readonly mode: 'OFF' | 'FLEXIBLE';

/**
* The maximum time window during which the schedule can be invoked.
*
* Must be between 1 to 1440 minutes.
*
* @default - no value
*/
public readonly maxWindow?: Duration;

private constructor(mode: 'OFF' | 'FLEXIBLE', maxWindow?: Duration) {
this.mode = mode;
this.maxWindow = maxWindow;
}
}

/**
* Construction properties for `Schedule`.
*/
Expand Down Expand Up @@ -104,6 +145,7 @@ export interface ScheduleProps {

/**
* Indicates whether the schedule is enabled.
*
* @default true
*/
readonly enabled?: boolean;
Expand All @@ -115,6 +157,15 @@ export interface ScheduleProps {
*/
readonly key?: kms.IKey;

/**
* A time window during which EventBridge Scheduler invokes the schedule.
*
* @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-flexible-time-windows.html
*
* @default TimeWindow.off()
*/
readonly timeWindow?: TimeWindow;

/**
* The date, in UTC, after which the schedule can begin invoking its target.
* EventBridge Scheduler ignores start for one-time schedules.
Expand Down Expand Up @@ -270,11 +321,16 @@ export class Schedule extends Resource implements ISchedule {

this.retryPolicy = targetConfig.retryPolicy;

const flexibleTimeWindow = props.timeWindow ?? TimeWindow.off();

this.validateTimeFrame(props.start, props.end);

const resource = new CfnSchedule(this, 'Resource', {
name: this.physicalName,
flexibleTimeWindow: { mode: 'OFF' },
flexibleTimeWindow: {
mode: flexibleTimeWindow.mode,
maximumWindowInMinutes: flexibleTimeWindow.maxWindow?.toMinutes(),
},
scheduleExpression: props.schedule.expressionString,
scheduleExpressionTimezone: props.schedule.timeZone?.timezoneName,
groupName: this.group?.groupName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
import * as targets from '@aws-cdk/aws-scheduler-targets-alpha';
import { App, Stack, TimeZone, Duration } from 'aws-cdk-lib';
import { ScheduleExpression, ScheduleTargetInput, ContextAttribute, Group, Schedule } from '@aws-cdk/aws-scheduler-alpha';
import { ScheduleExpression, ScheduleTargetInput, ContextAttribute, Group, Schedule, TimeWindow } from '@aws-cdk/aws-scheduler-alpha';

class Fixture extends cdk.Stack {
constructor(scope: Construct, id: string) {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,37 @@
}
}
},
"UseFlexibleTimeWindowBF55D3ED": {
"Type": "AWS::Scheduler::Schedule",
"Properties": {
"FlexibleTimeWindow": {
"MaximumWindowInMinutes": 10,
"Mode": "FLEXIBLE"
},
"ScheduleExpression": "rate(12 hours)",
"ScheduleExpressionTimezone": "Etc/UTC",
"State": "ENABLED",
"Target": {
"Arn": {
"Fn::GetAtt": [
"Function76856677",
"Arn"
]
},
"Input": "\"Input Text\"",
"RetryPolicy": {
"MaximumEventAgeInSeconds": 180,
"MaximumRetryAttempts": 3
},
"RoleArn": {
"Fn::GetAtt": [
"Role1ABCC5F0",
"Arn"
]
}
}
}
},
"ScheduleWithTimeFrameC1C8BDCC": {
"Type": "AWS::Scheduler::Schedule",
"Properties": {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,12 @@ new scheduler.Schedule(stack, 'CustomerKmsSchedule', {
key,
});

new scheduler.Schedule(stack, 'UseFlexibleTimeWindow', {
schedule: expression,
target: target,
timeWindow: scheduler.TimeWindow.flexible(cdk.Duration.minutes(10)),
});

const currentYear = new Date().getFullYear();
new scheduler.Schedule(stack, 'ScheduleWithTimeFrame', {
schedule: expression,
Expand Down
56 changes: 55 additions & 1 deletion packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Template } from 'aws-cdk-lib/assertions';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { IScheduleTarget, Schedule, ScheduleTargetConfig } from '../lib';
import { IScheduleTarget, Schedule, ScheduleTargetConfig, TimeWindow } from '../lib';
import { ScheduleExpression } from '../lib/schedule-expression';

class SomeLambdaTarget implements IScheduleTarget {
Expand Down Expand Up @@ -161,4 +161,58 @@ describe('Schedule', () => {
}).toThrow(`start must precede end, got start: ${start}, end: ${end}`);
});
});

describe('flexibleTimeWindow', () => {
test('flexibleTimeWindow mode is set to OFF by default', () => {
// WHEN
new Schedule(stack, 'TestSchedule', {
schedule: expr,
target: new SomeLambdaTarget(func, role),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Scheduler::Schedule', {
FlexibleTimeWindow: {
Mode: 'OFF',
},
});
});

test('flexibleTimeWindow mode can be set to FLEXIBLE', () => {
// WHEN
new Schedule(stack, 'TestSchedule', {
schedule: expr,
target: new SomeLambdaTarget(func, role),
timeWindow: TimeWindow.flexible(Duration.minutes(1440)),
});

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Scheduler::Schedule', {
FlexibleTimeWindow: {
Mode: 'FLEXIBLE',
MaximumWindowInMinutes: 1440,
},
});
});

test('throw error when maximumWindowInMinutes is greater than 1440', () => {
expect(() => {
new Schedule(stack, 'TestSchedule', {
schedule: expr,
target: new SomeLambdaTarget(func, role),
timeWindow: TimeWindow.flexible(Duration.minutes(1441)),
});
}).toThrow('The provided duration must be between 1 minute and 1440 minutes, got 1441');
});

test('throw error when maximumWindowInMinutes is less than 1', () => {
expect(() => {
new Schedule(stack, 'TestSchedule', {
schedule: expr,
target: new SomeLambdaTarget(func, role),
timeWindow: TimeWindow.flexible(Duration.minutes(0)),
});
}).toThrow('The provided duration must be between 1 minute and 1440 minutes, got 0');
});
});
});

0 comments on commit 6554e48

Please sign in to comment.