smart_message 0.0.7 → 0.0.8
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 +47 -0
- data/lib/smart_message/circuit_breaker.rb +22 -6
- data/lib/smart_message/dead_letter_queue.rb +344 -0
- data/lib/smart_message/transport/base.rb +17 -17
- data/lib/smart_message/version.rb +1 -1
- data/lib/smart_message.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e9fe12143456d1bc485554ad2d9f465a93e1d751860805bb2b5ee7496cccf031
|
4
|
+
data.tar.gz: bf4a18a9506120da4283a091e7ee6b921d02a01696c07bec92217883bea4e330
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ad5f81928567ffdbc6a980b02777c2468804a7a4f248c4c7b34690942c356f69859f525aaef02b463a02b2dd1efe76e321194117e7730a28ac99257f501ee9a8
|
7
|
+
data.tar.gz: 2bdab0d12acb85994e1121ef1308386f776bf36e08a086102cebdd9d1279198b2c89d9a160250ab9dca32e648305abffe0b32c87d63c2c5b4ec2ee43d77787d1
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,53 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
|
+
|
10
|
+
## [0.0.8] 2025-08-19
|
11
|
+
|
12
|
+
### Added
|
13
|
+
- **File-Based Dead Letter Queue System**: Production-ready DLQ implementation with comprehensive replay capabilities
|
14
|
+
- JSON Lines (.jsonl) format for efficient message storage and retrieval
|
15
|
+
- FIFO queue operations: `enqueue`, `dequeue`, `peek`, `size`, `clear`
|
16
|
+
- Configurable file paths with automatic directory creation
|
17
|
+
- Global default instance and custom instance support for different use cases
|
18
|
+
- Comprehensive replay functionality with transport override capabilities
|
19
|
+
- Administrative tools: `statistics`, `filter_by_class`, `filter_by_error_pattern`, `export_range`
|
20
|
+
- Thread-safe operations with Mutex protection for concurrent access
|
21
|
+
- Integration with circuit breaker fallbacks for automatic failed message capture
|
22
|
+
|
23
|
+
### Fixed
|
24
|
+
- **Code Quality: Dead Letter Queue Refactoring**: Eliminated multiple code smells for improved maintainability
|
25
|
+
- **Hardcoded Serialization Assumption**: Added `payload_format` tracking with flexible deserialization supporting multiple formats
|
26
|
+
- **Incomplete Header Restoration**: Complete header field restoration including `message_class`, `published_at`, `publisher_pid`, and `version`
|
27
|
+
- **Transport Override Not Applied**: Fixed replay logic to properly apply transport overrides before message publishing
|
28
|
+
- **Repeated File Reading Patterns**: Extracted common file iteration logic into reusable `read_entries_with_filter` method
|
29
|
+
- **Test Suite: Logger State Pollution**: Fixed cross-test contamination in logger tests
|
30
|
+
- **Root Cause**: Class-level logger state persisting between tests due to shared `@@logger` variable
|
31
|
+
- **Solution**: Added explicit logger reset in "handle nil logger gracefully" test to ensure clean state
|
32
|
+
- **Result**: Full test suite now passes with 0 failures, 0 errors (151 runs, 561 assertions)
|
33
|
+
|
34
|
+
### Enhanced
|
35
|
+
- **Dead Letter Queue Reliability**: Robust error handling and graceful degradation
|
36
|
+
- Automatic fallback to JSON deserialization for unknown payload formats with warning logs
|
37
|
+
- Graceful handling of corrupted DLQ entries without breaking queue operations
|
38
|
+
- Enhanced error context capture including stack traces and retry counts
|
39
|
+
- Backward compatibility maintained for existing DLQ files and message formats
|
40
|
+
- **Circuit Breaker Integration**: Enhanced fallback mechanism with DLQ integration
|
41
|
+
- Circuit breaker fallbacks now include proper circuit name identification for debugging
|
42
|
+
- Automatic serializer format detection and storage for accurate message replay
|
43
|
+
- Seamless integration between transport circuit breakers and dead letter queue storage
|
44
|
+
- **Code Maintainability**: Eliminated code duplication and improved design patterns
|
45
|
+
- DRY principle applied to file reading operations with reusable filter methods
|
46
|
+
- Consistent error handling patterns across all DLQ operations
|
47
|
+
- Clear separation of concerns between queue operations, replay logic, and administrative functions
|
48
|
+
|
49
|
+
### Documentation
|
50
|
+
- **Dead Letter Queue Configuration**: Multiple configuration options for different deployment scenarios
|
51
|
+
- Global default configuration: `SmartMessage::DeadLetterQueue.configure_default(path)`
|
52
|
+
- Instance-level configuration with custom paths for different message types
|
53
|
+
- Environment-based configuration using ENV variables for deployment flexibility
|
54
|
+
- Per-environment path configuration for development, staging, and production
|
55
|
+
|
9
56
|
## [0.0.7] 2025-08-19
|
10
57
|
### Added
|
11
58
|
- **Production-Grade Circuit Breaker Integration**: Comprehensive reliability patterns using BreakerMachines gem
|
@@ -159,23 +159,39 @@ module SmartMessage
|
|
159
159
|
|
160
160
|
# Configure fallback handlers for different scenarios
|
161
161
|
module Fallbacks
|
162
|
-
# Dead letter queue fallback
|
163
|
-
def self.dead_letter_queue(
|
162
|
+
# Dead letter queue fallback - stores failed messages to file-based DLQ
|
163
|
+
def self.dead_letter_queue(dlq_instance = nil)
|
164
164
|
proc do |exception, *args|
|
165
165
|
# Extract message details from args if available
|
166
166
|
message_header = args[0] if args[0].is_a?(SmartMessage::Header)
|
167
167
|
message_payload = args[1] if args.length > 1
|
168
168
|
|
169
|
-
#
|
170
|
-
|
171
|
-
|
169
|
+
# Use provided DLQ instance or default
|
170
|
+
dlq = dlq_instance || SmartMessage::DeadLetterQueue.default
|
171
|
+
|
172
|
+
# Store failed message in dead letter queue
|
173
|
+
sent_to_dlq = false
|
174
|
+
if message_header && message_payload
|
175
|
+
begin
|
176
|
+
dlq.enqueue(message_header, message_payload,
|
177
|
+
error: exception.message,
|
178
|
+
retry_count: 0,
|
179
|
+
serializer: 'json', # Default to JSON, could be enhanced to detect actual serializer
|
180
|
+
stack_trace: exception.backtrace&.join("\n")
|
181
|
+
)
|
182
|
+
sent_to_dlq = true
|
183
|
+
rescue => dlq_error
|
184
|
+
# DLQ storage failed - log but don't raise
|
185
|
+
puts "Warning: Failed to store message in DLQ: #{dlq_error.message}" if $DEBUG
|
186
|
+
end
|
172
187
|
end
|
173
188
|
|
174
189
|
{
|
175
190
|
circuit_breaker: {
|
191
|
+
circuit: :transport_publish, # Default circuit name, overridden by specific configurations
|
176
192
|
state: 'open',
|
177
193
|
error: exception.message,
|
178
|
-
sent_to_dlq:
|
194
|
+
sent_to_dlq: sent_to_dlq,
|
179
195
|
timestamp: Time.now.iso8601
|
180
196
|
}
|
181
197
|
}
|
@@ -0,0 +1,344 @@
|
|
1
|
+
# lib/smart_message/dead_letter_queue.rb
|
2
|
+
# encoding: utf-8
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'fileutils'
|
7
|
+
|
8
|
+
module SmartMessage
|
9
|
+
# File-based Dead Letter Queue implementation using JSON Lines format
|
10
|
+
# Provides FIFO queue operations with replay capabilities for failed messages
|
11
|
+
class DeadLetterQueue
|
12
|
+
attr_reader :file_path
|
13
|
+
|
14
|
+
# Default singleton instance
|
15
|
+
@@default_instance = nil
|
16
|
+
|
17
|
+
def self.default
|
18
|
+
@@default_instance ||= new
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.configure_default(file_path)
|
22
|
+
@@default_instance = new(file_path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(file_path = 'dead_letters.jsonl')
|
26
|
+
@file_path = File.expand_path(file_path)
|
27
|
+
@mutex = Mutex.new
|
28
|
+
ensure_directory_exists
|
29
|
+
end
|
30
|
+
|
31
|
+
# Core FIFO queue operations
|
32
|
+
|
33
|
+
# Add a failed message to the dead letter queue
|
34
|
+
# @param message_header [SmartMessage::Header] The message header
|
35
|
+
# @param message_payload [String] The serialized message payload
|
36
|
+
# @param error_info [Hash] Error details including :error, :retry_count, :transport, etc.
|
37
|
+
def enqueue(message_header, message_payload, error_info = {})
|
38
|
+
entry = {
|
39
|
+
timestamp: Time.now.iso8601,
|
40
|
+
header: message_header.to_hash,
|
41
|
+
payload: message_payload,
|
42
|
+
payload_format: error_info[:serializer] || 'json', # Track serialization format
|
43
|
+
error: error_info[:error] || 'Unknown error',
|
44
|
+
retry_count: error_info[:retry_count] || 0,
|
45
|
+
transport: error_info[:transport],
|
46
|
+
stack_trace: error_info[:stack_trace]
|
47
|
+
}
|
48
|
+
|
49
|
+
@mutex.synchronize do
|
50
|
+
File.open(@file_path, 'a') do |file|
|
51
|
+
file.puts entry.to_json
|
52
|
+
file.fsync # Ensure immediate write to disk
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
entry
|
57
|
+
end
|
58
|
+
|
59
|
+
# Remove and return the oldest message from the queue
|
60
|
+
# @return [Hash, nil] The oldest DLQ entry or nil if queue is empty
|
61
|
+
def dequeue
|
62
|
+
@mutex.synchronize do
|
63
|
+
return nil unless File.exist?(@file_path)
|
64
|
+
|
65
|
+
lines = File.readlines(@file_path)
|
66
|
+
return nil if lines.empty?
|
67
|
+
|
68
|
+
# Get first line (oldest)
|
69
|
+
oldest_line = lines.shift
|
70
|
+
oldest_entry = JSON.parse(oldest_line.strip, symbolize_names: true)
|
71
|
+
|
72
|
+
# Rewrite file without the first line
|
73
|
+
File.open(@file_path, 'w') do |file|
|
74
|
+
lines.each { |line| file.write(line) }
|
75
|
+
end
|
76
|
+
|
77
|
+
oldest_entry
|
78
|
+
end
|
79
|
+
rescue JSON::ParserError => e
|
80
|
+
puts "Warning: Corrupted DLQ entry skipped: #{e.message}" if $DEBUG
|
81
|
+
nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Look at the oldest message without removing it
|
85
|
+
# @return [Hash, nil] The oldest DLQ entry or nil if queue is empty
|
86
|
+
def peek
|
87
|
+
return nil unless File.exist?(@file_path)
|
88
|
+
|
89
|
+
File.open(@file_path, 'r') do |file|
|
90
|
+
first_line = file.readline
|
91
|
+
return nil if first_line.nil? || first_line.strip.empty?
|
92
|
+
JSON.parse(first_line.strip, symbolize_names: true)
|
93
|
+
end
|
94
|
+
rescue EOFError, JSON::ParserError
|
95
|
+
nil
|
96
|
+
end
|
97
|
+
|
98
|
+
# Get the number of messages in the queue
|
99
|
+
# @return [Integer] Number of messages in the DLQ
|
100
|
+
def size
|
101
|
+
return 0 unless File.exist?(@file_path)
|
102
|
+
File.readlines(@file_path).size
|
103
|
+
end
|
104
|
+
|
105
|
+
# Clear all messages from the queue
|
106
|
+
def clear
|
107
|
+
@mutex.synchronize do
|
108
|
+
File.delete(@file_path) if File.exist?(@file_path)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Replay capabilities
|
113
|
+
|
114
|
+
# Replay all messages in the queue
|
115
|
+
# @param transport [SmartMessage::Transport::Base] Optional transport override
|
116
|
+
# @return [Hash] Results summary
|
117
|
+
def replay_all(transport = nil)
|
118
|
+
results = { success: 0, failed: 0, errors: [] }
|
119
|
+
|
120
|
+
while (entry = dequeue)
|
121
|
+
result = replay_entry(entry, transport)
|
122
|
+
if result[:success]
|
123
|
+
results[:success] += 1
|
124
|
+
else
|
125
|
+
results[:failed] += 1
|
126
|
+
results[:errors] << result[:error]
|
127
|
+
# Re-enqueue failed replay attempts
|
128
|
+
header = SmartMessage::Header.new(entry[:header])
|
129
|
+
enqueue(header, entry[:payload],
|
130
|
+
error: "Replay failed: #{result[:error]}",
|
131
|
+
retry_count: (entry[:retry_count] || 0) + 1)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
results
|
136
|
+
end
|
137
|
+
|
138
|
+
# Replay the oldest message
|
139
|
+
# @param transport [SmartMessage::Transport::Base] Optional transport override
|
140
|
+
# @return [Hash] Result of replay attempt
|
141
|
+
def replay_one(transport = nil)
|
142
|
+
entry = dequeue
|
143
|
+
return { success: false, error: 'Queue is empty' } unless entry
|
144
|
+
|
145
|
+
replay_entry(entry, transport)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Replay a batch of messages
|
149
|
+
# @param count [Integer] Number of messages to replay
|
150
|
+
# @param transport [SmartMessage::Transport::Base] Optional transport override
|
151
|
+
# @return [Hash] Results summary
|
152
|
+
def replay_batch(count = 10, transport = nil)
|
153
|
+
results = { success: 0, failed: 0, errors: [] }
|
154
|
+
|
155
|
+
count.times do
|
156
|
+
break if size == 0
|
157
|
+
|
158
|
+
result = replay_one(transport)
|
159
|
+
if result[:success]
|
160
|
+
results[:success] += 1
|
161
|
+
else
|
162
|
+
results[:failed] += 1
|
163
|
+
results[:errors] << result[:error]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
results
|
168
|
+
end
|
169
|
+
|
170
|
+
# Administrative utilities
|
171
|
+
|
172
|
+
# Inspect messages in the queue without removing them
|
173
|
+
# @param limit [Integer] Maximum number of messages to show
|
174
|
+
# @return [Array<Hash>] Array of DLQ entries
|
175
|
+
def inspect_messages(limit: 10)
|
176
|
+
count = 0
|
177
|
+
read_entries_with_filter do |entry|
|
178
|
+
return [] if count >= limit
|
179
|
+
count += 1
|
180
|
+
entry
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Filter messages by message class
|
185
|
+
# @param message_class [String] The message class name to filter by
|
186
|
+
# @return [Array<Hash>] Filtered DLQ entries
|
187
|
+
def filter_by_class(message_class)
|
188
|
+
read_entries_with_filter do |entry|
|
189
|
+
entry if entry.dig(:header, :message_class) == message_class
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
# Filter messages by error pattern
|
194
|
+
# @param pattern [Regexp, String] Pattern to match against error messages
|
195
|
+
# @return [Array<Hash>] Filtered DLQ entries
|
196
|
+
def filter_by_error_pattern(pattern)
|
197
|
+
pattern = Regexp.new(pattern) if pattern.is_a?(String)
|
198
|
+
|
199
|
+
read_entries_with_filter do |entry|
|
200
|
+
entry if pattern.match?(entry[:error].to_s)
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
# Get statistics about the dead letter queue
|
205
|
+
# @return [Hash] Statistics summary
|
206
|
+
def statistics
|
207
|
+
stats = { total: 0, by_class: Hash.new(0), by_error: Hash.new(0) }
|
208
|
+
|
209
|
+
read_entries_with_filter do |entry|
|
210
|
+
stats[:total] += 1
|
211
|
+
|
212
|
+
message_class = entry.dig(:header, :message_class) || 'Unknown'
|
213
|
+
stats[:by_class][message_class] += 1
|
214
|
+
|
215
|
+
error = entry[:error] || 'Unknown error'
|
216
|
+
stats[:by_error][error] += 1
|
217
|
+
|
218
|
+
nil # Don't collect entries, just process for side effects
|
219
|
+
end
|
220
|
+
|
221
|
+
stats
|
222
|
+
end
|
223
|
+
|
224
|
+
# Export messages within a time range
|
225
|
+
# @param start_time [Time] Start of time range
|
226
|
+
# @param end_time [Time] End of time range
|
227
|
+
# @return [Array<Hash>] DLQ entries within the time range
|
228
|
+
def export_range(start_time, end_time)
|
229
|
+
read_entries_with_filter do |entry|
|
230
|
+
begin
|
231
|
+
timestamp = Time.parse(entry[:timestamp])
|
232
|
+
entry if timestamp >= start_time && timestamp <= end_time
|
233
|
+
rescue ArgumentError
|
234
|
+
# Skip entries with invalid timestamps
|
235
|
+
nil
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
# Replay a single DLQ entry by recreating the message instance
|
243
|
+
# @param entry [Hash] The DLQ entry to replay
|
244
|
+
# @param transport_override [SmartMessage::Transport::Base] Optional transport override
|
245
|
+
# @return [Hash] Result of replay attempt
|
246
|
+
def replay_entry(entry, transport_override = nil)
|
247
|
+
message_class_name = entry.dig(:header, :message_class)
|
248
|
+
return { success: false, error: 'Missing message class' } unless message_class_name
|
249
|
+
|
250
|
+
# Get the message class
|
251
|
+
message_class = message_class_name.constantize
|
252
|
+
|
253
|
+
# Deserialize the payload using the appropriate format
|
254
|
+
payload_data = deserialize_payload(entry[:payload], entry[:payload_format] || 'json')
|
255
|
+
return { success: false, error: 'Failed to deserialize payload' } unless payload_data
|
256
|
+
|
257
|
+
# Remove the header from payload data (it's stored separately in DLQ)
|
258
|
+
payload_data.delete(:_sm_header)
|
259
|
+
|
260
|
+
# Create new message instance with original data
|
261
|
+
message = message_class.new(**payload_data)
|
262
|
+
|
263
|
+
# Restore complete header information
|
264
|
+
restore_header_fields(message, entry[:header])
|
265
|
+
|
266
|
+
# Override transport if provided - this must be done before publishing
|
267
|
+
if transport_override
|
268
|
+
message.transport(transport_override)
|
269
|
+
end
|
270
|
+
|
271
|
+
# Attempt to publish the message
|
272
|
+
message.publish
|
273
|
+
|
274
|
+
{ success: true, message: message }
|
275
|
+
rescue => e
|
276
|
+
{ success: false, error: "#{e.class.name}: #{e.message}" }
|
277
|
+
end
|
278
|
+
|
279
|
+
# Deserialize payload based on format
|
280
|
+
# @param payload [String] The serialized payload
|
281
|
+
# @param format [String] The serialization format (json, etc.)
|
282
|
+
# @return [Hash, nil] Deserialized data or nil if failed
|
283
|
+
def deserialize_payload(payload, format)
|
284
|
+
case format.to_s.downcase
|
285
|
+
when 'json'
|
286
|
+
JSON.parse(payload, symbolize_names: true)
|
287
|
+
else
|
288
|
+
# For unknown formats, assume JSON as fallback but log warning
|
289
|
+
puts "Warning: Unknown payload format '#{format}', attempting JSON" if $DEBUG
|
290
|
+
JSON.parse(payload, symbolize_names: true)
|
291
|
+
end
|
292
|
+
rescue JSON::ParserError => e
|
293
|
+
puts "Error: Failed to deserialize payload as #{format}: #{e.message}" if $DEBUG
|
294
|
+
nil
|
295
|
+
end
|
296
|
+
|
297
|
+
# Restore all header fields from DLQ entry
|
298
|
+
# @param message [SmartMessage::Base] The message instance
|
299
|
+
# @param header_data [Hash] The stored header data
|
300
|
+
def restore_header_fields(message, header_data)
|
301
|
+
return unless header_data
|
302
|
+
|
303
|
+
# Restore all available header fields
|
304
|
+
message._sm_header.uuid = header_data[:uuid] if header_data[:uuid]
|
305
|
+
message._sm_header.message_class = header_data[:message_class] if header_data[:message_class]
|
306
|
+
message._sm_header.published_at = Time.parse(header_data[:published_at]) if header_data[:published_at]
|
307
|
+
message._sm_header.publisher_pid = header_data[:publisher_pid] if header_data[:publisher_pid]
|
308
|
+
message._sm_header.version = header_data[:version] if header_data[:version]
|
309
|
+
message._sm_header.from = header_data[:from] if header_data[:from]
|
310
|
+
message._sm_header.to = header_data[:to] if header_data[:to]
|
311
|
+
message._sm_header.reply_to = header_data[:reply_to] if header_data[:reply_to]
|
312
|
+
rescue => e
|
313
|
+
puts "Warning: Failed to restore some header fields: #{e.message}" if $DEBUG
|
314
|
+
end
|
315
|
+
|
316
|
+
# Generic file reading iterator with error handling
|
317
|
+
# @param block [Proc] Block to execute for each valid entry
|
318
|
+
# @return [Array] Results from the block
|
319
|
+
def read_entries_with_filter(&block)
|
320
|
+
return [] unless File.exist?(@file_path)
|
321
|
+
|
322
|
+
results = []
|
323
|
+
File.open(@file_path, 'r') do |file|
|
324
|
+
file.each_line do |line|
|
325
|
+
begin
|
326
|
+
entry = JSON.parse(line.strip, symbolize_names: true)
|
327
|
+
result = block.call(entry)
|
328
|
+
results << result if result
|
329
|
+
rescue JSON::ParserError
|
330
|
+
# Skip corrupted lines
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
results
|
336
|
+
end
|
337
|
+
|
338
|
+
# Ensure the directory for the DLQ file exists
|
339
|
+
def ensure_directory_exists
|
340
|
+
directory = File.dirname(@file_path)
|
341
|
+
FileUtils.mkdir_p(directory) unless Dir.exist?(directory)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|
@@ -156,20 +156,8 @@ module SmartMessage
|
|
156
156
|
# Use memory storage by default for transport circuits
|
157
157
|
storage BreakerMachines::Storage::Memory.new
|
158
158
|
|
159
|
-
# Fallback for publish failures
|
160
|
-
fallback
|
161
|
-
{
|
162
|
-
circuit_breaker: {
|
163
|
-
circuit: :transport_publish,
|
164
|
-
transport_type: self.class.name,
|
165
|
-
state: 'open',
|
166
|
-
error: exception.message,
|
167
|
-
error_class: exception.class.name,
|
168
|
-
timestamp: Time.now.iso8601,
|
169
|
-
fallback_triggered: true
|
170
|
-
}
|
171
|
-
}
|
172
|
-
end
|
159
|
+
# Fallback for publish failures - use DLQ fallback
|
160
|
+
fallback SmartMessage::CircuitBreaker::Fallbacks.dead_letter_queue
|
173
161
|
end
|
174
162
|
|
175
163
|
# Configure subscribe circuit breaker
|
@@ -182,7 +170,7 @@ module SmartMessage
|
|
182
170
|
|
183
171
|
storage BreakerMachines::Storage::Memory.new
|
184
172
|
|
185
|
-
# Fallback for subscribe failures
|
173
|
+
# Fallback for subscribe failures - log and return error info
|
186
174
|
fallback do |exception|
|
187
175
|
{
|
188
176
|
circuit_breaker: {
|
@@ -209,10 +197,22 @@ module SmartMessage
|
|
209
197
|
puts "Transport publish circuit breaker activated: #{self.class.name}"
|
210
198
|
puts "Error: #{fallback_result[:circuit_breaker][:error]}"
|
211
199
|
puts "Message: #{message_header.message_class}"
|
200
|
+
puts "Sent to DLQ: #{fallback_result[:circuit_breaker][:sent_to_dlq]}"
|
212
201
|
end
|
213
202
|
|
214
|
-
#
|
215
|
-
|
203
|
+
# If message wasn't sent to DLQ by circuit breaker, send it now
|
204
|
+
unless fallback_result.dig(:circuit_breaker, :sent_to_dlq)
|
205
|
+
begin
|
206
|
+
SmartMessage::DeadLetterQueue.default.enqueue(
|
207
|
+
message_header,
|
208
|
+
message_payload,
|
209
|
+
error: fallback_result.dig(:circuit_breaker, :error) || 'Circuit breaker activated',
|
210
|
+
transport: self.class.name
|
211
|
+
)
|
212
|
+
rescue => dlq_error
|
213
|
+
puts "Warning: Failed to store message in DLQ: #{dlq_error.message}" if $DEBUG
|
214
|
+
end
|
215
|
+
end
|
216
216
|
|
217
217
|
# Return the fallback result to indicate failure
|
218
218
|
fallback_result
|
data/lib/smart_message.rb
CHANGED
@@ -22,6 +22,7 @@ require_relative './simple_stats'
|
|
22
22
|
require_relative './smart_message/version'
|
23
23
|
require_relative './smart_message/errors'
|
24
24
|
require_relative './smart_message/circuit_breaker'
|
25
|
+
require_relative './smart_message/dead_letter_queue'
|
25
26
|
|
26
27
|
require_relative './smart_message/dispatcher.rb'
|
27
28
|
require_relative './smart_message/transport.rb'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: smart_message
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Dewayne VanHoozer
|
@@ -249,6 +249,7 @@ files:
|
|
249
249
|
- lib/smart_message.rb
|
250
250
|
- lib/smart_message/base.rb
|
251
251
|
- lib/smart_message/circuit_breaker.rb
|
252
|
+
- lib/smart_message/dead_letter_queue.rb
|
252
253
|
- lib/smart_message/dispatcher.rb
|
253
254
|
- lib/smart_message/dispatcher/.keep
|
254
255
|
- lib/smart_message/errors.rb
|