-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
14e6f2c
commit e4ea906
Showing
1 changed file
with
168 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
--- | ||
templateKey: blog-post | ||
title: >- | ||
Fetch Retries in Javascript with Structured Concurrency using Effection | ||
date: 2023-02-19T20:00:00.959Z | ||
author: Taras Mankovski, Min Kim | ||
description: >- | ||
WIP | ||
tags: [ "javascript", "structured concurrency"] | ||
img: /img/2023-12-18-announcing-effection-v3.png | ||
--- | ||
|
||
Intro - "you're a developer..." | ||
|
||
## Simple Fetch | ||
|
||
Writing a simple fetch call using effection | ||
|
||
```js | ||
import { main, useAbortSignal, call } from 'effection'; | ||
|
||
function* fetchURL() { | ||
const signal = yield* useAbortSignal(); | ||
const response = yield* call(fetch("https://foo.bar"), { signal }); | ||
|
||
if (response.ok) { | ||
return yield* call(response.json()); | ||
} | ||
} | ||
|
||
main(function* () { | ||
const result = yield* fetchURL(); | ||
console.log(result); | ||
}); | ||
``` | ||
|
||
explain main, call, useAbortSignal, yield* | ||
|
||
## Exponential Backoff | ||
|
||
Let's add retry logic with exponential backoff | ||
|
||
```js | ||
import { main, useAbortSignal, call, sleep } from 'effection'; | ||
|
||
function* fetchWithBackoff() { | ||
let attempt = -1; | ||
while (true) { | ||
const signal = yield* useAbortSignal(); | ||
const response = yield* call(fetch("https://foo.bar"), { signal }); | ||
|
||
if (response.ok) { | ||
return yield* call(response.json()); | ||
} | ||
let delayMs: number; | ||
|
||
// https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/ | ||
const backoff = Math.pow(2, attempt) * 1000; | ||
delayMs = Math.round((backoff * (1 + Math.random())) / 2); | ||
|
||
if (delayMs > 4000) { | ||
return new Error("reached timeout"); | ||
} | ||
|
||
yield* sleep(delayMs); | ||
attempt++; | ||
} | ||
} | ||
|
||
main(function* () { | ||
const result = yield* fetchWithBackoff(); | ||
console.log(result); | ||
}); | ||
``` | ||
|
||
explain sleep | ||
|
||
## Structured Concurrency | ||
|
||
Now let's add a timeout using race | ||
|
||
```js | ||
import { main, useAbortSignal, call, sleep, race } from 'effection'; | ||
|
||
function* fetchWithBackoff() { | ||
let attempt = -1; | ||
while (true) { | ||
const signal = yield* useAbortSignal(); | ||
const response = yield* call(fetch("https://foo.bar"), { signal }); | ||
|
||
if (response.ok) { | ||
return yield* call(response.json()); | ||
} | ||
let delayMs: number; | ||
|
||
// https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/ | ||
const backoff = Math.pow(2, attempt) * 1000; | ||
delayMs = Math.round((backoff * (1 + Math.random())) / 2); | ||
|
||
yield* sleep(delayMs); | ||
attempt++; | ||
} | ||
} | ||
|
||
main(function* () { | ||
const result = yield* race([ | ||
fetchWithBackoff(), | ||
sleep(60_000), | ||
]); | ||
console.log(result); | ||
}); | ||
``` | ||
|
||
explain race - abort signal does not need to be threaded through nor do we need to clear timeout, if timeout wins the race, the fetch will be aborted automatically and vice versa | ||
|
||
composable | ||
|
||
## Reusable | ||
|
||
we can go even further and make the retry function reusable | ||
|
||
```js | ||
function* retryWithBackoff<T>(fn: () => Operation<T>, options: { timeout: number }) { | ||
function* body() { | ||
let attempt = -1; | ||
|
||
while (true) { | ||
try { | ||
return yield* fn(); | ||
} catch { | ||
let delayMs: number; | ||
|
||
// https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/ | ||
const backoff = Math.pow(2, attempt) * 1000; | ||
delayMs = Math.round((backoff * (1 + Math.random())) / 2); | ||
|
||
yield* sleep(delayMs); | ||
attempt++; | ||
} | ||
} | ||
} | ||
|
||
return race([ | ||
body(), | ||
sleep(options.timeout) | ||
]); | ||
} | ||
``` | ||
|
||
then our main function can be: | ||
|
||
```js | ||
main (function* () { | ||
const result = yield* retryWithBackoff(function* () { | ||
const signal = yield* useAbortSignal(); | ||
const response = yield* call(fetch("https://foo.bar", { signal })); | ||
|
||
if (response.ok) { | ||
return yield* call(response.json); | ||
} else { | ||
throw new Error(response.statusText); | ||
} | ||
}, { | ||
timeout: 60_000, | ||
}); | ||
console.log(result); | ||
}); | ||
``` |