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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9bfbbd03a37973e1309f1c9d3a0aa1032cbfdfaae73f78f275a7fd9800e5e9ed
4
- data.tar.gz: 54e1bb92e23d7003e7d4205e0964e94ab7288d342f2032fd4f7f2a280a2341f1
3
+ metadata.gz: 79351972a8246031e7301fcadb2d3721fa99300edffc00a3cb1a2e5fa828c68b
4
+ data.tar.gz: 27bb14ea7cfe6a10de6367cd725a549a0e26ccec289ca6bdc192752df39c60db
5
5
  SHA512:
6
- metadata.gz: 9be3229d0844358a342b5fcc65a885e96f0ae7ae73ebcfe91b74046ed7ddc3e90d836d2fbe047f22434f9bd15beda36d5f0d0eb5bf4f8f597996fa7006a00101
7
- data.tar.gz: a9933869e79cd56229907bafe38b8c135357eb1e3b9ff8954e51f74f4a171b33dee8e5322a9178782c95e4686d96104d9ff4fbc4d801b5ff12066591cd68cd7b
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
- db_safe_key = Digest::SHA1.base64digest(request_key)
74
- lock = lock_implementation_for_connection(model.connection)
75
-
76
- raise Idempo::ConcurrentRequest unless lock.acquire(model.connection, request_key)
77
-
78
- begin
79
- yield(Store.new(db_safe_key, model))
80
- ensure
81
- lock.release(model.connection, request_key)
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,5 +1,3 @@
1
- require "json"
2
-
3
1
  class Idempo::ConcurrentRequestErrorApp
4
2
  RETRY_AFTER_SECONDS = 2.to_s
5
3
 
@@ -1,5 +1,3 @@
1
- require "json"
2
-
3
1
  class Idempo::MalformedKeyErrorApp
4
2
  def self.call(env)
5
3
  res = {
@@ -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
- did_insert = @requests_in_flight_mutex.synchronize do
28
- if @in_progress.include?(request_key)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Idempo
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.1"
5
5
  end
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
- require_relative "idempo/active_record_backend"
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.0
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-22 00:00:00.000000000 Z
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