Skip to content

Commit 6554e48

Browse files
authored
feat(scheduler): flexible time windows (#28098)
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*
1 parent 7c62d68 commit 6554e48

File tree

9 files changed

+227
-8
lines changed

9 files changed

+227
-8
lines changed

packages/@aws-cdk/aws-scheduler-alpha/README.md

+19-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ of millions of tasks across many AWS services without provisioning or managing u
2323
2. **Targets**: A target is an API operation that EventBridge Scheduler calls on your behalf every time your schedule runs. EventBridge Scheduler
2424
supports two types of targets: templated targets and universal targets. Templated targets invoke common API operations across a core groups of
2525
services. For example, EventBridge Scheduler supports templated targets for invoking AWS Lambda Function or starting execution of Step Function state
26-
machine. For API operations that are not supported by templated targets you can use customizeable universal targets. Universal targets support calling
26+
machine. For API operations that are not supported by templated targets you can use customizable universal targets. Universal targets support calling
2727
more than 6,000 API operations across over 270 AWS services.
2828
3. **Schedule Group**: A schedule group is an Amazon EventBridge Scheduler resource that you use to organize your schedules. Your AWS account comes
2929
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
@@ -157,7 +157,7 @@ new Schedule(this, 'Schedule', {
157157

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

163163
### Input
@@ -241,6 +241,23 @@ const schedule = new Schedule(this, 'Schedule', {
241241

242242
> Visit [Data protection in Amazon EventBridge Scheduler](https://docs.aws.amazon.com/scheduler/latest/UserGuide/data-protection.html) for more details.
243243
244+
## Configuring flexible time windows
245+
246+
You can configure flexible time windows by specifying the `timeWindow` property.
247+
Flexible time windows is disabled by default.
248+
249+
```ts
250+
declare const target: targets.LambdaInvoke;
251+
252+
const schedule = new Schedule(this, 'Schedule', {
253+
schedule: ScheduleExpression.rate(Duration.hours(12)),
254+
target,
255+
timeWindow: TimeWindow.flexible(Duration.hours(10)),
256+
});
257+
```
258+
259+
> Visit [Configuring flexible time windows](https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-flexible-time-windows.html) for more details.
260+
244261
## Error-handling
245262

246263
You can configure how your schedule handles failures, when EventBridge Scheduler is unable to deliver an event

packages/@aws-cdk/aws-scheduler-alpha/lib/schedule.ts

+57-1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,47 @@ export interface ScheduleTargetProps {
5959
readonly retryAttempts?: number;
6060
}
6161

62+
/**
63+
* A time window during which EventBridge Scheduler invokes the schedule.
64+
*/
65+
export class TimeWindow {
66+
/**
67+
* TimeWindow is disabled.
68+
*/
69+
public static off(): TimeWindow {
70+
return new TimeWindow('OFF');
71+
}
72+
73+
/**
74+
* TimeWindow is enabled.
75+
*/
76+
public static flexible(maxWindow: Duration): TimeWindow {
77+
if (maxWindow.toMinutes() < 1 || maxWindow.toMinutes() > 1440) {
78+
throw new Error(`The provided duration must be between 1 minute and 1440 minutes, got ${maxWindow.toMinutes()}`);
79+
}
80+
return new TimeWindow('FLEXIBLE', maxWindow);
81+
}
82+
83+
/**
84+
* Determines whether the schedule is invoked within a flexible time window.
85+
*/
86+
public readonly mode: 'OFF' | 'FLEXIBLE';
87+
88+
/**
89+
* The maximum time window during which the schedule can be invoked.
90+
*
91+
* Must be between 1 to 1440 minutes.
92+
*
93+
* @default - no value
94+
*/
95+
public readonly maxWindow?: Duration;
96+
97+
private constructor(mode: 'OFF' | 'FLEXIBLE', maxWindow?: Duration) {
98+
this.mode = mode;
99+
this.maxWindow = maxWindow;
100+
}
101+
}
102+
62103
/**
63104
* Construction properties for `Schedule`.
64105
*/
@@ -104,6 +145,7 @@ export interface ScheduleProps {
104145

105146
/**
106147
* Indicates whether the schedule is enabled.
148+
*
107149
* @default true
108150
*/
109151
readonly enabled?: boolean;
@@ -115,6 +157,15 @@ export interface ScheduleProps {
115157
*/
116158
readonly key?: kms.IKey;
117159

160+
/**
161+
* A time window during which EventBridge Scheduler invokes the schedule.
162+
*
163+
* @see https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-flexible-time-windows.html
164+
*
165+
* @default TimeWindow.off()
166+
*/
167+
readonly timeWindow?: TimeWindow;
168+
118169
/**
119170
* The date, in UTC, after which the schedule can begin invoking its target.
120171
* EventBridge Scheduler ignores start for one-time schedules.
@@ -270,11 +321,16 @@ export class Schedule extends Resource implements ISchedule {
270321

271322
this.retryPolicy = targetConfig.retryPolicy;
272323

324+
const flexibleTimeWindow = props.timeWindow ?? TimeWindow.off();
325+
273326
this.validateTimeFrame(props.start, props.end);
274327

275328
const resource = new CfnSchedule(this, 'Resource', {
276329
name: this.physicalName,
277-
flexibleTimeWindow: { mode: 'OFF' },
330+
flexibleTimeWindow: {
331+
mode: flexibleTimeWindow.mode,
332+
maximumWindowInMinutes: flexibleTimeWindow.maxWindow?.toMinutes(),
333+
},
278334
scheduleExpression: props.schedule.expressionString,
279335
scheduleExpressionTimezone: props.schedule.timeZone?.timezoneName,
280336
groupName: this.group?.groupName,

packages/@aws-cdk/aws-scheduler-alpha/rosetta/default.ts-fixture

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import * as sqs from 'aws-cdk-lib/aws-sqs';
88
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
99
import * as targets from '@aws-cdk/aws-scheduler-targets-alpha';
1010
import { App, Stack, TimeZone, Duration } from 'aws-cdk-lib';
11-
import { ScheduleExpression, ScheduleTargetInput, ContextAttribute, Group, Schedule } from '@aws-cdk/aws-scheduler-alpha';
11+
import { ScheduleExpression, ScheduleTargetInput, ContextAttribute, Group, Schedule, TimeWindow } from '@aws-cdk/aws-scheduler-alpha';
1212

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

packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.assets.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/aws-cdk-scheduler-schedule.template.json

+31
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,37 @@
349349
}
350350
}
351351
},
352+
"UseFlexibleTimeWindowBF55D3ED": {
353+
"Type": "AWS::Scheduler::Schedule",
354+
"Properties": {
355+
"FlexibleTimeWindow": {
356+
"MaximumWindowInMinutes": 10,
357+
"Mode": "FLEXIBLE"
358+
},
359+
"ScheduleExpression": "rate(12 hours)",
360+
"ScheduleExpressionTimezone": "Etc/UTC",
361+
"State": "ENABLED",
362+
"Target": {
363+
"Arn": {
364+
"Fn::GetAtt": [
365+
"Function76856677",
366+
"Arn"
367+
]
368+
},
369+
"Input": "\"Input Text\"",
370+
"RetryPolicy": {
371+
"MaximumEventAgeInSeconds": 180,
372+
"MaximumRetryAttempts": 3
373+
},
374+
"RoleArn": {
375+
"Fn::GetAtt": [
376+
"Role1ABCC5F0",
377+
"Arn"
378+
]
379+
}
380+
}
381+
}
382+
},
352383
"ScheduleWithTimeFrameC1C8BDCC": {
353384
"Type": "AWS::Scheduler::Schedule",
354385
"Properties": {

packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/manifest.json

+7-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.js.snapshot/tree.json

+49
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/@aws-cdk/aws-scheduler-alpha/test/integ.schedule.ts

+6
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,12 @@ new scheduler.Schedule(stack, 'CustomerKmsSchedule', {
9090
key,
9191
});
9292

93+
new scheduler.Schedule(stack, 'UseFlexibleTimeWindow', {
94+
schedule: expression,
95+
target: target,
96+
timeWindow: scheduler.TimeWindow.flexible(cdk.Duration.minutes(10)),
97+
});
98+
9399
const currentYear = new Date().getFullYear();
94100
new scheduler.Schedule(stack, 'ScheduleWithTimeFrame', {
95101
schedule: expression,

packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts

+55-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Template } from 'aws-cdk-lib/assertions';
44
import * as iam from 'aws-cdk-lib/aws-iam';
55
import * as kms from 'aws-cdk-lib/aws-kms';
66
import * as lambda from 'aws-cdk-lib/aws-lambda';
7-
import { IScheduleTarget, Schedule, ScheduleTargetConfig } from '../lib';
7+
import { IScheduleTarget, Schedule, ScheduleTargetConfig, TimeWindow } from '../lib';
88
import { ScheduleExpression } from '../lib/schedule-expression';
99

1010
class SomeLambdaTarget implements IScheduleTarget {
@@ -161,4 +161,58 @@ describe('Schedule', () => {
161161
}).toThrow(`start must precede end, got start: ${start}, end: ${end}`);
162162
});
163163
});
164+
165+
describe('flexibleTimeWindow', () => {
166+
test('flexibleTimeWindow mode is set to OFF by default', () => {
167+
// WHEN
168+
new Schedule(stack, 'TestSchedule', {
169+
schedule: expr,
170+
target: new SomeLambdaTarget(func, role),
171+
});
172+
173+
// THEN
174+
Template.fromStack(stack).hasResourceProperties('AWS::Scheduler::Schedule', {
175+
FlexibleTimeWindow: {
176+
Mode: 'OFF',
177+
},
178+
});
179+
});
180+
181+
test('flexibleTimeWindow mode can be set to FLEXIBLE', () => {
182+
// WHEN
183+
new Schedule(stack, 'TestSchedule', {
184+
schedule: expr,
185+
target: new SomeLambdaTarget(func, role),
186+
timeWindow: TimeWindow.flexible(Duration.minutes(1440)),
187+
});
188+
189+
// THEN
190+
Template.fromStack(stack).hasResourceProperties('AWS::Scheduler::Schedule', {
191+
FlexibleTimeWindow: {
192+
Mode: 'FLEXIBLE',
193+
MaximumWindowInMinutes: 1440,
194+
},
195+
});
196+
});
197+
198+
test('throw error when maximumWindowInMinutes is greater than 1440', () => {
199+
expect(() => {
200+
new Schedule(stack, 'TestSchedule', {
201+
schedule: expr,
202+
target: new SomeLambdaTarget(func, role),
203+
timeWindow: TimeWindow.flexible(Duration.minutes(1441)),
204+
});
205+
}).toThrow('The provided duration must be between 1 minute and 1440 minutes, got 1441');
206+
});
207+
208+
test('throw error when maximumWindowInMinutes is less than 1', () => {
209+
expect(() => {
210+
new Schedule(stack, 'TestSchedule', {
211+
schedule: expr,
212+
target: new SomeLambdaTarget(func, role),
213+
timeWindow: TimeWindow.flexible(Duration.minutes(0)),
214+
});
215+
}).toThrow('The provided duration must be between 1 minute and 1440 minutes, got 0');
216+
});
217+
});
164218
});

0 commit comments

Comments
 (0)