grape-idempotency 1.0.0 → 1.1.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: 756cff52967b35d996913c3d682847a4cd362c2cbd4a95d8bf766c2f037d86ce
4
- data.tar.gz: bf87694a06895bfd09749676574a078f71b698f59c23211a438112b22f332f06
3
+ metadata.gz: 115100d8cd1141c7c801a1501ede2db9eff3419a5b2974de93c4b79e7c812fd2
4
+ data.tar.gz: 567472c4ab8ad2aae2561d7af0e808c803a025caae34f75492c2b889b6468d7e
5
5
  SHA512:
6
- metadata.gz: bdaaa014a0a39fdbba3f850bdddbd27db5fca791f543609c4a847615eaeeac52d0dfb16e642d1310915bbdada5b46267291a1e43fec70299a027c5a9848285b2
7
- data.tar.gz: 2030a6016fcbaf64667b22a2be43af70239381e6d3311571e45c172d439bb67222ca57f2566becd847eb867d8ae9750ddcef70e1dd2444492191e2924f27e7cd
6
+ metadata.gz: fde07abe226552ba88086f20fcece9778ef13ee04c17c27376aea2d13386955944caea65ae28ff1d2abdda72bc501600a9afb107229e6da47b4dd02a56b2e5f5
7
+ data.tar.gz: 3a9a9fcd8f30d5f1deab9da2c719c54c1a0cbecafb5ce3b1196301bc60983bd3271153f4b02fed795d49d4188255507e0a8445d1fdd37cad1bbe0a0851b32e48
data/CHANGELOG.md CHANGED
@@ -4,6 +4,21 @@ All changes to `grape-idempotency` will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [1.1.0] - (Next)
8
+
9
+ ### Fix
10
+
11
+ * Your contribution here.
12
+
13
+ ### Changed
14
+
15
+ * Your contribution here.
16
+
17
+ ### Feature
18
+
19
+ * [#20](https://github.com/jcagarcia/grape-idempotency/pull/20): Manage `Redis` exceptions by default and allow the gem's consumer to manage them by itself - [@jcagarcia](https://github.com/jcagarcia).
20
+ * Your contribution here.
21
+
7
22
  ## [1.0.0] - 2023-11-23
8
23
 
9
24
  ### Changed
data/README.md CHANGED
@@ -12,7 +12,8 @@ Topics covered in this README:
12
12
  - [Installation](#installation-)
13
13
  - [Basic Usage](#basic-usage-)
14
14
  - [How it works](#how-it-works-)
15
- - [Making idempotency key header mandatory](#making-idempotency-key-header-mandatory-)
15
+ - [Making idempotency key header mandatory](#making-idempotency-key-header-mandatory-)
16
+ - [Redis Storage Connectivity Issue](#redis-storage-connectivity-issue)
16
17
  - [Configuration](#configuration-)
17
18
  - [Changelog](#changelog)
18
19
  - [Contributing](#contributing)
@@ -81,7 +82,7 @@ Results are only saved if an API endpoint begins its execution. If incoming para
81
82
 
82
83
  Additionally, this gem automatically appends the `Original-Request` header and the `Idempotency-Key` header to your API's response, enabling you to trace back to the initial request that generated that specific response.
83
84
 
84
- ## Making idempotency key header mandatory ⚠️
85
+ ### Making idempotency key header mandatory ⚠️
85
86
 
86
87
  For some endpoints, you want to enforce your consumers to provide idempotency key. So, when wrapping the code inside the `idempotent` method, you can mark it as `required`:
87
88
 
@@ -113,6 +114,37 @@ If the Idempotency-Key request header is missing for a idempotent operation requ
113
114
 
114
115
  If you want to change the error message returned in this scenario, check [How to configure idempotency key missing error message](#mandatory_header_response) section.
115
116
 
117
+ ### Redis Storage Connectivity Issue
118
+
119
+ By default, `Redis` exceptions are not handled by the `grape-idempotency` gem.
120
+
121
+ Therefore, if an exception arises while attempting to read, write or delete data from the `Redis` storage, the gem will re-raise the identical exception to your application. Thus, you will be responsible for handling it within your own code, such as:
122
+
123
+ ```ruby
124
+ require 'grape'
125
+ require 'grape-idempotency'
126
+
127
+ class API < Grape::API
128
+ post '/payments' do
129
+ begin
130
+ idempotent do
131
+ status 201
132
+ Payment.create!({
133
+ amount: params[:amount]
134
+ })
135
+ end
136
+ rescue Redis::BaseError => e
137
+ error!("Redis error! Idempotency is very important here and we cannot continue.", 500)
138
+ end
139
+ end
140
+ end
141
+ end
142
+ ```
143
+
144
+ If you want to avoid this functionality, and you want the gem handles the potential `Redis` exceptions, you have the option to configure the gem for handling these `Redis` exceptions. Please refer to the [manage_redis_exceptions](#manage_redis_exceptions) configuration property.
145
+
146
+ 🚨 WARNING: If a `Redis` exception appears AFTER performing the wrapped code, nothing will be re-raised. The process will continue working and the response will be returned to the consumer of your API. However, a `409 Conflict` response can be returned to your consumer if it retried the same call with the same idempotency key. This is because the gem was not able to associate the response of the original request to the original idempotency key because those connectivity issues.
147
+
116
148
  ## Configuration 🪚
117
149
 
118
150
  In addition to the storage aspect, you have the option to supply additional configuration details to tailor the gem to the specific requirements of your project.
@@ -169,6 +201,7 @@ If you want to provide your own logger, you want to change the level to `DEBUG`
169
201
 
170
202
  ```ruby
171
203
  Grape::Idempotency.configure do |c|
204
+ c.storage = @storage
172
205
  c.logger = Infrastructure::MyLogger.new
173
206
  c.logger_level = :debug
174
207
  c.logger_prefix = '[my-own-prefix]'
@@ -194,6 +227,21 @@ I, [2023-11-23T22:41:39.148523 #1] DEBUG -- : [my-own-prefix] Request has been
194
227
  I, [2023-11-23T22:41:39.148537 #1] DEBUG -- : [my-own-prefix] Returning the response from the original request.
195
228
  ```
196
229
 
230
+ ### manage_redis_exceptions
231
+
232
+ By default, the `grape-idempotency` gem is configured to re-raise `Redis` exceptions.
233
+
234
+ If you want to delegate the `Redis` exception management into the gem, you can configure it using the `manage_redis_exceptions` configuration property.
235
+
236
+ ```ruby
237
+ Grape::Idempotency.configure do |c|
238
+ c.storage = @storage
239
+ c.manage_redis_exceptions = true
240
+ end
241
+ ```
242
+
243
+ However, this approach carries a certain level of risk. In the case that `Redis` experiences an outage, the idempotent functionality will be lost, the endpoint will behave as no idempotent, and this issue may go unnoticed.
244
+
197
245
  ### conflict_error_response
198
246
 
199
247
  When providing a `Idempotency-Key: <key>` header, this gem compares incoming parameters to those of the original request (if exists) and returns a `409 - Conflict` status code if they don't match, preventing accidental misuse. The response body returned by the gem looks like:
@@ -22,7 +22,8 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.required_ruby_version = '>= 2.6'
24
24
 
25
- spec.add_runtime_dependency 'grape', '~> 1'
25
+ spec.add_runtime_dependency 'grape', '>= 1'
26
+ spec.add_runtime_dependency 'redis', '>= 4'
26
27
 
27
28
  spec.add_development_dependency 'bundler'
28
29
  spec.add_development_dependency 'rspec'
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Grape
4
4
  module Idempotency
5
- VERSION = '1.0.0'
5
+ VERSION = '1.1.0'
6
6
  end
7
7
  end
@@ -1,4 +1,5 @@
1
1
  require 'grape'
2
+ require 'redis'
2
3
  require 'logger'
3
4
  require 'securerandom'
4
5
  require 'grape/idempotency/version'
@@ -33,21 +34,21 @@ module Grape
33
34
  grape.error!(configuration.mandatory_header_response, 400) if required && !idempotency_key
34
35
  return block.call if !idempotency_key
35
36
 
36
- cached_request = get_from_cache(idempotency_key)
37
- log(:debug, "Request has been found for the provided idempotency key => #{cached_request}") if cached_request
38
- if cached_request && (cached_request["params"] != grape.request.params || cached_request["path"] != grape.request.path)
39
- log(:debug, "Request has conflicts. Same params? => #{cached_request["params"] != grape.request.params}. Same path? => #{cached_request["path"] != grape.request.path}")
37
+ stored_request = get_from_storage(idempotency_key)
38
+ log(:debug, "Request has been found for the provided idempotency key => #{stored_request}") if stored_request
39
+ if stored_request && (stored_request["params"] != grape.request.params || stored_request["path"] != grape.request.path)
40
+ log(:debug, "Request has conflicts. Same params? => #{stored_request["params"] != grape.request.params}. Same path? => #{stored_request["path"] != grape.request.path}")
40
41
  log(:debug, "Returning conflict error response.")
41
42
  grape.error!(configuration.conflict_error_response, 422)
42
- elsif cached_request && cached_request["processing"] == true
43
+ elsif stored_request && stored_request["processing"] == true
43
44
  log(:debug, "Returning processing error response.")
44
45
  grape.error!(configuration.processing_response, 409)
45
- elsif cached_request
46
+ elsif stored_request
46
47
  log(:debug, "Returning the response from the original request.")
47
- grape.status cached_request["status"]
48
- grape.header(ORIGINAL_REQUEST_HEADER, cached_request["original_request"])
48
+ grape.status stored_request["status"]
49
+ grape.header(ORIGINAL_REQUEST_HEADER, stored_request["original_request"])
49
50
  grape.header(configuration.idempotency_key_header, idempotency_key)
50
- return cached_request["response"]
51
+ return stored_request["response"]
51
52
  end
52
53
 
53
54
  log(:debug, "Previous request information has NOT been found for the provided idempotency key.")
@@ -76,26 +77,32 @@ module Grape
76
77
 
77
78
  grape.header(ORIGINAL_REQUEST_HEADER, original_request_id)
78
79
  grape.body response
80
+ rescue Redis::BaseError => e
81
+ raise
79
82
  rescue => e
80
83
  log(:debug, "An unexpected error was raised when performing the block.")
81
- if !cached_request && !response
84
+ if !stored_request && !response
82
85
  validate_config!
83
86
  log(:debug, "Storing error response.")
84
87
  original_request_id = get_request_id(grape.request.headers)
85
88
  stored_key = store_error_request(idempotency_key, grape.request.path, grape.request.params, grape.status, original_request_id, e)
86
- log(:debug, "Error response stored.")
87
- grape.header(ORIGINAL_REQUEST_HEADER, original_request_id)
88
- grape.header(configuration.idempotency_key_header, stored_key)
89
+ if stored_key
90
+ log(:debug, "Error response stored.")
91
+ grape.header(ORIGINAL_REQUEST_HEADER, original_request_id)
92
+ grape.header(configuration.idempotency_key_header, stored_key)
93
+ end
89
94
  end
90
95
  log(:debug, "Re-raising the error.")
91
96
  raise
92
97
  ensure
93
- if !cached_request && response
98
+ if !stored_request && response
94
99
  validate_config!
95
100
  log(:debug, "Storing response.")
96
101
  stored_key = store_request_response(idempotency_key, grape.request.path, grape.request.params, grape.status, original_request_id, response)
97
- log(:debug, "Response stored.")
98
- grape.header(configuration.idempotency_key_header, stored_key)
102
+ if stored_key
103
+ log(:debug, "Response stored.")
104
+ grape.header(configuration.idempotency_key_header, stored_key)
105
+ end
99
106
  end
100
107
  end
101
108
 
@@ -113,6 +120,9 @@ module Grape
113
120
 
114
121
  store_request_response(idempotency_key, path, params, status, original_request_id, response)
115
122
  storage.del(stored_error[:error_key])
123
+ rescue Redis::BaseError => e
124
+ log(:error, "Storage error => #{e.message} - #{e}")
125
+ nil
116
126
  end
117
127
 
118
128
  private
@@ -122,7 +132,10 @@ module Grape
122
132
  end
123
133
 
124
134
  def valid_storage?
125
- configuration.storage && configuration.storage.respond_to?(:set)
135
+ configuration.storage &&
136
+ configuration.storage.respond_to?(:get) &&
137
+ configuration.storage.respond_to?(:set) &&
138
+ configuration.storage.respond_to?(:del)
126
139
  end
127
140
 
128
141
  def get_idempotency_key(headers)
@@ -141,11 +154,15 @@ module Grape
141
154
  request_id || "req_#{SecureRandom.hex}"
142
155
  end
143
156
 
144
- def get_from_cache(idempotency_key)
157
+ def get_from_storage(idempotency_key)
145
158
  value = storage.get(key(idempotency_key))
146
159
  return unless value
147
160
 
148
161
  JSON.parse(value)
162
+ rescue Redis::BaseError => e
163
+ log(:error, "Storage error => #{e.message} - #{e}")
164
+ return if configuration.manage_redis_exceptions
165
+ raise
149
166
  end
150
167
 
151
168
  def store_processing_request(idempotency_key, path, params, request_id)
@@ -157,6 +174,9 @@ module Grape
157
174
  }
158
175
 
159
176
  storage.set(key(idempotency_key), body.to_json, ex: configuration.expires_in, nx: true)
177
+ rescue Redis::BaseError => e
178
+ return true if configuration.manage_redis_exceptions
179
+ raise
160
180
  end
161
181
 
162
182
  def store_request_response(idempotency_key, path, params, status, request_id, response)
@@ -170,6 +190,9 @@ module Grape
170
190
 
171
191
  storage.set(key(idempotency_key), body.to_json, ex: configuration.expires_in, nx: false)
172
192
 
193
+ idempotency_key
194
+ rescue Redis::BaseError => e
195
+ log(:error, "Storage error => #{e.message} - #{e}")
173
196
  idempotency_key
174
197
  end
175
198
 
@@ -187,6 +210,9 @@ module Grape
187
210
 
188
211
  storage.set(error_key(idempotency_key), body, ex: 30, nx: false)
189
212
 
213
+ idempotency_key
214
+ rescue Redis::BaseError => e
215
+ log(:error, "Storage error => #{e.message} - #{e}")
190
216
  idempotency_key
191
217
  end
192
218
 
@@ -207,6 +233,9 @@ module Grape
207
233
  }
208
234
  end
209
235
  end.first
236
+ rescue Redis::BaseError => e
237
+ log(:error, "Storage error => #{e.message} - #{e}")
238
+ nil
210
239
  end
211
240
 
212
241
  def is_an_error?(response)
@@ -252,7 +281,8 @@ module Grape
252
281
 
253
282
  class Configuration
254
283
  attr_accessor :storage, :logger, :logger_level, :logger_prefix, :expires_in, :idempotency_key_header,
255
- :request_id_header, :conflict_error_response, :processing_response, :mandatory_header_response
284
+ :request_id_header, :conflict_error_response, :processing_response, :mandatory_header_response,
285
+ :manage_redis_exceptions
256
286
 
257
287
  class Error < StandardError; end
258
288
 
@@ -264,6 +294,7 @@ module Grape
264
294
  @expires_in = 216_000
265
295
  @idempotency_key_header = "idempotency-key"
266
296
  @request_id_header = "x-request-id"
297
+ @manage_redis_exceptions = false
267
298
  @conflict_error_response = {
268
299
  "title" => "Idempotency-Key is already used",
269
300
  "detail" => "This operation is idempotent and it requires correct usage of Idempotency Key. Idempotency Key MUST not be reused across different payloads of this operation."
metadata CHANGED
@@ -1,29 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape-idempotency
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juan Carlos García
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-23 00:00:00.000000000 Z
11
+ date: 2023-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: grape
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '1'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: redis
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement