shopify_api_retry 0.1.2 → 0.1.3

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: 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: []