fbe 0.48.4 → 0.48.6

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.
data/lib/fbe/fb.rb CHANGED
@@ -31,34 +31,37 @@ def Fbe.fb(fb: $fb, global: $global, options: $options, loog: $loog)
31
31
  raise(Fbe::Error, 'The $global is not set') if global.nil?
32
32
  raise(Fbe::Error, 'The $options is not set') if options.nil?
33
33
  raise(Fbe::Error, 'The $loog is not set') if loog.nil?
34
- global[:fb] ||=
35
- begin
36
- rules = Dir.glob(File.join(File.join(__dir__, '../../rules'), '*.fe')).map { |f| File.read(f) }
37
- fbe = Factbase::Rules.new(
38
- fb,
39
- "(and \n#{rules.join("\n")}\n)",
40
- uid: '_id'
41
- )
42
- fbe =
43
- Factbase::Pre.new(fbe) do |f, fbt|
44
- max = fbt.query('(max _id)').one
45
- f._id = (max.nil? ? 0 : max) + 1
46
- f._time = Time.now
47
- f._version = [Factbase::VERSION, Judges::VERSION, options.action_version].compact.join('/')
48
- f._job = Integer(options.job_id.to_s, 10) if options.job_id
49
- end
50
- Factbase::Impatient.new(
51
- Factbase::Logged.new(
52
- Factbase::SyncFactbase.new(
53
- Factbase::IndexedFactbase.new(
54
- Factbase::CachedFactbase.new(
55
- fbe
34
+ global[:mutex] ||= Mutex.new
35
+ global[:mutex].synchronize do
36
+ global[:fb] ||=
37
+ begin
38
+ rules = Dir.glob(File.join(File.join(__dir__, '../../rules'), '*.fe')).map { |f| File.read(f) }
39
+ fbe = Factbase::Rules.new(
40
+ fb,
41
+ "(and \n#{rules.join("\n")}\n)",
42
+ uid: '_id'
43
+ )
44
+ fbe =
45
+ Factbase::Pre.new(fbe) do |f, fbt|
46
+ max = fbt.query('(max _id)').one
47
+ f._id = (max.nil? ? 0 : max) + 1
48
+ f._time = Time.now
49
+ f._version = [Factbase::VERSION, Judges::VERSION, options.action_version].compact.join('/')
50
+ f._job = Integer(options.job_id.to_s, 10) if options.job_id
51
+ end
52
+ Factbase::Impatient.new(
53
+ Factbase::Logged.new(
54
+ Factbase::SyncFactbase.new(
55
+ Factbase::IndexedFactbase.new(
56
+ Factbase::CachedFactbase.new(
57
+ fbe
58
+ )
56
59
  )
57
- )
60
+ ),
61
+ loog
58
62
  ),
59
- loog
60
- ),
61
- timeout: 60
62
- )
63
- end
63
+ timeout: 60
64
+ )
65
+ end
66
+ end
64
67
  end
@@ -15,13 +15,16 @@ require_relative '../fbe'
15
15
  # @param [Loog] loog Logging facility
16
16
  # @return [Fbe::Graph] The instance of the class
17
17
  def Fbe.github_graph(options: $options, global: $global, loog: $loog)
18
- global[:github_graph] ||=
19
- if options.testing.nil?
20
- Fbe::Graph.new(token: options.github_token || ENV.fetch('GITHUB_TOKEN', nil))
21
- else
22
- loog.debug('The connection to GitHub GraphQL API is mocked')
23
- Fbe::Graph::Fake.new
24
- end
18
+ global[:mutex] ||= Mutex.new
19
+ global[:mutex].synchronize do
20
+ global[:github_graph] ||=
21
+ if options.testing.nil?
22
+ Fbe::Graph.new(token: options.github_token || ENV.fetch('GITHUB_TOKEN', nil))
23
+ else
24
+ loog.debug('The connection to GitHub GraphQL API is mocked')
25
+ Fbe::Graph::Fake.new
26
+ end
27
+ end
25
28
  end
26
29
 
27
30
  # A client to GitHub GraphQL.
@@ -45,6 +48,9 @@ class Fbe::Graph # rubocop:disable Metrics/ClassLength
45
48
  # puts result.viewer.login #=> "octocat"
46
49
  def query(qry)
47
50
  result = client.query(client.parse(qry))
51
+ unless result.errors.empty?
52
+ raise(Fbe::Error, "GitHub GraphQL query failed: #{result.errors.messages.values.flatten.join('; ')}")
53
+ end
48
54
  result.data
49
55
  end
50
56
 
@@ -287,11 +293,12 @@ class Fbe::Graph # rubocop:disable Metrics/ClassLength
287
293
  # cursor = json['next_cursor']
288
294
  # end
289
295
  def pull_requests_with_reviews(owner, name, since, cursor: nil)
296
+ after = "after: \"#{cursor}\", " unless cursor.nil?
290
297
  result = query(
291
298
  <<~GRAPHQL
292
299
  {
293
300
  repository(owner: "#{owner}", name: "#{name}") {
294
- pullRequests(first: 100, after: "#{cursor}") {
301
+ pullRequests(#{after}first: 100) {
295
302
  nodes {
296
303
  id
297
304
  number
@@ -310,16 +317,16 @@ class Fbe::Graph # rubocop:disable Metrics/ClassLength
310
317
  }
311
318
  GRAPHQL
312
319
  ).to_h
320
+ nodes = result.dig('repository', 'pullRequests', 'nodes')
321
+ raise(Fbe::Error, "Repository '#{owner}/#{name}' not found") if nodes.nil?
313
322
  {
314
- 'pulls_with_reviews' => result
315
- .dig('repository', 'pullRequests', 'nodes')
316
- .filter_map do |pull|
317
- next if pull.dig('timelineItems', 'nodes').empty?
318
- {
319
- 'id' => pull['id'],
320
- 'number' => pull['number']
321
- }
322
- end,
323
+ 'pulls_with_reviews' => nodes.filter_map do |pull|
324
+ next if pull.dig('timelineItems', 'nodes').empty?
325
+ {
326
+ 'id' => pull['id'],
327
+ 'number' => pull['number']
328
+ }
329
+ end,
323
330
  'has_next_page' => result.dig('repository', 'pullRequests', 'pageInfo', 'hasNextPage'),
324
331
  'next_cursor' => result.dig('repository', 'pullRequests', 'pageInfo', 'endCursor')
325
332
  }
@@ -349,11 +356,12 @@ class Fbe::Graph # rubocop:disable Metrics/ClassLength
349
356
  def pull_request_reviews(owner, name, pulls: [])
350
357
  requests =
351
358
  pulls.map do |number, cursor|
359
+ after = "after: \"#{cursor}\", " unless cursor.nil?
352
360
  <<~GRAPHQL
353
361
  pr_#{number}: pullRequest(number: #{number}) {
354
362
  id
355
363
  number
356
- reviews(first: 100, after: "#{cursor}") {
364
+ reviews(#{after}first: 100) {
357
365
  nodes {
358
366
  id
359
367
  submittedAt
@@ -485,11 +493,12 @@ class Fbe::Graph # rubocop:disable Metrics/ClassLength
485
493
  total = 0
486
494
  cursor = nil
487
495
  loop do
496
+ after = "after: \"#{cursor}\", " unless cursor.nil?
488
497
  result = query(
489
498
  <<~GRAPHQL
490
499
  {
491
500
  repository(owner: "#{owner}", name: "#{name}") {
492
- releases(first: 25, after: "#{cursor}", orderBy: { field: CREATED_AT, direction: DESC }) {
501
+ releases(#{after}first: 25, orderBy: { field: CREATED_AT, direction: DESC }) {
493
502
  nodes {
494
503
  isDraft
495
504
  publishedAt
data/lib/fbe/if_absent.rb CHANGED
@@ -62,7 +62,7 @@ def Fbe.if_absent(fb: Fbe.fb, always: false)
62
62
  end
63
63
  end
64
64
  yield(f)
65
- q = attrs.except('_id', '_time', '_version').map do |k, v|
65
+ q = attrs.except(:_id, :_time, :_version).map do |k, v|
66
66
  vv = v.to_s
67
67
  if v.is_a?(String)
68
68
  vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
data/lib/fbe/iterate.rb CHANGED
@@ -105,28 +105,27 @@ class Fbe::Iterate
105
105
  @timeout = true
106
106
  end
107
107
 
108
- # Makes the iterator aware of GitHub API quota limits.
108
+ # Makes the iterator unaware of GitHub API quota limits.
109
109
  #
110
- # When enabled, the iterator will check quota status before processing
111
- # each repository and gracefully stop when the quota is exhausted.
112
- # This prevents API errors and allows for resuming later.
110
+ # When disabled, the iterator will not check quota status before processing
111
+ # each repository and will not gracefully stop when the quota is exhausted.
113
112
  #
114
113
  # @return [nil] Nothing is returned
115
- # @example Enable quota awareness
116
- # iterator.quota_aware
117
- # iterator.over { |repo, item| ... } # Will stop if quota exhausted
114
+ # @example Disable quota awareness
115
+ # iterator.quota_unaware
116
+ # iterator.over { |repo, item| ... } # Will not stop on quota exhaustion
118
117
  def quota_unaware
119
118
  @quota = false
120
119
  end
121
120
 
122
- # Makes the iterator aware of lifetime limits.
121
+ # Makes the iterator unaware of lifetime limits.
123
122
  #
124
123
  # @return [nil] Nothing is returned
125
124
  def lifetime_unaware
126
125
  @lifetime = false
127
126
  end
128
127
 
129
- # Makes the iterator aware of timeout limits.
128
+ # Makes the iterator unaware of timeout limits.
130
129
  #
131
130
  # @return [nil] Nothing is returned
132
131
  def timeout_unaware
@@ -149,6 +148,24 @@ class Fbe::Iterate
149
148
  @repeats = repeats
150
149
  end
151
150
 
151
+ # Sets the initial value for iteration tracking.
152
+ #
153
+ # This value is used as the starting point when no marker exists in the
154
+ # factbase for the current iteration label. It is also used as the
155
+ # restart value when a query returns nil, causing the iteration for
156
+ # that repository to begin again from this value.
157
+ #
158
+ # @param [Integer] value The initial value for iteration tracking
159
+ # @return [nil] Nothing is returned
160
+ # @raise [RuntimeError] If value is nil or not an Integer
161
+ # @example Start iteration from issue number 100
162
+ # iterator.since!(100)
163
+ def since!(value)
164
+ raise(Fbe::Error, 'Cannot set "since" to nil') if value.nil?
165
+ raise(Fbe::Error, 'The "since" must be an Integer') unless value.is_a?(Integer)
166
+ @since = value
167
+ end
168
+
152
169
  # Sets the query to execute for each iteration.
153
170
  #
154
171
  # The query can use two special variables:
@@ -255,19 +272,14 @@ class Fbe::Iterate
255
272
  ).map { |n| oct.repo_id_by_name(n) }
256
273
  started = Time.now
257
274
  restarted = []
258
- before =
259
- repos.to_h do |repo|
260
- [
261
- repo,
262
- @fb.query(
263
- "(agg (and
264
- (eq what 'iterate')
265
- (eq where 'github')
266
- (eq repository #{repo}))
267
- (first #{@label}))"
268
- ).one&.first || @since
269
- ]
270
- end
275
+ markers = {}
276
+ @fb.query("(and (eq what 'iterate') (eq where 'github'))").each do |f|
277
+ r = f['repository']&.first
278
+ next if r.nil? || markers.key?(r)
279
+ v = f[@label.to_s]&.first
280
+ markers[r] = v unless v.nil?
281
+ end
282
+ before = repos.to_h { |repo| [repo, markers[repo] || @since] }
271
283
  starts = before.dup
272
284
  values = {}
273
285
  loop do # rubocop:disable Metrics/BlockLength
@@ -313,7 +325,13 @@ class Fbe::Iterate
313
325
  @since
314
326
  else
315
327
  @loog.debug("Next is ##{nxt}, starting from it")
316
- yield(repo, nxt)
328
+ begin
329
+ yield(repo, nxt)
330
+ rescue Fbe::OffQuota
331
+ raise
332
+ rescue StandardError => e
333
+ raise(e.class, "Failure in repository ##{repo} at ##{nxt}: #{e.message}")
334
+ end
317
335
  end
318
336
  unless before[repo].is_a?(Integer)
319
337
  raise(Fbe::Error, "Iterator must return an Integer, but #{before[repo].class} was returned")
@@ -337,7 +355,7 @@ class Fbe::Iterate
337
355
  defined?(before) && !before.nil? &&
338
356
  defined?(starts) && !starts.nil?
339
357
  repos.each do |repo|
340
- next if before[repo] == starts[repo] || before[repo] == @since
358
+ next if before[repo] == starts[repo]
341
359
  f =
342
360
  Fbe.if_absent(fb: @fb, always: true) do |n|
343
361
  n.what = 'iterate'
data/lib/fbe/just_one.rb CHANGED
@@ -38,13 +38,16 @@ def Fbe.just_one(fb: Fbe.fb)
38
38
  others(map: attrs) do |*args|
39
39
  k = args[0]
40
40
  if k.end_with?('=')
41
- @map[k[0..-2].to_sym] = args[1]
41
+ v = args[1]
42
+ raise(Fbe::Error, "Can't set #{k[0..-2]} to nil") if v.nil?
43
+ raise(Fbe::Error, "Can't set #{k[0..-2]} to empty string") if v.is_a?(String) && v.empty?
44
+ @map[k[0..-2].to_sym] = v
42
45
  else
43
46
  @map[k.to_sym]
44
47
  end
45
48
  end
46
49
  yield(f)
47
- q = attrs.except('_id', '_time', '_version').map do |k, v|
50
+ q = attrs.except(:_id, :_time, :_version).map do |k, v|
48
51
  vv = v.to_s
49
52
  if v.is_a?(String)
50
53
  vv = "'#{vv.gsub('"', '\\\\"').gsub("'", "\\\\'")}'"
data/lib/fbe/kill_if.rb CHANGED
@@ -19,5 +19,6 @@ def Fbe.kill_if(facts, fb: Fbe.fb, fid: '_id')
19
19
  end
20
20
  ids << f[fid].first
21
21
  end
22
+ return 0 if ids.empty?
22
23
  fb.query("(or #{ids.map { |id| "(eq #{fid} #{id})" }.join})").delete!
23
24
  end
@@ -39,6 +39,19 @@ require_relative '../../fbe/middleware'
39
39
  # Copyright:: Copyright (c) 2024-2026 Zerocracy
40
40
  # License:: MIT
41
41
  class Fbe::Middleware::Formatter < Faraday::Logging::Formatter
42
+ # Registers a filter that masks the credential portion of an
43
+ # `Authorization` header so live tokens never reach the log.
44
+ # Matches any auth scheme: `Authorization: "Bearer xxx"`,
45
+ # `Authorization: "Basic yyy"`, etc. The scheme is preserved,
46
+ # the credential is replaced with `[FILTERED]`.
47
+ #
48
+ # @param [Logger] logger The Faraday-supplied logger
49
+ # @param [Hash] options Faraday formatter options
50
+ def initialize(logger:, options:)
51
+ super
52
+ filter(/(Authorization:\s*"?\s*\S+\s+)[^"\s]+/i, '\1[FILTERED]')
53
+ end
54
+
42
55
  # Captures HTTP request details for later use in error logging.
43
56
  #
44
57
  # @param [Hash] http Request data including method, url, headers, and body
@@ -55,17 +68,18 @@ class Fbe::Middleware::Formatter < Faraday::Logging::Formatter
55
68
  # @note Special handling for 403 JSON responses to show compact error message
56
69
  def response(http) # rubocop:disable Metrics/AbcSize
57
70
  return if http.status < 400
58
- if http.status == 403 && http.response_headers['content-type'].start_with?('application/json')
59
- warn(
60
- [
61
- "#{@req.method.upcase} #{apply_filters(@req.url.to_s)}",
62
- '->',
63
- http.status,
64
- '/',
65
- JSON.parse(http.response_body)['message']
66
- ].join(' ')
67
- )
68
- return
71
+ if http.status == 403 && http.response_headers['content-type']&.start_with?('application/json')
72
+ msg =
73
+ begin
74
+ parsed = JSON.parse(http.response_body)
75
+ parsed['message'] if parsed.is_a?(Hash)
76
+ rescue JSON::ParserError, TypeError
77
+ nil
78
+ end
79
+ unless msg.nil?
80
+ warn(["#{@req.method.upcase} #{apply_filters(@req.url.to_s)}", '->', http.status, '/', msg].join(' '))
81
+ return
82
+ end
69
83
  end
70
84
  if http.status >= 500 && http.response_headers['content-type']&.start_with?('text')
71
85
  error(
@@ -27,11 +27,14 @@ class Fbe::Middleware::RateLimit < Faraday::Middleware
27
27
  # Initializes the rate limit middleware.
28
28
  #
29
29
  # @param [Object] app The next middleware in the stack
30
- def initialize(app)
31
- super
30
+ def initialize(app, tracker = nil)
31
+ super(app)
32
32
  @cached = nil
33
33
  @remaining = nil
34
+ @searchleft = nil
34
35
  @counter = 0
36
+ @lock = Mutex.new
37
+ tracker[:rate_limit] = self unless tracker.nil?
35
38
  end
36
39
 
37
40
  # Processes the HTTP request and handles rate limit caching.
@@ -40,10 +43,22 @@ class Fbe::Middleware::RateLimit < Faraday::Middleware
40
43
  # @return [Faraday::Response] The response from cache or the next middleware
41
44
  def call(env)
42
45
  if env.url.path == '/rate_limit'
43
- handle_rate_limit_request(env)
46
+ @lock.synchronize { handle_rate_limit_request(env) }
44
47
  else
45
- track_request
46
- @app.call(env)
48
+ @lock.synchronize { track_request(env.url.path) }
49
+ @app.call(env).on_complete do |response_env|
50
+ @lock.synchronize { sync(response_env, env.url.path) }
51
+ end
52
+ end
53
+ end
54
+
55
+ # Returns the remaining requests count tracked by this middleware.
56
+ #
57
+ # @param [Symbol] resource The GitHub API resource (:core or :search)
58
+ # @return [Integer, nil] The remaining count, or nil when the resource is absent
59
+ def remaining(resource = :core)
60
+ @lock.synchronize do
61
+ resource == :search ? @searchleft : @remaining
47
62
  end
48
63
  end
49
64
 
@@ -56,22 +71,48 @@ class Fbe::Middleware::RateLimit < Faraday::Middleware
56
71
  def handle_rate_limit_request(env)
57
72
  if @cached.nil? || @counter >= 100
58
73
  response = @app.call(env)
59
- @cached = response.dup
74
+ @cached = response
60
75
  @remaining = extract_remaining_count(response)
76
+ @searchleft = extract_search_remaining_count(response)
61
77
  @counter = 0
62
78
  response
63
79
  else
64
- response = @cached.dup
65
- update_remaining_count(response)
66
- Faraday::Response.new(response_env(env, response))
80
+ Faraday::Response.new(response_env(env, @cached))
67
81
  end
68
82
  end
69
83
 
70
84
  # Tracks non-rate_limit requests and decrements counter.
71
- def track_request
72
- return if @remaining.nil?
73
- @remaining -= 1 if @remaining.positive?
85
+ def track_request(path = nil)
74
86
  @counter += 1
87
+ if path&.start_with?('/search/')
88
+ @searchleft -= 1 if @searchleft&.positive?
89
+ elsif @remaining&.positive?
90
+ @remaining -= 1
91
+ end
92
+ end
93
+
94
+ # Syncs the internal remaining count from a real API response header.
95
+ #
96
+ # When the response was served by Faraday::HttpCache from cache
97
+ # (indicated by +http_cache_trace+ containing +:fresh+), the
98
+ # +x-ratelimit-remaining+ header is stale, so we keep our
99
+ # decremented count. When the API was actually contacted,
100
+ # we seed unknown counters from headers, but avoid raising
101
+ # a counter already decremented by this middleware.
102
+ #
103
+ # @param [Faraday::Env] response_env The response environment
104
+ def sync(response_env, path = nil)
105
+ return if response_env[:http_cache_trace]&.include?(:fresh)
106
+ headers = response_env.response_headers
107
+ return unless headers
108
+ remaining = headers['x-ratelimit-remaining']
109
+ return unless remaining
110
+ count = Integer(remaining)
111
+ if path&.start_with?('/search/')
112
+ @searchleft = @searchleft.nil? ? count : [@searchleft, count].min
113
+ else
114
+ @remaining = @remaining.nil? ? count : [@remaining, count].min
115
+ end
75
116
  end
76
117
 
77
118
  # Extracts the remaining count from the response body.
@@ -80,52 +121,59 @@ class Fbe::Middleware::RateLimit < Faraday::Middleware
80
121
  # @return [Integer] The remaining requests count
81
122
  def extract_remaining_count(response)
82
123
  body = response.body
83
- if body.is_a?(String)
84
- begin
85
- body = JSON.parse(body)
86
- rescue JSON::ParserError
87
- return 0
88
- end
89
- end
90
- return 0 unless body.is_a?(Hash)
91
- body.dig('rate', 'remaining') || 0
124
+ body = JSON.parse(body) if body.is_a?(String)
125
+ value = body.dig('rate', 'remaining') if body.is_a?(Hash)
126
+ value ||= response.headers['x-ratelimit-remaining']
127
+ Integer(value || 0)
92
128
  end
93
129
 
94
- # Updates the remaining count in the response body.
130
+ # Extracts the search-resource remaining count from the response body.
95
131
  #
96
- # @param [Faraday::Response] response The cached response to update
97
- def update_remaining_count(response)
132
+ # @param [Faraday::Response] response The API response
133
+ # @return [Integer, nil] The remaining search-API requests count
134
+ def extract_search_remaining_count(response)
98
135
  body = response.body
99
- stringed = body.is_a?(String)
100
- if stringed
101
- begin
102
- body = JSON.parse(body)
103
- rescue JSON::ParserError
104
- return
136
+ body = JSON.parse(body) if body.is_a?(String)
137
+ return nil unless body.is_a?(Hash)
138
+ value = body.dig('resources', 'search', 'remaining')
139
+ value.nil? ? nil : Integer(value)
140
+ end
141
+
142
+ # Builds a fresh body with the current remaining counts written in,
143
+ # without mutating the cached response. Uses a JSON round-trip for
144
+ # the deep copy so we only handle JSON-shaped data.
145
+ #
146
+ # @param [Object] original The cached response body (Hash or JSON String)
147
+ # @return [Object] A new body of the same type with remaining counts updated
148
+ def patched_body(original)
149
+ stringed = original.is_a?(String)
150
+ body =
151
+ if stringed
152
+ JSON.parse(original)
153
+ elsif original.is_a?(Hash)
154
+ JSON.parse(original.to_json)
155
+ else
156
+ return original
105
157
  end
106
- end
107
- return unless body.is_a?(Hash) && body['rate']
108
- body['rate']['remaining'] = @remaining
109
- return unless stringed
110
- response.instance_variable_set(:@body, body.to_json)
158
+ body['rate']['remaining'] = @remaining if body['rate'] && !@remaining.nil?
159
+ body.dig('resources', 'core')&.[]=('remaining', @remaining) unless @remaining.nil?
160
+ body.dig('resources', 'search')&.[]=('remaining', @searchleft) unless @searchleft.nil?
161
+ stringed ? body.to_json : body
111
162
  end
112
163
 
113
- # Creates a response environment for the cached response.
164
+ # Builds a response environment that mirrors the cached response,
165
+ # preserving Faraday::Env invariants by dup-ing the original env
166
+ # and only overriding body and rate-limit headers.
114
167
  #
115
168
  # @param [Faraday::Env] env The original request environment
116
169
  # @param [Faraday::Response] response The cached response
117
- # @return [Hash] Response environment hash
170
+ # @return [Faraday::Env] Response env ready to wrap in Faraday::Response
118
171
  def response_env(env, response)
119
- headers = response.headers.dup
120
- headers['x-ratelimit-remaining'] = @remaining.to_s if @remaining
121
- {
122
- method: env.method,
123
- url: env.url,
124
- request_headers: env.request_headers,
125
- request_body: env.request_body,
126
- status: response.status,
127
- response_headers: headers,
128
- body: response.body
129
- }
172
+ served = response.env.dup
173
+ served.request_headers = env.request_headers
174
+ served.body = patched_body(response.body)
175
+ served.response_headers = response.headers.dup
176
+ served.response_headers['x-ratelimit-remaining'] = @remaining.to_s unless @remaining.nil?
177
+ served
130
178
  end
131
179
  end
@@ -86,8 +86,8 @@ class Fbe::Middleware::SqliteStore
86
86
  return unless value
87
87
  begin
88
88
  JSON.parse(Zlib::Inflate.inflate(value))
89
- rescue Zlib::Error => e
90
- @loog.info("Failed to decompress cached value for key: #{key}, error: #{e.message}, the key will be deleted")
89
+ rescue Zlib::Error, JSON::ParserError, TypeError => e
90
+ @loog.info("Failed to decode cached value for key: #{key}, error: #{e.message}, the key will be deleted")
91
91
  delete(key)
92
92
  end
93
93
  end
@@ -106,7 +106,7 @@ class Fbe::Middleware::SqliteStore
106
106
  # @return [nil]
107
107
  # @note Values larger than 10KB are not cached
108
108
  # @note Non-GET requests and URLs with query parameters are not cached
109
- def write(key, value) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
109
+ def write(key, value) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
110
110
  return if value.is_a?(Array) && value.any? do |vv|
111
111
  req = JSON.parse(vv[0])
112
112
  req['method'] != 'get'
@@ -129,6 +129,8 @@ class Fbe::Middleware::SqliteStore
129
129
  end
130
130
  end
131
131
  resp['response_headers']['cache-control'] = control
132
+ value = value.dup
133
+ value[0] = value[0].dup
132
134
  value[0][1] = JSON.dump(resp)
133
135
  end
134
136
  end
@@ -159,6 +161,13 @@ class Fbe::Middleware::SqliteStore
159
161
  perform { _1.execute('SELECT key, value FROM cache') }
160
162
  end
161
163
 
164
+ # Close the database connection explicitly.
165
+ # @return [nil]
166
+ def close
167
+ @db&.close
168
+ @db = nil
169
+ end
170
+
162
171
  private
163
172
 
164
173
  def perform(&) # rubocop:disable Metrics/AbcSize
@@ -248,7 +257,6 @@ class Fbe::Middleware::SqliteStore
248
257
  "new file size: #{Filesize.from(File.size(@path).to_s).pretty} bytes"
249
258
  )
250
259
  end
251
- at_exit { @db&.close }
252
260
  end
253
261
  @db.transaction(&)
254
262
  end