nats_pubsub 1.0.0

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.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/exe/nats_pubsub +44 -0
  3. data/lib/generators/nats_pubsub/config/config_generator.rb +174 -0
  4. data/lib/generators/nats_pubsub/config/templates/env.example.tt +46 -0
  5. data/lib/generators/nats_pubsub/config/templates/nats_pubsub.rb.tt +105 -0
  6. data/lib/generators/nats_pubsub/initializer/initializer_generator.rb +36 -0
  7. data/lib/generators/nats_pubsub/initializer/templates/nats_pubsub.rb +27 -0
  8. data/lib/generators/nats_pubsub/install/install_generator.rb +75 -0
  9. data/lib/generators/nats_pubsub/migrations/migrations_generator.rb +74 -0
  10. data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_inbox.rb.erb +88 -0
  11. data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_outbox.rb.erb +81 -0
  12. data/lib/generators/nats_pubsub/subscriber/subscriber_generator.rb +139 -0
  13. data/lib/generators/nats_pubsub/subscriber/templates/subscriber.rb.tt +117 -0
  14. data/lib/generators/nats_pubsub/subscriber/templates/subscriber_spec.rb.tt +116 -0
  15. data/lib/generators/nats_pubsub/subscriber/templates/subscriber_test.rb.tt +117 -0
  16. data/lib/nats_pubsub/active_record/publishable.rb +192 -0
  17. data/lib/nats_pubsub/cli.rb +105 -0
  18. data/lib/nats_pubsub/core/base_repository.rb +73 -0
  19. data/lib/nats_pubsub/core/config.rb +152 -0
  20. data/lib/nats_pubsub/core/config_presets.rb +139 -0
  21. data/lib/nats_pubsub/core/connection.rb +103 -0
  22. data/lib/nats_pubsub/core/constants.rb +190 -0
  23. data/lib/nats_pubsub/core/duration.rb +113 -0
  24. data/lib/nats_pubsub/core/error_action.rb +288 -0
  25. data/lib/nats_pubsub/core/event.rb +275 -0
  26. data/lib/nats_pubsub/core/health_check.rb +470 -0
  27. data/lib/nats_pubsub/core/logging.rb +72 -0
  28. data/lib/nats_pubsub/core/message_context.rb +193 -0
  29. data/lib/nats_pubsub/core/presets.rb +222 -0
  30. data/lib/nats_pubsub/core/retry_strategy.rb +71 -0
  31. data/lib/nats_pubsub/core/structured_logger.rb +141 -0
  32. data/lib/nats_pubsub/core/subject.rb +185 -0
  33. data/lib/nats_pubsub/instrumentation.rb +327 -0
  34. data/lib/nats_pubsub/middleware/active_record.rb +18 -0
  35. data/lib/nats_pubsub/middleware/chain.rb +92 -0
  36. data/lib/nats_pubsub/middleware/logging.rb +48 -0
  37. data/lib/nats_pubsub/middleware/retry_logger.rb +24 -0
  38. data/lib/nats_pubsub/middleware/structured_logging.rb +57 -0
  39. data/lib/nats_pubsub/models/event_model.rb +73 -0
  40. data/lib/nats_pubsub/models/inbox_event.rb +109 -0
  41. data/lib/nats_pubsub/models/model_codec_setup.rb +61 -0
  42. data/lib/nats_pubsub/models/model_utils.rb +57 -0
  43. data/lib/nats_pubsub/models/outbox_event.rb +113 -0
  44. data/lib/nats_pubsub/publisher/envelope_builder.rb +99 -0
  45. data/lib/nats_pubsub/publisher/fluent_batch.rb +262 -0
  46. data/lib/nats_pubsub/publisher/outbox_publisher.rb +97 -0
  47. data/lib/nats_pubsub/publisher/outbox_repository.rb +117 -0
  48. data/lib/nats_pubsub/publisher/publish_argument_parser.rb +108 -0
  49. data/lib/nats_pubsub/publisher/publish_result.rb +149 -0
  50. data/lib/nats_pubsub/publisher/publisher.rb +156 -0
  51. data/lib/nats_pubsub/rails/health_endpoint.rb +239 -0
  52. data/lib/nats_pubsub/railtie.rb +52 -0
  53. data/lib/nats_pubsub/subscribers/dlq_handler.rb +69 -0
  54. data/lib/nats_pubsub/subscribers/error_context.rb +137 -0
  55. data/lib/nats_pubsub/subscribers/error_handler.rb +110 -0
  56. data/lib/nats_pubsub/subscribers/graceful_shutdown.rb +128 -0
  57. data/lib/nats_pubsub/subscribers/inbox/inbox_message.rb +79 -0
  58. data/lib/nats_pubsub/subscribers/inbox/inbox_processor.rb +53 -0
  59. data/lib/nats_pubsub/subscribers/inbox/inbox_repository.rb +74 -0
  60. data/lib/nats_pubsub/subscribers/message_context.rb +86 -0
  61. data/lib/nats_pubsub/subscribers/message_processor.rb +225 -0
  62. data/lib/nats_pubsub/subscribers/message_router.rb +77 -0
  63. data/lib/nats_pubsub/subscribers/pool.rb +166 -0
  64. data/lib/nats_pubsub/subscribers/registry.rb +114 -0
  65. data/lib/nats_pubsub/subscribers/subscriber.rb +186 -0
  66. data/lib/nats_pubsub/subscribers/subscription_manager.rb +206 -0
  67. data/lib/nats_pubsub/subscribers/worker.rb +152 -0
  68. data/lib/nats_pubsub/tasks/install.rake +10 -0
  69. data/lib/nats_pubsub/testing/helpers.rb +199 -0
  70. data/lib/nats_pubsub/testing/matchers.rb +208 -0
  71. data/lib/nats_pubsub/testing/test_harness.rb +250 -0
  72. data/lib/nats_pubsub/testing.rb +157 -0
  73. data/lib/nats_pubsub/topology/overlap_guard.rb +88 -0
  74. data/lib/nats_pubsub/topology/stream.rb +102 -0
  75. data/lib/nats_pubsub/topology/stream_support.rb +170 -0
  76. data/lib/nats_pubsub/topology/subject_matcher.rb +77 -0
  77. data/lib/nats_pubsub/topology/topology.rb +24 -0
  78. data/lib/nats_pubsub/version.rb +8 -0
  79. data/lib/nats_pubsub/web/views/dashboard.erb +55 -0
  80. data/lib/nats_pubsub/web/views/inbox_detail.erb +91 -0
  81. data/lib/nats_pubsub/web/views/inbox_list.erb +62 -0
  82. data/lib/nats_pubsub/web/views/layout.erb +68 -0
  83. data/lib/nats_pubsub/web/views/outbox_detail.erb +77 -0
  84. data/lib/nats_pubsub/web/views/outbox_list.erb +62 -0
  85. data/lib/nats_pubsub/web.rb +181 -0
  86. data/lib/nats_pubsub.rb +290 -0
  87. metadata +225 -0
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsPubsub
4
+ # Immutable value object representing the result of a publish operation.
5
+ # Provides structured feedback instead of boolean returns, improving debuggability.
6
+ #
7
+ # @example Successful publish
8
+ # result = publisher.publish_to_topic('notifications', { text: 'Hello' })
9
+ # if result.success?
10
+ # puts "Published with event_id: #{result.event_id}"
11
+ # end
12
+ #
13
+ # @example Failed publish
14
+ # result = publisher.publish_to_topic('invalid', { })
15
+ # unless result.success?
16
+ # puts "Failed: #{result.reason} - #{result.details}"
17
+ # end
18
+ #
19
+ # @attr_reader success [Boolean] Whether publish succeeded
20
+ # @attr_reader event_id [String, nil] Event ID if successful
21
+ # @attr_reader subject [String, nil] NATS subject published to
22
+ # @attr_reader reason [Symbol, nil] Failure reason if unsuccessful
23
+ # @attr_reader details [String, nil] Detailed error message if unsuccessful
24
+ # @attr_reader error [Exception, nil] Original exception if available
25
+ class PublishResult
26
+ attr_reader :success, :event_id, :subject, :reason, :details, :error
27
+
28
+ # Create a successful publish result
29
+ #
30
+ # @param event_id [String] Event identifier
31
+ # @param subject [String] NATS subject
32
+ # @return [PublishResult] Success result
33
+ def self.success(event_id:, subject:)
34
+ new(success: true, event_id: event_id, subject: subject)
35
+ end
36
+
37
+ # Create a failed publish result
38
+ #
39
+ # @param reason [Symbol] Failure reason (:validation_error, :io_error, :timeout, etc.)
40
+ # @param details [String] Detailed error message
41
+ # @param subject [String, nil] NATS subject if known
42
+ # @param error [Exception, nil] Original exception
43
+ # @return [PublishResult] Failure result
44
+ def self.failure(reason:, details:, subject: nil, error: nil)
45
+ new(success: false, reason: reason, details: details, subject: subject, error: error)
46
+ end
47
+
48
+ # Initialize a PublishResult
49
+ #
50
+ # @param success [Boolean] Success flag
51
+ # @param event_id [String, nil] Event ID
52
+ # @param subject [String, nil] NATS subject
53
+ # @param reason [Symbol, nil] Failure reason
54
+ # @param details [String, nil] Error details
55
+ # @param error [Exception, nil] Original exception
56
+ def initialize(success:, event_id: nil, subject: nil, reason: nil, details: nil, error: nil)
57
+ @success = success
58
+ @event_id = event_id
59
+ @subject = subject
60
+ @reason = reason
61
+ @details = details
62
+ @error = error
63
+ freeze
64
+ end
65
+
66
+ # Check if publish was successful
67
+ #
68
+ # @return [Boolean] True if successful
69
+ def success?
70
+ @success
71
+ end
72
+
73
+ # Check if publish failed
74
+ #
75
+ # @return [Boolean] True if failed
76
+ def failure?
77
+ !@success
78
+ end
79
+
80
+ # Check if failure was due to validation error
81
+ #
82
+ # @return [Boolean] True if validation error
83
+ def validation_error?
84
+ @reason == :validation_error
85
+ end
86
+
87
+ # Check if failure was due to IO/network error
88
+ #
89
+ # @return [Boolean] True if IO error
90
+ def io_error?
91
+ @reason == :io_error
92
+ end
93
+
94
+ # Check if failure was due to timeout
95
+ #
96
+ # @return [Boolean] True if timeout
97
+ def timeout?
98
+ @reason == :timeout
99
+ end
100
+
101
+ # Get error message (for backward compatibility)
102
+ #
103
+ # @return [String, nil] Error message or nil if successful
104
+ def error_message
105
+ @details
106
+ end
107
+
108
+ # Convert to hash
109
+ #
110
+ # @return [Hash] Result as hash
111
+ def to_h
112
+ {
113
+ success: @success,
114
+ event_id: @event_id,
115
+ subject: @subject,
116
+ reason: @reason,
117
+ details: @details
118
+ }.compact
119
+ end
120
+
121
+ # String representation
122
+ #
123
+ # @return [String] Result description
124
+ def to_s
125
+ if success?
126
+ "PublishResult(success, event_id=#{@event_id}, subject=#{@subject})"
127
+ else
128
+ "PublishResult(failure, reason=#{@reason}, details=#{@details})"
129
+ end
130
+ end
131
+
132
+ # Inspect representation
133
+ #
134
+ # @return [String] Detailed inspection
135
+ def inspect
136
+ "#<PublishResult #{success? ? 'success' : 'failure'} #{to_h.inspect}>"
137
+ end
138
+
139
+ # For backward compatibility - acts like boolean in conditionals
140
+ #
141
+ # @return [Boolean] Success status
142
+ def to_bool
143
+ @success
144
+ end
145
+
146
+ # Allow result to be used in boolean contexts
147
+ alias to_boolean to_bool
148
+ end
149
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+ require_relative '../core/connection'
5
+ require_relative '../core/logging'
6
+ require_relative '../core/config'
7
+ require_relative '../core/retry_strategy'
8
+ require_relative '../models/model_utils'
9
+ require_relative 'envelope_builder'
10
+ require_relative 'outbox_repository'
11
+ require_relative 'outbox_publisher'
12
+ require_relative 'publish_result'
13
+ require_relative 'publish_argument_parser'
14
+
15
+ module NatsPubsub
16
+ # Publisher for PubSub events
17
+ # Provides a unified interface for publishing messages using either topics or domain/resource/action patterns
18
+ class Publisher
19
+ DEFAULT_RETRIES = 2
20
+
21
+ def initialize
22
+ @jts = Connection.connect!
23
+ end
24
+
25
+ # Publish a message using one of the supported patterns:
26
+ # 1. Topic-based: publish(topic, message, **opts) or publish(topic:, message:, **opts)
27
+ # 2. Domain/resource/action: publish(domain:, resource:, action:, payload:, **opts)
28
+ # 3. Multi-topic: publish(topics:, message:, **opts)
29
+ #
30
+ # @return [PublishResult] Result object with success status and details
31
+ #
32
+ # @example Topic-based (positional)
33
+ # result = publisher.publish('orders.created', { order_id: '123' })
34
+ #
35
+ # @example Topic-based (keyword)
36
+ # result = publisher.publish(topic: 'orders.created', message: { order_id: '123' })
37
+ #
38
+ # @example Domain/resource/action
39
+ # result = publisher.publish(domain: 'orders', resource: 'order', action: 'created', payload: { id: '123' })
40
+ #
41
+ # @example Multi-topic
42
+ # result = publisher.publish(topics: ['orders.created', 'notifications.sent'], message: { id: '123' })
43
+ def publish(*args, **kwargs)
44
+ parse_result = PublishArgumentParser.parse(*args, **kwargs)
45
+ parse_result.call(self)
46
+ end
47
+
48
+ # Publish to a single topic (internal method)
49
+ def publish_to_topic(topic, message, **options)
50
+ subject = EnvelopeBuilder.build_subject(topic)
51
+ envelope = EnvelopeBuilder.build_topic_envelope(topic, message, options)
52
+ event_id = envelope['event_id']
53
+
54
+ if NatsPubsub.config.use_outbox
55
+ OutboxPublisher.publish(
56
+ subject: subject,
57
+ envelope: envelope,
58
+ event_id: event_id
59
+ ) { with_retries { do_publish(subject, envelope, event_id) } }
60
+ else
61
+ with_retries { do_publish(subject, envelope, event_id) }
62
+ end
63
+ rescue StandardError => e
64
+ log_error(subject, event_id, e)
65
+ end
66
+
67
+ # Publish using domain/resource/action pattern (internal method)
68
+ def publish_event(domain, resource, action, payload, **options)
69
+ topic = "#{domain}.#{resource}.#{action}"
70
+ subject = EnvelopeBuilder.build_subject(topic)
71
+ envelope = EnvelopeBuilder.build_event_envelope(domain, resource, action, payload, options)
72
+ event_id = envelope['event_id']
73
+
74
+ if NatsPubsub.config.use_outbox
75
+ OutboxPublisher.publish(
76
+ subject: subject,
77
+ envelope: envelope,
78
+ event_id: event_id
79
+ ) { with_retries { do_publish(subject, envelope, event_id) } }
80
+ else
81
+ with_retries { do_publish(subject, envelope, event_id) }
82
+ end
83
+ rescue StandardError => e
84
+ log_error(subject, event_id, e)
85
+ end
86
+
87
+ # Publish to multiple topics (internal method)
88
+ def publish_to_topics(topics, message, **options)
89
+ results = {}
90
+ topics.each do |topic|
91
+ results[topic] = publish_to_topic(topic, message, **options)
92
+ end
93
+ results
94
+ end
95
+
96
+ private
97
+
98
+ def do_publish(subject, envelope, event_id)
99
+ headers = { 'nats-msg-id' => event_id }
100
+
101
+ ack = @jts.publish(subject, Oj.dump(envelope, mode: :compat), header: headers)
102
+ duplicate = ack.respond_to?(:duplicate?) && ack.duplicate?
103
+ msg = "Published #{subject} event_id=#{event_id}"
104
+ msg += ' (duplicate)' if duplicate
105
+
106
+ Logging.info(msg, tag: 'NatsPubsub::Publisher')
107
+
108
+ if ack.respond_to?(:error) && ack.error
109
+ Logging.error(
110
+ "Publish ack error: #{ack.error}",
111
+ tag: 'NatsPubsub::Publisher'
112
+ )
113
+ return PublishResult.failure(
114
+ reason: :publish_error,
115
+ details: "NATS ack error: #{ack.error}",
116
+ subject: subject,
117
+ error: ack.error
118
+ )
119
+ end
120
+
121
+ PublishResult.success(event_id: event_id, subject: subject)
122
+ end
123
+
124
+
125
+ # Retry only on transient NATS IO errors using RetryStrategy
126
+ def with_retries(retries = DEFAULT_RETRIES)
127
+ result = RetryStrategy.execute(retries: retries, operation_name: 'Publish') do
128
+ yield
129
+ end
130
+ result
131
+ rescue StandardError => e
132
+ # Return failure result for retry errors
133
+ subject = e.respond_to?(:subject) ? e.subject : 'unknown'
134
+ event_id = e.respond_to?(:event_id) ? e.event_id : 'unknown'
135
+ PublishResult.failure(
136
+ reason: :io_error,
137
+ details: "Retries exhausted: #{e.class} - #{e.message}",
138
+ subject: subject,
139
+ error: e
140
+ )
141
+ end
142
+
143
+ def log_error(subject, event_id, exc)
144
+ Logging.error(
145
+ "Publish failed: #{exc.class} #{exc.message}",
146
+ tag: 'NatsPubsub::Publisher'
147
+ )
148
+ PublishResult.failure(
149
+ reason: :exception,
150
+ details: "#{exc.class}: #{exc.message}",
151
+ subject: subject,
152
+ error: exc
153
+ )
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,239 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NatsPubsub
4
+ module Rails
5
+ # Rails helper for NatsPubsub health check endpoints
6
+ #
7
+ # Provides easy integration with Rails routing and controllers.
8
+ #
9
+ # @example Rails routes
10
+ # # config/routes.rb
11
+ # require 'nats_pubsub/rails/health_endpoint'
12
+ #
13
+ # Rails.application.routes.draw do
14
+ # mount NatsPubsub::Rails::HealthEndpoint => '/nats-health'
15
+ #
16
+ # # Or use individual endpoints
17
+ # get '/health/nats', to: NatsPubsub::Rails::HealthEndpoint.full_check
18
+ # get '/health/nats/quick', to: NatsPubsub::Rails::HealthEndpoint.quick_check
19
+ # get '/health/nats/liveness', to: NatsPubsub::Rails::HealthEndpoint.liveness
20
+ # get '/health/nats/readiness', to: NatsPubsub::Rails::HealthEndpoint.readiness
21
+ # end
22
+ #
23
+ # @example Controller action
24
+ # class HealthController < ApplicationController
25
+ # def nats
26
+ # render json: NatsPubsub::Rails::HealthEndpoint.check_health
27
+ # end
28
+ # end
29
+ #
30
+ class HealthEndpoint
31
+ # Rack application for health check endpoints
32
+ #
33
+ # Supports multiple endpoints:
34
+ # - GET / - Full health check
35
+ # - GET /quick - Quick connection check
36
+ # - GET /liveness - Liveness probe (always returns 200 if app is running)
37
+ # - GET /readiness - Readiness probe (checks if ready to accept traffic)
38
+ #
39
+ # @param env [Hash] Rack environment
40
+ # @return [Array] Rack response [status, headers, body]
41
+ def self.call(env)
42
+ request = ::Rack::Request.new(env)
43
+
44
+ case request.path_info
45
+ when '/', ''
46
+ full_check.call(env)
47
+ when '/quick'
48
+ quick_check.call(env)
49
+ when '/liveness'
50
+ liveness.call(env)
51
+ when '/readiness'
52
+ readiness.call(env)
53
+ else
54
+ not_found
55
+ end
56
+ end
57
+
58
+ # Full health check endpoint
59
+ #
60
+ # @return [Proc] Rack endpoint
61
+ def self.full_check
62
+ lambda do |_env|
63
+ result = Core::HealthCheck.check
64
+ status_code = http_status_for_health(result)
65
+
66
+ [
67
+ status_code,
68
+ { 'Content-Type' => 'application/json' },
69
+ [result.to_json]
70
+ ]
71
+ end
72
+ end
73
+
74
+ # Quick health check endpoint (connection only)
75
+ #
76
+ # @return [Proc] Rack endpoint
77
+ def self.quick_check
78
+ lambda do |_env|
79
+ result = Core::HealthCheck.quick_check
80
+ status_code = result.healthy? ? 200 : 503
81
+
82
+ [
83
+ status_code,
84
+ { 'Content-Type' => 'application/json' },
85
+ [result.to_json]
86
+ ]
87
+ end
88
+ end
89
+
90
+ # Liveness probe endpoint (Kubernetes-style)
91
+ #
92
+ # Always returns 200 if the application is running.
93
+ # Used to determine if the application should be restarted.
94
+ #
95
+ # @return [Proc] Rack endpoint
96
+ def self.liveness
97
+ lambda do |_env|
98
+ response = {
99
+ status: 'alive',
100
+ timestamp: Time.now.iso8601
101
+ }
102
+
103
+ [
104
+ 200,
105
+ { 'Content-Type' => 'application/json' },
106
+ [response.to_json]
107
+ ]
108
+ end
109
+ end
110
+
111
+ # Readiness probe endpoint (Kubernetes-style)
112
+ #
113
+ # Returns 200 if the application is ready to accept traffic.
114
+ # Checks NATS connection and basic connectivity.
115
+ #
116
+ # @return [Proc] Rack endpoint
117
+ def self.readiness
118
+ lambda do |_env|
119
+ result = Core::HealthCheck.quick_check
120
+
121
+ response = {
122
+ status: result.healthy? ? 'ready' : 'not_ready',
123
+ healthy: result.healthy?,
124
+ components: result.components,
125
+ timestamp: Time.now.iso8601
126
+ }
127
+
128
+ status_code = result.healthy? ? 200 : 503
129
+
130
+ [
131
+ status_code,
132
+ { 'Content-Type' => 'application/json' },
133
+ [response.to_json]
134
+ ]
135
+ end
136
+ end
137
+
138
+ # Check health and return hash (for controller use)
139
+ #
140
+ # @return [Hash] Health check result
141
+ def self.check_health
142
+ Core::HealthCheck.check.to_h
143
+ end
144
+
145
+ # Check quick health and return hash (for controller use)
146
+ #
147
+ # @return [Hash] Quick health check result
148
+ def self.quick_health
149
+ Core::HealthCheck.quick_check.to_h
150
+ end
151
+
152
+ # Helper for Rails controller
153
+ #
154
+ # @example
155
+ # class HealthController < ApplicationController
156
+ # include NatsPubsub::Rails::HealthEndpoint::ControllerHelper
157
+ #
158
+ # def nats
159
+ # render_nats_health
160
+ # end
161
+ #
162
+ # def nats_quick
163
+ # render_nats_health_quick
164
+ # end
165
+ # end
166
+ module ControllerHelper
167
+ # Render full health check
168
+ def render_nats_health
169
+ result = Core::HealthCheck.check
170
+ status_code = HealthEndpoint.http_status_for_health(result)
171
+
172
+ render json: result.to_h, status: status_code
173
+ end
174
+
175
+ # Render quick health check
176
+ def render_nats_health_quick
177
+ result = Core::HealthCheck.quick_check
178
+ status_code = result.healthy? ? 200 : 503
179
+
180
+ render json: result.to_h, status: status_code
181
+ end
182
+
183
+ # Render liveness check
184
+ def render_nats_liveness
185
+ render json: {
186
+ status: 'alive',
187
+ timestamp: Time.now.iso8601
188
+ }, status: 200
189
+ end
190
+
191
+ # Render readiness check
192
+ def render_nats_readiness
193
+ result = Core::HealthCheck.quick_check
194
+
195
+ render json: {
196
+ status: result.healthy? ? 'ready' : 'not_ready',
197
+ healthy: result.healthy?,
198
+ components: result.components,
199
+ timestamp: Time.now.iso8601
200
+ }, status: (result.healthy? ? 200 : 503)
201
+ end
202
+ end
203
+
204
+ class << self
205
+ # Determine HTTP status code from health result
206
+ #
207
+ # @param result [Core::HealthCheck::Result] Health check result
208
+ # @return [Integer] HTTP status code
209
+ def http_status_for_health(result)
210
+ case result.status
211
+ when :healthy
212
+ 200
213
+ when :degraded
214
+ 200 # Return 200 for degraded but still functional
215
+ when :unhealthy
216
+ 503
217
+ else
218
+ 503
219
+ end
220
+ end
221
+
222
+ private
223
+
224
+ def not_found
225
+ response = {
226
+ error: 'Not Found',
227
+ available_paths: ['/', '/quick', '/liveness', '/readiness']
228
+ }
229
+
230
+ [
231
+ 404,
232
+ { 'Content-Type' => 'application/json' },
233
+ [response.to_json]
234
+ ]
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'models/model_codec_setup'
4
+
5
+ module NatsPubsub
6
+ class Railtie < ::Rails::Railtie
7
+ # Configuration before Rails initialization
8
+ config.before_configuration do
9
+ # Set default configuration from environment
10
+ NatsPubsub.configure do |config|
11
+ config.env = ENV.fetch('RAILS_ENV', 'development')
12
+ config.app_name = Rails.application.class.module_parent_name.underscore
13
+ end
14
+ end
15
+
16
+ # Model codec setup after ActiveRecord loads
17
+ initializer 'nats_pubsub.defer_model_tweaks', after: :active_record do
18
+ ActiveSupport.on_load(:active_record) do
19
+ ActiveSupport::Reloader.to_prepare do
20
+ NatsPubsub::ModelCodecSetup.apply!
21
+ end
22
+ end
23
+ end
24
+
25
+ # Validate configuration after initialization
26
+ initializer 'nats_pubsub.validate_config', after: :load_config_initializers do
27
+ Rails.application.config.after_initialize do
28
+ next unless NatsPubsub.configuration
29
+
30
+ begin
31
+ NatsPubsub.configuration.validate!
32
+ rescue NatsPubsub::ConfigurationError => e
33
+ Rails.logger.warn "[NatsPubsub] Configuration warning: #{e.message}"
34
+ end
35
+ end
36
+ end
37
+
38
+ # Auto-discover subscribers in development
39
+ initializer 'nats_pubsub.auto_discover_subscribers' do
40
+ Rails.application.config.to_prepare do
41
+ if Rails.env.development? || Rails.env.test?
42
+ NatsPubsub::Subscribers::Registry.instance.discover_subscribers!
43
+ end
44
+ end
45
+ end
46
+
47
+ # Load rake tasks
48
+ rake_tasks do
49
+ load File.expand_path('tasks/install.rake', __dir__)
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'oj'
4
+ require 'time'
5
+ require 'base64'
6
+ require_relative '../core/logging'
7
+ require_relative '../core/config'
8
+
9
+ module NatsPubsub
10
+ module Subscribers
11
+ class DlqHandler
12
+ def initialize(jts)
13
+ @jts = jts
14
+ end
15
+
16
+ # Publishes failed message to Dead Letter Queue with explanatory headers/context
17
+ #
18
+ # @param msg [NATS::Msg] NATS message object
19
+ # @param ctx [MessageContext] Message context
20
+ # @param error_context [ErrorContext] Error context with failure details
21
+ # @return [Boolean] True if published successfully, false otherwise
22
+ def publish_to_dlq(msg, ctx, error_context:)
23
+ unless NatsPubsub.config.use_dlq
24
+ Logging.warn("DLQ disabled; skipping publish for event_id=#{ctx.event_id}", tag: 'NatsPubsub::Subscribers::DlqHandler')
25
+ return false
26
+ end
27
+
28
+ raw_base64 = Base64.strict_encode64(msg.data.to_s)
29
+ envelope = build_envelope(ctx, error_context, raw_base64)
30
+ headers = build_headers(msg.header, error_context.reason, ctx.deliveries, envelope)
31
+ @jts.publish(NatsPubsub.config.dlq_subject, msg.data, header: headers)
32
+ true
33
+ rescue StandardError => e
34
+ Logging.error(
35
+ "DLQ publish failed event_id=#{ctx.event_id}: #{e.class} #{e.message}",
36
+ tag: 'NatsPubsub::Subscribers::DlqHandler'
37
+ )
38
+ false
39
+ end
40
+
41
+ private
42
+
43
+ def build_envelope(ctx, error_context, raw_base64)
44
+ {
45
+ event_id: ctx.event_id,
46
+ reason: error_context.reason,
47
+ error_class: error_context.error_class,
48
+ error_message: error_context.error_message,
49
+ deliveries: ctx.deliveries,
50
+ original_subject: ctx.subject,
51
+ sequence: ctx.seq,
52
+ consumer: ctx.consumer,
53
+ stream: ctx.stream,
54
+ published_at: Time.now.utc.iso8601,
55
+ raw_base64: raw_base64
56
+ }
57
+ end
58
+
59
+ def build_headers(original_headers, reason, deliveries, envelope)
60
+ headers = (original_headers || {}).dup
61
+ headers['x-dead-letter'] = 'true'
62
+ headers['x-dlq-reason'] = reason
63
+ headers['x-deliveries'] = deliveries.to_s
64
+ headers['x-dlq-context'] = Oj.dump(envelope, mode: :compat)
65
+ headers
66
+ end
67
+ end
68
+ end
69
+ end