Skip to content

Commit

Permalink
Merge #581
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
meili-bors[bot] and Ja7ad authored Oct 2, 2024
2 parents 92b1ab7 + 1f0b432 commit 4f9f232
Show file tree
Hide file tree
Showing 8 changed files with 460 additions and 20 deletions.
68 changes: 61 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,19 @@

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

## Table of Contents <!-- omit in TOC -->
## Table of Contents

- [📖 Documentation](#-documentation)
- [ Supercharge your Meilisearch experience](#-supercharge-your-meilisearch-experience)
- [💫 Supercharge your Meilisearch experience](#-supercharge-your-meilisearch-experience)
- [🔧 Installation](#-installation)
- [🚀 Getting started](#-getting-started)
- [Add documents](#add-documents)
- [Basic search](#basic-search)
- [Custom search](#custom-search)
- [Custom search with filter](#custom-search-with-filters)
- [Customize client](#customize-client)
- [🤖 Compatibility with Meilisearch](#-compatibility-with-meilisearch)
- [⚡️ Benchmark performance](#-benchmark-performance)
- [💡 Learn more](#-learn-more)
- [⚙️ Contributing](#️-contributing)

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

### Run Meilisearch <!-- omit in toc -->
### Run Meilisearch

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

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

## 🚀 Getting started

#### Add documents <!-- omit in toc -->
#### Add documents

```go
package main
Expand Down Expand Up @@ -114,7 +120,7 @@ func main() {

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).

#### Basic Search <!-- omit in toc -->
#### Basic Search

```go
package main
Expand Down Expand Up @@ -156,7 +162,7 @@ JSON output:
}
```

#### Custom Search <!-- omit in toc -->
#### Custom Search

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

Expand Down Expand Up @@ -196,7 +202,7 @@ JSON output:
}
```

#### Custom Search With Filters <!-- omit in toc -->
#### Custom Search With Filters

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

Expand Down Expand Up @@ -234,10 +240,58 @@ searchRes, err := index.Search("wonder",
}
```

#### Customize Client

The client supports many customization options:

- `WithCustomClient` sets a custom `http.Client`.
- `WithCustomClientWithTLS` enables TLS for the HTTP client.
- `WithAPIKey` sets the API key or master [key](https://www.meilisearch.com/docs/reference/api/keys).
- `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.
- `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.
- `DisableRetries` disables the retry logic. By default, retries are enabled.

```go
package main

import (
"net/http"
"github.com/meilisearch/meilisearch-go"
)

func main() {
client := meilisearch.New("http://localhost:7700",
meilisearch.WithAPIKey("foobar"),
meilisearch.WithCustomClient(http.DefaultClient),
meilisearch.WithContentEncoding(meilisearch.GzipEncoding, meilisearch.BestCompression),
meilisearch.WithCustomRetries([]int{502}, 20),
)
}
```

## 🤖 Compatibility with Meilisearch

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.

## ⚡️ Benchmark Performance

The Meilisearch client performance was tested in [client_bench_test.go](/client_bench_test.go).

```shell
goos: linux
goarch: amd64
pkg: github.com/meilisearch/meilisearch-go
cpu: AMD Ryzen 7 5700U with Radeon Graphics
```

**Results**

```shell
Benchmark_ExecuteRequest-16 10000 105880 ns/op 7241 B/op 87 allocs/op
Benchmark_ExecuteRequestWithEncoding-16 2716 455548 ns/op 1041998 B/op 169 allocs/op
Benchmark_ExecuteRequestWithoutRetries-16 1 3002787257 ns/op 56528 B/op 332 allocs/op
```

## 💡 Learn more

The following sections in our main documentation website may interest you:
Expand Down
97 changes: 88 additions & 9 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"sync"
"time"
)

type client struct {
Expand All @@ -19,6 +20,18 @@ type client struct {
bufferPool *sync.Pool
encoder encoder
contentEncoding ContentEncoding
retryOnStatus map[int]bool
disableRetry bool
maxRetries uint8
retryBackoff func(attempt uint8) time.Duration
}

type clientConfig struct {
contentEncoding ContentEncoding
encodingCompressionLevel EncodingCompressionLevel
retryOnStatus map[int]bool
disableRetry bool
maxRetries uint8
}

type internalRequest struct {
Expand All @@ -34,7 +47,7 @@ type internalRequest struct {
functionName string
}

func newClient(cli *http.Client, host, apiKey string, ce ContentEncoding, cl EncodingCompressionLevel) *client {
func newClient(cli *http.Client, host, apiKey string, cfg clientConfig) *client {
c := &client{
client: cli,
host: host,
Expand All @@ -44,11 +57,28 @@ func newClient(cli *http.Client, host, apiKey string, ce ContentEncoding, cl Enc
return new(bytes.Buffer)
},
},
disableRetry: cfg.disableRetry,
maxRetries: cfg.maxRetries,
retryOnStatus: cfg.retryOnStatus,
}

if !ce.IsZero() {
c.contentEncoding = ce
c.encoder = newEncoding(ce, cl)
if c.retryOnStatus == nil {
c.retryOnStatus = map[int]bool{
502: true,
503: true,
504: true,
}
}

if !c.disableRetry && c.retryBackoff == nil {
c.retryBackoff = func(attempt uint8) time.Duration {
return time.Second * time.Duration(attempt)
}
}

if !cfg.contentEncoding.IsZero() {
c.contentEncoding = cfg.contentEncoding
c.encoder = newEncoding(cfg.contentEncoding, cfg.encodingCompressionLevel)
}

return c
Expand Down Expand Up @@ -197,12 +227,9 @@ func (c *client) sendRequest(

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

resp, err := c.client.Do(request)
resp, err := c.do(request, internalError)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, internalError.WithErrCode(MeilisearchTimeoutError, err)
}
return nil, internalError.WithErrCode(MeilisearchCommunicationError, err)
return nil, err
}

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

func (c *client) do(req *http.Request, internalError *Error) (resp *http.Response, err error) {
retriesCount := uint8(0)

for {
resp, err = c.client.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, internalError.WithErrCode(MeilisearchTimeoutError, err)
}
return nil, internalError.WithErrCode(MeilisearchCommunicationError, err)
}

// Exit if retries are disabled
if c.disableRetry {
break
}

// Check if response status is retryable and we haven't exceeded max retries
if c.retryOnStatus[resp.StatusCode] && retriesCount < c.maxRetries {
retriesCount++

// Close response body to prevent memory leaks
resp.Body.Close()

// Handle backoff with context cancellation support
backoff := c.retryBackoff(retriesCount)
timer := time.NewTimer(backoff)

select {
case <-req.Context().Done():
err := req.Context().Err()
timer.Stop()
return nil, internalError.WithErrCode(MeilisearchTimeoutError, err)
case <-timer.C:
// Retry after backoff
timer.Stop()
}

continue
}

break
}

// Return error if retries exceeded the maximum limit
if !c.disableRetry && retriesCount >= c.maxRetries {
return nil, internalError.WithErrCode(MeilisearchMaxRetriesExceeded, nil)
}

return resp, nil
}

func (c *client) handleStatusCode(req *internalRequest, statusCode int, body []byte, internalError *Error) error {
if req.acceptedStatusCodes != nil {

Expand Down
Loading

0 comments on commit 4f9f232

Please sign in to comment.