idempo 1.1.0 → 1.2.1
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/CHANGELOG.md +9 -0
- data/lib/idempo/active_record_backend.rb +20 -9
- data/lib/idempo/concurrent_request_error_app.rb +0 -2
- data/lib/idempo/malformed_key_error_app.rb +0 -2
- data/lib/idempo/memory_backend.rb +4 -20
- data/lib/idempo/memory_lock.rb +21 -0
- data/lib/idempo/version.rb +1 -1
- data/lib/idempo.rb +12 -8
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 79351972a8246031e7301fcadb2d3721fa99300edffc00a3cb1a2e5fa828c68b
|
4
|
+
data.tar.gz: 27bb14ea7cfe6a10de6367cd725a549a0e26ccec289ca6bdc192752df39c60db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e7cab37f569d2688a83b0d2c25c8f73b720c3fbc294ab6f2cd70686167f8c8dfb1864c738a33ffa28fec152f346e409cc0088be2d9a907974ac8c5370a9469fb
|
7
|
+
data.tar.gz: f2ad3d16e636499ce989c9093ad0c7144fb9d22fe89d63939045fbe9df593595ce349d4e9ae25cb70898020bc1e66ea0307889cd7b1eb3060e758675219e5add
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,12 @@
|
|
1
|
+
## [1.2.1] - 2024-02-22
|
2
|
+
|
3
|
+
- Use autoloading for internal modules. A user using Redis does not have to load the ActiveRecord storage backend, for example
|
4
|
+
- Ensure that the original Rack response body receives a `close` when reading out for caching
|
5
|
+
|
6
|
+
## [1.2.0] - 2024-02-22
|
7
|
+
|
8
|
+
- Use memory locking in addition to DB locking in `ActiveRecordBackend`
|
9
|
+
|
1
10
|
## [1.1.0] - 2024-02-22
|
2
11
|
|
3
12
|
- Use modern ActiveRecord migration options for better Rails 7.x compatibility
|
@@ -60,6 +60,7 @@ class Idempo::ActiveRecordBackend
|
|
60
60
|
|
61
61
|
def initialize
|
62
62
|
require "active_record"
|
63
|
+
@memory_lock = Idempo::MemoryLock.new
|
63
64
|
end
|
64
65
|
|
65
66
|
# Allows the model to be defined lazily without having to require active_record when this module gets loaded
|
@@ -70,15 +71,25 @@ class Idempo::ActiveRecordBackend
|
|
70
71
|
end
|
71
72
|
|
72
73
|
def with_idempotency_key(request_key)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
74
|
+
# We need to use an in-memory lock because database advisory locks are
|
75
|
+
# reentrant. Both Postgres and MySQL allow multiple acquisitions of the
|
76
|
+
# same advisory lock within the same connection - in most Rails/Rack apps
|
77
|
+
# this translates to "within the same thread". This means that if one
|
78
|
+
# elects to use a non-threading webserver (like Falcon), or tests Idempo
|
79
|
+
# within the same thread (like we do), they won't get advisory locking
|
80
|
+
# for concurrent requests. Therefore a staged lock is required. First we apply
|
81
|
+
# the memory lock (for same thread on this process/multiple threads on this
|
82
|
+
# process) and then once we have that - the DB lock.
|
83
|
+
@memory_lock.with(request_key) do
|
84
|
+
db_safe_key = Digest::SHA1.base64digest(request_key)
|
85
|
+
database_lock = lock_implementation_for_connection(model.connection)
|
86
|
+
raise Idempo::ConcurrentRequest unless database_lock.acquire(model.connection, request_key)
|
87
|
+
|
88
|
+
begin
|
89
|
+
yield(Store.new(db_safe_key, model))
|
90
|
+
ensure
|
91
|
+
database_lock.release(model.connection, request_key)
|
92
|
+
end
|
82
93
|
end
|
83
94
|
end
|
84
95
|
|
@@ -1,12 +1,9 @@
|
|
1
1
|
class Idempo::MemoryBackend
|
2
2
|
def initialize
|
3
|
-
require "set"
|
4
3
|
require_relative "response_store"
|
5
|
-
|
6
|
-
@requests_in_flight_mutex = Mutex.new
|
7
|
-
@in_progress = Set.new
|
8
|
-
@store_mutex = Mutex.new
|
4
|
+
@lock = Idempo::MemoryLock.new
|
9
5
|
@response_store = Idempo::ResponseStore.new
|
6
|
+
@store_mutex = Mutex.new
|
10
7
|
end
|
11
8
|
|
12
9
|
class Store < Struct.new(:store_mutex, :response_store, :key, keyword_init: true)
|
@@ -24,22 +21,9 @@ class Idempo::MemoryBackend
|
|
24
21
|
end
|
25
22
|
|
26
23
|
def with_idempotency_key(request_key)
|
27
|
-
|
28
|
-
|
29
|
-
false
|
30
|
-
else
|
31
|
-
@in_progress << request_key
|
32
|
-
true
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
raise Idempo::ConcurrentRequest unless did_insert
|
37
|
-
|
38
|
-
store = Store.new(store_mutex: @store_mutex, response_store: @response_store, key: request_key)
|
39
|
-
begin
|
24
|
+
@lock.with(request_key) do
|
25
|
+
store = Store.new(store_mutex: @store_mutex, response_store: @response_store, key: request_key)
|
40
26
|
yield(store)
|
41
|
-
ensure
|
42
|
-
@requests_in_flight_mutex.synchronize { @in_progress.delete(request_key) }
|
43
27
|
end
|
44
28
|
end
|
45
29
|
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# A memory lock prevents multiple requests with the same request
|
2
|
+
# fingerprint from running concurrently
|
3
|
+
class Idempo::MemoryLock
|
4
|
+
def initialize
|
5
|
+
@requests_in_flight_mutex = Mutex.new
|
6
|
+
@in_progress = Set.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def with(request_key)
|
10
|
+
@requests_in_flight_mutex.synchronize do
|
11
|
+
if @in_progress.include?(request_key)
|
12
|
+
raise Idempo::ConcurrentRequest
|
13
|
+
else
|
14
|
+
@in_progress << request_key
|
15
|
+
end
|
16
|
+
end
|
17
|
+
yield
|
18
|
+
ensure
|
19
|
+
@requests_in_flight_mutex.synchronize { @in_progress.delete(request_key) }
|
20
|
+
end
|
21
|
+
end
|
data/lib/idempo/version.rb
CHANGED
data/lib/idempo.rb
CHANGED
@@ -6,16 +6,19 @@ require "json"
|
|
6
6
|
require "measurometer"
|
7
7
|
require "msgpack"
|
8
8
|
require "zlib"
|
9
|
+
require "set"
|
9
10
|
|
10
|
-
|
11
|
-
require_relative "idempo/concurrent_request_error_app"
|
12
|
-
require_relative "idempo/malformed_key_error_app"
|
13
|
-
require_relative "idempo/memory_backend"
|
14
|
-
require_relative "idempo/redis_backend"
|
15
|
-
require_relative "idempo/request_fingerprint"
|
16
|
-
require_relative "idempo/version"
|
11
|
+
require "idempo/version"
|
17
12
|
|
18
13
|
class Idempo
|
14
|
+
autoload :ConcurrentRequestErrorApp, "idempo/concurrent_request_error_app"
|
15
|
+
autoload :MalformedKeyErrorApp, "idempo/malformed_key_error_app"
|
16
|
+
autoload :MemoryBackend, "idempo/memory_backend"
|
17
|
+
autoload :RedisBackend, "idempo/redis_backend"
|
18
|
+
autoload :ActiveRecordBackend, "idempo/active_record_backend"
|
19
|
+
autoload :RequestFingerprint, "idempo/request_fingerprint"
|
20
|
+
autoload :MemoryLock, "idempo/memory_lock"
|
21
|
+
|
19
22
|
DEFAULT_TTL_SECONDS = 30
|
20
23
|
SAVED_RESPONSE_BODY_SIZE_LIMIT = 4 * 1024 * 1024
|
21
24
|
|
@@ -85,7 +88,6 @@ class Idempo
|
|
85
88
|
# Buffer the Rack response body, we can only do that once (it is non-rewindable)
|
86
89
|
body_chunks = []
|
87
90
|
rack_response_body.each { |chunk| body_chunks << chunk.dup }
|
88
|
-
rack_response_body.close if rack_response_body.respond_to?(:close)
|
89
91
|
|
90
92
|
# Only keep headers which are strings
|
91
93
|
stringified_headers = headers.each_with_object({}) do |(header, value), filtered|
|
@@ -101,6 +103,8 @@ class Idempo
|
|
101
103
|
# (when we unserialize our response again) does a realloc, while slicing at the start
|
102
104
|
# does not
|
103
105
|
[deflated_message_packed_str, body_chunks]
|
106
|
+
ensure
|
107
|
+
rack_response_body.close if rack_response_body.respond_to?(:close)
|
104
108
|
end
|
105
109
|
|
106
110
|
def response_may_be_persisted?(status, headers, body)
|
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.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-02-
|
12
|
+
date: 2024-02-27 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rack
|
@@ -192,6 +192,7 @@ files:
|
|
192
192
|
- lib/idempo/concurrent_request_error_app.rb
|
193
193
|
- lib/idempo/malformed_key_error_app.rb
|
194
194
|
- lib/idempo/memory_backend.rb
|
195
|
+
- lib/idempo/memory_lock.rb
|
195
196
|
- lib/idempo/redis_backend.rb
|
196
197
|
- lib/idempo/request_fingerprint.rb
|
197
198
|
- lib/idempo/response_store.rb
|