shopify_api_retry 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 949d65d19ed205d18a64ea886ef817bcd9d8b26895036228dddc3dcf4d91f0ca
4
- data.tar.gz: 6b776cf5985be1593e791a81c6f4ce31c88ca4f029c7ba03bd8e24ca5f44f26f
3
+ metadata.gz: 52d7c3cac56c5a6bc3146fe32d68853bd36411718a9a24a23eecb1be5055ef2e
4
+ data.tar.gz: f6164b541570442bd62781338ab391f17d9d6a79cfa3d182960efc3046041b0a
5
5
  SHA512:
6
- metadata.gz: 781e898074a0f3b9aaed28f1202087173e42483920375f7921384854437f9ed40ad77a0ef37be5d1221f5e76141c988a35e50368da22e83b5e8722c185558e70
7
- data.tar.gz: 243547c486e88cfabed0ea4a2c4fbba9524aa44234c5852110cf4a485f28c551309e78a055222576da2bcc6240232d783706807ebeee31c41956dddd6e130495
6
+ metadata.gz: 4098e6e2399e42830d4cad365b047c4ad8a1d1a24027e1a800ad04b7d3aa78d3acd5e07c279cdfd802bec916542211c6ad0ca67b819187329639ec762403dcc7
7
+ data.tar.gz: 6dcff84ded14acbfd948fe239ddc0b1cc71d25963e064f4f01db030b3d1cc5a157e6b0216a248c4f384c7f19f73e965e5c9da428e6438a04d2ccd81110148660
data/Changes ADDED
@@ -0,0 +1,20 @@
1
+ --------------------
2
+ 2021-05-08 v0.1.3
3
+ --------------------
4
+ * Add support for retrying GraphQL requests
5
+ * Mark ShopifyAPIRetry.retry as deprecated
6
+
7
+ --------------------
8
+ 2021-03-11 v0.1.2
9
+ --------------------
10
+ * Check proper base class for ActiveResource error handling
11
+
12
+ --------------------
13
+ 2021-03-09 v0.1.1
14
+ --------------------
15
+ * Fix typo in deprication warning
16
+
17
+ --------------------
18
+ 2021-01-31 v0.1.0
19
+ --------------------
20
+ * Support for retrying user-defined errors
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
- # ShopifyAPIRetry
1
+ # Shopify API Retry
2
2
 
3
3
  ![CI](https://github.com/ScreenStaring/shopify_api_retry/workflows/CI/badge.svg)
4
4
 
5
- Simple Ruby module to retry a [Shopify API request](https://github.com/Shopify/shopify_api) if rate limited (HTTP 429) or other errors
6
- occur.
5
+ Simple Ruby module to retry a [`ShopifyAPI` request](https://github.com/Shopify/shopify_api) if rate-limited or other errors
6
+ occur. Works with the REST and GraphQL APIs.
7
7
 
8
8
  ## Installation
9
9
 
@@ -21,25 +21,27 @@ gem install shopify_api_retry
21
21
 
22
22
  ## Usage
23
23
 
24
- By default requests are retried when a Shopify rate limit error is returned. The retry happens once after waiting for
24
+ ### REST API
25
+
26
+ By default requests are retried when a Shopify rate limit error (HTTP 429) is returned. The retry happens once after waiting for
25
27
  [the seconds given by the HTTP `Retry-After` header](https://shopify.dev/concepts/about-apis/rate-limits):
26
28
  ```rb
27
29
  require "shopify_api_retry" # requires "shopify_api" for you
28
30
 
29
- ShopifyAPIRetry.retry { customer.update_attribute(:tags, "foo") }
30
- customer = ShopifyAPIRetry.retry { ShopifyAPI::Customer.find(id) }
31
+ ShopifyAPIRetry::REST.retry { customer.update_attribute(:tags, "foo") }
32
+ customer = ShopifyAPIRetry::REST.retry { ShopifyAPI::Customer.find(id) }
31
33
  ```
32
34
 
33
35
  You can override this:
34
36
  ```rb
35
- ShopifyAPIRetry.retry(:wait => 3, :tries => 5) { customer.update_attribute(:tags, "foo") }
37
+ ShopifyAPIRetry::REST.retry(:wait => 3, :tries => 5) { customer.update_attribute(:tags, "foo") }
36
38
  ```
37
39
  This will try the request 5 times, waiting 3 seconds between each attempt. If a retry fails after the given number
38
- of `:tries` the original error will be raised.
40
+ of `:tries` the last error will be raised.
39
41
 
40
42
  You can also retry requests when other errors occur:
41
43
  ```rb
42
- ShopifyAPIRetry.retry "5XX" => { :wait => 10, :tries => 2 } do
44
+ ShopifyAPIRetry::REST.retry "5XX" => { :wait => 10, :tries => 2 } do
43
45
  customer.update_attribute(:tags, "foo")
44
46
  end
45
47
  ```
@@ -47,24 +49,75 @@ This still retries rate limit requests, but also all HTTP 5XX errors.
47
49
 
48
50
  Classes can be specified too:
49
51
  ```rb
50
- ShopifyAPIRetry.retry SocketError => { :wait => 1, :tries => 5 } do
52
+ ShopifyAPIRetry::REST.retry SocketError => { :wait => 1, :tries => 5 } do
51
53
  customer.update_attribute(:tags, "foo")
52
54
  end
53
55
  ```
54
56
 
55
- Global defaults can be set as well:
57
+ You can also set [global defaults](#global-defaults).
58
+
59
+ ### GraphQL API
60
+
61
+ By default a retry attempt is made when your [GraphQL request is rate-limited](https://shopify.dev/concepts/about-apis/rate-limits#graphql-admin-api-rate-limits).
62
+ In order to calculate the proper amount of time to wait before retrying you must set the `X-GraphQL-Cost-Include-Fields` header
63
+ (_maybe this library should do this?_):
64
+
65
+ ```rb
66
+ require "shopify_api_retry" # requires "shopify_api" for you
67
+
68
+ ShopifyAPI::Base.headers["X-GraphQL-Cost-Include-Fields"] = "true"
69
+ ```
70
+
71
+ Once this is set run your queries and mutations and rate-limited requests will be retried.
72
+ The retry happens once, calculating the wait time from the API's cost data:
73
+ ```rb
74
+ result = ShopifyAPIRetry::GraphQL.retry { ShopifyAPI::GraphQL.client.query(YOUR_QUERY) }
75
+ p result.data.whatever
76
+ ```
77
+
78
+ To calculate the retry time **the query's return value must be returned by the block**.
79
+
80
+ You can override the retry times:
81
+ ```rb
82
+ result = ShopifyAPIRetry::GraphQL.retry(:wait => 4, :tries => 4) do
83
+ ShopifyAPI::GraphQL.client.query(YOUR_QUERY)
84
+ end
85
+ p result.data.whatever
86
+ ```
87
+
88
+ Like retry attempts made with the REST API you can specify errors to retry but, due to the GraphQL specification, these must not
89
+ be HTTP status codes and are only relevant to network connection-related errors:
90
+
91
+ ```rb
92
+ result = ShopifyAPIRetry::GraphQL.retry SocketError => { :wait => 1, :tries => 5 } do
93
+ ShopifyAPI::GraphQL.client.query(YOUR_QUERY)
94
+ end
95
+ ```
96
+
97
+ To give wait and try times for specifically for rate limit errors, use the key `:graphql`:
98
+ ```rb
99
+ result = ShopifyAPIRetry::GraphQL.retry :graphql => { :wait => 10, :tries => 5 } do
100
+ ShopifyAPI::GraphQL.client.query(YOUR_QUERY)
101
+ end
102
+ ```
103
+
104
+ Note that specifying a `:wait` via `:graphql` or without an error key will skip calculating the retry based on the API
105
+ response's cost data.
106
+
107
+ ### Global Defaults
108
+
109
+ You can configure global defaults to be used by REST and GraphQL calls. For example:
110
+
56
111
  ```rb
57
112
  ShopifyAPIRetry.configure do |config|
58
113
  config.default_wait = 2.5
59
114
  config.default_tries = 10
60
115
 
61
- # Use defaults for these
62
- config.on ["5XX", Net::TimeoutError]
116
+ # Use default_* for these
117
+ config.on ["5XX", Net::ReadTimeout]
63
118
 
64
119
  config.on SocketError, :tries => 2, :wait => 1
65
120
  end
66
-
67
- ShopifyAPIRetry.retry { customer.update_attribute(:tags, "foo") }
68
121
  ```
69
122
 
70
123
  ## License
@@ -1,14 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "shopify_api"
3
+ # We don't use this. We require it as convenience for the caller in case they do.
4
+ begin
5
+ require "shopify_api"
6
+ rescue LoadError
7
+ end
4
8
 
5
9
  module ShopifyAPIRetry
6
- VERSION = "0.1.0"
7
-
8
- HTTP_RETRY_AFTER = "Retry-After"
9
- HTTP_RETRY_STATUS = "429"
10
+ VERSION = "0.2.0"
10
11
 
11
- class Config
12
+ class Config # :nodoc:
12
13
  attr_writer :default_wait
13
14
  attr_writer :default_tries
14
15
 
@@ -40,8 +41,38 @@ module ShopifyAPIRetry
40
41
  @default_tries = nil
41
42
  end
42
43
 
44
+ def merge(userconfig)
45
+ config = to_h
46
+ return config unless userconfig
47
+
48
+ if userconfig.is_a?(Integer)
49
+ warn "#{self.class}: using an Integer for the retry time is deprecated and will be removed, use :wait => #{userconfig} instead"
50
+ userconfig = { :wait => userconfig }
51
+ elsif !userconfig.is_a?(Hash)
52
+ raise ArgumentError, "config must be a Hash"
53
+ end
54
+
55
+ userconfig.each do |k, v|
56
+ if v.is_a?(Hash)
57
+ config[k.to_s] = v.dup
58
+ else
59
+ config["graphql"][k] = config[REST::HTTP_RETRY_STATUS][k] = v
60
+ end
61
+ end
62
+
63
+ config.values.each do |cfg|
64
+ raise ArgumentError, "seconds to wait must be >= 0" if cfg[:wait] && cfg[:wait] < 0
65
+ end
66
+
67
+ config
68
+ end
69
+
43
70
  def to_h
44
- settings = { HTTP_RETRY_STATUS => { :tries => default_tries, :wait => default_wait } }
71
+ settings = {
72
+ "graphql" => { :tries => default_tries, :wait => default_wait },
73
+ REST::HTTP_RETRY_STATUS => { :tries => default_tries, :wait => default_wait }
74
+ }
75
+
45
76
  @settings.each_with_object(settings) { |(k, v), o| o[k.to_s] = v.dup }
46
77
  end
47
78
  end
@@ -58,75 +89,123 @@ module ShopifyAPIRetry
58
89
  @config
59
90
  end
60
91
 
61
- #
62
- # Execute the provided block. If an HTTP 429 response is returned try
63
- # it again. If any errors are provided try them according to their wait/return spec.
64
- #
65
- # If no spec is provided the value of the HTTP header <code>Retry-After</code>
66
- # is waited for before retrying. If it's not given (it always is) +2+ is used.
67
- #
68
- # If retry fails the original error is raised.
69
- #
70
- # Returns the value of the block.
71
- #
72
- def retry(cfg = nil)
73
- raise ArgumentError, "block required" unless block_given?
74
-
75
- attempts = build_config(cfg)
76
-
77
- begin
78
- result = yield
79
- rescue => e
80
- handler = attempts[e.class.name]
81
- raise if handler.nil? && (!e.is_a?(ActiveResource::ClientError) || !e.response.respond_to?(:code))
82
-
83
- handler ||= attempts[e.response.code] || attempts["#{e.response.code[0]}XX"]
84
- handler[:wait] ||= e.response[HTTP_RETRY_AFTER] || config.default_wait if e.response.code == HTTP_RETRY_STATUS
92
+ class Request # :nodoc:
93
+ def self.retry(cfg = nil, &block)
94
+ new(cfg).retry(&block)
95
+ end
96
+
97
+ def initialize(cfg = nil)
98
+ @handlers = ShopifyAPIRetry.config.merge(cfg)
99
+ end
100
+
101
+ def retry(&block)
102
+ raise ArgumentError, "block required" unless block_given?
103
+
104
+ begin
105
+ result = request(&block)
106
+ rescue => e
107
+ handler = find_handler(e)
108
+ raise unless handler && snooze(handler)
85
109
 
110
+ retry
111
+ end
112
+
113
+ result
114
+ end
115
+
116
+ protected
117
+
118
+ attr_reader :handlers
119
+
120
+ def request
121
+ yield
122
+ end
123
+
124
+ def find_handler(error)
125
+ @handlers[error.class.name]
126
+ end
127
+
128
+ def snooze(handler)
86
129
  handler[:attempts] ||= 1
87
- raise if handler[:attempts] == handler[:tries]
130
+ return false if handler[:attempts] == handler[:tries]
88
131
 
89
- snooze = handler[:wait].to_i
132
+ snooze = handler[:wait].to_f
90
133
  waited = sleep snooze
91
- snooze -= waited
92
- # Worth looping?
134
+ snooze = snooze - waited
135
+ # sleep returns the rounded time slept but sometimes it's rounded up, others it's down
136
+ # given this, we may sleep for more than requested
93
137
  sleep snooze if snooze > 0
94
138
 
95
139
  handler[:attempts] += 1
96
140
 
97
- retry
141
+ true
98
142
  end
99
-
100
- result
101
143
  end
102
144
 
103
- module_function :retry
145
+ class REST < Request
146
+ HTTP_RETRY_AFTER = "Retry-After"
147
+ HTTP_RETRY_STATUS = "429"
148
+
149
+ protected
104
150
 
105
- def self.build_config(userconfig)
106
- config = ShopifyAPIRetry.config.to_h
107
- return config unless userconfig
151
+ def find_handler(error)
152
+ handler = super
153
+ return handler if handler || (!error.is_a?(ActiveResource::ConnectionError) || !error.response.respond_to?(:code))
108
154
 
109
- if userconfig.is_a?(Integer)
110
- userconfig = { :wait => cfg }
111
- warn "passing an Integer to retry is deprecated and will be removed, use an :wait => #{cfg} instead"
112
- elsif !userconfig.is_a?(Hash)
113
- raise ArgumentError, "config must be a Hash"
155
+ handler = handlers[error.response.code] || handlers["#{error.response.code[0]}XX"]
156
+ handler[:wait] ||= error.response[HTTP_RETRY_AFTER] || config.default_wait if error.response.code == HTTP_RETRY_STATUS
157
+
158
+ handler
114
159
  end
160
+ end
115
161
 
116
- userconfig.each do |k, v|
117
- if v.is_a?(Hash)
118
- config[k.to_s] = v.dup
119
- else
120
- config[HTTP_RETRY_STATUS][k] = v
162
+ class GraphQL < Request
163
+ CONVERSION_WARNING = "#{name}.retry: skipping retry, cannot convert GraphQL response to a Hash: %s. " \
164
+ "To retry requests your block's return value must be a Hash or something that can be converted via #to_h"
165
+ protected
166
+
167
+ def request
168
+ loop do
169
+ data = og_data = yield
170
+
171
+ # Shopify's client does not return a Hash but
172
+ # technically we work with any Hash response
173
+ unless data.is_a?(Hash)
174
+ unless data.respond_to?(:to_h)
175
+ warn CONVERSION_WARNING % "respond_to?(:to_h) is false"
176
+ return og_data
177
+ end
178
+
179
+ begin
180
+ data = data.to_h
181
+ rescue TypeError, ArgumentError => e
182
+ warn CONVERSION_WARNING % e.message
183
+ return og_data
184
+ end
185
+ end
186
+
187
+ cost = data.dig("extensions", "cost")
188
+ # If this is nil then the X-GraphQL-Cost-Include-Fields header was not set
189
+ # If actualQueryCost is present then the query was not rate limited
190
+ return og_data if cost.nil? || cost["actualQueryCost"]
191
+
192
+ handler = handlers["graphql"]
193
+ handler[:wait] ||= sleep_time(cost)
194
+
195
+ return og_data unless snooze(handler)
121
196
  end
122
197
  end
123
198
 
124
- config.values.each do |cfg|
125
- raise ArgumentError, "seconds to wait must be >= 0" if cfg[:wait] && cfg[:wait] < 0
199
+ def sleep_time(cost)
200
+ status = cost["throttleStatus"]
201
+ (cost["requestedQueryCost"] - status["currentlyAvailable"]) / status["restoreRate"]# + 0.33
126
202
  end
203
+ end
127
204
 
128
- config
205
+ def retry(cfg = nil, &block)
206
+ warn "#{name}.retry has been deprecated, use ShopifyAPIRetry::REST.retry or ShopifyAPIRetry::GraphQL.retry"
207
+ REST.new(cfg).retry(&block)
129
208
  end
130
209
 
131
- private_class_method :build_config
210
+ module_function :retry
132
211
  end
@@ -1,11 +1,14 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "shopify_api_retry"
4
+
1
5
  Gem::Specification.new do |spec|
2
6
  spec.name = "shopify_api_retry"
3
- spec.version = "0.1.0"
7
+ spec.version = ShopifyAPIRetry::VERSION
4
8
  spec.authors = ["Skye Shaw"]
5
9
  spec.email = ["skye.shaw@gmail.com"]
6
10
 
7
- spec.summary = %q{Retry a ShopifyAPI request if an HTTP 429 (too many requests) is returned}
8
- spec.description = %q{Simple module to retry a ShopifyAPI request if an HTTP 429 (too many requests) is returned. No monkey patching.}
11
+ spec.summary = %q{Retry a ShopifyAPI request if if rate-limited or other errors occur. Works with the REST and GraphQL APIs.}
9
12
  spec.homepage = "https://github.com/ScreenStaring/shopify_api_retry"
10
13
  spec.license = "MIT"
11
14
 
@@ -18,7 +21,10 @@ Gem::Specification.new do |spec|
18
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
22
  spec.require_paths = ["lib"]
20
23
 
21
- spec.add_dependency "shopify_api", ">= 4.0"
24
+ spec.required_ruby_version = ">= 2.3"
25
+
26
+ # I don't think we _really_ need this just too lazy to remove now.
27
+ spec.add_development_dependency "shopify_api", ">= 4.0"
22
28
 
23
29
  spec.add_development_dependency "minitest", "~> 5.0"
24
30
  spec.add_development_dependency "bundler"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify_api_retry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Skye Shaw
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-02-01 00:00:00.000000000 Z
11
+ date: 2021-12-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: shopify_api
@@ -17,7 +17,7 @@ dependencies:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '4.0'
20
- type: :runtime
20
+ type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
@@ -66,8 +66,7 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '12.0'
69
- description: Simple module to retry a ShopifyAPI request if an HTTP 429 (too many
70
- requests) is returned. No monkey patching.
69
+ description:
71
70
  email:
72
71
  - skye.shaw@gmail.com
73
72
  executables: []
@@ -77,6 +76,7 @@ files:
77
76
  - ".github/workflows/ci.yml"
78
77
  - ".gitignore"
79
78
  - ".travis.yml"
79
+ - Changes
80
80
  - Gemfile
81
81
  - LICENSE.txt
82
82
  - README.md
@@ -95,7 +95,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
95
95
  requirements:
96
96
  - - ">="
97
97
  - !ruby/object:Gem::Version
98
- version: '0'
98
+ version: '2.3'
99
99
  required_rubygems_version: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - ">="
@@ -106,5 +106,6 @@ rubyforge_project:
106
106
  rubygems_version: 2.7.6
107
107
  signing_key:
108
108
  specification_version: 4
109
- summary: Retry a ShopifyAPI request if an HTTP 429 (too many requests) is returned
109
+ summary: Retry a ShopifyAPI request if if rate-limited or other errors occur. Works
110
+ with the REST and GraphQL APIs.
110
111
  test_files: []