Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Elegant/GoodMethodName:
- delete_one
- dump_headers
- extract_remaining_count
- extract_reset_time
- extract_search_remaining_count
- faraday_value
- github_graph
Expand Down
51 changes: 48 additions & 3 deletions lib/fbe/middleware/rate_limit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
#
# This middleware intercepts calls to the /rate_limit endpoint and caches
# the results locally. It tracks the remaining requests count and decrements
# it for each API call. Every 100 requests, it refreshes the cached data
# by allowing the request to pass through to the GitHub API.
# it for each API call. It refreshes after 100 requests or when a reported
# quota reset passes.
#
# @example Usage in Faraday middleware stack
# connection = Faraday.new do |f|
Expand All @@ -32,6 +32,8 @@ def initialize(app, tracker = nil)
@cached = nil
@remaining = nil
@searchleft = nil
@core_reset = nil
@search_reset = nil
@counter = 0
@lock = Mutex.new
@refresh = Mutex.new
Expand Down Expand Up @@ -63,6 +65,7 @@ def call(env)
# @return [Integer, nil] The remaining count, or nil when the resource is absent
def remaining(resource = :core)
@lock.synchronize do
return nil if expired?(resource)
resource == :search ? @searchleft : @remaining
end
end
Expand All @@ -74,18 +77,52 @@ def remaining(resource = :core)
# @param [Faraday::Env] env The request environment
# @return [Faraday::Response] Cached or fresh response
def handle_rate_limit_request(env)
stale = @lock.synchronize { @cached.nil? || @counter >= 100 }
stale = @lock.synchronize { @cached.nil? || expired? || @counter >= 100 }
return @lock.synchronize { Faraday::Response.new(response_env(env, @cached)) } unless stale
response = @app.call(env)
@lock.synchronize do
@cached = response
@remaining = extract_remaining_count(response)
@searchleft = extract_search_remaining_count(response)
@core_reset = extract_reset_time(response, :core)
@search_reset = extract_reset_time(response, :search)
@counter = 0
end
response
end

# Whether a quota reset has invalidated the tracked count.
#
# @param [Symbol, nil] resource The quota resource, or nil to check all resources
# @return [Boolean] True when a known reset time has passed
def expired?(resource = nil)
resets =
if resource.nil?
[@core_reset, @search_reset]
else
[resource == :search ? @search_reset : @core_reset]
end
now = Time.now.to_i
resets.any? { |reset| !reset.nil? && now >= reset }
end

# Reads a resource reset time from a quota response.
#
# @param [Faraday::Response] response The quota response
# @param [Symbol] resource The quota resource
# @return [Integer, nil] Unix reset time, or nil when it is absent
def extract_reset_time(response, resource)
body = response.body
body = JSON.parse(body) if body.is_a?(String)
resets = []
if resource == :core
resets << response.headers['x-ratelimit-reset']
resets << body.dig('rate', 'reset') if body.is_a?(Hash)
end
resets << body.dig('resources', resource.to_s, 'reset') if body.is_a?(Hash)
resets.filter_map { |value| Integer(value, exception: false) }.min
end

# Tracks non-rate_limit requests and decrements counter.
#
# @return [Symbol, nil] The counter the request was taken off, or NIL if none was
Expand Down Expand Up @@ -138,6 +175,14 @@ def sync(response_env, path = nil)
else
@remaining = count
end
reset = Integer(headers['x-ratelimit-reset'], exception: false)
unless reset.nil?
if path&.start_with?('/search/')
@search_reset = reset
else
@core_reset = reset
end
end
end

# Extracts the remaining count from the response body.
Expand Down
3 changes: 2 additions & 1 deletion lib/fbe/octo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ def Fbe.octo(options: $options, global: $global, loog: $loog) # rubocop:disable
o =
decoor(o, loog:, trace:, limits:, mutex:) do # rubocop:disable Metrics/BlockLength
def print_trace!(all: false, max: 5)
left = @origin.rate_limit!.remaining unless @mutex.synchronize { @trace.empty? }
@mutex.synchronize do
if @trace.empty?
@loog.debug('GitHub API trace is empty')
Expand Down Expand Up @@ -173,7 +174,7 @@ def print_trace!(all: false, max: 5)
@loog.info(
"GitHub API trace (#{grouped.count} URLs vs #{shown.count} requests, " \
"#{@trace.count - shown.count} fast ones skipped, " \
"#{@origin.rate_limit!.remaining} quota left):\n#{message}"
"#{left || 'unknown'} quota left):\n#{message}"
)
@trace.clear
end
Expand Down
Loading
Loading