bearcat 1.6.5.beta1 → 1.6.5.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/bearcat/client.rb +37 -3
- data/lib/bearcat/version.rb +1 -1
- data/spec/bearcat/client_spec.rb +245 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e391dc610d18bc23fbfc10173270a88d3f9dca109b63ea75320bcc8c683c811d
|
|
4
|
+
data.tar.gz: 602353626c34e485f421e46e81cf7391b4647aa44dd2555c79a6ba5517fc8d0f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a58bd62214f1b522ef0fbce0185b686009dd057f2e4e94c1e348ced20e8932819760bb4e2103acc2a115ffc31abaf8e6efa1897dcfbe4b688add87d395eb1b4
|
|
7
|
+
data.tar.gz: c355451022375fc33951674974bfd7ca8986a70a43fa05a07aba5f93352cd8d18f2c0ace525aeb3c88e33262a9f7ce3f248fcb9243a98b70cdab356c1602ae7c
|
data/lib/bearcat/client.rb
CHANGED
|
@@ -25,9 +25,43 @@ module Bearcat
|
|
|
25
25
|
@registered_endpoints
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
def retry_request
|
|
29
|
+
response = nil
|
|
30
|
+
retries_count = 0
|
|
31
|
+
begin
|
|
32
|
+
response = yield
|
|
33
|
+
rescue => e
|
|
34
|
+
# Handle 5xx server errors with retry logic
|
|
35
|
+
if e.respond_to?(:status) && [500, 501, 502, 503, 504].include?(e.status)
|
|
36
|
+
# Retry if we haven't exhausted max_retries
|
|
37
|
+
if config[:max_retries].present? && retries_count != -1 && retries_count < config[:max_retries]
|
|
38
|
+
retries_count += 1
|
|
39
|
+
sleep(config[:retry_delay] || 0) if (config[:retry_delay] || 0) > 0
|
|
40
|
+
retry
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Retries exhausted or max_retries reached, call callback if present
|
|
44
|
+
# This is called when: retries are exhausted OR we never retried but max_retries is configured
|
|
45
|
+
if config[:on_retries_exceeded].present? && retries_count != -1
|
|
46
|
+
result = config[:on_retries_exceeded].call(e)
|
|
47
|
+
# If callback returns :retry, allow one final retry after modifying parameters
|
|
48
|
+
if result == :retry
|
|
49
|
+
retries_count = -1
|
|
50
|
+
retry
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
raise
|
|
56
|
+
end
|
|
57
|
+
response
|
|
58
|
+
end
|
|
59
|
+
|
|
28
60
|
def request(method, &block)
|
|
29
|
-
response =
|
|
30
|
-
|
|
61
|
+
response = retry_request do
|
|
62
|
+
rate_limited_request do
|
|
63
|
+
connection.send(method, &block)
|
|
64
|
+
end
|
|
31
65
|
end
|
|
32
66
|
ApiArray.process_response(response, self)
|
|
33
67
|
end
|
|
@@ -59,7 +93,7 @@ module Bearcat
|
|
|
59
93
|
def rate_limited_request
|
|
60
94
|
return yield unless rate_limiter
|
|
61
95
|
|
|
62
|
-
canvas_rate_limits= 0
|
|
96
|
+
canvas_rate_limits = 0
|
|
63
97
|
response = nil
|
|
64
98
|
|
|
65
99
|
begin
|
data/lib/bearcat/version.rb
CHANGED
data/spec/bearcat/client_spec.rb
CHANGED
|
@@ -10,4 +10,249 @@ describe Bearcat::Client do
|
|
|
10
10
|
client = Bearcat::Client.new(token: "test_token")
|
|
11
11
|
client.config[:token].should == "test_token"
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
describe "#retry_request" do
|
|
15
|
+
let(:client) { Bearcat::Client.new(prefix: "http://canvas.test", token: "test_token") }
|
|
16
|
+
|
|
17
|
+
context "when request succeeds on first attempt" do
|
|
18
|
+
it "returns the response without retrying" do
|
|
19
|
+
call_count = 0
|
|
20
|
+
response = client.retry_request do
|
|
21
|
+
call_count += 1
|
|
22
|
+
"success"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
expect(response).to eq("success")
|
|
26
|
+
expect(call_count).to eq(1)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
context "when request fails with 500 error" do
|
|
31
|
+
it "raises error immediately when max_retries is not configured" do
|
|
32
|
+
client = Bearcat::Client.new(prefix: "http://canvas.test", token: "test_token")
|
|
33
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
34
|
+
.to_return(status: 500)
|
|
35
|
+
|
|
36
|
+
expect {
|
|
37
|
+
client.list_accounts
|
|
38
|
+
}.to raise_error(Footrest::HttpError::InternalServerError)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "retries the specified number of times before raising" do
|
|
42
|
+
client = Bearcat::Client.new(
|
|
43
|
+
prefix: "http://canvas.test",
|
|
44
|
+
token: "test_token",
|
|
45
|
+
max_retries: 3
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
49
|
+
.to_return(status: 500)
|
|
50
|
+
|
|
51
|
+
expect {
|
|
52
|
+
client.list_accounts
|
|
53
|
+
}.to raise_error(Footrest::HttpError::InternalServerError)
|
|
54
|
+
|
|
55
|
+
expect(WebMock).to have_requested(:get, "http://canvas.test/api/v1/accounts").times(4) # 1 initial + 3 retries
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "succeeds if retry succeeds before max_retries" do
|
|
59
|
+
client = Bearcat::Client.new(
|
|
60
|
+
prefix: "http://canvas.test",
|
|
61
|
+
token: "test_token",
|
|
62
|
+
max_retries: 3
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
66
|
+
.to_return(status: 500)
|
|
67
|
+
.to_return(status: 500)
|
|
68
|
+
.to_return(status: 200, body: '[]', headers: { 'Content-Type' => 'application/json' })
|
|
69
|
+
|
|
70
|
+
result = client.list_accounts
|
|
71
|
+
expect(result.members).to eq([])
|
|
72
|
+
expect(WebMock).to have_requested(:get, "http://canvas.test/api/v1/accounts").times(3) # 1 initial + 2 retries
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it "calls on_retries_exceeded callback when retries are exhausted" do
|
|
76
|
+
callback_called = false
|
|
77
|
+
received_error = nil
|
|
78
|
+
client = Bearcat::Client.new(
|
|
79
|
+
prefix: "http://canvas.test",
|
|
80
|
+
token: "test_token",
|
|
81
|
+
max_retries: 2,
|
|
82
|
+
on_retries_exceeded: ->(error) {
|
|
83
|
+
callback_called = true
|
|
84
|
+
received_error = error
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
89
|
+
.to_return(status: 500)
|
|
90
|
+
|
|
91
|
+
client.list_accounts rescue nil
|
|
92
|
+
expect(callback_called).to be_truthy
|
|
93
|
+
expect(received_error).to be_a(Footrest::HttpError::InternalServerError)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it "passes error details to on_retries_exceeded callback" do
|
|
97
|
+
received_error = nil
|
|
98
|
+
client = Bearcat::Client.new(
|
|
99
|
+
prefix: "http://canvas.test",
|
|
100
|
+
token: "test_token",
|
|
101
|
+
max_retries: 1,
|
|
102
|
+
on_retries_exceeded: ->(error) { received_error = error }
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
106
|
+
.to_return(status: 503, body: "Service Unavailable")
|
|
107
|
+
|
|
108
|
+
client.list_accounts rescue nil
|
|
109
|
+
expect(received_error).to be_a(Footrest::HttpError::ServiceUnavailable)
|
|
110
|
+
expect(received_error.response[:status]).to eq(503)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it "raises error when retries are exhausted and no callback is provided" do
|
|
114
|
+
client = Bearcat::Client.new(
|
|
115
|
+
prefix: "http://canvas.test",
|
|
116
|
+
token: "test_token",
|
|
117
|
+
max_retries: 2
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
121
|
+
.to_return(status: 500)
|
|
122
|
+
|
|
123
|
+
expect {
|
|
124
|
+
client.list_accounts
|
|
125
|
+
}.to raise_error(Footrest::HttpError::InternalServerError)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
context "when request fails with different 5xx errors" do
|
|
130
|
+
it "retries on 501 error" do
|
|
131
|
+
client = Bearcat::Client.new(
|
|
132
|
+
prefix: "http://canvas.test",
|
|
133
|
+
token: "test_token",
|
|
134
|
+
max_retries: 2
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
138
|
+
.to_return(status: 501)
|
|
139
|
+
|
|
140
|
+
expect {
|
|
141
|
+
client.list_accounts
|
|
142
|
+
}.to raise_error(Footrest::HttpError::NotImplemented)
|
|
143
|
+
|
|
144
|
+
expect(WebMock).to have_requested(:get, "http://canvas.test/api/v1/accounts").times(3)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
it "retries on 502 error" do
|
|
148
|
+
client = Bearcat::Client.new(
|
|
149
|
+
prefix: "http://canvas.test",
|
|
150
|
+
token: "test_token",
|
|
151
|
+
max_retries: 2
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
155
|
+
.to_return(status: 502)
|
|
156
|
+
|
|
157
|
+
expect {
|
|
158
|
+
client.list_accounts
|
|
159
|
+
}.to raise_error(Footrest::HttpError::BadGateway)
|
|
160
|
+
|
|
161
|
+
expect(WebMock).to have_requested(:get, "http://canvas.test/api/v1/accounts").times(3)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it "retries on 503 error" do
|
|
165
|
+
client = Bearcat::Client.new(
|
|
166
|
+
prefix: "http://canvas.test",
|
|
167
|
+
token: "test_token",
|
|
168
|
+
max_retries: 2
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
172
|
+
.to_return(status: 503)
|
|
173
|
+
|
|
174
|
+
expect {
|
|
175
|
+
client.list_accounts
|
|
176
|
+
}.to raise_error(Footrest::HttpError::ServiceUnavailable)
|
|
177
|
+
|
|
178
|
+
expect(WebMock).to have_requested(:get, "http://canvas.test/api/v1/accounts").times(3)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
context "when request fails with non-5xx errors" do
|
|
183
|
+
it "does not retry on 404 error" do
|
|
184
|
+
client = Bearcat::Client.new(
|
|
185
|
+
prefix: "http://canvas.test",
|
|
186
|
+
token: "test_token",
|
|
187
|
+
max_retries: 3
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts/999")
|
|
191
|
+
.to_return(status: 404)
|
|
192
|
+
|
|
193
|
+
expect {
|
|
194
|
+
client.account(999)
|
|
195
|
+
}.to raise_error(Footrest::HttpError::NotFound)
|
|
196
|
+
|
|
197
|
+
expect(WebMock).to have_requested(:get, "http://canvas.test/api/v1/accounts/999").times(1)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it "does not retry on 401 error" do
|
|
201
|
+
client = Bearcat::Client.new(
|
|
202
|
+
prefix: "http://canvas.test",
|
|
203
|
+
token: "test_token",
|
|
204
|
+
max_retries: 3
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
208
|
+
.to_return(status: 401)
|
|
209
|
+
|
|
210
|
+
expect {
|
|
211
|
+
client.list_accounts
|
|
212
|
+
}.to raise_error(Footrest::HttpError::Unauthorized)
|
|
213
|
+
|
|
214
|
+
expect(WebMock).to have_requested(:get, "http://canvas.test/api/v1/accounts").times(1)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
context "retry delay" do
|
|
219
|
+
it "uses custom retry_delay between retries" do
|
|
220
|
+
client = Bearcat::Client.new(
|
|
221
|
+
prefix: "http://canvas.test",
|
|
222
|
+
token: "test_token",
|
|
223
|
+
max_retries: 2,
|
|
224
|
+
retry_delay: 0.5
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
228
|
+
.to_return(status: 500)
|
|
229
|
+
|
|
230
|
+
start_time = Time.now
|
|
231
|
+
client.list_accounts rescue nil
|
|
232
|
+
elapsed_time = Time.now - start_time
|
|
233
|
+
|
|
234
|
+
# Should have slept twice (2 retries) * 0.5 seconds = ~1 second
|
|
235
|
+
expect(elapsed_time).to be >= 1.0
|
|
236
|
+
expect(elapsed_time).to be < 1.5
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it "defaults to no delay when retry_delay is not specified" do
|
|
240
|
+
client = Bearcat::Client.new(
|
|
241
|
+
prefix: "http://canvas.test",
|
|
242
|
+
token: "test_token",
|
|
243
|
+
max_retries: 2
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
stub_request(:get, "http://canvas.test/api/v1/accounts")
|
|
247
|
+
.to_return(status: 500)
|
|
248
|
+
|
|
249
|
+
start_time = Time.now
|
|
250
|
+
client.list_accounts rescue nil
|
|
251
|
+
elapsed_time = Time.now - start_time
|
|
252
|
+
|
|
253
|
+
# Should complete quickly with no sleep (default retry_delay: 0)
|
|
254
|
+
expect(elapsed_time).to be < 0.5
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
13
258
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bearcat
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.6.5.
|
|
4
|
+
version: 1.6.5.beta2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Instructure CustomDev
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rake
|