Skip to content

Commit e4ea906

Browse files
committed
WIP effection retry blogpost
1 parent 14e6f2c commit e4ea906

File tree

1 file changed

+168
-0
lines changed

1 file changed

+168
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
---
2+
templateKey: blog-post
3+
title: >-
4+
Fetch Retries in Javascript with Structured Concurrency using Effection
5+
date: 2023-02-19T20:00:00.959Z
6+
author: Taras Mankovski, Min Kim
7+
description: >-
8+
WIP
9+
tags: [ "javascript", "structured concurrency"]
10+
img: /img/2023-12-18-announcing-effection-v3.png
11+
---
12+
13+
Intro - "you're a developer..."
14+
15+
## Simple Fetch
16+
17+
Writing a simple fetch call using effection
18+
19+
```js
20+
import { main, useAbortSignal, call } from 'effection';
21+
22+
function* fetchURL() {
23+
const signal = yield* useAbortSignal();
24+
const response = yield* call(fetch("https://foo.bar"), { signal });
25+
26+
if (response.ok) {
27+
return yield* call(response.json());
28+
}
29+
}
30+
31+
main(function* () {
32+
const result = yield* fetchURL();
33+
console.log(result);
34+
});
35+
```
36+
37+
explain main, call, useAbortSignal, yield*
38+
39+
## Exponential Backoff
40+
41+
Let's add retry logic with exponential backoff
42+
43+
```js
44+
import { main, useAbortSignal, call, sleep } from 'effection';
45+
46+
function* fetchWithBackoff() {
47+
let attempt = -1;
48+
while (true) {
49+
const signal = yield* useAbortSignal();
50+
const response = yield* call(fetch("https://foo.bar"), { signal });
51+
52+
if (response.ok) {
53+
return yield* call(response.json());
54+
}
55+
let delayMs: number;
56+
57+
// https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/
58+
const backoff = Math.pow(2, attempt) * 1000;
59+
delayMs = Math.round((backoff * (1 + Math.random())) / 2);
60+
61+
if (delayMs > 4000) {
62+
return new Error("reached timeout");
63+
}
64+
65+
yield* sleep(delayMs);
66+
attempt++;
67+
}
68+
}
69+
70+
main(function* () {
71+
const result = yield* fetchWithBackoff();
72+
console.log(result);
73+
});
74+
```
75+
76+
explain sleep
77+
78+
## Structured Concurrency
79+
80+
Now let's add a timeout using race
81+
82+
```js
83+
import { main, useAbortSignal, call, sleep, race } from 'effection';
84+
85+
function* fetchWithBackoff() {
86+
let attempt = -1;
87+
while (true) {
88+
const signal = yield* useAbortSignal();
89+
const response = yield* call(fetch("https://foo.bar"), { signal });
90+
91+
if (response.ok) {
92+
return yield* call(response.json());
93+
}
94+
let delayMs: number;
95+
96+
// https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/
97+
const backoff = Math.pow(2, attempt) * 1000;
98+
delayMs = Math.round((backoff * (1 + Math.random())) / 2);
99+
100+
yield* sleep(delayMs);
101+
attempt++;
102+
}
103+
}
104+
105+
main(function* () {
106+
const result = yield* race([
107+
fetchWithBackoff(),
108+
sleep(60_000),
109+
]);
110+
console.log(result);
111+
});
112+
```
113+
114+
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
115+
116+
composable
117+
118+
## Reusable
119+
120+
we can go even further and make the retry function reusable
121+
122+
```js
123+
function* retryWithBackoff<T>(fn: () => Operation<T>, options: { timeout: number }) {
124+
function* body() {
125+
let attempt = -1;
126+
127+
while (true) {
128+
try {
129+
return yield* fn();
130+
} catch {
131+
let delayMs: number;
132+
133+
// https://aws.amazon.com/ru/blogs/architecture/exponential-backoff-and-jitter/
134+
const backoff = Math.pow(2, attempt) * 1000;
135+
delayMs = Math.round((backoff * (1 + Math.random())) / 2);
136+
137+
yield* sleep(delayMs);
138+
attempt++;
139+
}
140+
}
141+
}
142+
143+
return race([
144+
body(),
145+
sleep(options.timeout)
146+
]);
147+
}
148+
```
149+
150+
then our main function can be:
151+
152+
```js
153+
main (function* () {
154+
const result = yield* retryWithBackoff(function* () {
155+
const signal = yield* useAbortSignal();
156+
const response = yield* call(fetch("https://foo.bar", { signal }));
157+
158+
if (response.ok) {
159+
return yield* call(response.json);
160+
} else {
161+
throw new Error(response.statusText);
162+
}
163+
}, {
164+
timeout: 60_000,
165+
});
166+
console.log(result);
167+
});
168+
```

0 commit comments

Comments
 (0)