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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f09805d7d2ec4b6d8e56f0d6ef0cc298dbe1a7df27e8cc542a45e401792b653
4
- data.tar.gz: 7117efd37aed82f6e4edea29a2c7b45357262e3e31b2e1a7e348a37521c11af8
3
+ metadata.gz: '00195248b99a69659167bae866e907aec9f44421d918bef44722ee79619692e7'
4
+ data.tar.gz: 17855572f61d3c2edca50a49b316dcd6a6dc5deaeb9067edeaf3005cb150b8bd
5
5
  SHA512:
6
- metadata.gz: 400b2ed412cad240f18e305c1ce57cc80a96e90f5103db11f1bcb8053734c819c976512058ee9c72eff74891b817524e86d525815b32ef50642670ddffc16a54
7
- data.tar.gz: e7924c51b71ac88ec1103888f45a43e9aa77da54f84395472b3b188a4c9f443045113d97385b7416c9e1cfb8bd364ed7daf95229c28c9800caf92df83e17cace
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
@@ -9,6 +9,7 @@ gem 'mock_redis'
9
9
  gem 'rspec', '~> 3.0'
10
10
 
11
11
  gem 'connection_pool'
12
+ gem 'dry-monitor'
12
13
  gem 'hanami-controller', '~> 1.3'
13
14
  gem 'pry-byebug'
14
15
  gem 'rubocop', '~> 1.21'
data/Gemfile.lock CHANGED
@@ -1,9 +1,10 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- idempotency (0.1.3)
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
@@ -36,6 +36,7 @@ Gem::Specification.new do |spec|
36
36
 
37
37
  spec.add_dependency 'base64'
38
38
  spec.add_dependency 'dry-configurable'
39
+ spec.add_dependency 'dry-monitor'
39
40
  spec.add_dependency 'msgpack'
40
41
  spec.add_dependency 'redis'
41
42
  end
@@ -5,4 +5,12 @@ class Idempotency
5
5
  RACK_HEADER_KEY = 'HTTP_IDEMPOTENCY_KEY'
6
6
  HEADER_KEY = 'Idempotency-Key'
7
7
  end
8
+
9
+ module Events
10
+ ALL_EVENTS = [
11
+ CACHE_HIT = :cache_hit,
12
+ CACHE_MISS = :cache_miss,
13
+ LOCK_CONFLICT = :lock_conflict
14
+ ].freeze
15
+ end
8
16
  end
@@ -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
@@ -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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Idempotency
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.4'
5
5
  end
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.3
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-14 00:00:00.000000000 Z
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