Skip to content

Commit f4a5cef

Browse files
committed
Use max_retries and give a bigger default timeout value
1 parent 6ee848b commit f4a5cef

File tree

5 files changed

+74
-21
lines changed

5 files changed

+74
-21
lines changed

.rubocop.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ Style/WordArray:
2323

2424
Style/Documentation:
2525
Enabled: false
26+
27+
Metrics/BlockLength:
28+
Exclude:
29+
- 'spec/**/*.rb'

.rubocop_todo.yml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
# This configuration was generated by
22
# `rubocop --auto-gen-config`
3-
# on 2025-02-05 09:53:00 UTC using RuboCop version 1.69.2.
3+
# on 2025-03-31 12:39:31 UTC using RuboCop version 1.71.2.
44
# The point is for the user to remove these configuration records
55
# one by one as the offenses are removed from the code base.
66
# Note that changes in the inspected code, or installation of new
77
# versions of RuboCop, may require this file to be generated again.
88

9-
# Offense count: 74
10-
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
11-
# AllowedMethods: refine
12-
Metrics/BlockLength:
13-
Max: 695
9+
# Offense count: 1
10+
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
11+
Metrics/AbcSize:
12+
Max: 21
1413

1514
# Offense count: 4
1615
# Configuration parameters: CountComments, CountAsOne.
1716
Metrics/ClassLength:
1817
Max: 537
1918

19+
# Offense count: 4
20+
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
21+
Metrics/MethodLength:
22+
Max: 15
23+
2024
# Offense count: 3
2125
# Configuration parameters: Max, CountKeywordArgs.
2226
Metrics/ParameterLists:

lib/meilisearch/http_request.rb

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ class HTTPRequest
1010
attr_reader :options, :headers
1111

1212
DEFAULT_OPTIONS = {
13-
timeout: 1,
14-
max_retries: 0,
13+
timeout: 10,
14+
max_retries: 2,
15+
retry_multiplier: 1.2,
1516
convert_body?: true
1617
}.freeze
1718

@@ -29,7 +30,8 @@ def http_get(relative_path = '', query_params = {}, options = {})
2930
config: {
3031
query_params: query_params,
3132
headers: remove_headers(@headers.dup.merge(options[:headers] || {}), 'Content-Type'),
32-
options: @options.merge(options)
33+
options: @options.merge(options),
34+
method_type: :get
3335
}
3436
)
3537
end
@@ -42,7 +44,8 @@ def http_post(relative_path = '', body = nil, query_params = nil, options = {})
4244
query_params: query_params,
4345
body: body,
4446
headers: @headers.dup.merge(options[:headers] || {}),
45-
options: @options.merge(options)
47+
options: @options.merge(options),
48+
method_type: :post
4649
}
4750
)
4851
end
@@ -55,7 +58,8 @@ def http_put(relative_path = '', body = nil, query_params = nil, options = {})
5558
query_params: query_params,
5659
body: body,
5760
headers: @headers.dup.merge(options[:headers] || {}),
58-
options: @options.merge(options)
61+
options: @options.merge(options),
62+
method_type: :put
5963
}
6064
)
6165
end
@@ -68,7 +72,8 @@ def http_patch(relative_path = '', body = nil, query_params = nil, options = {})
6872
query_params: query_params,
6973
body: body,
7074
headers: @headers.dup.merge(options[:headers] || {}),
71-
options: @options.merge(options)
75+
options: @options.merge(options),
76+
method_type: :patch
7277
}
7378
)
7479
end
@@ -80,7 +85,8 @@ def http_delete(relative_path = '', query_params = nil, options = {})
8085
config: {
8186
query_params: query_params,
8287
headers: remove_headers(@headers.dup.merge(options[:headers] || {}), 'Content-Type'),
83-
options: @options.merge(options)
88+
options: @options.merge(options),
89+
method_type: :delete
8490
}
8591
)
8692
end
@@ -102,15 +108,23 @@ def remove_headers(data, *keys)
102108
data.delete_if { |k| keys.include?(k) }
103109
end
104110

105-
def send_request(http_method, relative_path, config: {})
106-
config = http_config(config[:query_params], config[:body], config[:options], config[:headers])
111+
def send_request(http_method, relative_path, config:)
112+
attempts = 0
113+
retry_multiplier = config.dig(:options, :retry_multiplier)
114+
max_retries = config.dig(:options, :max_retries)
115+
request_config = http_config(config[:query_params], config[:body], config[:options], config[:headers])
107116

108117
begin
109-
response = http_method.call(@base_url + relative_path, config)
118+
response = http_method.call(@base_url + relative_path, request_config)
110119
rescue Errno::ECONNREFUSED, Errno::EPIPE => e
111120
raise CommunicationError, e.message
112-
rescue Net::ReadTimeout, Net::OpenTimeout => e
113-
raise TimeoutError, e.message
121+
rescue Net::OpenTimeout, Net::ReadTimeout => e
122+
attempts += 1
123+
raise TimeoutError, e.message unless attempts <= max_retries && safe_to_retry?(config[:method_type], e)
124+
125+
sleep(retry_multiplier**attempts)
126+
127+
retry
114128
end
115129

116130
validate(response)
@@ -132,5 +146,10 @@ def validate(response)
132146

133147
response.parsed_response
134148
end
149+
150+
# Ensures the only retryable error is a timeout didn't reached the server
151+
def safe_to_retry?(method_type, error)
152+
method_type == :get || ([:post, :put, :patch, :delete].include?(method_type) && error.is_a?(Net::OpenTimeout))
153+
end
135154
end
136155
end

spec/meilisearch/index/base_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@
100100
end
101101

102102
it 'supports options' do
103-
options = { timeout: 2, max_retries: 1 }
103+
options = { timeout: 2, retry_multiplier: 1.2, max_retries: 1 }
104104
expected_headers = {
105105
'Authorization' => "Bearer #{MASTER_KEY}",
106106
'User-Agent' => Meilisearch.qualified_version
@@ -109,7 +109,7 @@
109109
new_client = Meilisearch::Client.new(URL, MASTER_KEY, options)
110110
new_client.create_index('books').await
111111
index = new_client.fetch_index('books')
112-
expect(index.options).to eq({ max_retries: 1, timeout: 2, convert_body?: true })
112+
expect(index.options).to eq({ max_retries: 1, retry_multiplier: 1.2, timeout: 2, convert_body?: true })
113113

114114
expect(described_class).to receive(:get).with(
115115
"#{URL}/indexes/books",
@@ -128,7 +128,7 @@
128128

129129
it 'supports client_agents' do
130130
custom_agent = 'Meilisearch Rails (v0.0.1)'
131-
options = { timeout: 2, max_retries: 1, client_agents: [custom_agent] }
131+
options = { timeout: 2, retry_multiplier: 1.2, max_retries: 1, client_agents: [custom_agent] }
132132
expected_headers = {
133133
'Authorization' => "Bearer #{MASTER_KEY}",
134134
'User-Agent' => "#{custom_agent};#{Meilisearch.qualified_version}"

spec/meilisearch_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,32 @@
3535
expect(new_client.headers).to have_key('User-Agent')
3636
expect(new_client.headers['User-Agent']).to eq(described_class.qualified_version)
3737
end
38+
39+
it 'retries the request when the request is retryable' do
40+
allow(Meilisearch::HTTPRequest).to receive(:get).and_raise(Net::ReadTimeout)
41+
42+
begin
43+
new_client = Meilisearch::Client.new(URL, MASTER_KEY, max_retries: 3, retry_multiplier: 0.1)
44+
new_client.indexes
45+
rescue Meilisearch::TimeoutError
46+
nil
47+
end
48+
49+
expect(Meilisearch::HTTPRequest).to have_received(:get).exactly(4).times
50+
end
51+
52+
it 'does not retry the request when the request is not retryable' do
53+
allow(Meilisearch::HTTPRequest).to receive(:get).and_raise(Errno::ECONNREFUSED)
54+
55+
begin
56+
new_client = Meilisearch::Client.new(URL, MASTER_KEY, max_retries: 10)
57+
new_client.indexes
58+
rescue Meilisearch::CommunicationError
59+
nil
60+
end
61+
62+
expect(Meilisearch::HTTPRequest).to have_received(:get).once
63+
end
3864
end
3965

4066
RSpec.describe MeiliSearch do

0 commit comments

Comments
 (0)