Skip to content

Commit 4f9f232

Browse files
Merge #581
581: Feat support retry pattern on status code 502, 503, 504 r=curquiza a=Ja7ad # Pull Request ## Related issue Fixes #569 ## What does this PR do? - Add retry pattern logic in client as default can manage retry pattern via options. ## PR checklist Please check if your PR fulfills the following requirements: - [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)? - [x] Have you read the contributing guidelines? - [x] Have you made sure that the title is accurate and descriptive of the changes? Thank you so much for contributing to Meilisearch! Co-authored-by: Javad <[email protected]>
2 parents 92b1ab7 + 1f0b432 commit 4f9f232

File tree

8 files changed

+460
-20
lines changed

8 files changed

+460
-20
lines changed

README.md

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,19 @@
2929

3030
**Meilisearch** is an open-source search engine. [Learn more about Meilisearch.](https://github.com/meilisearch/Meilisearch)
3131

32-
## Table of Contents <!-- omit in TOC -->
32+
## Table of Contents
3333

3434
- [📖 Documentation](#-documentation)
35-
- [ Supercharge your Meilisearch experience](#-supercharge-your-meilisearch-experience)
35+
- [💫 Supercharge your Meilisearch experience](#-supercharge-your-meilisearch-experience)
3636
- [🔧 Installation](#-installation)
3737
- [🚀 Getting started](#-getting-started)
38+
- [Add documents](#add-documents)
39+
- [Basic search](#basic-search)
40+
- [Custom search](#custom-search)
41+
- [Custom search with filter](#custom-search-with-filters)
42+
- [Customize client](#customize-client)
3843
- [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch)
44+
- [⚡️ Benchmark performance](#-benchmark-performance)
3945
- [💡 Learn more](#-learn-more)
4046
- [⚙️ Contributing](#️-contributing)
4147

@@ -57,7 +63,7 @@ With `go get` in command line:
5763
go get github.com/meilisearch/meilisearch-go
5864
```
5965

60-
### Run Meilisearch <!-- omit in toc -->
66+
### Run Meilisearch
6167

6268
There are many easy ways to [download and run a Meilisearch instance](https://www.meilisearch.com/docs/learn/getting_started/installation).
6369

@@ -75,7 +81,7 @@ NB: you can also download Meilisearch from **Homebrew** or **APT** or even run i
7581

7682
## 🚀 Getting started
7783

78-
#### Add documents <!-- omit in toc -->
84+
#### Add documents
7985

8086
```go
8187
package main
@@ -114,7 +120,7 @@ func main() {
114120

115121
With the `taskUID`, you can check the status (`enqueued`, `canceled`, `processing`, `succeeded` or `failed`) of your documents addition using the [task endpoint](https://www.meilisearch.com/docs/reference/api/tasks).
116122

117-
#### Basic Search <!-- omit in toc -->
123+
#### Basic Search
118124

119125
```go
120126
package main
@@ -156,7 +162,7 @@ JSON output:
156162
}
157163
```
158164

159-
#### Custom Search <!-- omit in toc -->
165+
#### Custom Search
160166

161167
All the supported options are described in the [search parameters](https://www.meilisearch.com/docs/reference/api/search#search-parameters) section of the documentation.
162168

@@ -196,7 +202,7 @@ JSON output:
196202
}
197203
```
198204

199-
#### Custom Search With Filters <!-- omit in toc -->
205+
#### Custom Search With Filters
200206

201207
If you want to enable filtering, you must add your attributes to the `filterableAttributes` index setting.
202208

@@ -234,10 +240,58 @@ searchRes, err := index.Search("wonder",
234240
}
235241
```
236242

243+
#### Customize Client
244+
245+
The client supports many customization options:
246+
247+
- `WithCustomClient` sets a custom `http.Client`.
248+
- `WithCustomClientWithTLS` enables TLS for the HTTP client.
249+
- `WithAPIKey` sets the API key or master [key](https://www.meilisearch.com/docs/reference/api/keys).
250+
- `WithContentEncoding` configures [content encoding](https://www.meilisearch.com/docs/reference/api/overview#content-encoding) for requests and responses. Currently, gzip, deflate, and brotli are supported.
251+
- `WithCustomRetries` customizes retry behavior based on specific HTTP status codes (`retryOnStatus`, defaults to 502, 503, and 504) and allows setting the maximum number of retries.
252+
- `DisableRetries` disables the retry logic. By default, retries are enabled.
253+
254+
```go
255+
package main
256+
257+
import (
258+
"net/http"
259+
"github.com/meilisearch/meilisearch-go"
260+
)
261+
262+
func main() {
263+
client := meilisearch.New("http://localhost:7700",
264+
meilisearch.WithAPIKey("foobar"),
265+
meilisearch.WithCustomClient(http.DefaultClient),
266+
meilisearch.WithContentEncoding(meilisearch.GzipEncoding, meilisearch.BestCompression),
267+
meilisearch.WithCustomRetries([]int{502}, 20),
268+
)
269+
}
270+
```
271+
237272
## 🤖 Compatibility with Meilisearch
238273

239274
This package guarantees compatibility with [version v1.x of Meilisearch](https://github.com/meilisearch/meilisearch/releases/latest), but some features may not be present. Please check the [issues](https://github.com/meilisearch/meilisearch-go/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22+label%3Aenhancement) for more info.
240275

276+
## ⚡️ Benchmark Performance
277+
278+
The Meilisearch client performance was tested in [client_bench_test.go](/client_bench_test.go).
279+
280+
```shell
281+
goos: linux
282+
goarch: amd64
283+
pkg: github.com/meilisearch/meilisearch-go
284+
cpu: AMD Ryzen 7 5700U with Radeon Graphics
285+
```
286+
287+
**Results**
288+
289+
```shell
290+
Benchmark_ExecuteRequest-16 10000 105880 ns/op 7241 B/op 87 allocs/op
291+
Benchmark_ExecuteRequestWithEncoding-16 2716 455548 ns/op 1041998 B/op 169 allocs/op
292+
Benchmark_ExecuteRequestWithoutRetries-16 1 3002787257 ns/op 56528 B/op 332 allocs/op
293+
```
294+
241295
## 💡 Learn more
242296

243297
The following sections in our main documentation website may interest you:

client.go

Lines changed: 88 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"net/http"
1111
"net/url"
1212
"sync"
13+
"time"
1314
)
1415

1516
type client struct {
@@ -19,6 +20,18 @@ type client struct {
1920
bufferPool *sync.Pool
2021
encoder encoder
2122
contentEncoding ContentEncoding
23+
retryOnStatus map[int]bool
24+
disableRetry bool
25+
maxRetries uint8
26+
retryBackoff func(attempt uint8) time.Duration
27+
}
28+
29+
type clientConfig struct {
30+
contentEncoding ContentEncoding
31+
encodingCompressionLevel EncodingCompressionLevel
32+
retryOnStatus map[int]bool
33+
disableRetry bool
34+
maxRetries uint8
2235
}
2336

2437
type internalRequest struct {
@@ -34,7 +47,7 @@ type internalRequest struct {
3447
functionName string
3548
}
3649

37-
func newClient(cli *http.Client, host, apiKey string, ce ContentEncoding, cl EncodingCompressionLevel) *client {
50+
func newClient(cli *http.Client, host, apiKey string, cfg clientConfig) *client {
3851
c := &client{
3952
client: cli,
4053
host: host,
@@ -44,11 +57,28 @@ func newClient(cli *http.Client, host, apiKey string, ce ContentEncoding, cl Enc
4457
return new(bytes.Buffer)
4558
},
4659
},
60+
disableRetry: cfg.disableRetry,
61+
maxRetries: cfg.maxRetries,
62+
retryOnStatus: cfg.retryOnStatus,
4763
}
4864

49-
if !ce.IsZero() {
50-
c.contentEncoding = ce
51-
c.encoder = newEncoding(ce, cl)
65+
if c.retryOnStatus == nil {
66+
c.retryOnStatus = map[int]bool{
67+
502: true,
68+
503: true,
69+
504: true,
70+
}
71+
}
72+
73+
if !c.disableRetry && c.retryBackoff == nil {
74+
c.retryBackoff = func(attempt uint8) time.Duration {
75+
return time.Second * time.Duration(attempt)
76+
}
77+
}
78+
79+
if !cfg.contentEncoding.IsZero() {
80+
c.contentEncoding = cfg.contentEncoding
81+
c.encoder = newEncoding(cfg.contentEncoding, cfg.encodingCompressionLevel)
5282
}
5383

5484
return c
@@ -197,12 +227,9 @@ func (c *client) sendRequest(
197227

198228
request.Header.Set("User-Agent", GetQualifiedVersion())
199229

200-
resp, err := c.client.Do(request)
230+
resp, err := c.do(request, internalError)
201231
if err != nil {
202-
if errors.Is(err, context.DeadlineExceeded) {
203-
return nil, internalError.WithErrCode(MeilisearchTimeoutError, err)
204-
}
205-
return nil, internalError.WithErrCode(MeilisearchCommunicationError, err)
232+
return nil, err
206233
}
207234

208235
if body != nil {
@@ -213,6 +240,58 @@ func (c *client) sendRequest(
213240
return resp, nil
214241
}
215242

243+
func (c *client) do(req *http.Request, internalError *Error) (resp *http.Response, err error) {
244+
retriesCount := uint8(0)
245+
246+
for {
247+
resp, err = c.client.Do(req)
248+
if err != nil {
249+
if errors.Is(err, context.DeadlineExceeded) {
250+
return nil, internalError.WithErrCode(MeilisearchTimeoutError, err)
251+
}
252+
return nil, internalError.WithErrCode(MeilisearchCommunicationError, err)
253+
}
254+
255+
// Exit if retries are disabled
256+
if c.disableRetry {
257+
break
258+
}
259+
260+
// Check if response status is retryable and we haven't exceeded max retries
261+
if c.retryOnStatus[resp.StatusCode] && retriesCount < c.maxRetries {
262+
retriesCount++
263+
264+
// Close response body to prevent memory leaks
265+
resp.Body.Close()
266+
267+
// Handle backoff with context cancellation support
268+
backoff := c.retryBackoff(retriesCount)
269+
timer := time.NewTimer(backoff)
270+
271+
select {
272+
case <-req.Context().Done():
273+
err := req.Context().Err()
274+
timer.Stop()
275+
return nil, internalError.WithErrCode(MeilisearchTimeoutError, err)
276+
case <-timer.C:
277+
// Retry after backoff
278+
timer.Stop()
279+
}
280+
281+
continue
282+
}
283+
284+
break
285+
}
286+
287+
// Return error if retries exceeded the maximum limit
288+
if !c.disableRetry && retriesCount >= c.maxRetries {
289+
return nil, internalError.WithErrCode(MeilisearchMaxRetriesExceeded, nil)
290+
}
291+
292+
return resp, nil
293+
}
294+
216295
func (c *client) handleStatusCode(req *internalRequest, statusCode int, body []byte, internalError *Error) error {
217296
if req.acceptedStatusCodes != nil {
218297

0 commit comments

Comments
 (0)