idempotent-request 0.1.2 → 0.1.7

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: 25467dbc0ee42d2648e2dd267edb23be7972a6af0ba2d233e21d455c663fc643
4
- data.tar.gz: 4dc4e7373dce6370eb850134d25c164fd67ef5c54dc12c3585267d4774d1f58f
3
+ metadata.gz: 5791ba0801f04874aca3ba71bde40a860d314409f63608f29c11a0e736c887b8
4
+ data.tar.gz: ccc7d160e6fb70fa22730f13c1fc6f8aa433953dcb4715499387039bf16a7fe0
5
5
  SHA512:
6
- metadata.gz: 9af0cf3e406cbea6807cdfedc32110984ac63225dbe9ffe671cfcd8b842dd639db8e80ead393b1bbb7a14d6977f4edbe02aec913f4a5b2e41093a8cf12577c23
7
- data.tar.gz: b54740669825c315a164fb42a4a58e54fd1ac1cc6e2802d3c40f4c8e5123bbfb9742e93f53f42667c392982e68784ff3721cb6123e7ea77b41cd4ca3ae762dca
6
+ metadata.gz: 147935f35c5b61def21a0a25f7e3ba4e5bc7b4f76e5d9034da1b44506ab4b064444405fd28086061ea71d9e2100b107c178d0fa101731cf9d65a9736906f7994
7
+ data.tar.gz: d11013511ed396e74388644d4f9cdbc80d35780bf305861920524f96a0ec682329fe58f033f96e98c0d16bc64fb9a111bd4c6fde2d060c3f307ec3331a1d3a41
data/.travis.yml CHANGED
@@ -1,13 +1,21 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - 2.4.2
5
- deploy:
6
- skip_cleanup: true
7
- provider: rubygems
8
- api_key:
9
- secure: JOZUQM6qAYJN/N0k4UmV3jBSKMaW3oMYiZ4boPrGm7MsUY8ETHXSYR+dDhrWOvDR4Wdr6DVbTmMtXkWJHaQaM/LkrjN2FM6mTuDfmhkcvi9VBOMstptwXeHMS9fWeogtQwHyd+pKac9vUlaMhRYyoeYyJ1i6/LHua4NftZZ8oPu/9kMuKDD3Rj8zPJNui+fGcZxq90XdxjafCfRJu7EnzSZXzVi5msaEBKcUVxsHxprBjyjmp/yh/Wn0GLyBkYYXkKEUNx9DhxVtWaNG2FfOzznj3HLXQgBdz1or1tDxG8iIegC5jEj4G+gLtldLhMYWfHDaEr4iqKLF1LkjO9TK0RjcVzGNHNxCTQaDDcjgimcVNozWg707IzT5Ap6jG4Y9JWk5KY4ysOaVFqhemafoDwQcXkPb+39N/tpeBPCByCVNrZ+5lgWaHvs+iDv0X9PyAFs5nTz6/u6bz7GDZ91oJOGs9OW6szzHwkQn5TN4omdGiOFca7lviz6OyjXeSn+a6whU2DsRXxp+omrPT/gELLGl21Wd3GGTpiGAdnu/vwvoKOZqGfkr8HS/Bozc0S/vafFJ5KoPAkNQ9iwxPgL1xcEbSF5uK67T2EYVKg7z4psXDeWGBWzk8657SBCB1VCgujkmhg+lWt0TGgrckupKSyUXL4/nnAECgizSLJSbgRE=
10
- gem: idempotent-request
11
- on:
12
- tags: true
13
- repo: qonto/idempotent-request
4
+ - 2.4.10
5
+ - 2.5.8
6
+ - 2.6.6
7
+ - 2.7.1
8
+
9
+ jobs:
10
+ include:
11
+ - stage: gem release
12
+ rvm: 2.7.1
13
+ deploy:
14
+ skip_cleanup: true
15
+ provider: rubygems
16
+ api_key:
17
+ secure: JOZUQM6qAYJN/N0k4UmV3jBSKMaW3oMYiZ4boPrGm7MsUY8ETHXSYR+dDhrWOvDR4Wdr6DVbTmMtXkWJHaQaM/LkrjN2FM6mTuDfmhkcvi9VBOMstptwXeHMS9fWeogtQwHyd+pKac9vUlaMhRYyoeYyJ1i6/LHua4NftZZ8oPu/9kMuKDD3Rj8zPJNui+fGcZxq90XdxjafCfRJu7EnzSZXzVi5msaEBKcUVxsHxprBjyjmp/yh/Wn0GLyBkYYXkKEUNx9DhxVtWaNG2FfOzznj3HLXQgBdz1or1tDxG8iIegC5jEj4G+gLtldLhMYWfHDaEr4iqKLF1LkjO9TK0RjcVzGNHNxCTQaDDcjgimcVNozWg707IzT5Ap6jG4Y9JWk5KY4ysOaVFqhemafoDwQcXkPb+39N/tpeBPCByCVNrZ+5lgWaHvs+iDv0X9PyAFs5nTz6/u6bz7GDZ91oJOGs9OW6szzHwkQn5TN4omdGiOFca7lviz6OyjXeSn+a6whU2DsRXxp+omrPT/gELLGl21Wd3GGTpiGAdnu/vwvoKOZqGfkr8HS/Bozc0S/vafFJ5KoPAkNQ9iwxPgL1xcEbSF5uK67T2EYVKg7z4psXDeWGBWzk8657SBCB1VCgujkmhg+lWt0TGgrckupKSyUXL4/nnAECgizSLJSbgRE=
18
+ gem: idempotent-request
19
+ on:
20
+ tags: true
21
+ repo: qonto/idempotent-request
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Idempotent Request Changelog #
2
2
 
3
+ ## v0.1.5 ##
4
+
5
+ * use ActiveSupport::Notifications to instrument events
6
+ * fix an issue when getting an exception inside application would not delete lock, so client could receive 429 after 500
7
+
8
+ ## v0.1.4 ##
9
+
10
+ * Fix an issue, when http response from backend != 200..226 caused lock to be taken
11
+
12
+ ## v0.1.3 ##
13
+
14
+ * Fix an issue, when concurrent requests sent to an endpoint won't be protected by idempotency until the 1st request is finished
15
+
3
16
  ## v0.1.2 ##
4
17
 
5
18
  * Fix rack response
data/README.md CHANGED
@@ -84,6 +84,23 @@ module IdempotentRequest
84
84
  end
85
85
  ```
86
86
 
87
+
88
+ ### Use ActiveSupport::Notifications to read events
89
+
90
+ ```ruby
91
+ # config/initializers/idempotent_request.rb
92
+ ActiveSupport::Notifications.subscribe('idempotent.request') do |name, start, finish, request_id, payload|
93
+ notification = payload[:request].env['idempotent.request']
94
+ if notification['read']
95
+ Rails.logger.info "IdempotentRequest: Hit cached response from key #{notification['key']}, response: #{notification['read']}"
96
+ elsif notification['write']
97
+ Rails.logger.info "IdempotentRequest: Write: key #{notification['key']}, status: #{notification['write'][0]}, headers: #{notification['write'][1]}, unlocked? #{notification['unlocked']}"
98
+ elsif notification['concurrent_request_response']
99
+ Rails.logger.warn "IdempotentRequest: Concurrent request detected with key #{notification['key']}"
100
+ end
101
+ end
102
+ ```
103
+
87
104
  ## Custom options
88
105
 
89
106
  ```ruby
@@ -92,7 +109,8 @@ config.middleware.use IdempotentRequest::Middleware,
92
109
  header_key: 'X-Qonto-Idempotency-Key', # by default Idempotency-key
93
110
  policy: IdempotentRequest::Policy,
94
111
  callback: IdempotentRequest::RailsCallback,
95
- storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day, namespace: 'idempotency_keys')
112
+ storage: IdempotentRequest::RedisStorage.new(::Redis.current, expire_time: 1.day, namespace: 'idempotency_keys'),
113
+ conflict_response_status: 409
96
114
  ```
97
115
 
98
116
  ### Policy
@@ -117,7 +135,7 @@ end
117
135
 
118
136
  ### Callback
119
137
 
120
- Get notified when the client sends a request with the same idempotency key:
138
+ Get notified when a client sends a request with the same idempotency key:
121
139
 
122
140
  ```ruby
123
141
  class RailsCallback
@@ -133,6 +151,10 @@ class RailsCallback
133
151
  end
134
152
  ```
135
153
 
154
+ ### Conflict response status
155
+
156
+ Define http status code that should be returned when a client sends concurrent requests with the same idempotency key.
157
+
136
158
  ## Contributing
137
159
 
138
160
  Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/idempotent-request. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
@@ -23,9 +23,9 @@ Gem::Specification.new do |spec|
23
23
  spec.add_dependency 'rack', '~> 2.0'
24
24
  spec.add_dependency 'oj', '~> 3.0'
25
25
 
26
- spec.add_development_dependency 'bundler', '~> 1.15'
27
- spec.add_development_dependency 'rake', '~> 10.0'
26
+ spec.add_development_dependency 'bundler'
27
+ spec.add_development_dependency 'rake', '>= 12.3.3'
28
28
  spec.add_development_dependency 'rspec', '~> 3.0'
29
29
  spec.add_development_dependency 'fakeredis', '~> 0.6'
30
- spec.add_development_dependency 'pry', '~> 0.11'
30
+ spec.add_development_dependency 'byebug', '~> 10.0'
31
31
  end
@@ -4,6 +4,8 @@ module IdempotentRequest
4
4
  @app = app
5
5
  @config = config
6
6
  @policy = config.fetch(:policy)
7
+ @notifier = ActiveSupport::Notifications if defined?(ActiveSupport::Notifications)
8
+ @conflict_response_status = config.fetch(:conflict_response_status, 429)
7
9
  end
8
10
 
9
11
  def call(env)
@@ -13,14 +15,45 @@ module IdempotentRequest
13
15
 
14
16
  def process(env)
15
17
  set_request(env)
18
+ request.env['idempotent.request'] = {}
16
19
  return app.call(request.env) unless process?
17
- storage = RequestManager.new(request, config)
18
- storage.read || storage.write(*app.call(request.env))
20
+ request.env['idempotent.request']['key'] = request.key
21
+ response = read_idempotent_request || write_idempotent_request || concurrent_request_response
22
+ instrument(request)
23
+ response
19
24
  end
20
25
 
21
26
  private
22
27
 
23
- attr_reader :app, :env, :config, :request, :policy
28
+ def storage
29
+ @storage ||= RequestManager.new(request, config)
30
+ end
31
+
32
+ def read_idempotent_request
33
+ request.env['idempotent.request']['read'] = storage.read
34
+ end
35
+
36
+ def write_idempotent_request
37
+ return unless storage.lock
38
+ begin
39
+ result = app.call(request.env)
40
+ request.env['idempotent.request']['write'] = result
41
+ storage.write(*result)
42
+ ensure
43
+ request.env['idempotent.request']['unlocked'] = storage.unlock
44
+ result
45
+ end
46
+ end
47
+
48
+ def concurrent_request_response
49
+ status = @conflict_response_status
50
+ headers = { 'Content-Type' => 'application/json' }
51
+ body = [ Oj.dump('error' => 'Concurrent requests detected') ]
52
+ request.env['idempotent.request']['concurrent_request_response'] = true
53
+ Rack::Response.new(body, status, headers).finish
54
+ end
55
+
56
+ attr_reader :app, :env, :config, :request, :policy, :notifier
24
57
 
25
58
  def process?
26
59
  !request.key.to_s.empty? && should_be_idempotent?
@@ -31,6 +64,10 @@ module IdempotentRequest
31
64
  policy.new(request).should?
32
65
  end
33
66
 
67
+ def instrument(request)
68
+ notifier.instrument('idempotent.request', request: request) if notifier
69
+ end
70
+
34
71
  def set_request(env)
35
72
  @env = env
36
73
  @request ||= Request.new(env, config)
@@ -8,25 +8,37 @@ module IdempotentRequest
8
8
  @expire_time = config[:expire_time]
9
9
  end
10
10
 
11
+ def lock(key)
12
+ setnx_with_expiration(lock_key(key), Time.now.to_f)
13
+ end
14
+
15
+ def unlock(key)
16
+ redis.del(lock_key(key))
17
+ end
18
+
11
19
  def read(key)
12
20
  redis.get(namespaced_key(key))
13
21
  end
14
22
 
15
23
  def write(key, payload)
16
- redis.set(
17
- namespaced_key(key),
18
- payload,
19
- {}.tap do |options|
20
- options[:nx] = true
21
- options[:ex] = expire_time.to_i if expire_time.to_i > 0
22
- end
23
- )
24
+ setnx_with_expiration(namespaced_key(key), payload)
24
25
  end
25
26
 
26
27
  private
27
28
 
28
- def namespaced_key(idempotency_key)
29
- [namespace, idempotency_key.strip]
29
+ def setnx_with_expiration(key, data)
30
+ options = {nx: true}
31
+ options[:ex] = expire_time.to_i if expire_time.to_i > 0
32
+
33
+ redis.set(key, data, **options)
34
+ end
35
+
36
+ def lock_key(key)
37
+ namespaced_key("lock:#{key}")
38
+ end
39
+
40
+ def namespaced_key(key)
41
+ [namespace, key.strip]
30
42
  .compact
31
43
  .join(':')
32
44
  .downcase
@@ -22,10 +22,9 @@ module IdempotentRequest
22
22
  private
23
23
 
24
24
  def header_name
25
- key = @header_name
26
- .to_s
27
- .upcase
28
- .gsub('-', '_')
25
+ key = @header_name.to_s
26
+ .upcase
27
+ .tr('-', '_')
29
28
 
30
29
  key.start_with?('HTTP_') ? key : "HTTP_#{key}"
31
30
  end
@@ -8,6 +8,14 @@ module IdempotentRequest
8
8
  @callback = config[:callback]
9
9
  end
10
10
 
11
+ def lock
12
+ storage.lock(key)
13
+ end
14
+
15
+ def unlock
16
+ storage.unlock(key)
17
+ end
18
+
11
19
  def read
12
20
  status, headers, response = parse_data(storage.read(key)).values
13
21
 
@@ -19,8 +27,11 @@ module IdempotentRequest
19
27
  def write(*data)
20
28
  status, headers, response = data
21
29
  response = response.body if response.respond_to?(:body)
22
- return data unless (200..226).include?(status)
23
- storage.write(key, payload(status, headers, response))
30
+
31
+ if (200..226).cover?(status)
32
+ storage.write(key, payload(status, headers, response))
33
+ end
34
+
24
35
  data
25
36
  end
26
37
 
@@ -33,17 +44,15 @@ module IdempotentRequest
33
44
  end
34
45
 
35
46
  def payload(status, headers, response)
36
- Oj.dump({
37
- status: status,
38
- headers: headers.to_h,
39
- response: Array(response)
40
- })
47
+ Oj.dump(status: status,
48
+ headers: headers.to_h,
49
+ response: Array(response))
41
50
  end
42
51
 
43
- def run_callback(action, args)
52
+ def run_callback(action, **args)
44
53
  return unless @callback
45
54
 
46
- @callback.new(request).send(action, args)
55
+ @callback.new(request).send(action, **args)
47
56
  end
48
57
 
49
58
  def key
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module IdempotentRequest
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.7"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idempotent-request
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmytro Zakharov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-04-24 00:00:00.000000000 Z
11
+ date: 2021-05-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -42,30 +42,30 @@ dependencies:
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '1.15'
47
+ version: '0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '1.15'
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: rake
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - "~>"
59
+ - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: '10.0'
61
+ version: 12.3.3
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
66
+ - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: '10.0'
68
+ version: 12.3.3
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rspec
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -95,19 +95,19 @@ dependencies:
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0.6'
97
97
  - !ruby/object:Gem::Dependency
98
- name: pry
98
+ name: byebug
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
101
  - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '0.11'
103
+ version: '10.0'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '0.11'
110
+ version: '10.0'
111
111
  description:
112
112
  email:
113
113
  - dmytro@qonto.eu
@@ -152,8 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
152
152
  - !ruby/object:Gem::Version
153
153
  version: '0'
154
154
  requirements: []
155
- rubyforge_project:
156
- rubygems_version: 2.7.6
155
+ rubygems_version: 3.0.8
157
156
  signing_key:
158
157
  specification_version: 4
159
158
  summary: Rack middleware ensuring at most once requests for mutating endpoints.