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

Retry with backoff #30

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open

Conversation

minkimcello
Copy link

@minkimcello minkimcello commented Dec 16, 2024

Motivation

Sometimes when an API call fails, we want to be able to retry those requests until it succeeds (within a reasonable amount of time).

useRetryWithBackoff will re-attempt an operation with incremental backoff until the configured timeout exceeds.

Approach

Just copied/pasted what I wrote for the README 👇

There's a default timeout set to 30 seconds. If you'd like to set a different
timeout, you'll need to either pass in options as a second argument to
useRetryWithBackoff:

import { main } from "effection";
import { useRetryWithBackoff } from "@effection-contrib/retry-backoff";

await main(function* () {
  yield* useRetryWithBackoff(function* () {
    yield* call(() => fetch("https://foo.bar/"));
  }, { timeout: 45_000 });
});

Or set the timeout via the context so that the same timeout can be applied to all
of your retry operations:

import { main } from "effection";
import {
  RetryBackoffContext,
  useRetryWithBackoff,
} from "@effection-contrib/retry-backoff";

await main(function* () {
  yield* RetryBackoffContext.set({ timeout: 45_000 });
  yield* retryWithBackoff(function* () {
    yield* call(() => fetch("https://foo.bar/"));
  });
});

Learning

Screenshots

@minkimcello minkimcello force-pushed the mk/retry branch 2 times, most recently from 1f22f51 to 2c35e7a Compare December 17, 2024 14:26
@minkimcello minkimcello marked this pull request as ready for review December 17, 2024 15:03
initRetryWithBackoff,
useRetryWithBackoff,
} from "@effection-contrib/retry-backoff";
import { myOperation } from "./myOperation";
Copy link
Member

Choose a reason for hiding this comment

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

Instead of doing the import, can we just inline the operation and use fetch so people see what that looks like?

Copy link
Author

Choose a reason for hiding this comment

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

Done

Comment on lines 68 to 87
export function* initRetryWithBackoff(
defaults: RetryWithContextDefaults,
) {
// deno-lint-ignore require-yield
function* init(): Operation<RetryWithContextDefaults> {
return defaults;
}

return yield* ensureContext(
RetryWithBackoffContext,
init(),
);
}

export function* ensureContext<T>(Context: ContextType<T>, init: Operation<T>) {
if (!(yield* Context.get())) {
yield* Context.set(yield* init);
}
return yield* Context.expect();
}
Copy link
Member

Choose a reason for hiding this comment

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

Since we're using default now, we can drop this. We don't need this code anymore

Copy link
Author

Choose a reason for hiding this comment

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

Don't we still need it if someone wants to override the default for all retries?

import { myOperation } from "./myOperation";

await main(function* () {
yield* initRetryWithBackoff({ timeout: 45_000 });
Copy link
Member

Choose a reason for hiding this comment

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

Let's remove this function.

Copy link
Author

Choose a reason for hiding this comment

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

Are you talking about the example myOperation function? or main?

Copy link
Member

@taras taras Dec 17, 2024

Choose a reason for hiding this comment

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

the example myOperation function, so the example reads like

await main(function*() {
  yield* retryBackoff(function*() {
     const result = call(() => fetch(....));
     ...
  });
})

Copy link
Author

Choose a reason for hiding this comment

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

oh okay. I took care of that from this comment

timeout: number;
}

const RetryWithBackoffContext = createContext<RetryWithContextDefaults>(
Copy link
Member

Choose a reason for hiding this comment

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

Let's rename this to RetryBackoffContext.

Copy link
Author

Choose a reason for hiding this comment

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

Done

}
);

export function* useRetryWithBackoff<T> (
Copy link
Member

Choose a reason for hiding this comment

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

We need docs for this function.

Copy link
Author

Choose a reason for hiding this comment

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

Done

Copy link
Author

Choose a reason for hiding this comment

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

What would be the right way to write docs for this context?

/**
  * Description
  *
  * @property {function} set
  * @param {{ timeout: number }}
*/
export const RetryBackoffContext = createContext<RetryWithContextDefaults>(

Copy link
Member

Choose a reason for hiding this comment

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

That's right. You don't need to describe the parameters and properties because those will be generated from typescript


await main(function* () {
yield* useRetryWithBackoff(function* () {
yield* call(fetch("https://foo.bar/"));
Copy link
Member

Choose a reason for hiding this comment

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

This won't be compatible with v4, so let's use function syntax: yield* call(() => fetch(...))

Copy link
Author

Choose a reason for hiding this comment

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

Done

await main(function* () {
yield* initRetryWithBackoff({ timeout: 45_000 });
yield* retryWithBackoff(function* () {
yield* call(fetch("https://foo.bar/"));
Copy link
Member

Choose a reason for hiding this comment

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

Same here, let's use the function syntax that's compatible with v4

Copy link
Author

Choose a reason for hiding this comment

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

And done

Copy link
Member

@cowboyd cowboyd left a comment

Choose a reason for hiding this comment

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

This is a very common use-case. What about just making this @effection-contrib/retry and have many strategies (retriableWithBackoff) being just one.

interface RetriableOptions {
  backoff(failedAttempts: number): number;
  maxAttempts: number;
}
//. constant backoff
const retriable = createRetriable({ backoff: () => 3, maxAttempts: Math.Infinity}); 
yield* retriable(operation);

//exponential backoff
const retriable = createRetriable({ backoff: (attempts} => 2^attempts, maxAttempts: 100});
yield* retriable(operation);

You can even model the default as a the default that is on the context:

const defaultRetriable = createRetriable({ backoff: () => 10, maxAttempts: 10 });
const DefaulthRetriableContext = createContext<<T>(op: Operation<T>) => Operation<T>>('@effection-contrib/default-retriable', defaultRetriable);

export function* retriable<T>(operation: Operation<T>): Operation<T> {
  let defaultImpl = yield* DefaultRetriableContext.expect();
  return yield* defaultImpl(operation);
}

My suspicion is that most folks will want to roll their own.

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