shopify_api_retry 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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: []