idempotency 0.1.3 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|