Skip to content

Commit b079242

Browse files
committed
We should only WARN on http codes we intend to retry on
1 parent e714aa0 commit b079242

2 files changed

Lines changed: 61 additions & 13 deletions

File tree

lib/entitlements/service/github.rb

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -338,10 +338,10 @@ def pending_members_from_graphql
338338
def graphql_http_post(query)
339339
1.upto(MAX_GRAPHQL_RETRIES) do |try_number|
340340
result = graphql_http_post_real(query)
341-
if result[:code] < 500
341+
if !graphql_result_retryable?(result)
342342
return result
343343
elsif try_number >= MAX_GRAPHQL_RETRIES
344-
Entitlements.logger.error "Query still failing after #{MAX_GRAPHQL_RETRIES} tries. Giving up."
344+
Entitlements.logger.error "Query still failing after #{MAX_GRAPHQL_RETRIES} tries (last code: #{result[:code]}). Giving up."
345345
return result
346346
else
347347
Entitlements.logger.warn "GraphQL failed on try #{try_number} of #{MAX_GRAPHQL_RETRIES}. Will retry."
@@ -350,6 +350,34 @@ def graphql_http_post(query)
350350
end
351351
end
352352

353+
# Helper: determine whether a result hash from `graphql_http_post_real` represents
354+
# a transient failure that the retry wrapper will retry. Used by the wrapper itself
355+
# and to decide log severity inside `graphql_http_post_real`.
356+
#
357+
# result - Hash returned by `graphql_http_post_real`.
358+
#
359+
# Returns true if the result is retryable (HTTP 5xx or synthetic 5xx), false otherwise.
360+
Contract ({ code: Integer, data: C::Or[nil, Hash] }) => C::Bool
361+
def graphql_result_retryable?(result)
362+
result[:code] >= 500
363+
end
364+
365+
# Helper: log `message` to `Entitlements.logger` at the given severity. Restricts
366+
# the dispatch to a known set of severities so callers can pick a level computed
367+
# at runtime without going through `send`/`public_send`.
368+
#
369+
# severity - Symbol, one of :warn or :error.
370+
# message - String message to log.
371+
#
372+
# Returns nothing.
373+
Contract C::Or[:warn, :error], String => C::Any
374+
def log_at_severity(severity, message)
375+
case severity
376+
when :warn then Entitlements.logger.warn(message)
377+
when :error then Entitlements.logger.error(message)
378+
end
379+
end
380+
353381
# Helper method: Do the HTTP POST to the GitHub API for GraphQL.
354382
#
355383
# query - String with the data to be posted.
@@ -370,23 +398,34 @@ def graphql_http_post_real(query)
370398
response = http.request(request)
371399

372400
if response.code != "200"
373-
Entitlements.logger.error "Got HTTP #{response.code} POSTing to #{uri}"
374-
Entitlements.logger.error response.body
401+
# The retry wrapper retries on 5xx, so log those at WARN to avoid misleading
402+
# the operator with an ERROR for a transient failure that we recover from.
403+
# Terminal non-2xx responses (4xx) stay at ERROR.
404+
severity = response.code.start_with?("5") ? :warn : :error
405+
log_at_severity(severity, "Got HTTP #{response.code} POSTing to #{uri}")
406+
log_at_severity(severity, response.body)
375407
return { code: response.code.to_i, data: { "body" => response.body } }
376408
end
377409

378410
begin
379411
data = JSON.parse(response.body)
380412
if data.key?("errors")
381-
Entitlements.logger.error "Errors reported: #{data['errors'].inspect}"
413+
# Synthesized 500 below triggers a retry, so log at WARN. Note: some GraphQL
414+
# `errors` are permanent (bad query, auth, schema). The retry wrapper's final
415+
# "Giving up" ERROR will surface persistent cases.
416+
Entitlements.logger.warn "Errors reported: #{data['errors'].inspect}"
382417
return { code: 500, data: }
383418
end
384419
{ code: response.code.to_i, data: }
385420
rescue JSON::ParserError => e
386-
Entitlements.logger.error "#{e.class} #{e.message}: #{response.body.inspect}"
421+
# Synthesized 500 below triggers a retry; log at WARN.
422+
Entitlements.logger.warn "#{e.class} #{e.message}: #{response.body.inspect}"
387423
{ code: 500, data: { "body" => response.body } }
388424
end
389425
rescue => e
426+
# Catch-all for any unexpected exception (network blip OR local code bug).
427+
# We retry below via the synthesized 500, but log at ERROR because this
428+
# branch can mask programming errors that operators must see.
390429
Entitlements.logger.error "Caught #{e.class} POSTing to #{uri}: #{e.message}"
391430
{ code: 500, data: nil }
392431
end

spec/unit/entitlements/service/github_spec.rb

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@
352352
expect_any_instance_of(Object).to receive(:sleep).with(2).exactly(1).times
353353
expect(logger).to receive(:warn).with("GraphQL failed on try 1 of 3. Will retry.")
354354
expect(logger).to receive(:warn).with("GraphQL failed on try 2 of 3. Will retry.")
355-
expect(logger).to receive(:error).with("Query still failing after 3 tries. Giving up.")
355+
expect(logger).to receive(:error).with("Query still failing after 3 tries (last code: 502). Giving up.")
356356

357357
response = subject.send(:graphql_http_post, query)
358358
expect(response[:code]).to eq(502)
@@ -368,15 +368,15 @@
368368
expect(response).to eq(code: 200, data: answer)
369369
end
370370

371-
it "logs and returns code=500 and exception message for an unhandled exception" do
371+
it "logs at error and returns code=500 and exception message for an unhandled exception" do
372372
exc = StandardError.new("Oh no you don't")
373373
stub_request(:post, "https://github.fake/api/v3/graphql").to_raise(exc)
374374
expect(logger).to receive(:error).with("Caught StandardError POSTing to https://github.fake/api/v3/graphql: Oh no you don't")
375375
response = subject.send(:graphql_http_post_real, "nonsense")
376376
expect(response).to eq(code: 500, data: nil)
377377
end
378378

379-
it "logs and returns code and body for non-200 response" do
379+
it "logs at error and returns code and body for a non-200 4xx response" do
380380
answer = { "errors" => ["message" => "Something busted"] }
381381
stub_request(:post, "https://github.fake/api/v3/graphql").to_return(status: 429, body: JSON.generate(answer))
382382
expect(logger).to receive(:error).with("Got HTTP 429 POSTing to https://github.fake/api/v3/graphql")
@@ -385,18 +385,27 @@
385385
expect(response).to eq(code: 429, data: { "body" => JSON.generate(answer) })
386386
end
387387

388-
it "logs and returns raw text for JSON parsing error" do
388+
it "logs at warn and returns code and body for a 5xx response (retryable)" do
389+
body = '{"message": "We couldn\'t respond to your request in time. Sorry about that."}'
390+
stub_request(:post, "https://github.fake/api/v3/graphql").to_return(status: 504, body:)
391+
expect(logger).to receive(:warn).with("Got HTTP 504 POSTing to https://github.fake/api/v3/graphql")
392+
expect(logger).to receive(:warn).with(body)
393+
response = subject.send(:graphql_http_post_real, "nonsense")
394+
expect(response).to eq(code: 504, data: { "body" => body })
395+
end
396+
397+
it "logs at warn and returns raw text for JSON parsing error" do
389398
answer = "mor chicken mor rewardz!"
390399
stub_request(:post, "https://github.fake/api/v3/graphql").to_return(status: 200, body: answer)
391-
expect(logger).to receive(:error).with("JSON::ParserError unexpected character: 'mor' at line 1 column 1: \"mor chicken mor rewardz!\"")
400+
expect(logger).to receive(:warn).with("JSON::ParserError unexpected character: 'mor' at line 1 column 1: \"mor chicken mor rewardz!\"")
392401
response = subject.send(:graphql_http_post_real, "nonsense")
393402
expect(response).to eq(code: 500, data: { "body" => "mor chicken mor rewardz!" })
394403
end
395404

396-
it "logs and returns when errors are reported in a 200" do
405+
it "logs at warn and returns when errors are reported in a 200" do
397406
answer = { "errors" => ["ENOTENOUGHCHICKEn"] }
398407
stub_request(:post, "https://github.fake/api/v3/graphql").to_return(status: 200, body: JSON.generate(answer))
399-
expect(logger).to receive(:error).with("Errors reported: [\"ENOTENOUGHCHICKEn\"]")
408+
expect(logger).to receive(:warn).with("Errors reported: [\"ENOTENOUGHCHICKEn\"]")
400409
response = subject.send(:graphql_http_post_real, "nonsense")
401410
expect(response).to eq(code: 500, data: answer)
402411
end

0 commit comments

Comments
 (0)