idempo 1.1.0 → 1.2.1

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: 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