prosody 0.1.1
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 +7 -0
- data/.cargo/config.toml +2 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.standard.yml +9 -0
- data/.taplo.toml +6 -0
- data/ARCHITECTURE.md +591 -0
- data/CHANGELOG.md +92 -0
- data/Cargo.lock +3513 -0
- data/Cargo.toml +77 -0
- data/LICENSE +21 -0
- data/Makefile +36 -0
- data/README.md +946 -0
- data/Rakefile +26 -0
- data/ext/prosody/Cargo.toml +38 -0
- data/ext/prosody/extconf.rb +6 -0
- data/ext/prosody/src/admin.rs +171 -0
- data/ext/prosody/src/bridge/callback.rs +60 -0
- data/ext/prosody/src/bridge/mod.rs +332 -0
- data/ext/prosody/src/client/config.rs +819 -0
- data/ext/prosody/src/client/mod.rs +379 -0
- data/ext/prosody/src/gvl.rs +149 -0
- data/ext/prosody/src/handler/context.rs +436 -0
- data/ext/prosody/src/handler/message.rs +144 -0
- data/ext/prosody/src/handler/mod.rs +338 -0
- data/ext/prosody/src/handler/trigger.rs +93 -0
- data/ext/prosody/src/lib.rs +82 -0
- data/ext/prosody/src/logging.rs +353 -0
- data/ext/prosody/src/scheduler/cancellation.rs +67 -0
- data/ext/prosody/src/scheduler/handle.rs +50 -0
- data/ext/prosody/src/scheduler/mod.rs +169 -0
- data/ext/prosody/src/scheduler/processor.rs +166 -0
- data/ext/prosody/src/scheduler/result.rs +197 -0
- data/ext/prosody/src/tracing_util.rs +56 -0
- data/ext/prosody/src/util.rs +219 -0
- data/lib/prosody/configuration.rb +333 -0
- data/lib/prosody/handler.rb +177 -0
- data/lib/prosody/native_stubs.rb +417 -0
- data/lib/prosody/processor.rb +321 -0
- data/lib/prosody/sentry.rb +36 -0
- data/lib/prosody/version.rb +10 -0
- data/lib/prosody.rb +42 -0
- data/release-please-config.json +10 -0
- data/sig/configuration.rbs +252 -0
- data/sig/handler.rbs +79 -0
- data/sig/processor.rbs +100 -0
- data/sig/prosody.rbs +171 -0
- data/sig/version.rbs +9 -0
- metadata +193 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosody
|
|
4
|
+
# Configuration class for the Prosody messaging client.
|
|
5
|
+
#
|
|
6
|
+
# This class provides a flexible configuration system for the Prosody client,
|
|
7
|
+
# with strong typing, validation, and convenient default values. It handles
|
|
8
|
+
# various parameter types including strings, arrays, durations, integers,
|
|
9
|
+
# and special case configurations like operation mode and health probe settings.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# config = Prosody::Configuration.new(
|
|
13
|
+
# bootstrap_servers: "kafka:9092",
|
|
14
|
+
# group_id: "my-consumer-group"
|
|
15
|
+
# )
|
|
16
|
+
#
|
|
17
|
+
# @example With block syntax
|
|
18
|
+
# config = Prosody::Configuration.new do |c|
|
|
19
|
+
# c.bootstrap_servers = ["kafka1:9092", "kafka2:9092"]
|
|
20
|
+
# c.group_id = "my-consumer-group"
|
|
21
|
+
# c.max_concurrency = 10
|
|
22
|
+
# end
|
|
23
|
+
class Configuration
|
|
24
|
+
# Defines a configuration parameter with type conversion and optional default value.
|
|
25
|
+
#
|
|
26
|
+
# This DSL helper creates getter and setter methods for a configuration parameter,
|
|
27
|
+
# with the setter applying type conversion. The getter returns the default value
|
|
28
|
+
# when the parameter hasn't been set.
|
|
29
|
+
#
|
|
30
|
+
# @param name [Symbol, String] The parameter name
|
|
31
|
+
# @param converter [Proc] A lambda that converts and validates the input value
|
|
32
|
+
# @param default [Object, nil] The default value returned when parameter is unset
|
|
33
|
+
# @return [void]
|
|
34
|
+
def self.config_param(name, converter: ->(v) { v }, default: nil)
|
|
35
|
+
name_sym = name.to_sym
|
|
36
|
+
define_method(name) { @config.key?(name_sym) ? @config[name_sym] : default }
|
|
37
|
+
define_method(:"#{name}=") { |value| @config[name_sym] = value.nil? ? nil : converter.call(value) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Converts a duration value to float seconds.
|
|
41
|
+
#
|
|
42
|
+
# Handles ActiveSupport::Duration objects (if available) and numeric values,
|
|
43
|
+
# converting them to floating-point seconds.
|
|
44
|
+
#
|
|
45
|
+
# @param v [Numeric, ActiveSupport::Duration] The duration value to convert
|
|
46
|
+
# @return [Float] The duration in seconds as a float
|
|
47
|
+
# @raise [ArgumentError] If the input is not a valid duration type
|
|
48
|
+
def self.duration_converter(v)
|
|
49
|
+
if defined?(ActiveSupport::Duration) && v.is_a?(ActiveSupport::Duration)
|
|
50
|
+
v.to_f
|
|
51
|
+
elsif v.is_a?(Numeric)
|
|
52
|
+
v.to_f
|
|
53
|
+
else
|
|
54
|
+
raise ArgumentError, "Invalid type for duration: #{v.inspect}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Initializes a new Configuration instance.
|
|
59
|
+
#
|
|
60
|
+
# @param kwargs [Hash] Initial configuration parameters as keyword arguments
|
|
61
|
+
# @yield [self] Yields self for block-based configuration
|
|
62
|
+
# @return [Configuration] The new configuration instance
|
|
63
|
+
def initialize(kwargs = {})
|
|
64
|
+
@config = {}
|
|
65
|
+
kwargs.each { |k, v| public_send(:"#{k}=", v) }
|
|
66
|
+
yield self if block_given?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# List of Kafka bootstrap server addresses.
|
|
70
|
+
# Accepts a string (single server) or array of strings (multiple servers).
|
|
71
|
+
config_param :bootstrap_servers,
|
|
72
|
+
converter: lambda { |v|
|
|
73
|
+
if v.is_a?(String)
|
|
74
|
+
[v.to_s]
|
|
75
|
+
elsif v.respond_to?(:to_a)
|
|
76
|
+
v.to_a.map(&:to_s)
|
|
77
|
+
else
|
|
78
|
+
raise ArgumentError, "Invalid type for bootstrap_servers"
|
|
79
|
+
end
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# List of Kafka topics to subscribe to.
|
|
83
|
+
# Accepts a string (single topic) or array of strings (multiple topics).
|
|
84
|
+
config_param :subscribed_topics,
|
|
85
|
+
converter: lambda { |v|
|
|
86
|
+
if v.is_a?(String)
|
|
87
|
+
[v.to_s]
|
|
88
|
+
elsif v.respond_to?(:to_a)
|
|
89
|
+
v.to_a.map(&:to_s)
|
|
90
|
+
else
|
|
91
|
+
raise ArgumentError, "Invalid type for subscribed_topics"
|
|
92
|
+
end
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# List of event types the consumer is allowed to process.
|
|
96
|
+
# Accepts a string (single event type) or array of strings (multiple event types).
|
|
97
|
+
config_param :allowed_events,
|
|
98
|
+
converter: lambda { |v|
|
|
99
|
+
if v.is_a?(String)
|
|
100
|
+
[v.to_s]
|
|
101
|
+
elsif v.respond_to?(:to_a)
|
|
102
|
+
v.to_a.map(&:to_s)
|
|
103
|
+
else
|
|
104
|
+
raise ArgumentError, "Invalid type for allowed_events"
|
|
105
|
+
end
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Whether to use mock mode (for testing).
|
|
109
|
+
# When enabled, creates a mock Kafka implementation instead of connecting to real servers.
|
|
110
|
+
config_param :mock, converter: ->(v) { v.nil? ? nil : !!v }
|
|
111
|
+
|
|
112
|
+
# Maximum time to wait for a send operation to complete (in seconds).
|
|
113
|
+
config_param :send_timeout, converter: ->(v) { duration_converter(v) }
|
|
114
|
+
|
|
115
|
+
# Threshold in seconds after which a stalled consumer is detected.
|
|
116
|
+
config_param :stall_threshold, converter: ->(v) { duration_converter(v) }
|
|
117
|
+
|
|
118
|
+
# Shutdown budget; handlers run freely until cancellation fires near the end of the timeout.
|
|
119
|
+
config_param :shutdown_timeout, converter: ->(v) { duration_converter(v) }
|
|
120
|
+
|
|
121
|
+
# Interval between Kafka poll operations (in seconds).
|
|
122
|
+
config_param :poll_interval, converter: ->(v) { duration_converter(v) }
|
|
123
|
+
|
|
124
|
+
# Interval between offset commit operations (in seconds).
|
|
125
|
+
config_param :commit_interval, converter: ->(v) { duration_converter(v) }
|
|
126
|
+
|
|
127
|
+
# Base delay for retry operations (in seconds).
|
|
128
|
+
config_param :retry_base, converter: ->(v) { duration_converter(v) }
|
|
129
|
+
|
|
130
|
+
# Maximum delay between retries (in seconds).
|
|
131
|
+
config_param :max_retry_delay, converter: ->(v) { duration_converter(v) }
|
|
132
|
+
|
|
133
|
+
# Global shared cache capacity across all partitions for message deduplication.
|
|
134
|
+
# Default 8192. Set to 0 to disable deduplication entirely.
|
|
135
|
+
config_param :idempotence_cache_size, converter: ->(v) { Integer(v) }
|
|
136
|
+
|
|
137
|
+
# Version string for cache-busting deduplication hashes. Changing this
|
|
138
|
+
# invalidates all previously recorded dedup entries.
|
|
139
|
+
config_param :idempotence_version, converter: lambda(&:to_s)
|
|
140
|
+
|
|
141
|
+
# TTL for deduplication records in Cassandra.
|
|
142
|
+
config_param :idempotence_ttl, converter: ->(v) { duration_converter(v) }
|
|
143
|
+
|
|
144
|
+
# Maximum number of concurrent message processing tasks.
|
|
145
|
+
config_param :max_concurrency, converter: ->(v) { Integer(v) }
|
|
146
|
+
|
|
147
|
+
# Maximum number of messages to process before committing offsets.
|
|
148
|
+
config_param :max_uncommitted, converter: ->(v) { Integer(v) }
|
|
149
|
+
|
|
150
|
+
# Maximum number of retry attempts.
|
|
151
|
+
config_param :max_retries, converter: ->(v) { Integer(v) }
|
|
152
|
+
|
|
153
|
+
# Kafka consumer group ID.
|
|
154
|
+
config_param :group_id, converter: lambda(&:to_s)
|
|
155
|
+
|
|
156
|
+
# Identifier for the system producing messages.
|
|
157
|
+
config_param :source_system, converter: lambda(&:to_s)
|
|
158
|
+
|
|
159
|
+
# Topic to send failed messages to.
|
|
160
|
+
config_param :failure_topic, converter: lambda(&:to_s)
|
|
161
|
+
|
|
162
|
+
# List of Cassandra contact nodes (hostnames or IPs).
|
|
163
|
+
# Accepts a string (single node) or array of strings (multiple nodes).
|
|
164
|
+
config_param :cassandra_nodes,
|
|
165
|
+
converter: lambda { |v|
|
|
166
|
+
if v.is_a?(String)
|
|
167
|
+
[v.to_s]
|
|
168
|
+
elsif v.respond_to?(:to_a)
|
|
169
|
+
v.to_a.map(&:to_s)
|
|
170
|
+
else
|
|
171
|
+
raise ArgumentError, "Invalid type for cassandra_nodes"
|
|
172
|
+
end
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Keyspace to use for storing timer data in Cassandra.
|
|
176
|
+
config_param :cassandra_keyspace, converter: lambda(&:to_s)
|
|
177
|
+
|
|
178
|
+
# Preferred datacenter for Cassandra query routing.
|
|
179
|
+
config_param :cassandra_datacenter, converter: lambda(&:to_s)
|
|
180
|
+
|
|
181
|
+
# Preferred rack identifier for Cassandra topology-aware routing.
|
|
182
|
+
config_param :cassandra_rack, converter: lambda(&:to_s)
|
|
183
|
+
|
|
184
|
+
# Username for authenticating with Cassandra.
|
|
185
|
+
config_param :cassandra_user, converter: lambda(&:to_s)
|
|
186
|
+
|
|
187
|
+
# Password for authenticating with Cassandra.
|
|
188
|
+
config_param :cassandra_password, converter: lambda(&:to_s)
|
|
189
|
+
|
|
190
|
+
# Retention period for failed/unprocessed timer data in Cassandra.
|
|
191
|
+
# Accepts duration objects or numeric values (in seconds).
|
|
192
|
+
config_param :cassandra_retention, converter: ->(v) { duration_converter(v) }
|
|
193
|
+
|
|
194
|
+
# Timer slab partitioning duration in seconds.
|
|
195
|
+
# Controls how timers are grouped for storage and retrieval.
|
|
196
|
+
config_param :slab_size, converter: ->(v) { duration_converter(v) }
|
|
197
|
+
|
|
198
|
+
# Scheduler configuration
|
|
199
|
+
#
|
|
200
|
+
# Target proportion of execution time for failure/retry task processing (0.0 to 1.0).
|
|
201
|
+
# Controls bandwidth allocation between Normal and Failure task classes.
|
|
202
|
+
config_param :scheduler_failure_weight, converter: ->(v) { Float(v) }
|
|
203
|
+
|
|
204
|
+
# Wait duration at which urgency boost reaches maximum intensity (in seconds).
|
|
205
|
+
config_param :scheduler_max_wait, converter: ->(v) { duration_converter(v) }
|
|
206
|
+
|
|
207
|
+
# Maximum urgency boost (in seconds of virtual time) for waiting tasks.
|
|
208
|
+
config_param :scheduler_wait_weight, converter: ->(v) { Float(v) }
|
|
209
|
+
|
|
210
|
+
# Cache capacity for tracking per-key virtual time in the scheduler.
|
|
211
|
+
config_param :scheduler_cache_size, converter: ->(v) { Integer(v) }
|
|
212
|
+
|
|
213
|
+
# Monopolization configuration
|
|
214
|
+
#
|
|
215
|
+
# Whether monopolization detection is enabled.
|
|
216
|
+
config_param :monopolization_enabled, converter: ->(v) { v.nil? ? nil : !!v }
|
|
217
|
+
|
|
218
|
+
# Threshold for monopolization detection (0.0 to 1.0).
|
|
219
|
+
config_param :monopolization_threshold, converter: ->(v) { Float(v) }
|
|
220
|
+
|
|
221
|
+
# Rolling window duration (in seconds) for monopolization detection.
|
|
222
|
+
config_param :monopolization_window, converter: ->(v) { duration_converter(v) }
|
|
223
|
+
|
|
224
|
+
# Cache size for tracking key execution intervals.
|
|
225
|
+
config_param :monopolization_cache_size, converter: ->(v) { Integer(v) }
|
|
226
|
+
|
|
227
|
+
# Defer configuration
|
|
228
|
+
#
|
|
229
|
+
# Whether deferral is enabled for new messages.
|
|
230
|
+
config_param :defer_enabled, converter: ->(v) { v.nil? ? nil : !!v }
|
|
231
|
+
|
|
232
|
+
# Base exponential backoff delay for deferred retries (in seconds).
|
|
233
|
+
config_param :defer_base, converter: ->(v) { duration_converter(v) }
|
|
234
|
+
|
|
235
|
+
# Maximum delay between deferred retries (in seconds).
|
|
236
|
+
config_param :defer_max_delay, converter: ->(v) { duration_converter(v) }
|
|
237
|
+
|
|
238
|
+
# Failure rate threshold for enabling deferral (0.0 to 1.0).
|
|
239
|
+
config_param :defer_failure_threshold, converter: ->(v) { Float(v) }
|
|
240
|
+
|
|
241
|
+
# Sliding window duration (in seconds) for failure rate tracking.
|
|
242
|
+
config_param :defer_failure_window, converter: ->(v) { duration_converter(v) }
|
|
243
|
+
|
|
244
|
+
# Cache size for defer middleware.
|
|
245
|
+
config_param :defer_cache_size, converter: ->(v) { Integer(v) }
|
|
246
|
+
|
|
247
|
+
# Timeout for Kafka seek operations (in seconds).
|
|
248
|
+
config_param :defer_seek_timeout, converter: ->(v) { duration_converter(v) }
|
|
249
|
+
|
|
250
|
+
# Messages to read sequentially before seeking.
|
|
251
|
+
config_param :defer_discard_threshold, converter: ->(v) { Integer(v) }
|
|
252
|
+
|
|
253
|
+
# Timeout configuration
|
|
254
|
+
#
|
|
255
|
+
# Fixed timeout duration for handler execution (in seconds).
|
|
256
|
+
# If unset, defaults to 80% of stall_threshold.
|
|
257
|
+
config_param :timeout, converter: ->(v) { duration_converter(v) }
|
|
258
|
+
|
|
259
|
+
# Telemetry emitter configuration
|
|
260
|
+
#
|
|
261
|
+
# Kafka topic to produce internal telemetry events to.
|
|
262
|
+
# Overrides the PROSODY_TELEMETRY_TOPIC environment variable.
|
|
263
|
+
config_param :telemetry_topic, converter: lambda(&:to_s)
|
|
264
|
+
|
|
265
|
+
# Whether the telemetry emitter is enabled.
|
|
266
|
+
# Overrides the PROSODY_TELEMETRY_ENABLED environment variable.
|
|
267
|
+
config_param :telemetry_enabled, converter: ->(v) { v.nil? ? nil : !!v }
|
|
268
|
+
|
|
269
|
+
# Span linking for message execution spans.
|
|
270
|
+
# Controls how the receive span connects to the OTel context from the Kafka producer.
|
|
271
|
+
# Accepted values: "child" (child-of relationship) or "follows_from".
|
|
272
|
+
# Overrides the PROSODY_MESSAGE_SPANS environment variable. Default: "child".
|
|
273
|
+
config_param :message_spans, converter: ->(v) { v.to_s }
|
|
274
|
+
|
|
275
|
+
# Span linking for timer execution spans.
|
|
276
|
+
# Controls how timer spans connect to the OTel context stored at schedule time.
|
|
277
|
+
# Accepted values: "child" (child-of relationship) or "follows_from".
|
|
278
|
+
# Overrides the PROSODY_TIMER_SPANS environment variable. Default: "follows_from".
|
|
279
|
+
config_param :timer_spans, converter: ->(v) { v.to_s }
|
|
280
|
+
|
|
281
|
+
# Operation mode of the client.
|
|
282
|
+
#
|
|
283
|
+
# Valid values:
|
|
284
|
+
# - "pipeline" or :pipeline - Prioritizes throughput and ordering
|
|
285
|
+
# - "low_latency" or :low_latency - Prioritizes latency over throughput
|
|
286
|
+
# - "best_effort" or :best_effort - Makes best effort tradeoffs
|
|
287
|
+
config_param :mode,
|
|
288
|
+
converter: lambda { |v|
|
|
289
|
+
return nil if v.nil?
|
|
290
|
+
|
|
291
|
+
s = v.to_s.downcase
|
|
292
|
+
case s
|
|
293
|
+
when "pipeline"
|
|
294
|
+
:pipeline
|
|
295
|
+
when "low_latency"
|
|
296
|
+
:low_latency
|
|
297
|
+
when "best_effort"
|
|
298
|
+
:best_effort
|
|
299
|
+
else
|
|
300
|
+
raise ArgumentError, "Invalid mode: #{v}"
|
|
301
|
+
end
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Configuration for the health probe port.
|
|
305
|
+
#
|
|
306
|
+
# Accepts:
|
|
307
|
+
# - nil (uses default configuration)
|
|
308
|
+
# - false or :disabled (explicitly disables the probe port)
|
|
309
|
+
# - Port number (0-65535) to use for the health probe
|
|
310
|
+
config_param :probe_port,
|
|
311
|
+
converter: lambda { |v|
|
|
312
|
+
if v.nil?
|
|
313
|
+
nil
|
|
314
|
+
elsif v == false
|
|
315
|
+
false
|
|
316
|
+
elsif (v.is_a?(Symbol) || v.is_a?(String)) && v.to_sym == :disabled
|
|
317
|
+
false
|
|
318
|
+
else
|
|
319
|
+
port = Integer(v)
|
|
320
|
+
raise ArgumentError, "Invalid port number" unless port.between?(0, 65_535)
|
|
321
|
+
|
|
322
|
+
port
|
|
323
|
+
end
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# Returns a Ruby hash with only non-nil values, suitable for Rust deserialization.
|
|
327
|
+
#
|
|
328
|
+
# @return [Hash] Configuration hash with all nil values removed
|
|
329
|
+
def to_hash
|
|
330
|
+
@config.dup.compact
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prosody
|
|
4
|
+
# Base error class for all Prosody-specific exceptions.
|
|
5
|
+
#
|
|
6
|
+
# Specific error types may extend this class to provide more detailed
|
|
7
|
+
# error information and classification.
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
|
|
10
|
+
# --------------------------------------------------------------------------
|
|
11
|
+
# 1) Base error classes with a `#permanent?` contract
|
|
12
|
+
# --------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
# Abstract base for all errors raised by EventHandler methods.
|
|
15
|
+
# Subclasses **must** implement `#permanent?` to indicate retry behavior.
|
|
16
|
+
#
|
|
17
|
+
# @abstract
|
|
18
|
+
class EventHandlerError < Error
|
|
19
|
+
# Indicates whether this error is permanent (no retry) or
|
|
20
|
+
# transient (retryable).
|
|
21
|
+
#
|
|
22
|
+
# @return [Boolean] true if permanent (no retry), false if transient (retryable)
|
|
23
|
+
# @raise [NotImplementedError] if not overridden by subclass
|
|
24
|
+
def permanent?
|
|
25
|
+
raise NotImplementedError, "#{self.class} must implement #permanent?"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Error indicating that the failure is temporary and can be retried.
|
|
30
|
+
#
|
|
31
|
+
# @see EventHandlerError#permanent?
|
|
32
|
+
class TransientError < EventHandlerError
|
|
33
|
+
# @return [false] indicates this error is retryable
|
|
34
|
+
def permanent? = false
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Error indicating that the failure is permanent and should not be retried.
|
|
38
|
+
#
|
|
39
|
+
# @see EventHandlerError#permanent?
|
|
40
|
+
class PermanentError < EventHandlerError
|
|
41
|
+
# @return [true] indicates this error is not retryable
|
|
42
|
+
def permanent? = true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# --------------------------------------------------------------------------
|
|
46
|
+
# 2) Mixin that provides "decorators" for wrapping methods
|
|
47
|
+
# --------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
# Mixin providing class-level methods to wrap instance methods so that
|
|
50
|
+
# specified exceptions are re-wrapped as PermanentError or TransientError.
|
|
51
|
+
#
|
|
52
|
+
# @example
|
|
53
|
+
# class MyHandler < Prosody::EventHandler
|
|
54
|
+
# extend Prosody::ErrorClassification
|
|
55
|
+
#
|
|
56
|
+
# # Treat TypeError as permanent (no retry)
|
|
57
|
+
# permanent :on_message, TypeError
|
|
58
|
+
#
|
|
59
|
+
# # Treat JSON::ParserError as transient (retryable)
|
|
60
|
+
# transient :on_message, JSON::ParserError
|
|
61
|
+
#
|
|
62
|
+
# def on_message(context, message)
|
|
63
|
+
# # Process the message...
|
|
64
|
+
# end
|
|
65
|
+
# end
|
|
66
|
+
module ErrorClassification
|
|
67
|
+
# Wraps the given instance method so that specified exception types
|
|
68
|
+
# are caught and re-raised as Prosody::PermanentError.
|
|
69
|
+
#
|
|
70
|
+
# @param [Symbol] method_name the name of the method to wrap
|
|
71
|
+
# @param [Class<Exception>] exception_classes one or more Exception subclasses to catch
|
|
72
|
+
# @return [void]
|
|
73
|
+
# @raise [ArgumentError] if no exception classes given
|
|
74
|
+
# @raise [NameError] if method_name is not defined on this class or its ancestors
|
|
75
|
+
def permanent(method_name, *exception_classes)
|
|
76
|
+
wrap_errors(method_name, exception_classes, PermanentError)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Wraps the given instance method so that specified exception types
|
|
80
|
+
# are caught and re-raised as Prosody::TransientError.
|
|
81
|
+
#
|
|
82
|
+
# @param [Symbol] method_name the name of the method to wrap
|
|
83
|
+
# @param [Class<Exception>] exception_classes one or more Exception subclasses to catch
|
|
84
|
+
# @return [void]
|
|
85
|
+
# @raise [ArgumentError] if no exception classes given
|
|
86
|
+
# @raise [NameError] if method_name is not defined on this class or its ancestors
|
|
87
|
+
def transient(method_name, *exception_classes)
|
|
88
|
+
wrap_errors(method_name, exception_classes, TransientError)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
# Core implementation: prepends a module that defines the same method name,
|
|
94
|
+
# rescuing the specified exceptions and re-raising them as the given error_class.
|
|
95
|
+
#
|
|
96
|
+
# @param [Symbol] method_name the method to wrap
|
|
97
|
+
# @param [Array<Class<Exception>>] exception_classes exceptions to catch
|
|
98
|
+
# @param [Class<EventHandlerError>] error_class the error class to wrap caught exceptions in
|
|
99
|
+
# @return [void]
|
|
100
|
+
def wrap_errors(method_name, exception_classes, error_class)
|
|
101
|
+
# Must specify at least one exception class
|
|
102
|
+
if exception_classes.empty?
|
|
103
|
+
raise ArgumentError, "At least one exception class must be provided"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Ensure the method exists (in this class or its ancestors)
|
|
107
|
+
unless method_defined?(method_name)
|
|
108
|
+
raise NameError, "Method `#{method_name}` is not defined"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Build a prepended wrapper module
|
|
112
|
+
wrapper = Module.new do
|
|
113
|
+
define_method(method_name) do |*args, &block|
|
|
114
|
+
super(*args, &block)
|
|
115
|
+
rescue *exception_classes => e
|
|
116
|
+
# The new exception's #cause will be set automatically
|
|
117
|
+
raise error_class.new(e.message)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
prepend wrapper
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# --------------------------------------------------------------------------
|
|
126
|
+
# 3) Base EventHandler that users will subclass
|
|
127
|
+
# --------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
# Abstract base class for handling incoming messages and timers from Prosody.
|
|
130
|
+
# Subclasses **must** implement `#on_message` to process received messages.
|
|
131
|
+
# Subclasses **may** implement `#on_timer` to process timer events.
|
|
132
|
+
# They may also use `permanent` or `transient` decorators to control retry logic.
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# class MyHandler < Prosody::EventHandler
|
|
136
|
+
# extend Prosody::ErrorClassification
|
|
137
|
+
#
|
|
138
|
+
# permanent :on_message, ArgumentError
|
|
139
|
+
# transient :on_message, RuntimeError
|
|
140
|
+
# permanent :on_timer, ArgumentError
|
|
141
|
+
# transient :on_timer, RuntimeError
|
|
142
|
+
#
|
|
143
|
+
# def on_message(context, message)
|
|
144
|
+
# # Process message...
|
|
145
|
+
# end
|
|
146
|
+
#
|
|
147
|
+
# def on_timer(context, trigger)
|
|
148
|
+
# # Process timer event...
|
|
149
|
+
# end
|
|
150
|
+
# end
|
|
151
|
+
class EventHandler
|
|
152
|
+
extend ErrorClassification
|
|
153
|
+
|
|
154
|
+
# Process a single message received from Prosody.
|
|
155
|
+
# This method must be implemented by subclasses to define
|
|
156
|
+
# custom message handling logic.
|
|
157
|
+
#
|
|
158
|
+
# @param [Context] context the message context
|
|
159
|
+
# @param [Message] message the message payload
|
|
160
|
+
# @raise [NotImplementedError] if not overridden by subclass
|
|
161
|
+
# @return [void]
|
|
162
|
+
def on_message(context, message)
|
|
163
|
+
raise NotImplementedError, "Subclasses must implement #on_message"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Process a timer event when it fires.
|
|
167
|
+
# This method must be implemented by subclasses to handle
|
|
168
|
+
# scheduled timer events if they can fire.
|
|
169
|
+
#
|
|
170
|
+
# @param [Context] context the event context
|
|
171
|
+
# @param [Timer] timer the timer containing key, time, and span
|
|
172
|
+
# @return [void]
|
|
173
|
+
def on_timer(context, timer)
|
|
174
|
+
raise NotImplementedError, "Subclasses must implement #on_timer"
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|