idempo 1.2.1 → 1.3.0
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 +4 -4
- data/.github/workflows/ci.yml +6 -6
- data/Appraisals +9 -0
- data/CHANGELOG.md +14 -6
- data/README.md +6 -3
- data/Rakefile +5 -1
- data/idempo.gemspec +3 -3
- data/lib/idempo/concurrent_request_error_app.rb +1 -1
- data/lib/idempo/malformed_key_error_app.rb +1 -1
- data/lib/idempo/request_fingerprint.rb +16 -3
- data/lib/idempo/version.rb +1 -1
- data/lib/idempo.rb +22 -5
- metadata +28 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3db7fbb3c612bae5675890f118986c3d026e49c73f3c74c24f15fea51e6a67c7
|
4
|
+
data.tar.gz: 65a193261af6e424fe8f7e3437d28e02c20ddb4c9ae199c9556f39676474b8ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd32562481a479070ed2806ef3201d370417fd9acfbbb8499d1aefb2f1000f278ee3dff47ccb0ac640f33001be8c41cccba7c498980d07c893dee519e53f539b
|
7
|
+
data.tar.gz: df3b05f901ac7e49bb82bfde9a522e2a179b96953db10751b7814858597a5a0af0aa82ff0fd6824e3a11cae6bdf4ebc84004a35b09cfae7a39958114a307eef8
|
data/.github/workflows/ci.yml
CHANGED
@@ -15,7 +15,7 @@ jobs:
|
|
15
15
|
strategy:
|
16
16
|
matrix:
|
17
17
|
ruby:
|
18
|
-
-
|
18
|
+
- "2.7"
|
19
19
|
steps:
|
20
20
|
- name: Checkout
|
21
21
|
uses: actions/checkout@v4
|
@@ -34,14 +34,13 @@ jobs:
|
|
34
34
|
- name: Standard (Lint)
|
35
35
|
run: bundle exec rake standard
|
36
36
|
test:
|
37
|
-
name: Specs
|
37
|
+
name: "Specs (Rack 2 and 3)"
|
38
38
|
runs-on: ubuntu-22.04
|
39
39
|
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
|
40
40
|
strategy:
|
41
41
|
matrix:
|
42
42
|
ruby:
|
43
|
-
-
|
44
|
-
- '3.2'
|
43
|
+
- "2.7"
|
45
44
|
services:
|
46
45
|
mysql:
|
47
46
|
image: mysql:5.7
|
@@ -65,13 +64,14 @@ jobs:
|
|
65
64
|
steps:
|
66
65
|
- name: Checkout
|
67
66
|
uses: actions/checkout@v4
|
67
|
+
|
68
68
|
- name: Setup Ruby
|
69
69
|
uses: ruby/setup-ruby@v1
|
70
70
|
with:
|
71
71
|
ruby-version: ${{ matrix.ruby }}
|
72
72
|
bundler-cache: true
|
73
|
-
- name: RSpec
|
74
|
-
run: bundle exec rspec
|
73
|
+
- name: RSpec via Appraisal
|
74
|
+
run: "bundle exec appraisal install && bundle exec appraisal rspec"
|
75
75
|
env:
|
76
76
|
MYSQL_HOST: 127.0.0.1
|
77
77
|
MYSQL_PORT: 3306
|
data/Appraisals
ADDED
data/CHANGELOG.md
CHANGED
@@ -1,13 +1,21 @@
|
|
1
|
-
##
|
1
|
+
## 1.3.0
|
2
|
+
|
3
|
+
- Streamline integration with both Rack 2 and 3, add tests for request fingerprinting.
|
4
|
+
|
5
|
+
## 1.2.2
|
6
|
+
|
7
|
+
- Support `#to_ary` on Rack response bodies on newer Rails/Rack versions
|
8
|
+
|
9
|
+
## 1.2.1
|
2
10
|
|
3
11
|
- Use autoloading for internal modules. A user using Redis does not have to load the ActiveRecord storage backend, for example
|
4
12
|
- Ensure that the original Rack response body receives a `close` when reading out for caching
|
5
13
|
|
6
|
-
##
|
14
|
+
## 1.2.0
|
7
15
|
|
8
16
|
- Use memory locking in addition to DB locking in `ActiveRecordBackend`
|
9
17
|
|
10
|
-
##
|
18
|
+
## 1.1.0
|
11
19
|
|
12
20
|
- Use modern ActiveRecord migration options for better Rails 7.x compatibility
|
13
21
|
- Ensure Github actions CI can run and uses Postgres appropriately
|
@@ -15,16 +23,16 @@
|
|
15
23
|
- Implement `#prune!` on storage backends
|
16
24
|
- Reformat all code using [standard](https://github.com/standardrb/standard) instead of wetransfer_style as it is both more relaxed and more modern
|
17
25
|
|
18
|
-
##
|
26
|
+
## 1.0.0
|
19
27
|
|
20
28
|
- Release 1.0 as the API can be considered stable and the gem has been in production for years
|
21
29
|
|
22
|
-
##
|
30
|
+
## 0.2.0
|
23
31
|
|
24
32
|
- Allow setting the global default TTL for the cached responses
|
25
33
|
- Allow customisation of the request key computation (so that the client can decide whether to include/exclude `Authorization` and the like)
|
26
34
|
- Extract the error response generating apps into separate modules, to make them easier to override
|
27
35
|
|
28
|
-
##
|
36
|
+
## 0.1.0
|
29
37
|
|
30
38
|
- Initial release
|
data/README.md
CHANGED
@@ -10,7 +10,8 @@ instead of calling your application.
|
|
10
10
|
Idempo supports a number of backends, we recommend using Redis if you have multiple application servers / dynos and MemoryBackend if you are only using one single Puma worker. To initialize with Redis as backend pass the `backend:` parameter when adding the middleware:
|
11
11
|
|
12
12
|
```ruby
|
13
|
-
|
13
|
+
be = Idempo::RedisBackend.new(Rails.application.config.redis_connection_pool)
|
14
|
+
use Idempo, backend: be
|
14
15
|
```
|
15
16
|
|
16
17
|
and to initialize with a memory store as backend:
|
@@ -65,7 +66,8 @@ end
|
|
65
66
|
Then configure Idempo to use the backend (in your `application.rb`):
|
66
67
|
|
67
68
|
```ruby
|
68
|
-
|
69
|
+
be = Idempo::ActiveRecordBackend.new
|
70
|
+
config.middleware.insert Idempo, backend: be
|
69
71
|
```
|
70
72
|
|
71
73
|
In your regular tasks (cron or Rake) you will want to add a call to delete old Idempo responses (there is an index on `expire_at`):
|
@@ -87,7 +89,8 @@ use Idempo, backend: Idempo::RedisBackend.new
|
|
87
89
|
If you have a configured Redis connection pool (and you should) - pass it to the initializer:
|
88
90
|
|
89
91
|
```ruby
|
90
|
-
|
92
|
+
be = Idempo::RedisBackend.new(config.redis_connection_pool)
|
93
|
+
config.middleware.insert Idempo, backend: be
|
91
94
|
```
|
92
95
|
|
93
96
|
All data stored in Redis will have TTLs and will expire automatically. Redis scripts ensure that updates to the stored idempotent responses and locking happen atomically.
|
data/Rakefile
CHANGED
data/idempo.gemspec
CHANGED
@@ -29,16 +29,15 @@ Gem::Specification.new do |spec|
|
|
29
29
|
# Specify which files should be added to the gem when it is released.
|
30
30
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
31
31
|
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
32
|
-
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
32
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features|gemfiles)/}) }
|
33
33
|
end
|
34
34
|
spec.bindir = "exe"
|
35
35
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
36
36
|
spec.require_paths = ["lib"]
|
37
37
|
|
38
|
-
# Uncomment to register a new dependency of your gem
|
39
|
-
spec.add_dependency "rack"
|
40
38
|
spec.add_dependency "msgpack"
|
41
39
|
spec.add_dependency "measurometer", "~> 1.3"
|
40
|
+
spec.add_dependency "rack", "< 4"
|
42
41
|
|
43
42
|
spec.add_development_dependency "rake", "~> 13.0"
|
44
43
|
spec.add_development_dependency "rspec", "~> 3.0"
|
@@ -48,6 +47,7 @@ Gem::Specification.new do |spec|
|
|
48
47
|
spec.add_development_dependency "mysql2"
|
49
48
|
spec.add_development_dependency "pg"
|
50
49
|
spec.add_development_dependency "standard"
|
50
|
+
spec.add_development_dependency "appraisal"
|
51
51
|
|
52
52
|
# For more information and examples about making a new gem, checkout our
|
53
53
|
# guide at: https://bundler.io/guides/creating_gem.html
|
@@ -8,6 +8,6 @@ class Idempo::ConcurrentRequestErrorApp
|
|
8
8
|
message: "Another request with this idempotency key is still in progress, please try again later"
|
9
9
|
}
|
10
10
|
}
|
11
|
-
[429, {"
|
11
|
+
[429, {"retry-after" => RETRY_AFTER_SECONDS, "content-type" => "application/json"}, [JSON.pretty_generate(res)]]
|
12
12
|
end
|
13
13
|
end
|
@@ -6,6 +6,6 @@ class Idempo::MalformedKeyErrorApp
|
|
6
6
|
message: "The Idempotency-Key header provided was empty or malformed"
|
7
7
|
}
|
8
8
|
}
|
9
|
-
[400, {"
|
9
|
+
[400, {"content-type" => "application/json"}, [JSON.pretty_generate(res)]]
|
10
10
|
end
|
11
11
|
end
|
@@ -5,11 +5,24 @@ module Idempo::RequestFingerprint
|
|
5
5
|
d << rack_request.url << "\n"
|
6
6
|
d << rack_request.request_method << "\n"
|
7
7
|
d << rack_request.get_header("HTTP_AUTHORIZATION").to_s << "\n"
|
8
|
-
|
9
|
-
|
8
|
+
|
9
|
+
# Under Rack 3.0 the rack.input may or may not be rewindable (this is done to support
|
10
|
+
# streaming HTTP request bodies). If we know a request body is rewindable we can read it
|
11
|
+
# out in full and add it to the request fingerprint. If the request body cannot be
|
12
|
+
# rewound, we can't really rely on it as it can be fairly large (and we want the
|
13
|
+
# downstream app to read the request body, not us).
|
14
|
+
if rack_request.env["rack.input"].respond_to?(:rewind)
|
15
|
+
read_and_rewind(rack_request.env["rack.input"], d)
|
10
16
|
end
|
17
|
+
|
11
18
|
Base64.strict_encode64(d.digest)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.read_and_rewind(source_io, to_destination_io)
|
22
|
+
while (chunk = source_io.read(1024 * 65))
|
23
|
+
to_destination_io << chunk
|
24
|
+
end
|
12
25
|
ensure
|
13
|
-
|
26
|
+
source_io.rewind
|
14
27
|
end
|
15
28
|
end
|
data/lib/idempo/version.rb
CHANGED
data/lib/idempo.rb
CHANGED
@@ -7,6 +7,7 @@ require "measurometer"
|
|
7
7
|
require "msgpack"
|
8
8
|
require "zlib"
|
9
9
|
require "set"
|
10
|
+
require "rack"
|
10
11
|
|
11
12
|
require "idempo/version"
|
12
13
|
|
@@ -54,9 +55,17 @@ class Idempo
|
|
54
55
|
return from_persisted_response(stored_response)
|
55
56
|
end
|
56
57
|
|
57
|
-
status,
|
58
|
+
status, raw_headers, body = @app.call(env)
|
59
|
+
headers = downcase_keys(raw_headers)
|
60
|
+
|
61
|
+
expires_in_seconds = (headers.delete("x-idempo-persist-for-seconds") || @persist_for_seconds).to_i
|
62
|
+
|
63
|
+
# In some cases `body` could respond to to_ary. In this case, we don't need to
|
64
|
+
# call .close on the body afterwards, as it is supposed to self-close as per Rack 3.0 SPEC
|
65
|
+
#
|
66
|
+
# @see https://github.com/rack/rack/blob/main/SPEC.rdoc#the-body-
|
67
|
+
body = body.to_ary if body.respond_to?(:to_ary)
|
58
68
|
|
59
|
-
expires_in_seconds = (headers.delete("X-Idempo-Persist-For-Seconds") || @persist_for_seconds).to_i
|
60
69
|
if response_may_be_persisted?(status, headers, body)
|
61
70
|
# Body is replaced with a cached version since a Rack response body is not rewindable
|
62
71
|
marshaled_response, body = serialize_response(status, headers, body)
|
@@ -76,6 +85,12 @@ class Idempo
|
|
76
85
|
|
77
86
|
private
|
78
87
|
|
88
|
+
def downcase_keys(raw_headers)
|
89
|
+
raw_headers.each_with_object({}) do |(name, value), hh|
|
90
|
+
hh[name.to_s.downcase] = value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
79
94
|
def from_persisted_response(marshaled_response)
|
80
95
|
if marshaled_response[-2..] != ":1"
|
81
96
|
raise Error, "Unknown serialization of the marshaled response"
|
@@ -104,21 +119,23 @@ class Idempo
|
|
104
119
|
# does not
|
105
120
|
[deflated_message_packed_str, body_chunks]
|
106
121
|
ensure
|
122
|
+
# This will not be applied to response bodies of Array type.
|
107
123
|
rack_response_body.close if rack_response_body.respond_to?(:close)
|
108
124
|
end
|
109
125
|
|
110
126
|
def response_may_be_persisted?(status, headers, body)
|
111
|
-
return false if headers.delete("
|
127
|
+
return false if headers.delete("x-idempo-policy") == "no-store"
|
112
128
|
return false unless status_may_be_persisted?(status)
|
113
129
|
return false unless body_size_within_limit?(headers, body)
|
114
130
|
true
|
115
131
|
end
|
116
132
|
|
117
133
|
def body_size_within_limit?(response_headers, body)
|
118
|
-
|
134
|
+
if response_headers["content-length"]
|
135
|
+
return response_headers["content-length"].to_i <= SAVED_RESPONSE_BODY_SIZE_LIMIT
|
136
|
+
end
|
119
137
|
|
120
138
|
return false unless body.is_a?(Array) # Arbitrary iterable of unknown size
|
121
|
-
|
122
139
|
sum_of_string_bytesizes(body) <= SAVED_RESPONSE_BODY_SIZE_LIMIT
|
123
140
|
end
|
124
141
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: idempo
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -9,10 +9,10 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-
|
12
|
+
date: 2024-11-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
15
|
+
name: msgpack
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
18
|
- - ">="
|
@@ -26,33 +26,33 @@ dependencies:
|
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: '0'
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
|
-
name:
|
29
|
+
name: measurometer
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
31
31
|
requirements:
|
32
|
-
- - "
|
32
|
+
- - "~>"
|
33
33
|
- !ruby/object:Gem::Version
|
34
|
-
version: '
|
34
|
+
version: '1.3'
|
35
35
|
type: :runtime
|
36
36
|
prerelease: false
|
37
37
|
version_requirements: !ruby/object:Gem::Requirement
|
38
38
|
requirements:
|
39
|
-
- - "
|
39
|
+
- - "~>"
|
40
40
|
- !ruby/object:Gem::Version
|
41
|
-
version: '
|
41
|
+
version: '1.3'
|
42
42
|
- !ruby/object:Gem::Dependency
|
43
|
-
name:
|
43
|
+
name: rack
|
44
44
|
requirement: !ruby/object:Gem::Requirement
|
45
45
|
requirements:
|
46
|
-
- - "
|
46
|
+
- - "<"
|
47
47
|
- !ruby/object:Gem::Version
|
48
|
-
version: '
|
48
|
+
version: '4'
|
49
49
|
type: :runtime
|
50
50
|
prerelease: false
|
51
51
|
version_requirements: !ruby/object:Gem::Requirement
|
52
52
|
requirements:
|
53
|
-
- - "
|
53
|
+
- - "<"
|
54
54
|
- !ruby/object:Gem::Version
|
55
|
-
version: '
|
55
|
+
version: '4'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
57
|
name: rake
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
@@ -165,6 +165,20 @@ dependencies:
|
|
165
165
|
- - ">="
|
166
166
|
- !ruby/object:Gem::Version
|
167
167
|
version: '0'
|
168
|
+
- !ruby/object:Gem::Dependency
|
169
|
+
name: appraisal
|
170
|
+
requirement: !ruby/object:Gem::Requirement
|
171
|
+
requirements:
|
172
|
+
- - ">="
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
type: :development
|
176
|
+
prerelease: false
|
177
|
+
version_requirements: !ruby/object:Gem::Requirement
|
178
|
+
requirements:
|
179
|
+
- - ">="
|
180
|
+
- !ruby/object:Gem::Version
|
181
|
+
version: '0'
|
168
182
|
description: Provides idempotency keys for Rack applications.
|
169
183
|
email:
|
170
184
|
- me@julik.nl
|
@@ -177,6 +191,7 @@ files:
|
|
177
191
|
- ".gitignore"
|
178
192
|
- ".rubocop.yml"
|
179
193
|
- ".standard.yml"
|
194
|
+
- Appraisals
|
180
195
|
- CHANGELOG.md
|
181
196
|
- Gemfile
|
182
197
|
- LICENSE.txt
|