smart_message 0.0.8 → 0.0.10

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 +4 -4
  2. data/.gitignore +1 -0
  3. data/.irbrc +24 -0
  4. data/CHANGELOG.md +119 -0
  5. data/Gemfile.lock +6 -1
  6. data/README.md +389 -17
  7. data/docs/README.md +3 -1
  8. data/docs/addressing.md +119 -13
  9. data/docs/architecture.md +184 -46
  10. data/docs/dead_letter_queue.md +673 -0
  11. data/docs/dispatcher.md +87 -0
  12. data/docs/examples.md +59 -1
  13. data/docs/getting-started.md +8 -1
  14. data/docs/logging.md +382 -326
  15. data/docs/message_deduplication.md +488 -0
  16. data/docs/message_filtering.md +451 -0
  17. data/examples/01_point_to_point_orders.rb +54 -53
  18. data/examples/02_publish_subscribe_events.rb +14 -10
  19. data/examples/03_many_to_many_chat.rb +16 -8
  20. data/examples/04_redis_smart_home_iot.rb +20 -10
  21. data/examples/05_proc_handlers.rb +12 -11
  22. data/examples/06_custom_logger_example.rb +95 -100
  23. data/examples/07_error_handling_scenarios.rb +4 -2
  24. data/examples/08_entity_addressing_basic.rb +18 -6
  25. data/examples/08_entity_addressing_with_filtering.rb +27 -9
  26. data/examples/09_dead_letter_queue_demo.rb +559 -0
  27. data/examples/09_regex_filtering_microservices.rb +407 -0
  28. data/examples/10_header_block_configuration.rb +263 -0
  29. data/examples/10_message_deduplication.rb +209 -0
  30. data/examples/11_global_configuration_example.rb +219 -0
  31. data/examples/README.md +102 -0
  32. data/examples/dead_letters.jsonl +12 -0
  33. data/examples/performance_metrics/benchmark_results_ractor_20250818_205603.json +135 -0
  34. data/examples/performance_metrics/benchmark_results_ractor_20250818_205831.json +135 -0
  35. data/examples/performance_metrics/benchmark_results_test_20250818_204942.json +130 -0
  36. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204942.json +130 -0
  37. data/examples/performance_metrics/benchmark_results_threadpool_20250818_204959.json +130 -0
  38. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205044.json +130 -0
  39. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205109.json +130 -0
  40. data/examples/performance_metrics/benchmark_results_threadpool_20250818_205252.json +130 -0
  41. data/examples/performance_metrics/benchmark_results_unknown_20250819_172852.json +130 -0
  42. data/examples/performance_metrics/compare_benchmarks.rb +519 -0
  43. data/examples/performance_metrics/dead_letters.jsonl +3100 -0
  44. data/examples/performance_metrics/performance_benchmark.rb +344 -0
  45. data/examples/show_logger.rb +367 -0
  46. data/examples/show_me.rb +145 -0
  47. data/examples/temp.txt +94 -0
  48. data/examples/tmux_chat/bot_agent.rb +4 -2
  49. data/examples/tmux_chat/human_agent.rb +4 -2
  50. data/examples/tmux_chat/room_monitor.rb +4 -2
  51. data/examples/tmux_chat/shared_chat_system.rb +6 -3
  52. data/lib/smart_message/addressing.rb +259 -0
  53. data/lib/smart_message/base.rb +123 -599
  54. data/lib/smart_message/circuit_breaker.rb +2 -1
  55. data/lib/smart_message/configuration.rb +199 -0
  56. data/lib/smart_message/ddq/base.rb +71 -0
  57. data/lib/smart_message/ddq/memory.rb +109 -0
  58. data/lib/smart_message/ddq/redis.rb +168 -0
  59. data/lib/smart_message/ddq.rb +31 -0
  60. data/lib/smart_message/dead_letter_queue.rb +27 -10
  61. data/lib/smart_message/deduplication.rb +174 -0
  62. data/lib/smart_message/dispatcher.rb +259 -61
  63. data/lib/smart_message/header.rb +5 -0
  64. data/lib/smart_message/logger/base.rb +21 -1
  65. data/lib/smart_message/logger/default.rb +88 -138
  66. data/lib/smart_message/logger/lumberjack.rb +324 -0
  67. data/lib/smart_message/logger/null.rb +81 -0
  68. data/lib/smart_message/logger.rb +17 -9
  69. data/lib/smart_message/messaging.rb +100 -0
  70. data/lib/smart_message/plugins.rb +132 -0
  71. data/lib/smart_message/serializer/base.rb +25 -8
  72. data/lib/smart_message/serializer/json.rb +5 -4
  73. data/lib/smart_message/subscription.rb +196 -0
  74. data/lib/smart_message/transport/base.rb +72 -41
  75. data/lib/smart_message/transport/memory_transport.rb +7 -5
  76. data/lib/smart_message/transport/redis_transport.rb +15 -45
  77. data/lib/smart_message/transport/stdout_transport.rb +18 -8
  78. data/lib/smart_message/transport.rb +1 -34
  79. data/lib/smart_message/utilities.rb +142 -0
  80. data/lib/smart_message/version.rb +1 -1
  81. data/lib/smart_message/versioning.rb +85 -0
  82. data/lib/smart_message/wrapper.rb.bak +132 -0
  83. data/lib/smart_message.rb +74 -28
  84. data/smart_message.gemspec +3 -0
  85. metadata +83 -3
  86. data/lib/smart_message/serializer.rb +0 -10
  87. data/lib/smart_message/wrapper.rb +0 -43
@@ -182,7 +182,8 @@ module SmartMessage
182
182
  sent_to_dlq = true
183
183
  rescue => dlq_error
184
184
  # DLQ storage failed - log but don't raise
185
- puts "Warning: Failed to store message in DLQ: #{dlq_error.message}" if $DEBUG
185
+ # Note: Logger might not be available in circuit breaker context
186
+ warn "Warning: Failed to store message in DLQ: #{dlq_error.message}"
186
187
  end
187
188
  end
188
189
 
@@ -0,0 +1,199 @@
1
+ # lib/smart_message/configuration.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ # Global configuration class for SmartMessage framework
7
+ #
8
+ # This class provides a centralized way for applications to configure
9
+ # default behavior for all SmartMessage classes. Applications can set
10
+ # global defaults for logger, transport, and serializer, which will be
11
+ # used by all message classes unless explicitly overridden.
12
+ #
13
+ # IMPORTANT: No configuration = NO LOGGING
14
+ # Applications must explicitly configure logging if they want it.
15
+ #
16
+ # Usage:
17
+ # # No configuration block = NO LOGGING (default behavior)
18
+ #
19
+ # # Use framework default logger (Lumberjack) with custom log file:
20
+ # SmartMessage.configure do |config|
21
+ # config.logger = "log/my_app.log" # String path = Lumberjack logger
22
+ # end
23
+ #
24
+ # # Use framework default logger with STDOUT/STDERR:
25
+ # SmartMessage.configure do |config|
26
+ # config.logger = STDOUT # Log to STDOUT
27
+ # config.logger = STDERR # Log to STDERR
28
+ # end
29
+ #
30
+ # # Use framework default logger with default file (log/smart_message.log):
31
+ # SmartMessage.configure do |config|
32
+ # config.logger = :default # Framework default
33
+ # end
34
+ #
35
+ # # Configure Lumberjack logger options:
36
+ # SmartMessage.configure do |config|
37
+ # config.logger = :default # Use framework default
38
+ # config.log_level = :debug # :debug, :info, :warn, :error, :fatal
39
+ # config.log_format = :json # :text or :json
40
+ # config.log_include_source = true # Include file:line source info
41
+ # config.log_structured_data = true # Include structured message data
42
+ # config.log_colorize = true # Enable colorized output (console only)
43
+ # config.log_options = { # Additional Lumberjack options
44
+ # roll_by_date: true, # Enable date-based log rolling
45
+ # date_pattern: '%Y-%m-%d', # Date pattern for rolling
46
+ # roll_by_size: true, # Enable size-based log rolling
47
+ # max_file_size: 50 * 1024 * 1024, # Max file size before rolling (50 MB)
48
+ # keep_files: 10 # Number of rolled files to keep
49
+ # }
50
+ # end
51
+ #
52
+ # # Use custom logger:
53
+ # SmartMessage.configure do |config|
54
+ # config.logger = MyApp::Logger.new # Custom logger object
55
+ # config.transport = MyApp::Transport.new
56
+ # config.serializer = MyApp::Serializer.new
57
+ # end
58
+ #
59
+ # # Explicitly disable logging:
60
+ # SmartMessage.configure do |config|
61
+ # config.logger = nil # Explicit no logging
62
+ # end
63
+ #
64
+ # # Individual message classes use these defaults automatically
65
+ # class OrderMessage < SmartMessage::Base
66
+ # property :order_id
67
+ # # No config block needed - uses global defaults
68
+ # end
69
+ #
70
+ # # Override global defaults when needed
71
+ # class SpecialMessage < SmartMessage::Base
72
+ # config do
73
+ # logger MyApp::SpecialLogger.new # Override just the logger
74
+ # # transport and serializer still use global defaults
75
+ # end
76
+ # end
77
+ class Configuration
78
+ attr_accessor :transport, :serializer, :log_level, :log_format, :log_include_source, :log_structured_data, :log_colorize, :log_options
79
+ attr_reader :logger
80
+
81
+ def initialize
82
+ @logger = nil
83
+ @transport = nil
84
+ @serializer = nil
85
+ @logger_explicitly_set_to_nil = false
86
+ @log_level = nil
87
+ @log_format = nil
88
+ @log_include_source = nil
89
+ @log_structured_data = nil
90
+ @log_colorize = nil
91
+ @log_options = {}
92
+ end
93
+
94
+ # Custom logger setter to track explicit nil assignment
95
+ def logger=(value)
96
+ @logger = value
97
+ @logger_explicitly_set_to_nil = value.nil?
98
+ end
99
+
100
+ # Reset configuration to defaults
101
+ def reset!
102
+ @logger = nil
103
+ @transport = nil
104
+ @serializer = nil
105
+ @logger_explicitly_set_to_nil = false
106
+ @log_level = nil
107
+ @log_format = nil
108
+ @log_include_source = nil
109
+ @log_structured_data = nil
110
+ @log_colorize = nil
111
+ @log_options = {}
112
+ end
113
+
114
+ # Check if logger is configured (including explicit nil for no logging)
115
+ def logger_configured?
116
+ !@logger.nil? || @logger_explicitly_set_to_nil || @logger == :default || @logger.is_a?(String) || @logger == STDOUT || @logger == STDERR
117
+ end
118
+
119
+ # Check if transport is configured
120
+ def transport_configured?
121
+ !@transport.nil?
122
+ end
123
+
124
+ # Check if serializer is configured
125
+ def serializer_configured?
126
+ !@serializer.nil?
127
+ end
128
+
129
+ # Get the configured logger or no logging
130
+ def default_logger
131
+ case @logger
132
+ when nil
133
+ # If explicitly set to nil, use null logger (no logging)
134
+ if @logger_explicitly_set_to_nil
135
+ SmartMessage::Logger::Null.new
136
+ else
137
+ # Not configured, NO LOGGING
138
+ SmartMessage::Logger::Null.new
139
+ end
140
+ when :default
141
+ # Explicitly requested framework default
142
+ framework_default_logger
143
+ when String
144
+ # String path means use Lumberjack logger with that file path
145
+ SmartMessage::Logger::Lumberjack.new(**logger_options.merge(log_file: @logger))
146
+ when STDOUT, STDERR
147
+ # STDOUT/STDERR constants mean use Lumberjack logger with that output
148
+ SmartMessage::Logger::Lumberjack.new(**logger_options.merge(log_file: @logger))
149
+ else
150
+ @logger
151
+ end
152
+ end
153
+
154
+ # Get the configured transport or framework default
155
+ def default_transport
156
+ @transport || framework_default_transport
157
+ end
158
+
159
+ # Get the configured serializer or framework default
160
+ def default_serializer
161
+ @serializer || framework_default_serializer
162
+ end
163
+
164
+ private
165
+
166
+ # Framework's built-in default logger (Lumberjack)
167
+ def framework_default_logger
168
+ SmartMessage::Logger::Lumberjack.new(**logger_options)
169
+ end
170
+
171
+ # Build logger options from configuration
172
+ def logger_options
173
+ options = {}
174
+ options[:level] = @log_level if @log_level
175
+ options[:format] = @log_format if @log_format
176
+ options[:include_source] = @log_include_source unless @log_include_source.nil?
177
+ options[:structured_data] = @log_structured_data unless @log_structured_data.nil?
178
+ options[:colorize] = @log_colorize unless @log_colorize.nil?
179
+
180
+ # Merge in log_options (for roll_by_date, roll_by_size, max_file_size, etc.)
181
+ options.merge!(@log_options) if @log_options && @log_options.is_a?(Hash)
182
+
183
+ options
184
+ end
185
+
186
+ # Framework's built-in default transport (Redis)
187
+ def framework_default_transport
188
+ SmartMessage::Transport::RedisTransport.new
189
+ end
190
+
191
+ # Framework's built-in default serializer (JSON)
192
+ def framework_default_serializer
193
+ SmartMessage::Serializer::Json.new
194
+ rescue => e
195
+ # Fallback if JSON serializer is not available
196
+ nil
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,71 @@
1
+ # lib/smart_message/ddq/base.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ module SmartMessage
6
+ module DDQ
7
+ # Base class for Deduplication Queue implementations
8
+ #
9
+ # Defines the interface that all DDQ storage backends must implement.
10
+ # Provides circular queue semantics with O(1) lookup performance.
11
+ class Base
12
+ attr_reader :size, :logger
13
+
14
+ def initialize(size)
15
+ @size = size
16
+ @logger = SmartMessage::Logger.default
17
+ validate_size!
18
+ end
19
+
20
+ # Check if a UUID exists in the queue
21
+ # @param uuid [String] The UUID to check
22
+ # @return [Boolean] true if UUID exists, false otherwise
23
+ def contains?(uuid)
24
+ raise NotImplementedError, "Subclasses must implement #contains?"
25
+ end
26
+
27
+ # Add a UUID to the queue (removes oldest if full)
28
+ # @param uuid [String] The UUID to add
29
+ # @return [void]
30
+ def add(uuid)
31
+ raise NotImplementedError, "Subclasses must implement #add"
32
+ end
33
+
34
+ # Get current queue statistics
35
+ # @return [Hash] Statistics about the queue
36
+ def stats
37
+ {
38
+ size: @size,
39
+ storage_type: storage_type,
40
+ implementation: self.class.name
41
+ }
42
+ end
43
+
44
+ # Clear all entries from the queue
45
+ # @return [void]
46
+ def clear
47
+ raise NotImplementedError, "Subclasses must implement #clear"
48
+ end
49
+
50
+ # Get the storage type identifier
51
+ # @return [Symbol] Storage type (:memory, :redis, etc.)
52
+ def storage_type
53
+ raise NotImplementedError, "Subclasses must implement #storage_type"
54
+ end
55
+
56
+ private
57
+
58
+ def validate_size!
59
+ unless @size.is_a?(Integer) && @size > 0
60
+ raise ArgumentError, "DDQ size must be a positive integer, got: #{@size.inspect}"
61
+ end
62
+ end
63
+
64
+ def validate_uuid!(uuid)
65
+ unless uuid.is_a?(String) && !uuid.empty?
66
+ raise ArgumentError, "UUID must be a non-empty string, got: #{uuid.inspect}"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,109 @@
1
+ # lib/smart_message/ddq/memory.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require 'set'
6
+ require_relative 'base'
7
+
8
+ module SmartMessage
9
+ module DDQ
10
+ # Memory-based Deduplication Queue implementation
11
+ #
12
+ # Uses a hybrid approach with Array for circular queue behavior
13
+ # and Set for O(1) lookup performance. Thread-safe with Mutex protection.
14
+ class Memory < Base
15
+ def initialize(size)
16
+ super(size)
17
+ @circular_array = Array.new(@size)
18
+ @lookup_set = Set.new
19
+ @index = 0
20
+ @mutex = Mutex.new
21
+ @count = 0
22
+
23
+ logger.debug { "[SmartMessage::DDQ::Memory] Initialized with size: #{@size}" }
24
+ end
25
+
26
+ # Check if a UUID exists in the queue (O(1) operation)
27
+ # @param uuid [String] The UUID to check
28
+ # @return [Boolean] true if UUID exists, false otherwise
29
+ def contains?(uuid)
30
+ validate_uuid!(uuid)
31
+
32
+ @mutex.synchronize do
33
+ @lookup_set.include?(uuid)
34
+ end
35
+ end
36
+
37
+ # Add a UUID to the queue, removing oldest if full (O(1) operation)
38
+ # @param uuid [String] The UUID to add
39
+ # @return [void]
40
+ def add(uuid)
41
+ validate_uuid!(uuid)
42
+
43
+ @mutex.synchronize do
44
+ # Don't add if already exists
45
+ return if @lookup_set.include?(uuid)
46
+
47
+ # Remove old entry if slot is occupied
48
+ old_uuid = @circular_array[@index]
49
+ if old_uuid
50
+ @lookup_set.delete(old_uuid)
51
+ logger.debug { "[SmartMessage::DDQ::Memory] Evicted UUID: #{old_uuid}" }
52
+ end
53
+
54
+ # Add new entry
55
+ @circular_array[@index] = uuid
56
+ @lookup_set.add(uuid)
57
+ @index = (@index + 1) % @size
58
+ @count = [@count + 1, @size].min
59
+
60
+ logger.debug { "[SmartMessage::DDQ::Memory] Added UUID: #{uuid}, count: #{@count}" }
61
+ end
62
+ end
63
+
64
+ # Get current queue statistics
65
+ # @return [Hash] Statistics about the queue
66
+ def stats
67
+ @mutex.synchronize do
68
+ super.merge(
69
+ current_count: @count,
70
+ utilization: (@count.to_f / @size * 100).round(2),
71
+ next_index: @index
72
+ )
73
+ end
74
+ end
75
+
76
+ # Clear all entries from the queue
77
+ # @return [void]
78
+ def clear
79
+ @mutex.synchronize do
80
+ @circular_array = Array.new(@size)
81
+ @lookup_set.clear
82
+ @index = 0
83
+ @count = 0
84
+
85
+ logger.debug { "[SmartMessage::DDQ::Memory] Cleared all entries" }
86
+ end
87
+ end
88
+
89
+ # Get the storage type identifier
90
+ # @return [Symbol] Storage type
91
+ def storage_type
92
+ :memory
93
+ end
94
+
95
+ # Get current entries (for debugging/testing)
96
+ # @return [Array<String>] Current UUIDs in insertion order
97
+ def entries
98
+ @mutex.synchronize do
99
+ result = []
100
+ @count.times do |i|
101
+ idx = (@index - @count + i) % @size
102
+ result << @circular_array[idx] if @circular_array[idx]
103
+ end
104
+ result
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,168 @@
1
+ # lib/smart_message/ddq/redis.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'base'
6
+
7
+ module SmartMessage
8
+ module DDQ
9
+ # Redis-based Deduplication Queue implementation
10
+ #
11
+ # Uses Redis SET for O(1) lookup and LIST for circular queue behavior.
12
+ # Supports distributed deduplication across multiple processes/servers.
13
+ class Redis < Base
14
+ attr_reader :redis, :key_prefix, :ttl
15
+
16
+ def initialize(size, options = {})
17
+ super(size)
18
+ @redis = options[:redis] || default_redis_connection
19
+ @key_prefix = options[:key_prefix] || 'smart_message:ddq'
20
+ @ttl = options[:ttl] || 3600 # 1 hour default TTL
21
+
22
+ validate_redis_connection!
23
+
24
+ logger.debug { "[SmartMessage::DDQ::Redis] Initialized with size: #{@size}, TTL: #{@ttl}s" }
25
+ end
26
+
27
+ # Check if a UUID exists in the queue (O(1) operation)
28
+ # @param uuid [String] The UUID to check
29
+ # @return [Boolean] true if UUID exists, false otherwise
30
+ def contains?(uuid)
31
+ validate_uuid!(uuid)
32
+
33
+ result = @redis.sismember(set_key, uuid)
34
+ logger.debug { "[SmartMessage::DDQ::Redis] UUID #{uuid} exists: #{result}" }
35
+ result
36
+ rescue ::Redis::BaseError => e
37
+ logger.error { "[SmartMessage::DDQ::Redis] Error checking UUID #{uuid}: #{e.message}" }
38
+ # Fail open - allow processing if Redis is down
39
+ false
40
+ end
41
+
42
+ # Add a UUID to the queue, removing oldest if full (O(1) amortized)
43
+ # @param uuid [String] The UUID to add
44
+ # @return [void]
45
+ def add(uuid)
46
+ validate_uuid!(uuid)
47
+
48
+ # Check if UUID already exists first (avoid unnecessary work)
49
+ return if @redis.sismember(set_key, uuid)
50
+
51
+ # Use Redis transaction for atomicity
52
+ @redis.multi do |pipeline|
53
+ # Add to set for O(1) lookup
54
+ pipeline.sadd(set_key, uuid)
55
+
56
+ # Add to list for ordering/eviction
57
+ pipeline.lpush(list_key, uuid)
58
+
59
+ # Trim list to maintain size (removes oldest)
60
+ pipeline.ltrim(list_key, 0, @size - 1)
61
+
62
+ # Set TTL on both keys
63
+ pipeline.expire(set_key, @ttl)
64
+ pipeline.expire(list_key, @ttl)
65
+ end
66
+
67
+ # Get and remove evicted items from set (outside transaction)
68
+ list_length = @redis.llen(list_key)
69
+ if list_length > @size
70
+ evicted_uuids = @redis.lrange(list_key, @size, -1)
71
+ evicted_uuids.each do |evicted_uuid|
72
+ @redis.srem(set_key, evicted_uuid)
73
+ end
74
+ end
75
+
76
+ logger.debug { "[SmartMessage::DDQ::Redis] Added UUID: #{uuid}" }
77
+ rescue ::Redis::BaseError => e
78
+ logger.error { "[SmartMessage::DDQ::Redis] Error adding UUID #{uuid}: #{e.message}" }
79
+ # Don't raise - deduplication failure shouldn't break message processing
80
+ end
81
+
82
+ # Get current queue statistics
83
+ # @return [Hash] Statistics about the queue
84
+ def stats
85
+ set_size = @redis.scard(set_key)
86
+ list_size = @redis.llen(list_key)
87
+
88
+ super.merge(
89
+ current_count: set_size,
90
+ list_count: list_size,
91
+ utilization: (set_size.to_f / @size * 100).round(2),
92
+ ttl_remaining: @redis.ttl(set_key),
93
+ redis_info: {
94
+ host: redis_host,
95
+ port: redis_port,
96
+ db: redis_db
97
+ }
98
+ )
99
+ rescue ::Redis::BaseError => e
100
+ logger.error { "[SmartMessage::DDQ::Redis] Error getting stats: #{e.message}" }
101
+ super.merge(error: e.message)
102
+ end
103
+
104
+ # Clear all entries from the queue
105
+ # @return [void]
106
+ def clear
107
+ @redis.multi do |pipeline|
108
+ pipeline.del(set_key)
109
+ pipeline.del(list_key)
110
+ end
111
+
112
+ logger.debug { "[SmartMessage::DDQ::Redis] Cleared all entries" }
113
+ rescue ::Redis::BaseError => e
114
+ logger.error { "[SmartMessage::DDQ::Redis] Error clearing queue: #{e.message}" }
115
+ end
116
+
117
+ # Get the storage type identifier
118
+ # @return [Symbol] Storage type
119
+ def storage_type
120
+ :redis
121
+ end
122
+
123
+ # Get current entries (for debugging/testing)
124
+ # @return [Array<String>] Current UUIDs in insertion order (newest first)
125
+ def entries
126
+ @redis.lrange(list_key, 0, -1)
127
+ rescue ::Redis::BaseError => e
128
+ logger.error { "[SmartMessage::DDQ::Redis] Error getting entries: #{e.message}" }
129
+ []
130
+ end
131
+
132
+ private
133
+
134
+ def set_key
135
+ "#{@key_prefix}:set"
136
+ end
137
+
138
+ def list_key
139
+ "#{@key_prefix}:list"
140
+ end
141
+
142
+ def default_redis_connection
143
+ require 'redis'
144
+ ::Redis.new
145
+ rescue LoadError
146
+ raise LoadError, "Redis gem not available. Install with: gem install redis"
147
+ end
148
+
149
+ def validate_redis_connection!
150
+ @redis.ping
151
+ rescue ::Redis::BaseError => e
152
+ raise ConnectionError, "Failed to connect to Redis: #{e.message}"
153
+ end
154
+
155
+ def redis_host
156
+ @redis.connection[:host] rescue 'unknown'
157
+ end
158
+
159
+ def redis_port
160
+ @redis.connection[:port] rescue 'unknown'
161
+ end
162
+
163
+ def redis_db
164
+ @redis.connection[:db] rescue 'unknown'
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,31 @@
1
+ # lib/smart_message/ddq.rb
2
+ # encoding: utf-8
3
+ # frozen_string_literal: true
4
+
5
+ require_relative 'ddq/base'
6
+ require_relative 'ddq/memory'
7
+ require_relative 'ddq/redis'
8
+
9
+ module SmartMessage
10
+ # Deduplication Queue (DDQ) for preventing duplicate message processing
11
+ #
12
+ # Provides a circular queue with O(1) lookup performance for detecting
13
+ # duplicate messages based on UUID. Supports both memory and Redis storage.
14
+ module DDQ
15
+ # Default configuration
16
+ DEFAULT_SIZE = 100
17
+ DEFAULT_STORAGE = :memory
18
+
19
+ # Create a DDQ instance based on storage type
20
+ def self.create(storage_type, size = DEFAULT_SIZE, options = {})
21
+ case storage_type.to_sym
22
+ when :memory
23
+ Memory.new(size)
24
+ when :redis
25
+ Redis.new(size, options)
26
+ else
27
+ raise ArgumentError, "Unknown DDQ storage type: #{storage_type}. Supported types: :memory, :redis"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -26,20 +26,35 @@ module SmartMessage
26
26
  @file_path = File.expand_path(file_path)
27
27
  @mutex = Mutex.new
28
28
  ensure_directory_exists
29
+
30
+ logger.debug { "[SmartMessage::DeadLetterQueue] Initialized with file path: #{@file_path}" }
31
+ rescue => e
32
+ logger&.error { "[SmartMessage] Error in dead letter queue initialization: #{e.class.name} - #{e.message}" }
33
+ raise
34
+ end
35
+
36
+ private
37
+
38
+ def logger
39
+ @logger ||= SmartMessage::Logger.default
29
40
  end
41
+
42
+ public
30
43
 
31
44
  # Core FIFO queue operations
32
45
 
33
46
  # 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
47
+ # @param message [SmartMessage::Base] The message instance
36
48
  # @param error_info [Hash] Error details including :error, :retry_count, :transport, etc.
37
- def enqueue(message_header, message_payload, error_info = {})
49
+ def enqueue(message, error_info = {})
50
+ message_header = message._sm_header
51
+ message_payload = message.encode
52
+
38
53
  entry = {
39
54
  timestamp: Time.now.iso8601,
40
55
  header: message_header.to_hash,
41
56
  payload: message_payload,
42
- payload_format: error_info[:serializer] || 'json', # Track serialization format
57
+ payload_format: error_info[:serializer] || 'json',
43
58
  error: error_info[:error] || 'Unknown error',
44
59
  retry_count: error_info[:retry_count] || 0,
45
60
  transport: error_info[:transport],
@@ -77,7 +92,7 @@ module SmartMessage
77
92
  oldest_entry
78
93
  end
79
94
  rescue JSON::ParserError => e
80
- puts "Warning: Corrupted DLQ entry skipped: #{e.message}" if $DEBUG
95
+ logger.warn { "[SmartMessage] Warning: Corrupted DLQ entry skipped: #{e.message}" }
81
96
  nil
82
97
  end
83
98
 
@@ -209,8 +224,10 @@ module SmartMessage
209
224
  read_entries_with_filter do |entry|
210
225
  stats[:total] += 1
211
226
 
212
- message_class = entry.dig(:header, :message_class) || 'Unknown'
213
- stats[:by_class][message_class] += 1
227
+ full_class_name = entry.dig(:header, :message_class) || 'Unknown'
228
+ # Extract short class name (everything after the last ::)
229
+ short_class_name = full_class_name.split('::').last || full_class_name
230
+ stats[:by_class][short_class_name] += 1
214
231
 
215
232
  error = entry[:error] || 'Unknown error'
216
233
  stats[:by_error][error] += 1
@@ -286,11 +303,11 @@ module SmartMessage
286
303
  JSON.parse(payload, symbolize_names: true)
287
304
  else
288
305
  # For unknown formats, assume JSON as fallback but log warning
289
- puts "Warning: Unknown payload format '#{format}', attempting JSON" if $DEBUG
306
+ logger.warn { "[SmartMessage] Warning: Unknown payload format '#{format}', attempting JSON" }
290
307
  JSON.parse(payload, symbolize_names: true)
291
308
  end
292
309
  rescue JSON::ParserError => e
293
- puts "Error: Failed to deserialize payload as #{format}: #{e.message}" if $DEBUG
310
+ logger.error { "[SmartMessage] Error in payload deserialization: #{e.class.name} - #{e.message}" }
294
311
  nil
295
312
  end
296
313
 
@@ -310,7 +327,7 @@ module SmartMessage
310
327
  message._sm_header.to = header_data[:to] if header_data[:to]
311
328
  message._sm_header.reply_to = header_data[:reply_to] if header_data[:reply_to]
312
329
  rescue => e
313
- puts "Warning: Failed to restore some header fields: #{e.message}" if $DEBUG
330
+ logger.warn { "[SmartMessage] Warning: Failed to restore some header fields: #{e.message}" }
314
331
  end
315
332
 
316
333
  # Generic file reading iterator with error handling