Skip to content

Commit 90a6ac6

Browse files
authored
Merge pull request #300 from github/reuse-graphql-http
Create a per-org shared connection for graphql requests
2 parents 7d46a91 + 3298437 commit 90a6ac6

4 files changed

Lines changed: 107 additions & 6 deletions

File tree

‎Gemfile.lock‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
entitlements-github-plugin (1.2.3)
4+
entitlements-github-plugin (1.2.4)
55
contracts (~> 0.17.0)
66
faraday (~> 2.0)
77
faraday-retry (~> 2.0)

‎lib/entitlements/service/github.rb‎

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,17 +369,15 @@ def graphql_result_retryable?(result)
369369
# Returns { code: <Integer>, data: <response data structure> }
370370
Contract String => { code: Integer, data: C::Or[nil, Hash] }
371371
def graphql_http_post_real(query)
372-
uri = URI.parse(File.join(octokit.api_endpoint, "graphql"))
373-
http = Net::HTTP.new(uri.host, uri.port)
374-
http.use_ssl = uri.scheme == "https"
372+
uri = graphql_uri
375373

376374
request = Net::HTTP::Post.new(uri)
377375
request.add_field("Authorization", "bearer #{token}")
378376
request.add_field("Content-Type", "application/json")
379377
request.body = JSON.generate("query" => query)
380378

381379
begin
382-
response = http.request(request)
380+
response = graphql_http.request(request)
383381

384382
if response.code != "200"
385383
# The retry wrapper retries on 5xx, so log those at WARN to avoid misleading
@@ -411,11 +409,41 @@ def graphql_http_post_real(query)
411409
# Catch-all for any unexpected exception (network blip OR local code bug).
412410
# We retry below via the synthesized 500, but log at ERROR because this
413411
# branch can mask programming errors that operators must see.
412+
reset_graphql_http
414413
Entitlements.logger.error "Caught #{e.class} POSTing to #{uri}: #{e.message}"
415414
{ code: 500, data: nil }
416415
end
417416
end
418417

418+
# Return the URI used for GraphQL requests.
419+
Contract C::None => URI::Generic
420+
def graphql_uri
421+
@graphql_uri ||= URI.parse(File.join(octokit.api_endpoint, "graphql"))
422+
end
423+
424+
# Return a started HTTP connection that can be reused across GraphQL requests.
425+
Contract C::None => Net::HTTP
426+
def graphql_http
427+
@graphql_http ||= begin
428+
uri = graphql_uri
429+
http = Net::HTTP.new(uri.host, uri.port)
430+
http.use_ssl = uri.scheme == "https"
431+
http.start
432+
http
433+
end
434+
end
435+
436+
# Discard a failed GraphQL connection so the retry wrapper creates a new one.
437+
Contract C::None => nil
438+
def reset_graphql_http
439+
http, @graphql_http = @graphql_http, nil
440+
http.finish if http&.started?
441+
nil
442+
rescue IOError, SystemCallError => e
443+
Entitlements.logger.warn "Failed to close GraphQL connection: #{e.message}"
444+
nil
445+
end
446+
419447
# Create a unique signature for this GitHub instance to identify it in a global cache.
420448
#
421449
# Takes no arguments.

‎lib/version.rb‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@
22

33
module Entitlements
44
module Version
5-
VERSION = "1.2.3"
5+
VERSION = "1.2.4"
66
end
77
end

‎spec/unit/entitlements/service/github_spec.rb‎

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,33 @@
312312
let(:answer) { { "foo" => "bar" } }
313313
let(:query) { "my query here" }
314314

315+
it "reconnects and retries after the connection times out" do
316+
response = instance_double(Net::HTTPOK, code: "200", body: JSON.generate(answer))
317+
timed_out_http = Net::HTTP.new("github.fake", 443)
318+
replacement_http = Net::HTTP.new("github.fake", 443)
319+
320+
expect(Net::HTTP).to receive(:new).
321+
with("github.fake", 443).
322+
twice.
323+
and_return(timed_out_http, replacement_http)
324+
expect(timed_out_http).to receive(:use_ssl=).with(true)
325+
expect(timed_out_http).to receive(:start)
326+
expect(timed_out_http).to receive(:request).
327+
and_raise(Net::ReadTimeout, "execution expired")
328+
expect(timed_out_http).to receive(:started?).and_return(true)
329+
expect(timed_out_http).to receive(:finish)
330+
expect(replacement_http).to receive(:use_ssl=).with(true)
331+
expect(replacement_http).to receive(:start)
332+
expect(replacement_http).to receive(:request).and_return(response)
333+
expect_any_instance_of(Object).to receive(:sleep).with(1).once
334+
expect(logger).to receive(:error).
335+
with("Caught Net::ReadTimeout POSTing to https://github.fake/api/v3/graphql: Net::ReadTimeout with \"execution expired\"")
336+
expect(logger).to receive(:warn).
337+
with("GraphQL failed on try 1 of 3. Will retry.")
338+
339+
expect(subject.send(:graphql_http_post, query)).to eq(code: 200, data: answer)
340+
end
341+
315342
it "returns immediately for success" do
316343
expect(subject).to receive(:graphql_http_post_real).with(query).and_return(code: 200, data: answer)
317344
expect_any_instance_of(Object).not_to receive(:sleep)
@@ -361,6 +388,52 @@
361388
end
362389

363390
describe "#graphql_http_post_real" do
391+
it "reuses a started HTTP connection across requests" do
392+
answer = { "foo" => "bar" }
393+
response = instance_double(Net::HTTPOK, code: "200", body: JSON.generate(answer))
394+
http = Net::HTTP.new("github.fake", 443)
395+
396+
expect(Net::HTTP).to receive(:new).with("github.fake", 443).once.and_return(http)
397+
expect(http).to receive(:use_ssl=).with(true).once
398+
expect(http).to receive(:start).once
399+
expect(http).to receive(:request).twice.and_return(response)
400+
401+
2.times do
402+
expect(subject.send(:graphql_http_post_real, "nonsense")).to eq(code: 200, data: answer)
403+
end
404+
end
405+
406+
it "discards a failed connection so the next request reconnects" do
407+
answer = { "foo" => "bar" }
408+
response = instance_double(Net::HTTPOK, code: "200", body: JSON.generate(answer))
409+
failed_http = Net::HTTP.new("github.fake", 443)
410+
replacement_http = Net::HTTP.new("github.fake", 443)
411+
412+
expect(Net::HTTP).to receive(:new).with("github.fake", 443).twice.and_return(failed_http, replacement_http)
413+
expect(failed_http).to receive(:use_ssl=).with(true)
414+
expect(failed_http).to receive(:start)
415+
expect(failed_http).to receive(:request).and_raise(EOFError, "closed connection")
416+
expect(failed_http).to receive(:started?).and_return(true)
417+
expect(failed_http).to receive(:finish)
418+
expect(replacement_http).to receive(:use_ssl=).with(true)
419+
expect(replacement_http).to receive(:start)
420+
expect(replacement_http).to receive(:request).and_return(response)
421+
expect(logger).to receive(:error).
422+
with("Caught EOFError POSTing to https://github.fake/api/v3/graphql: closed connection")
423+
424+
expect(subject.send(:graphql_http_post_real, "nonsense")).to eq(code: 500, data: nil)
425+
expect(subject.send(:graphql_http_post_real, "nonsense")).to eq(code: 200, data: answer)
426+
end
427+
428+
it "logs a warning if a failed connection cannot be closed" do
429+
http = instance_double(Net::HTTP, started?: true)
430+
subject.instance_variable_set(:@graphql_http, http)
431+
432+
expect(http).to receive(:finish).and_raise(IOError, "already closed")
433+
expect(logger).to receive(:warn).with("Failed to close GraphQL connection: already closed")
434+
expect(subject.send(:reset_graphql_http)).to be_nil
435+
end
436+
364437
it "returns code=200 and parsed JSON for a successful response" do
365438
answer = { "foo" => ["bar", "baz" => "fizz"] }
366439
stub_request(:post, "https://github.fake/api/v3/graphql").to_return(status: 200, body: JSON.generate(answer))

0 commit comments

Comments
 (0)