Skip to content

Commit cda123b

Browse files
shortcutsantgilles
andauthored
fix(go): retry strategy (#5159)
Co-authored-by: Antoine GILLES <[email protected]>
1 parent 63ee31f commit cda123b

File tree

6 files changed

+144
-3
lines changed

6 files changed

+144
-3
lines changed

clients/algoliasearch-client-go/algolia/transport/transport.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,27 @@ func New(cfg Configuration) *Transport {
4444
return transport
4545
}
4646

47+
func prepareRetryableRequest(req *http.Request) (*http.Request, error) {
48+
// Read the original body
49+
if req.Body == nil {
50+
return req, nil // Nothing to do if there's no body
51+
}
52+
53+
bodyBytes, err := io.ReadAll(req.Body)
54+
if err != nil {
55+
return nil, fmt.Errorf("cannot read body: %v", err)
56+
}
57+
_ = req.Body.Close() // close the original body
58+
59+
// Set up GetBody to recreate the body for retries
60+
req.Body = io.NopCloser(bytes.NewReader(bodyBytes))
61+
req.GetBody = func() (io.ReadCloser, error) {
62+
return io.NopCloser(bytes.NewReader(bodyBytes)), nil
63+
}
64+
65+
return req, nil
66+
}
67+
4768
func (t *Transport) Request(ctx context.Context, req *http.Request, k call.Kind, c RequestConfiguration) (*http.Response, []byte, error) {
4869
var intermediateNetworkErrors []error
4970

@@ -52,7 +73,13 @@ func (t *Transport) Request(ctx context.Context, req *http.Request, k call.Kind,
5273
req.Header.Add("Content-Encoding", "gzip")
5374
}
5475

55-
for _, h := range t.retryStrategy.GetTryableHosts(k) {
76+
// Prepare the request to be retryable.
77+
req, err := prepareRetryableRequest(req)
78+
if err != nil {
79+
return nil, nil, err
80+
}
81+
82+
for i, h := range t.retryStrategy.GetTryableHosts(k) {
5683
// Handle per-request timeout by using a context with timeout.
5784
// Note that because we are in a loop, the cancel() callback cannot be
5885
// deferred. Instead, we call it precisely after the end of each loop or
@@ -62,8 +89,17 @@ func (t *Transport) Request(ctx context.Context, req *http.Request, k call.Kind,
6289
var (
6390
ctxTimeout time.Duration
6491
connectTimeout time.Duration
92+
err error
6593
)
6694

95+
// Reassign a fresh body for the retry
96+
if i > 0 && req.GetBody != nil {
97+
req.Body, err = req.GetBody()
98+
if err != nil {
99+
break
100+
}
101+
}
102+
67103
switch {
68104
case k == call.Read && c.ReadTimeout != nil:
69105
ctxTimeout = *c.ReadTimeout
@@ -113,6 +149,9 @@ func (t *Transport) Request(ctx context.Context, req *http.Request, k call.Kind,
113149
default:
114150
if err != nil {
115151
intermediateNetworkErrors = append(intermediateNetworkErrors, err)
152+
} else if res != nil {
153+
msg := fmt.Sprintf("cannot perform request:\n\tStatusCode=%d\n\tmethod=%s\n\turl=%s\n\t", res.StatusCode, req.Method, req.URL)
154+
intermediateNetworkErrors = append(intermediateNetworkErrors, errors.New(msg))
116155
}
117156
if res != nil && res.Body != nil {
118157
if err = res.Body.Close(); err != nil {

scripts/cts/runCts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Language } from '../types.ts';
88
import { assertValidAccountCopyIndex } from './testServer/accountCopyIndex.ts';
99
import { printBenchmarkReport } from './testServer/benchmark.ts';
1010
import { assertChunkWrapperValid } from './testServer/chunkWrapper.ts';
11+
import { assertValidErrors } from './testServer/error.ts';
1112
import { startTestServer } from './testServer/index.ts';
1213
import { assertPushMockValid } from './testServer/pushMock.ts';
1314
import { assertValidReplaceAllObjects } from './testServer/replaceAllObjects.ts';
@@ -152,6 +153,7 @@ export async function runCts(
152153
const skip = (lang: Language): number => (languages.includes(lang) ? 1 : 0);
153154
const only = skip;
154155

156+
assertValidErrors(languages.length);
155157
assertValidTimeouts(languages.length);
156158
assertChunkWrapperValid(languages.length - skip('dart'));
157159
assertValidReplaceAllObjects(languages.length - skip('dart'));

scripts/cts/testServer/error.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import type { Server } from 'http';
2+
3+
import { expect } from 'chai';
4+
import type express from 'express';
5+
6+
import { setupServer } from './index.ts';
7+
8+
const errorState: Record<string, { errorCount: number; maxError: number }> = {};
9+
10+
export function assertValidErrors(expectedCount: number): void {
11+
// assert that the retry strategy uses the correct timings, by checking the time between each request, and how long each request took before being timed out
12+
// there should be no delay between requests, only an increase in error.
13+
if (Object.keys(errorState).length !== expectedCount) {
14+
throw new Error(`Expected ${expectedCount} error(s)`);
15+
}
16+
17+
for (const [lang, state] of Object.entries(errorState)) {
18+
// python has sync and async tests
19+
if (lang === 'python') {
20+
expect(state.errorCount).to.equal(state.maxError * 2);
21+
22+
return;
23+
}
24+
25+
expect(state.errorCount).to.equal(state.maxError);
26+
}
27+
}
28+
29+
function addRoutes(app: express.Express): void {
30+
app.post('/1/test/error/:lang', (req, res) => {
31+
const lang = req.params.lang;
32+
if (!errorState[lang]) {
33+
errorState[lang] = {
34+
errorCount: 0,
35+
maxError: 3,
36+
};
37+
}
38+
39+
errorState[lang].errorCount++;
40+
41+
if (errorState[lang].errorCount % errorState[lang].maxError !== 0) {
42+
res.status(500).json({ message: 'error test server response' });
43+
return;
44+
}
45+
46+
res.status(200).json({ status: 'ok' });
47+
});
48+
}
49+
50+
export function errorServer(): Promise<Server> {
51+
return setupServer('error', 6671, addRoutes);
52+
}
53+
54+
export function errorServerRetriedOnce(): Promise<Server> {
55+
return setupServer('errorRetriedOnce', 6672, addRoutes);
56+
}
57+
58+
export function errorServerRetriedTwice(): Promise<Server> {
59+
return setupServer('errorRetriedTwice', 6673, addRoutes);
60+
}

scripts/cts/testServer/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { algoliaMockServer } from './algoliaMock.ts';
1212
import { apiKeyServer } from './apiKey.ts';
1313
import { benchmarkServer } from './benchmark.ts';
1414
import { chunkWrapperServer } from './chunkWrapper.ts';
15+
import { errorServer, errorServerRetriedOnce, errorServerRetriedTwice } from './error.ts';
1516
import { gzipServer } from './gzip.ts';
1617
import { pushMockServer } from './pushMock.ts';
1718
import { replaceAllObjectsServer } from './replaceAllObjects.ts';
@@ -27,6 +28,9 @@ export async function startTestServer(suites: Record<CTSType, boolean>): Promise
2728
if (suites.client) {
2829
toStart.push(
2930
timeoutServer(),
31+
errorServer(),
32+
errorServerRetriedOnce(),
33+
errorServerRetriedTwice(),
3034
gzipServer(),
3135
timeoutServerBis(),
3236
accountCopyIndexServer(),

scripts/cts/testServer/replaceAllObjectsWithTransformation.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ function addRoutes(app: Express): void {
8888
case 'move': {
8989
const lang = req.body.destination.replace('cts_e2e_replace_all_objects_with_transformation_', '');
9090
expect(raowtState).to.include.keys(lang);
91-
console.log(raowtState[lang]);
9291
expect(raowtState[lang]).to.deep.equal({
9392
copyCount: 2,
9493
pushCount: 4,

tests/CTS/client/search/api.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
]
110110
},
111111
{
112-
"testName": "tests the retry strategy error",
112+
"testName": "tests the retry strategy on timeout",
113113
"autoCreateClient": false,
114114
"steps": [
115115
{
@@ -148,6 +148,43 @@
148148
}
149149
]
150150
},
151+
{
152+
"testName": "tests the retry strategy on 5xx",
153+
"autoCreateClient": false,
154+
"steps": [
155+
{
156+
"type": "createClient",
157+
"parameters": {
158+
"appId": "test-app-id",
159+
"apiKey": "test-api-key",
160+
"customHosts": [
161+
{
162+
"port": 6671
163+
},
164+
{
165+
"port": 6672
166+
},
167+
{
168+
"port": 6673
169+
}
170+
]
171+
}
172+
},
173+
{
174+
"type": "method",
175+
"method": "customPost",
176+
"parameters": {
177+
"path": "1/test/error/${{language}}"
178+
},
179+
"expected": {
180+
"type": "response",
181+
"match": {
182+
"status": "ok"
183+
}
184+
}
185+
}
186+
]
187+
},
151188
{
152189
"testName": "test the compression strategy",
153190
"autoCreateClient": false,

0 commit comments

Comments
 (0)