idempotency 0.1.3 → 0.1.4
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 +16 -0
- data/Gemfile +1 -0
- data/Gemfile.lock +10 -1
- data/README.md +30 -1
- data/idempotency.gemspec +1 -0
- data/lib/idempotency/constants.rb +8 -0
- data/lib/idempotency/hanami.rb +2 -2
- data/lib/idempotency/instrumentation/statsd_listener.rb +43 -0
- data/lib/idempotency/rails.rb +2 -2
- data/lib/idempotency/version.rb +1 -1
- data/lib/idempotency.rb +47 -3
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '00195248b99a69659167bae866e907aec9f44421d918bef44722ee79619692e7'
|
4
|
+
data.tar.gz: 17855572f61d3c2edca50a49b316dcd6a6dc5deaeb9067edeaf3005cb150b8bd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 96fd4a1711a56f0a1dd2e3397e35eadc3b205bec1fa6e1dc9bf07a51258c5e511d262832fa13f364759394e16f9eb40cf4a63f73ecc8b30f247cf4e30ff6b2e6
|
7
|
+
data.tar.gz: 4eb3cba2a688b36aae230a8a024d12626556829ad3959264fd19abd29b10cf7a181bdbcc3906b67e30938a470c633ab00ebb1d31b3eb114076f8771a43985fef
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
## [Change Log]
|
2
2
|
|
3
|
+
## [0.1.4] - 2025-01-14
|
4
|
+
|
5
|
+
- Support metrics logging via StatsdListener
|
6
|
+
|
7
|
+
## [0.1.3] - 2025-01-14
|
8
|
+
|
9
|
+
- Support testing by mocking MockRedis#evalsha
|
10
|
+
|
11
|
+
## [0.1.2] - 2025-01-07
|
12
|
+
|
13
|
+
- Support auto-releasing gem
|
14
|
+
|
15
|
+
## [0.1.1] - 2025-01-07
|
16
|
+
|
17
|
+
- Allow configuring idempotent methods instead of hardcoding POST
|
18
|
+
|
3
19
|
## [0.1.0] - 2024-11-13
|
4
20
|
|
5
21
|
- Initial release
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
idempotency (0.1.
|
4
|
+
idempotency (0.1.4)
|
5
5
|
base64
|
6
6
|
dry-configurable
|
7
|
+
dry-monitor
|
7
8
|
msgpack
|
8
9
|
redis
|
9
10
|
|
@@ -24,6 +25,13 @@ GEM
|
|
24
25
|
concurrent-ruby (~> 1.0)
|
25
26
|
logger
|
26
27
|
zeitwerk (~> 2.6)
|
28
|
+
dry-events (1.0.1)
|
29
|
+
concurrent-ruby (~> 1.0)
|
30
|
+
dry-core (~> 1.0, < 2)
|
31
|
+
dry-monitor (1.0.1)
|
32
|
+
dry-configurable (~> 1.0, < 2)
|
33
|
+
dry-core (~> 1.0, < 2)
|
34
|
+
dry-events (~> 1.0, < 2)
|
27
35
|
hanami-controller (1.3.3)
|
28
36
|
hanami-utils (~> 1.3)
|
29
37
|
rack (~> 2.0)
|
@@ -90,6 +98,7 @@ PLATFORMS
|
|
90
98
|
|
91
99
|
DEPENDENCIES
|
92
100
|
connection_pool
|
101
|
+
dry-monitor
|
93
102
|
hanami-controller (~> 1.3)
|
94
103
|
idempotency!
|
95
104
|
mock_redis
|
data/README.md
CHANGED
@@ -28,7 +28,14 @@ Idempotency.configure do |config|
|
|
28
28
|
}
|
29
29
|
|
30
30
|
config.idempotent_methods = %w[POST PUT PATCH]
|
31
|
-
config.idempotent_statuses = (200..299).to_a
|
31
|
+
config.idempotent_statuses = (200..299).to_a + (400..499).to_a
|
32
|
+
|
33
|
+
# Metrics configuration
|
34
|
+
config.metrics.statsd_client = statsd_client # Your StatsD client instance
|
35
|
+
config.metrics.namespace = 'my_service_name' # Optional namespace for metrics
|
36
|
+
|
37
|
+
# Custom instrumentation listeners (optional)
|
38
|
+
config.instrumentation_listeners = [my_custom_listener] # Array of custom listeners
|
32
39
|
end
|
33
40
|
```
|
34
41
|
|
@@ -100,3 +107,25 @@ RSpec.configure do |config|
|
|
100
107
|
end
|
101
108
|
```
|
102
109
|
|
110
|
+
### Instrumentation
|
111
|
+
|
112
|
+
The gem supports instrumentation through StatsD out of the box. When you configure a StatsD client in the configuration, the StatsdListener will be automatically set up. It tracks the following metrics:
|
113
|
+
|
114
|
+
- `idempotency_cache_hit_count` - Incremented when a cached response is found
|
115
|
+
- `idempotency_cache_miss_count` - Incremented when no cached response exists
|
116
|
+
- `idempotency_lock_conflict_count` - Incremented when concurrent requests conflict
|
117
|
+
- `idempotency_cache_duration_seconds` - Histogram of operation duration
|
118
|
+
|
119
|
+
Each metric includes tags:
|
120
|
+
- `action` - Either the specified action name or `"{HTTP_METHOD}:{PATH}"`
|
121
|
+
- `namespace` - Your configured namespace (if provided)
|
122
|
+
- `metric` - The metric name (for duration histogram only)
|
123
|
+
|
124
|
+
To enable StatsD instrumentation, simply configure the metrics settings:
|
125
|
+
|
126
|
+
```ruby
|
127
|
+
Idempotency.configure do |config|
|
128
|
+
config.metrics.statsd_client = Datadog::Statsd.new
|
129
|
+
config.metrics.namespace = 'my_service_name'
|
130
|
+
end
|
131
|
+
```
|
data/idempotency.gemspec
CHANGED
data/lib/idempotency/hanami.rb
CHANGED
@@ -4,9 +4,9 @@ require_relative '../idempotency'
|
|
4
4
|
|
5
5
|
class Idempotency
|
6
6
|
module Hanami
|
7
|
-
def use_cache(request_identifiers = [], lock_duration: nil)
|
7
|
+
def use_cache(request_identifiers = [], lock_duration: nil, action: self.class.name)
|
8
8
|
response_status, response_headers, response_body = Idempotency.use_cache(
|
9
|
-
request, request_identifiers, lock_duration:
|
9
|
+
request, request_identifiers, lock_duration:, action:
|
10
10
|
) do
|
11
11
|
yield
|
12
12
|
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../idempotency'
|
4
|
+
|
5
|
+
class Idempotency
|
6
|
+
module Instrumentation
|
7
|
+
class StatsdListener
|
8
|
+
EVENT_NAME_TO_METRIC_MAPPINGS = {
|
9
|
+
Events::CACHE_HIT => 'idempotency_cache_hit_count',
|
10
|
+
Events::CACHE_MISS => 'idempotency_cache_miss_count',
|
11
|
+
Events::LOCK_CONFLICT => 'idempotency_lock_conflict_count'
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def initialize(statsd_client, namespace = nil)
|
15
|
+
@statsd_client = statsd_client
|
16
|
+
@namespace = namespace
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_subscriptions
|
20
|
+
EVENT_NAME_TO_METRIC_MAPPINGS.each do |event_name, metric|
|
21
|
+
Idempotency.notifier.subscribe(event_name) do |event|
|
22
|
+
send_metric(metric, event.payload)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :namespace, :statsd_client
|
30
|
+
|
31
|
+
def send_metric(metric_name, event_data)
|
32
|
+
action = event_data[:action] || "#{event_data[:request].request_method}:#{event_data[:request].path}"
|
33
|
+
tags = ["action:#{action}"]
|
34
|
+
tags << "namespace:#{@namespace}" if @namespace
|
35
|
+
|
36
|
+
@statsd_client.increment(metric_name, tags:)
|
37
|
+
@statsd_client.histogram(
|
38
|
+
'idempotency_cache_duration_seconds', event_data[:duration], tags: tags + ["metric:#{metric_name}"]
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/idempotency/rails.rb
CHANGED
@@ -4,9 +4,9 @@ require_relative '../idempotency'
|
|
4
4
|
|
5
5
|
class Idempotency
|
6
6
|
module Rails
|
7
|
-
def use_cache(request_identifiers = [], lock_duration: nil)
|
7
|
+
def use_cache(request_identifiers = [], lock_duration: nil, action: "#{controller_name}##{action_name}")
|
8
8
|
response_status, response_headers, response_body = Idempotency.use_cache(
|
9
|
-
request, request_identifiers, lock_duration:
|
9
|
+
request, request_identifiers, lock_duration:, action:
|
10
10
|
) do
|
11
11
|
yield
|
12
12
|
|
data/lib/idempotency/version.rb
CHANGED
data/lib/idempotency.rb
CHANGED
@@ -5,12 +5,29 @@ require 'json'
|
|
5
5
|
require 'base64'
|
6
6
|
require_relative 'idempotency/cache'
|
7
7
|
require_relative 'idempotency/constants'
|
8
|
+
require_relative 'idempotency/instrumentation/statsd_listener'
|
9
|
+
require 'dry-monitor'
|
8
10
|
|
9
11
|
class Idempotency
|
10
12
|
extend Dry::Configurable
|
13
|
+
@monitor = Monitor.new
|
14
|
+
|
15
|
+
def self.notifier
|
16
|
+
@monitor.synchronize do
|
17
|
+
@notifier ||= Dry::Monitor::Notifications.new(:idempotency_gem).tap do |n|
|
18
|
+
Events::ALL_EVENTS.each { |event| n.register_event(event) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
11
22
|
|
12
23
|
setting :redis_pool
|
13
24
|
setting :logger
|
25
|
+
setting :instrumentation_listeners, default: []
|
26
|
+
setting :metrics do
|
27
|
+
setting :namespace
|
28
|
+
setting :statsd_client
|
29
|
+
end
|
30
|
+
|
14
31
|
setting :default_lock_expiry, default: 300 # 5 minutes
|
15
32
|
setting :idempotent_methods, default: %w[POST PUT PATCH DELETE]
|
16
33
|
setting :idempotent_statuses, default: (200..299).to_a + (400..499).to_a
|
@@ -21,16 +38,31 @@ class Idempotency
|
|
21
38
|
}.to_json
|
22
39
|
end
|
23
40
|
|
41
|
+
def self.configure
|
42
|
+
super
|
43
|
+
|
44
|
+
if config.metrics.statsd_client
|
45
|
+
config.instrumentation_listeners << Idempotency::Instrumentation::StatsdListener.new(
|
46
|
+
config.metrics.statsd_client,
|
47
|
+
config.metrics.namespace
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
51
|
+
config.instrumentation_listeners.each(&:setup_subscriptions)
|
52
|
+
end
|
53
|
+
|
24
54
|
def initialize(config: Idempotency.config, cache: Cache.new(config:))
|
25
55
|
@config = config
|
26
56
|
@cache = cache
|
27
57
|
end
|
28
58
|
|
29
|
-
def self.use_cache(request, request_identifiers, lock_duration: nil, &blk)
|
30
|
-
new.use_cache(request, request_identifiers, lock_duration:, &blk)
|
59
|
+
def self.use_cache(request, request_identifiers, lock_duration: nil, action: nil, &blk)
|
60
|
+
new.use_cache(request, request_identifiers, lock_duration:, action:, &blk)
|
31
61
|
end
|
32
62
|
|
33
|
-
def use_cache(request, request_identifiers, lock_duration:) # rubocop:disable Metrics/AbcSize
|
63
|
+
def use_cache(request, request_identifiers, lock_duration: nil, action: nil) # rubocop:disable Metrics/AbcSize
|
64
|
+
duration_start = Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
65
|
+
|
34
66
|
return yield unless cache_request?(request)
|
35
67
|
|
36
68
|
request_headers = request.env
|
@@ -42,6 +74,8 @@ class Idempotency
|
|
42
74
|
|
43
75
|
if (cached_status, cached_headers, cached_body = cached_response)
|
44
76
|
cached_headers.merge!(Constants::HEADER_KEY => idempotency_key)
|
77
|
+
instrument(Events::CACHE_HIT, request:, action:, duration: calculate_duration(duration_start))
|
78
|
+
|
45
79
|
return [cached_status, cached_headers, cached_body]
|
46
80
|
end
|
47
81
|
|
@@ -55,8 +89,10 @@ class Idempotency
|
|
55
89
|
response_headers.merge!({ Constants::HEADER_KEY => idempotency_key })
|
56
90
|
end
|
57
91
|
|
92
|
+
instrument(Events::CACHE_MISS, request:, action:, duration: calculate_duration(duration_start))
|
58
93
|
[response_status, response_headers, response_body]
|
59
94
|
rescue Idempotency::Cache::LockConflict
|
95
|
+
instrument(Events::LOCK_CONFLICT, request:, action:, duration: calculate_duration(duration_start))
|
60
96
|
[409, {}, config.response_body.concurrent_error]
|
61
97
|
end
|
62
98
|
|
@@ -64,6 +100,14 @@ class Idempotency
|
|
64
100
|
|
65
101
|
attr_reader :config, :cache
|
66
102
|
|
103
|
+
def instrument(event_name, **metadata)
|
104
|
+
Idempotency.notifier.instrument(event_name, **metadata)
|
105
|
+
end
|
106
|
+
|
107
|
+
def calculate_duration(start_time)
|
108
|
+
Process.clock_gettime(::Process::CLOCK_MONOTONIC) - start_time
|
109
|
+
end
|
110
|
+
|
67
111
|
def calculate_fingerprint(request, idempotency_key, request_identifiers)
|
68
112
|
d = Digest::SHA256.new
|
69
113
|
d << idempotency_key
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: idempotency
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vu Hoang
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-01-
|
11
|
+
date: 2025-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: base64
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - ">="
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: dry-monitor
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: msgpack
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -90,6 +104,7 @@ files:
|
|
90
104
|
- lib/idempotency/cache.rb
|
91
105
|
- lib/idempotency/constants.rb
|
92
106
|
- lib/idempotency/hanami.rb
|
107
|
+
- lib/idempotency/instrumentation/statsd_listener.rb
|
93
108
|
- lib/idempotency/rails.rb
|
94
109
|
- lib/idempotency/testing/helpers.rb
|
95
110
|
- lib/idempotency/version.rb
|