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

[feature] custom retry callback #332

Merged
merged 6 commits into from
Nov 21, 2023

Conversation

MikePresman
Copy link
Contributor

@MikePresman MikePresman commented Nov 6, 2023

This PR implements two fundamental changes.

Edit: 1. I have added PATCH and POST as given default retryMethods see review by airhorns
2. I have added the option to pass a callback to handle retries in a custom fashion as defined by the client.

Point 2. Described

The motivation for this change is that out of the box reply-from does not retry 500's so gracefully handling such cases is not possible. As a result with this change you are able to write your own callback to retry 500's as you wish (or any other kind of error - using 500's here as an exemplar).

This can be done quite simply.

const customRetryLogic = (req, res, registerDefaultRetry, defaultRetryAfter) => {
   if (res && res.statusCode === 500 && req.method === 'GET') {
     return 300
   }
   return null
 }

...
Fastify.reply(From, customRetry: { handler: customRetryLogic, retries: 10 }}

Note this new API is an object which has two keys. handler and retries where handler is a required key and retries is optional but strongly encouraged.


There is an important intricacy to this API.

If no customRetry object is passed then business as usual in the library. Retries work as they are expected for default cases.

But if a customRetry is passed then it is up to responsibility of the callback to be aware that if they expect default behaviour to co-exist they must register it. If they don't (the above example DID NOT) all default cases of errors (such as 503s) won't be retried.

Example to get default behaviour in a customRetry

const customRetryLogic = (req, res, registerDefaultRetry, defaultRetryAfter) => {
   // Registering the default retry logic so if a default case occurs we handle it.
   if (registerDefaultRetry()){
     return defaultRetryAfter; // Some new magic is that we can override our headers and pass a new retry-after value.
   }

   if (res && res.statusCode === 500 && req.method === 'GET') {
     return 300
   }
   return null
 }

CC: @airhorns

Checklist

@MikePresman MikePresman force-pushed the add-custom-retry-handler branch 3 times, most recently from 1a0782e to 25ad054 Compare November 6, 2023 17:25
@MikePresman MikePresman changed the title Add custom retry handler [feature] custom retry callback Nov 6, 2023
@MikePresman MikePresman marked this pull request as ready for review November 6, 2023 17:48
index.js Outdated
@@ -29,7 +29,7 @@ const fastifyReplyFrom = fp(function from (fastify, opts, next) {
])

const retryMethods = new Set(opts.retryMethods || [
'GET', 'HEAD', 'OPTIONS', 'TRACE'
'GET', 'HEAD', 'OPTIONS', 'TRACE', 'POST', 'PATCH'
Copy link
Member

Choose a reason for hiding this comment

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

I think this makes this a breaking change when it doesn't need to be. I think it'd be simpler if we just passed this in gadget side?

index.js Outdated
if (res && res.headers['retry-after']) {
retryAfter = res.headers['retry-after']
const defaultRetryAfter = () => {
let retryAfter = 42 * Math.random() * (retries + 1)
Copy link
Member

Choose a reason for hiding this comment

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

What do you think about including the 503 retry behaviour in this function as well? To me it doesn't really seem extra special such that a custom handler may want to be a client of that logic as well.

index.js Outdated
if (!reply.sent) {
// always retry on 503 errors

const defaultRetry = () => {
Copy link
Member

Choose a reason for hiding this comment

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

Why two separate functions for the duration and if we should retry or not? I think it'd be simpler with one function that returns null or a number, null meaning don't retry, and a number meaning retry after that many milliseconds.

README.md Outdated
Comment on lines 277 to 288
const customRetryLogic = (req, res, registerDefaultRetry, defaultRetryAfter) => {
//If this block is not included all non 500 errors will not be retried
if (registerDefaultRetry()){
return defaultRetryAfter;
}

//Custom retry logic
if (res && res.statusCode === 500 && req.method === 'GET') {
return 300
}
return null
}
Copy link
Member

Choose a reason for hiding this comment

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

I think it might be clearer not to describe this as "registering" another retry handler (both in this PR and in the test code), we're not really adding a handler to an event emitter or something, but just passing an option.

Also, I think that a custom retry handler needs to be passed the number of attempts so far if it wants to calculate an exponential delay. What do you think about this API instaead

Suggested change
const customRetryLogic = (req, res, registerDefaultRetry, defaultRetryAfter) => {
//If this block is not included all non 500 errors will not be retried
if (registerDefaultRetry()){
return defaultRetryAfter;
}
//Custom retry logic
if (res && res.statusCode === 500 && req.method === 'GET') {
return 300
}
return null
}
const customRetryLogic = (req, res, attemptCount, getDefaultDelay) => {
const defaultDelay = getDefaultDelay();
if (defaultDelay) return defaultDelay();
if (res && res.statusCode === 500 && req.method === 'GET') {
return 300
}
return null
}

index.js Outdated
@@ -251,29 +252,51 @@ function isFastifyMultipartRegistered (fastify) {
return (fastify.hasContentTypeParser('multipart') || fastify.hasContentTypeParser('multipart/form-data')) && fastify.hasRequestDecorator('multipart')
}

function createRequestRetry (requestImpl, reply, retriesCount, retryOnError, maxRetriesOn503) {
function createRequestRetry (requestImpl, reply, retriesCount, retryOnError, maxRetriesOn503, customRetry) {
Copy link
Member

Choose a reason for hiding this comment

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

The method signature to this function seems a little complicated. I think it might be simpler if the caller just built one "give me the retry delay" function, and passed it into this thing. That function can do whatever it needs to based on the options, and kind of bottles up all the logic, so this thing doesn't really need to care about the "when" of retrying, and is just about actually doing it based on the output of that function.

}
})

test("a server 503's with a custom handler for 500 and the custom handler registers the default so it passes", async (t) => {
Copy link
Member

Choose a reason for hiding this comment

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

If you are down with the language change

Suggested change
test("a server 503's with a custom handler for 500 and the custom handler registers the default so it passes", async (t) => {
test("custom retry delay functions can invoke the default delay", async (t) => {

Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

lgtm

Copy link
Member

@airhorns airhorns left a comment

Choose a reason for hiding this comment

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

LGTM after comments!

@@ -14,7 +14,7 @@ npm i @fastify/reply-from
```

## Compatibility with @fastify/multipart
`@fastify/reply-from` and [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) should not be registered as sibling plugins nor should they be registered in plugins which have a parent-child relationship.<br> The two plugins are incompatible, in the sense that the behavior of `@fastify/reply-from` might not be the expected one when the above-mentioned conditions are not respected.<br> This is due to the fact that `@fastify/multipart` consumes the multipart content by parsing it, hence this content is not forwarded to the target service by `@fastify/reply-from`.<br>
`@fastify/reply-from` and [`@fastify/multipart`](https://github.com/fastify/fastify-multipart) should not be registered as sibling plugins nor should they be registered in plugins which have a parent-child relationship.`<br>` The two plugins are incompatible, in the sense that the behavior of `@fastify/reply-from` might not be the expected one when the above-mentioned conditions are not respected.`<br>` This is due to the fact that `@fastify/multipart` consumes the multipart content by parsing it, hence this content is not forwarded to the target service by `@fastify/reply-from`.`<br>`
Copy link
Member

Choose a reason for hiding this comment

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

Can you avoid making these unrelated changes in this PR 🙏

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I ran lint incorrectly my bad !
Will fix.

@airhorns airhorns merged commit b79a22d into fastify:master Nov 21, 2023
38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants