-
Notifications
You must be signed in to change notification settings - Fork 72
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
Conversation
🦋 Changeset detectedLatest commit: ca2d0bd The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
export type Cron = | ||
| `${string} ${string} ${string} ${string} ${string}` | ||
| `${string} ${string} ${string} ${string} ${string} ${string}`; | ||
export type Rate = | ||
| `every ${number}m` | ||
| `every ${number}h` | ||
| `every day` | ||
| `every week` | ||
| `every month` | ||
| `every year`; | ||
export type TimeInterval = Rate | Cron; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Types were done this way because I started getting Expression produces a union type that is too complex to represent
Typescript error when I introduced wildcards for cron. Cron type is less strict and we perform our own cron validation to validate what we support (which could change in the future).
const { value, unit } = parseRate(interval); | ||
|
||
if (value && !isPositiveWholeNumber(value)) { | ||
throw new Error(`schedule must be set with a positive whole number`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AmplifyUserError
) { | ||
const timeout = lambda.timeout.toSeconds(); | ||
|
||
throw new Error( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AmplifyUserError
const timeout = lambda.timeout.toSeconds(); | ||
|
||
throw new Error( | ||
`schedule must be greater than the timeout of ${timeout} ${ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`schedule must be greater than the timeout of ${timeout} ${ | |
`Function schedule rate must be greater than the function timeout of ${timeout} ${ |
} | ||
} else { | ||
if (!isValidCron(interval)) { | ||
// TODO: Better error messaging here or throw within each part of isValidCron in order to give more concise error messages |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a good callout, I think we should go ahead and do this. My personal preference is to collect all of the validation errors into one object and then throw a single error that explains all of the validation violations. This avoids fixing one thing, getting another error, fixing that, getting another error, etc.
schedule: Schedule.cron(translateToCronOptions(interval)), | ||
}); | ||
|
||
rule.addTarget(new targets.LambdaFunction(this.lambda)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to create a new target for each rule or can we reuse the same target since they are all pointing to the same lambda?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch, I believe we should be able to create the target once and reuse for each rule.
/** | ||
* Initialize EventBridge rules | ||
*/ | ||
constructor( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
non-blocker: since this class is purely internal it's not a big deal, but this is a lot of logic for a ctor. I think this is a case where we may not even need a class and just a convertTimeIntervalsToEventRules
function would suffice for this functionality.
I also think the caller should be responsible for attaching the event rule to the lambda rather than it happening internally here. It's a small single-responsibility violation.
}; | ||
default: | ||
// This should never happen with strict types | ||
throw new Error('Could not determine the schedule for the function'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AmplifyUserError?
@@ -228,6 +235,7 @@ class DataStorageAuthWithTriggerTestProject extends TestProjectBase { | |||
await this.checkLambdaResponse(node16Lambda[0], expectedResponse); | |||
await this.checkLambdaResponse(funcWithSsm[0], 'It is working'); | |||
await this.checkLambdaResponse(funcWithAwsSdk[0], 'It is working'); | |||
await this.checkLambdaResponse(funcWithSchedule[0], 'It is working'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
technically this isn't checking that the event trigger is working, just that the lambda can be invoked successfully. I'm not sure how worthwhile it is, but a more comprehensive test might be to create a lambda that sets a value in parameter store (or leaves some trace of execution), set an "every second" schedule for that function, wait a couple seconds after the deployment completes, then check for the execution breadcrumb.
Or maybe a simpler test is to check that the lambda can be invoked (ie what you have here) + check cloudtrail for the invocation event
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EventBridge has a minimum of 1 minute for rule schedules so we would have to wait for ~1 minute for the event to invoke the lambda.
I was thinking the unit tests checking the event rule is properly added (which I just realized I should also make sure the rule target is properly set) and this E2E test is enough to have confidence this will work. I can explore the 2nd option you suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see a way around fully testing this without having the test wait the minimum time it takes for the event to trigger.
Still trying different ways to reduce flakiness with current E2E implementation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the 70 second wait you added is okay. If it proves to be slow / flaky we can reevaluate the strategy and value of the test. Thanks for investigating this!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! Yeah I was playing with the time considering EventBridge may have a delay of several seconds so 70 seconds seemed like a good number with the minimum time of 1 minute for schedules.
packages/backend-function/API.md
Outdated
}; | ||
|
||
// @public (undocumented) | ||
export type NodeVersion = 16 | 18 | 20; | ||
|
||
// @public (undocumented) | ||
export type Rate = `every ${number}m` | `every ${number}h` | `every day` | `every week` | `every month` | `every year`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggestion for alternative namings:
type TimeInterval = `every xx`;
type CronSchedule = `{string} ...`
type Schedule = TimeInterval | CronSchedule
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was avoiding type Schedule
because of the naming clash with the Schedule class from aws-cdk-lib/aws-events
. Alternatively we can use FunctionSchedule
?
} else { | ||
if (!isValidCron(interval)) { | ||
// TODO: Better error messaging here or throw within each part of isValidCron in order to give more concise error messages | ||
throw new Error(`schedule cron expression is not valid`); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The message may not be always true, e.g. if I say every 5 months
it will throw invalid cron expression but I specified an interval.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With strict typing and the more exhaustive validation you mentioned below, I believe everything that gets to this point will be validation for cron expressions
} | ||
|
||
const isRate = (timeInterval: TimeInterval): timeInterval is Rate => { | ||
return timeInterval.split(' ')[0] === 'every'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More exhaustive validation
return timeInterval.split(' ')[0] === 'every'; | |
const parts = timeInterval.split(' '); | |
return parts[0] === 'every' && (['m', 'h', 'day', 'week', 'year', 'month'].some(a => parts[1].endsWith(a))) && parts.length === 2 |
Because customers could be using this in a JS only app or have TS errors suppressed in their files or just creative enough to pass every 5 weeks until 2025
(Which our type would allow thinking it's a cron)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can also get rid of it by just using tryParseRate
.
const regex = /\d/; | ||
if (interval.match(regex)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would this not match just one digit? Ideally we should parse with a more confidence that it is indeed a rate. E.g. with regex ^/\d+[mh]$|^day|year|month$/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, my goal was to just see if there is a number in the second part of the "rate string" in order to get the number value and unit (m
or h
). Otherwise the second part would be day
, week
, etc.
The Rate
type is doing most of the heavy lifting when I initially made the validation (which I'll revisit based on your previous point on customers using JS only or have TS errors suppressed)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, just one naming nit if you want to take it
Co-authored-by: Edward Foyle <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, couple nits if you want to address
await this.checkLambdaResponse(funcWithSchedule[0], 'It is working'); | ||
|
||
// test schedule function event trigger once | ||
if (this.checkInvocationCount) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm assuming this is here to prevent duplicate checking, but I'm confused why it's necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This reduced a lot of flakiness that I was seeing by only testing for invocation count on the first deployment instead of also on subsequent sandbox updates. This will also help us reduce time on e2e tests as we don't have to wait 70 seconds for each assertPostDeployment
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good. Could be helpful to leave that context in a comment on this code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in ca2d0bd
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Take or leave this comment: #1527 (comment)
Changes
Add the ability to schedule functions:
every
then the rest is based on the unit of timeevery
followed by a space then a positive whole number and eitherm
for minute(s) orh
for hour(s)every 5m
orevery 1h
every
followed by a space then eitherday
,week
,month
, oryear
every day
orevery year
L
wildcard#
wildcardW
wildcardValidation
Checklist
run-e2e
label set.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.