shopify_api_retry 0.1.2 → 0.1.3

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: f6fa48159d2bd071e31e86baf44caa8c15050a0667b7ae8644883421f1187292
4
- data.tar.gz: c87b2749f3ba8c6a94b210a393833622efcfa2c27e5afdda8b3c593632fc516a
3
+ metadata.gz: 984c779f4d3aaa7e07881169dad03f42417e85da7467a68e4301762b417e8a82
4
+ data.tar.gz: a85e72b9ebd882d2c9a40f394b6669303e4b02880e4f9077bb7594eef4f14257
5
5
  SHA512:
6
- metadata.gz: d246a7623ae0a810c1f1051f5887f65b16e08a80c69e4574c7d72cd4286cb29d04b40d3211fdcec0b517a6f3c9ebfd3d428092ee98d80353d9a3ee713d84dd89
7
- data.tar.gz: 649b825c24075f79e1fba6daee7f7952c5f8a42bfed29eae1cfd1a576cbcf49de8bc325498aa3d5bd210615259104a96505059b31f913a70b93ce46f531c49ca
6
+ metadata.gz: deea19e090d2189044a9d81b425428723fd49b76f5b35f0623aa0b11fd142bcd2f07f5e4001b4e790cbf087afc217067c0efcc4b2c0af16c1ff7755c36e171c2
7
+ data.tar.gz: fe7621c89dec61b46d7970a077e323e4fa6518acb4a8372f69c8212eaca619c673699e99e3bd32f8c8446e1ae9c57fc92de03cdd3f7d508600c0ccf6c0462a1c
data/Changes CHANGED
@@ -1,3 +1,9 @@
1
+ --------------------
2
+ 2021-05-08 v0.1.3
3
+ --------------------
4
+ * Add support for retrying GraphQL requests
5
+ * Mark ShopifyAPIRetry.retry as deprecated
6
+
1
7
  --------------------
2
8
  2021-03-11 v0.1.2
3
9
  --------------------
data/README.md CHANGED
@@ -2,8 +2,8 @@
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,7 +21,9 @@ 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
@@ -35,7 +37,7 @@ You can override this:
35
37
  ShopifyAPIRetry.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
@@ -52,7 +54,58 @@ ShopifyAPIRetry.retry SocketError => { :wait => 1, :tries => 5 } do
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
+
56
109
  ```rb
57
110
  ShopifyAPIRetry.configure do |config|
58
111
  config.default_wait = 2.5
@@ -63,8 +116,6 @@ ShopifyAPIRetry.configure do |config|
63
116
 
64
117
  config.on SocketError, :tries => 2, :wait => 1
65
118
  end
66
-
67
- ShopifyAPIRetry.retry { customer.update_attribute(:tags, "foo") }
68
119
  ```
69
120
 
70
121
  ## License
@@ -3,12 +3,9 @@
3
3
  require "shopify_api"
4
4
 
5
5
  module ShopifyAPIRetry
6
- VERSION = "0.1.2"
6
+ VERSION = "0.1.3"
7
7
 
8
- HTTP_RETRY_AFTER = "Retry-After"
9
- HTTP_RETRY_STATUS = "429"
10
-
11
- class Config
8
+ class Config # :nodoc:
12
9
  attr_writer :default_wait
13
10
  attr_writer :default_tries
14
11
 
@@ -40,8 +37,38 @@ module ShopifyAPIRetry
40
37
  @default_tries = nil
41
38
  end
42
39
 
40
+ def merge(userconfig)
41
+ config = to_h
42
+ return config unless userconfig
43
+
44
+ if userconfig.is_a?(Integer)
45
+ warn "#{self.class}: using an Integer for the retry time is deprecated and will be removed, use :wait => #{userconfig} instead"
46
+ userconfig = { :wait => userconfig }
47
+ elsif !userconfig.is_a?(Hash)
48
+ raise ArgumentError, "config must be a Hash"
49
+ end
50
+
51
+ userconfig.each do |k, v|
52
+ if v.is_a?(Hash)
53
+ config[k.to_s] = v.dup
54
+ else
55
+ config["graphql"][k] = config[REST::HTTP_RETRY_STATUS][k] = v
56
+ end
57
+ end
58
+
59
+ config.values.each do |cfg|
60
+ raise ArgumentError, "seconds to wait must be >= 0" if cfg[:wait] && cfg[:wait] < 0
61
+ end
62
+
63
+ config
64
+ end
65
+
43
66
  def to_h
44
- settings = { HTTP_RETRY_STATUS => { :tries => default_tries, :wait => default_wait } }
67
+ settings = {
68
+ "graphql" => { :tries => default_tries, :wait => default_wait },
69
+ REST::HTTP_RETRY_STATUS => { :tries => default_tries, :wait => default_wait }
70
+ }
71
+
45
72
  @settings.each_with_object(settings) { |(k, v), o| o[k.to_s] = v.dup }
46
73
  end
47
74
  end
@@ -58,75 +85,123 @@ module ShopifyAPIRetry
58
85
  @config
59
86
  end
60
87
 
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::ConnectionError) || !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
88
+ class Request # :nodoc:
89
+ def self.retry(cfg = nil, &block)
90
+ new(cfg).retry(&block)
91
+ end
92
+
93
+ def initialize(cfg = nil)
94
+ @handlers = ShopifyAPIRetry.config.merge(cfg)
95
+ end
96
+
97
+ def retry(&block)
98
+ raise ArgumentError, "block required" unless block_given?
85
99
 
100
+ begin
101
+ result = request(&block)
102
+ rescue => e
103
+ handler = find_handler(e)
104
+ raise unless handler && snooze(handler)
105
+
106
+ retry
107
+ end
108
+
109
+ result
110
+ end
111
+
112
+ protected
113
+
114
+ attr_reader :handlers
115
+
116
+ def request
117
+ yield
118
+ end
119
+
120
+ def find_handler(error)
121
+ @handlers[error.class.name]
122
+ end
123
+
124
+ def snooze(handler)
86
125
  handler[:attempts] ||= 1
87
- raise if handler[:attempts] == handler[:tries]
126
+ return false if handler[:attempts] == handler[:tries]
88
127
 
89
- snooze = handler[:wait].to_i
128
+ snooze = handler[:wait].to_f
90
129
  waited = sleep snooze
91
- snooze -= waited
92
- # Worth looping?
130
+ snooze = snooze - waited
131
+ # sleep returns the rounded time slept but sometimes it's rounded up, others it's down
132
+ # given this, we may sleep for more than requested
93
133
  sleep snooze if snooze > 0
94
134
 
95
135
  handler[:attempts] += 1
96
136
 
97
- retry
137
+ true
98
138
  end
99
-
100
- result
101
139
  end
102
140
 
103
- module_function :retry
141
+ class REST < Request
142
+ HTTP_RETRY_AFTER = "Retry-After"
143
+ HTTP_RETRY_STATUS = "429"
144
+
145
+ protected
104
146
 
105
- def self.build_config(userconfig)
106
- config = ShopifyAPIRetry.config.to_h
107
- return config unless userconfig
147
+ def find_handler(error)
148
+ handler = super
149
+ return handler if handler || (!error.is_a?(ActiveResource::ConnectionError) || !error.response.respond_to?(:code))
108
150
 
109
- if userconfig.is_a?(Integer)
110
- userconfig = { :wait => config }
111
- warn "passing an Integer to retry is deprecated and will be removed, use :wait => #{config} instead"
112
- elsif !userconfig.is_a?(Hash)
113
- raise ArgumentError, "config must be a Hash"
151
+ handler = handlers[error.response.code] || handlers["#{error.response.code[0]}XX"]
152
+ handler[:wait] ||= error.response[HTTP_RETRY_AFTER] || config.default_wait if error.response.code == HTTP_RETRY_STATUS
153
+
154
+ handler
114
155
  end
156
+ end
115
157
 
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
158
+ class GraphQL < Request
159
+ CONVERSION_WARNING = "#{name}.retry: skipping retry, cannot convert GraphQL response to a Hash: %s. " \
160
+ "To retry requests your block's return value must be a Hash or something that can be converted via #to_h"
161
+ protected
162
+
163
+ def request
164
+ loop do
165
+ data = og_data = yield
166
+
167
+ # Shopify's client does not return a Hash but
168
+ # technically we work with any Hash response
169
+ unless data.is_a?(Hash)
170
+ unless data.respond_to?(:to_h)
171
+ warn CONVERSION_WARNING % "respond_to?(:to_h) is false"
172
+ return og_data
173
+ end
174
+
175
+ begin
176
+ data = data.to_h
177
+ rescue TypeError, ArgumentError => e
178
+ warn CONVERSION_WARNING % e.message
179
+ return og_data
180
+ end
181
+ end
182
+
183
+ cost = data.dig("extensions", "cost")
184
+ # If this is nil then the X-GraphQL-Cost-Include-Fields header was not set
185
+ # If actualQueryCost is present then the query was not rate limited
186
+ return og_data if cost.nil? || cost["actualQueryCost"]
187
+
188
+ handler = handlers["graphql"]
189
+ handler[:wait] ||= sleep_time(cost)
190
+
191
+ return og_data unless snooze(handler)
121
192
  end
122
193
  end
123
194
 
124
- config.values.each do |cfg|
125
- raise ArgumentError, "seconds to wait must be >= 0" if cfg[:wait] && cfg[:wait] < 0
195
+ def sleep_time(cost)
196
+ status = cost["throttleStatus"]
197
+ (cost["requestedQueryCost"] - status["currentlyAvailable"]) / status["restoreRate"]# + 0.33
126
198
  end
199
+ end
127
200
 
128
- config
201
+ def retry(cfg = nil, &block)
202
+ warn "#{name}.retry has been deprecated, use ShopifyAPIRetry::REST.retry or ShopifyAPIRetry::GraphQL.retry"
203
+ REST.new(cfg).retry(&block)
129
204
  end
130
205
 
131
- private_class_method :build_config
206
+ module_function :retry
132
207
  end
@@ -1,11 +1,10 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = "shopify_api_retry"
3
- spec.version = "0.1.2"
3
+ spec.version = "0.1.3"
4
4
  spec.authors = ["Skye Shaw"]
5
5
  spec.email = ["skye.shaw@gmail.com"]
6
6
 
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.}
7
+ spec.summary = %q{Retry a ShopifyAPI request if if rate-limited or other errors occur. Works with the REST and GraphQL APIs.}
9
8
  spec.homepage = "https://github.com/ScreenStaring/shopify_api_retry"
10
9
  spec.license = "MIT"
11
10
 
@@ -18,6 +17,7 @@ Gem::Specification.new do |spec|
18
17
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
18
  spec.require_paths = ["lib"]
20
19
 
20
+ spec.required_ruby_version = ">= 2.3"
21
21
  spec.add_dependency "shopify_api", ">= 4.0"
22
22
 
23
23
  spec.add_development_dependency "minitest", "~> 5.0"
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.2
4
+ version: 0.1.3
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-03-11 00:00:00.000000000 Z
11
+ date: 2021-05-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: shopify_api
@@ -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: []
@@ -96,7 +95,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
96
95
  requirements:
97
96
  - - ">="
98
97
  - !ruby/object:Gem::Version
99
- version: '0'
98
+ version: '2.3'
100
99
  required_rubygems_version: !ruby/object:Gem::Requirement
101
100
  requirements:
102
101
  - - ">="
@@ -107,5 +106,6 @@ rubyforge_project:
107
106
  rubygems_version: 2.7.6
108
107
  signing_key:
109
108
  specification_version: 4
110
- 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.
111
111
  test_files: []