prosody 0.5.0-aarch64-linux → 0.6.0-aarch64-linux

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.
@@ -3,7 +3,7 @@
3
3
  require "prosody"
4
4
  require "logger"
5
5
 
6
- # Keyed-state example: per-key value/map/deque collections that survive across
6
+ # Keyed-state example: per-key value/map/set/deque collections that survive across
7
7
  # events. Definitions are declared once and reused for both registration (on the
8
8
  # client) and binding (inside the handler). Every state op yields the fiber,
9
9
  # never the thread.
@@ -12,6 +12,7 @@ require "logger"
12
12
  # keyed_state.rbs for payload and state types checked by Steep.
13
13
  CART = Prosody.value("cart", ttl: 30 * 24 * 3600) # ValueState
14
14
  TOTALS = Prosody.map("totals") # keys are always String
15
+ SEEN = Prosody.set("seen", ttl: 7 * 24 * 3600) # String members, no payload
15
16
  BACKLOG = Prosody.message_deque("backlog", capacity: 100) # bounded window of messages
16
17
 
17
18
  class KeyedStateHandler < Prosody::EventHandler
@@ -19,6 +20,7 @@ class KeyedStateHandler < Prosody::EventHandler
19
20
  puts "Excise #{message.key}"
20
21
  context.state(CART).clear
21
22
  context.state(TOTALS).clear
23
+ context.state(SEEN).clear
22
24
  context.state(BACKLOG).clear
23
25
  nil
24
26
  end
@@ -29,6 +31,10 @@ class KeyedStateHandler < Prosody::EventHandler
29
31
 
30
32
  def on_message(context, message)
31
33
  payload = message.payload
34
+ seen = context.state(SEEN)
35
+ return if seen.include?(payload["order_id"]) # skip an order seen before
36
+ seen << payload["order_id"]
37
+
32
38
  cart = context.state(CART) # bound for this attempt only
33
39
  current = cart.get || {"items" => []} # Hash, or nil when absent
34
40
  cart.set(current.merge("items" => current["items"] + [payload["order_id"]]))
@@ -37,6 +43,8 @@ class KeyedStateHandler < Prosody::EventHandler
37
43
  totals.set(message.key, payload["total"])
38
44
  # Steep infers key as String and total as Integer from TOTALS's RBS type.
39
45
  totals.each_pair { |key, total| @logger.info(format_total(key, total)) }
46
+ # Query keywords narrow a traversal: here, the first ten keys after this one.
47
+ totals.each_key(after: message.key, limit: 10) { |key| @logger.info("next key: #{key}") }
40
48
 
41
49
  backlog = context.state(BACKLOG)
42
50
  backlog.push(message) # stores the full Prosody::Message
@@ -63,7 +71,7 @@ if __FILE__ == $PROGRAM_NAME
63
71
  mock: true,
64
72
  group_id: "keyed-state-example",
65
73
  subscribed_topics: "orders",
66
- state_collections: [CART, TOTALS, BACKLOG]
74
+ state_collections: [CART, TOTALS, SEEN, BACKLOG]
67
75
  )
68
76
  client.subscribe(KeyedStateHandler.new(logger: Logger.new($stdout)))
69
77
  client.shutdown
@@ -3,6 +3,7 @@ type keyed_state_order_event = { "order_id" => String, "total" => Integer }
3
3
 
4
4
  CART: Prosody::_ValueDefinition[keyed_state_cart]
5
5
  TOTALS: Prosody::_MapDefinition[Integer]
6
+ SEEN: Prosody::_SetDefinition
6
7
  BACKLOG: Prosody::_MessageDequeDefinition[keyed_state_order_event]
7
8
 
8
9
  class KeyedStateHandler < Prosody::EventHandler[keyed_state_order_event]
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/prosody/3.2/prosody.so
3
- size: 35858144 bytes
4
- sha256: 82bf4030a8a814044822b2ed39fc8ceeb38f73ec8084593f46fe2a235c240132
3
+ size: 37148304 bytes
4
+ sha256: 59593e6c5f8d787d1ef88b0da37e4ea9e89362f2dee9a71f68c2b331a066e5d7
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/prosody/3.3/prosody.so
3
- size: 35841368 bytes
4
- sha256: c6a206ba1cab3f02dab0057d38a73393d06e7c723edc38ddbb2914ddc3e9a289
3
+ size: 37160880 bytes
4
+ sha256: b80346fb441d4f421b17cfa6cb1d12fdf0aa5f9cd34a81ae86537dd8ad0e1b92
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: data/lib/prosody/3.4/prosody.so
3
- size: 35849680 bytes
4
- sha256: 2baed460b6cb7fc66d07ae87ef568d6517a5232aa48f74d289c48a1b63604229
3
+ size: 37160744 bytes
4
+ sha256: a37eb1bfbf9826d6bc07d86a05211a7c7580f8bba9bb19413883badc0c11482c
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosody
4
+ # Ruby-side lifecycle helpers for the native {Prosody::Client}.
5
+ class Client
6
+ # Creates a client, yields it, and shuts it down when the block exits.
7
+ # The client shuts down also when the block raises. Repeated shutdown
8
+ # calls wait for the same operation, so the block can also call
9
+ # {#shutdown}.
10
+ #
11
+ # @param config [Hash, Configuration] Client configuration
12
+ # @yieldparam client [Client] the new client
13
+ # @return [Object] the value of the block
14
+ # @raise [ArgumentError] if no block is given or the configuration is invalid
15
+ # @raise [RuntimeError] if client initialization or shutdown fails
16
+ #
17
+ # @example
18
+ # Prosody::Client.open(bootstrap_servers: "localhost:9092") do |client|
19
+ # client.send_message("my-topic", "key", {"hello" => "world"})
20
+ # end
21
+ def self.open(config)
22
+ raise ArgumentError, "Prosody::Client.open requires a block" unless block_given?
23
+
24
+ client = new(config)
25
+ begin
26
+ yield client
27
+ ensure
28
+ client.shutdown
29
+ end
30
+ end
31
+ end
32
+ end
@@ -124,14 +124,18 @@ module Prosody
124
124
  # Interval between offset commit operations (in seconds).
125
125
  config_param :commit_interval, converter: ->(v) { duration_converter(v) }
126
126
 
127
+ # Interval between librdkafka statistics reports (in seconds).
128
+ # Env: PROSODY_STATISTICS_INTERVAL. Default: 5 seconds.
129
+ config_param :statistics_interval, converter: ->(v) { duration_converter(v) }
130
+
127
131
  # Base delay for retry operations (in seconds).
128
132
  config_param :retry_base, converter: ->(v) { duration_converter(v) }
129
133
 
130
134
  # Maximum delay between retries (in seconds).
131
135
  config_param :max_retry_delay, converter: ->(v) { duration_converter(v) }
132
136
 
133
- # Global shared cache capacity across all partitions for message deduplication.
134
- # Must be at least 1. Default: 8192.
137
+ # Capacity of the producer idempotence cache and of the consumer
138
+ # deduplication cache. Must be at least 1. Default: 8192.
135
139
  config_param :idempotence_cache_size, converter: ->(v) { Integer(v) }
136
140
 
137
141
  # Version string for cache-busting deduplication hashes. Changing this
@@ -303,7 +307,7 @@ module Prosody
303
307
 
304
308
  # Keyed-state collections to register before subscribe.
305
309
  #
306
- # Accepts an array of StateDefinition objects (from Prosody.value/map/deque
310
+ # Accepts an array of StateDefinition objects (from Prosody.value/map/set/deque
307
311
  # and their message_* siblings) or already-serialized registration hashes.
308
312
  # Duplicate names within the set are rejected by the native layer.
309
313
  config_param :state_collections,
@@ -312,13 +316,15 @@ module Prosody
312
316
  list.map { |d| d.respond_to?(:to_state_config) ? d.to_state_config : d }
313
317
  }
314
318
 
315
- # Subsystem under which published JSON collections are advertised.
319
+ # Subsystem under which published JSON and set collections are advertised.
316
320
  # Uses PROSODY_SUBSYSTEM when omitted. Published collections require it.
317
321
  config_param :subsystem, converter: lambda(&:to_s)
318
322
 
319
- # Disk workspace for the local keyed-state cache. Each live client
320
- # needs its own directory. Falls back to the
321
- # PROSODY_STATE_CACHE_DIR environment variable. Must not be an empty string.
323
+ # Directory for the local keyed-state caches. Each consumer opens its
324
+ # cache in a new subdirectory and removes it when the consumer stops, so
325
+ # clients can share the directory. Falls back to the
326
+ # PROSODY_STATE_CACHE_DIR environment variable, then to
327
+ # <temp>/prosody/keyed-state. Must not be an empty string.
322
328
  config_param :state_cache_dir, converter: lambda(&:to_s)
323
329
 
324
330
  # Capacity of the owning keyed-state cache, such as "64 MiB". Uses
@@ -326,6 +332,13 @@ module Prosody
326
332
  # selects its default.
327
333
  config_param :state_owned_cache_size, converter: lambda(&:to_s)
328
334
 
335
+ # Bytes of in-memory writes the local keyed-state cache holds for each
336
+ # assigned partition before it flushes them to disk, such as "16 MiB".
337
+ # Memory use scales with the number of assigned partitions. Uses
338
+ # PROSODY_STATE_MEMTABLE_SIZE when omitted. Otherwise, the engine uses
339
+ # its default of 64 MiB.
340
+ config_param :state_memtable_size, converter: lambda(&:to_s)
341
+
329
342
  # Capacity of the published-state read-through cache, such as "1 MiB".
330
343
  # Uses PROSODY_STATE_READ_CACHE_SIZE when omitted. It then uses the owned
331
344
  # cache size when set, or 1 MiB when both sizes are unset.
@@ -336,11 +349,6 @@ module Prosody
336
349
  config_param :state_read_cache,
337
350
  converter: ->(v) { (v == true || v == false) ? v : Float(v) }
338
351
 
339
- # Delay in whole seconds between staging a provisional cell and the
340
- # keyed-state recovery sweep. Every registered TTL must strictly exceed this.
341
- # Must be a whole number of seconds >= 1 (validated natively).
342
- config_param :state_recovery_delay, converter: ->(v) { duration_converter(v) }
343
-
344
352
  # Operation mode of the client.
345
353
  #
346
354
  # Valid values:
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prosody
4
+ # The demand a handler call serves, from +context.demand+.
5
+ #
6
+ # +kind+ is +:normal+ for a normal delivery and +:failure+ for a retry after
7
+ # a failure. +retries+ is the number of retries: 0 for a normal delivery and
8
+ # 1 on the first retry. After Prosody defers an event, the count starts
9
+ # again at 1. The count is an estimate. Keep an exact attempt count in
10
+ # keyed state if the handler needs one.
11
+ #
12
+ # @example Alert only on a retry
13
+ # def on_message(context, message)
14
+ # alert(message.key) if context.demand.failure?
15
+ # end
16
+ Demand = Data.define(:kind, :retries) do
17
+ # Whether this is a normal delivery.
18
+ #
19
+ # @return [Boolean]
20
+ def normal? = kind == :normal
21
+
22
+ # Whether this is a retry after a failure.
23
+ #
24
+ # @return [Boolean]
25
+ def failure? = kind == :failure
26
+ end
27
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Native stubs for {Prosody::Client}: sending and subscribing.
4
+
5
+ module Prosody
6
+ # Main client for interacting with the Prosody messaging system.
7
+ # Provides methods for sending messages and subscribing to Kafka topics.
8
+ #
9
+ # @see ext/prosody/src/client/mod.rs for implementation
10
+ class Client
11
+ # Creates a new Prosody client with the given configuration.
12
+ #
13
+ # @param config [Hash, Configuration] Client configuration
14
+ # @return [Client] A new client instance
15
+ # @raise [ArgumentError] If the configuration is invalid
16
+ # @raise [RuntimeError] If client initialization fails
17
+ #
18
+ # @example Creating a client with a Configuration object
19
+ # config = Prosody::Configuration.new do |c|
20
+ # c.bootstrap_servers = "localhost:9092"
21
+ # c.group_id = "my-consumer-group"
22
+ # end
23
+ # client = Prosody::Client.new(config)
24
+ #
25
+ # @example Creating a client with a hash
26
+ # client = Prosody::Client.new(
27
+ # bootstrap_servers: "localhost:9092",
28
+ # group_id: "my-consumer-group"
29
+ # )
30
+ def self.new(config)
31
+ raise NotImplementedError, "This method is implemented natively in Rust"
32
+ end
33
+
34
+ # Returns the current state of the consumer.
35
+ #
36
+ # The consumer can be in one of four states:
37
+ # - `:shut_down` - The client is shut down
38
+ # - `:unconfigured` - The consumer has not been configured yet
39
+ # - `:configured` - The consumer is configured but not running
40
+ # - `:running` - The consumer is actively consuming messages
41
+ #
42
+ # @return [Symbol] The current consumer state
43
+ def consumer_state
44
+ raise NotImplementedError, "This method is implemented natively in Rust"
45
+ end
46
+
47
+ # Returns the number of Kafka partitions currently assigned to this consumer.
48
+ #
49
+ # This method can be used to monitor the consumer's workload and ensure
50
+ # proper load balancing across multiple consumer instances.
51
+ #
52
+ # @return [Integer] The number of assigned partitions
53
+ def assigned_partition_count
54
+ raise NotImplementedError, "This method is implemented natively in Rust"
55
+ end
56
+
57
+ # Checks if the consumer is stalled.
58
+ #
59
+ # A stalled consumer is one that has stopped processing messages due to
60
+ # errors or reaching processing limits. This can be used to detect unhealthy
61
+ # consumers that need attention.
62
+ #
63
+ # @return [Boolean] true if the consumer is stalled, false otherwise
64
+ def stalled?
65
+ raise NotImplementedError, "This method is implemented natively in Rust"
66
+ end
67
+
68
+ # Sends a message to the specified Kafka topic.
69
+ #
70
+ # @param topic [String] The destination topic name
71
+ # @param key [String] The message key for partitioning
72
+ # @param payload [Prosody::json_value] The JSON-compatible message payload
73
+ # @return [void]
74
+ # @raise [RuntimeError] If the message cannot be sent
75
+ #
76
+ # @example Sending a simple message
77
+ # client.send_message("my-topic", "user-123", {
78
+ # "event" => "login", "timestamp" => Time.now.to_i
79
+ # })
80
+ def send_message(topic, key, payload)
81
+ raise NotImplementedError, "This method is implemented natively in Rust"
82
+ end
83
+
84
+ # Sends an excise record to the specified Kafka topic.
85
+ #
86
+ # @param topic [String] The destination topic name
87
+ # @param key [String] The message key for partitioning
88
+ # @return [void]
89
+ # @raise [RuntimeError] If the excise record cannot be sent
90
+ def excise(topic, key)
91
+ raise NotImplementedError, "This method is implemented natively in Rust"
92
+ end
93
+
94
+ # Subscribes to Kafka topics using the provided handler.
95
+ # The handler must implement `on_message`, `on_excise`, and `on_timer`.
96
+ #
97
+ # @param handler [EventHandler] A handler object that processes messages
98
+ # @return [void]
99
+ # @raise [ArgumentError] If a required handler method is missing
100
+ # @raise [RuntimeError] If subscription fails
101
+ #
102
+ # @example Subscribing with a handler
103
+ # class MyHandler < Prosody::EventHandler
104
+ # def on_message(context, message)
105
+ # puts "Received message: #{message.payload}"
106
+ # end
107
+ #
108
+ # def on_excise(_context, message)
109
+ # puts "Excised key: #{message.key}"
110
+ # end
111
+ #
112
+ # def on_timer(_context, _timer)
113
+ # end
114
+ # end
115
+ #
116
+ # client.subscribe(MyHandler.new)
117
+ def subscribe(handler)
118
+ raise NotImplementedError, "This method is implemented natively in Rust"
119
+ end
120
+
121
+ # Unsubscribes from all topics, stopping message processing.
122
+ #
123
+ # This method gracefully shuts down the consumer, completing any in-flight
124
+ # messages before stopping.
125
+ #
126
+ # @return [void]
127
+ # @raise [RuntimeError] If unsubscription fails
128
+ #
129
+ def unsubscribe
130
+ raise NotImplementedError, "This method is implemented natively in Rust"
131
+ end
132
+
133
+ # Shuts down the consumer and all client services.
134
+ #
135
+ # @return [void]
136
+ # @raise [RuntimeError] If shutdown fails
137
+ #
138
+ # @example Shutting down a client
139
+ # client.shutdown
140
+ def shutdown
141
+ raise NotImplementedError, "This method is implemented natively in Rust"
142
+ end
143
+
144
+ # Returns the configured source system identifier.
145
+ #
146
+ # The source system is used to identify the originating service or
147
+ # component in produced messages, enabling loop detection.
148
+ #
149
+ # @return [String] The source system identifier
150
+ #
151
+ # @example Getting the source system
152
+ # puts client.source_system # => "my-service"
153
+ def source_system
154
+ raise NotImplementedError, "This method is implemented natively in Rust"
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Native stubs for {Prosody::Context}: event metadata, cancellation, and timer
4
+ # scheduling.
5
+
6
+ module Prosody
7
+ # Represents the context of a Kafka message, providing metadata and control
8
+ # capabilities for message handling.
9
+ #
10
+ # Instances of this class are created by the native code and passed to your
11
+ # EventHandler's #on_message method.
12
+ #
13
+ # @see ext/prosody/src/handler/context/mod.rs for implementation
14
+ class Context
15
+ # @private
16
+ def initialize
17
+ raise NotImplementedError, "This class is implemented natively in Rust"
18
+ end
19
+
20
+ # Checks if cancellation has been requested.
21
+ #
22
+ # This method can be called within message handlers to detect when the
23
+ # handler should exit. Cancellation includes message-level cancellation
24
+ # (e.g., handler timeout) and partition shutdown. During shutdown,
25
+ # cancellation is delayed until near the end of the shutdown timeout to
26
+ # allow in-flight work to complete.
27
+ #
28
+ # @return [Boolean] true if cancellation has been requested, false otherwise
29
+ #
30
+ # @example Checking for cancellation in a loop
31
+ # def on_message(context, message)
32
+ # items = message.payload["items"]
33
+ # items.each do |item|
34
+ # return if context.should_cancel?
35
+ # process_item(item)
36
+ # end
37
+ # end
38
+ def should_cancel?
39
+ raise NotImplementedError, "This method is implemented natively in Rust"
40
+ end
41
+
42
+ # Returns the demand this handler call serves.
43
+ #
44
+ # A normal delivery has kind +:normal+ and 0 retries. A retry after a
45
+ # failure has kind +:failure+ and a retry count that is 1 on the first
46
+ # retry. The count is an estimate; see {Prosody::Demand}.
47
+ #
48
+ # @return [Prosody::Demand]
49
+ #
50
+ # @example Reading the retry count
51
+ # def on_message(context, message)
52
+ # logger.warn("retry #{context.demand.retries}") if context.demand.failure?
53
+ # end
54
+ def demand
55
+ raise NotImplementedError, "This method is implemented natively in Rust"
56
+ end
57
+
58
+ # Blocks until cancellation is signaled.
59
+ #
60
+ # Cancellation includes message-level cancellation (e.g., handler timeout)
61
+ # and partition shutdown. During shutdown, cancellation is delayed until near
62
+ # the end of the shutdown timeout to allow in-flight work to complete.
63
+ # This method is useful for long-running handlers that need to wait for
64
+ # external events while remaining responsive to cancellation.
65
+ #
66
+ # @return [void]
67
+ #
68
+ # @example Waiting for cancellation
69
+ # def on_message(context, message)
70
+ # # Do some work, then wait for cancellation
71
+ # context.on_cancel
72
+ # end
73
+ def on_cancel
74
+ raise NotImplementedError, "This method is implemented natively in Rust"
75
+ end
76
+
77
+ # Schedules a timer to fire at the specified time.
78
+ #
79
+ # Timers allow you to delay execution or implement timeout behavior within
80
+ # your message handlers. When a timer fires, your handler's #on_timer method
81
+ # will be called with the timer object.
82
+ #
83
+ # @param time [Time] When the timer should fire
84
+ # @return [void]
85
+ # @raise [ArgumentError] If the time is invalid or outside the supported range (1970-2106)
86
+ # @raise [RuntimeError] If timer scheduling fails
87
+ #
88
+ # @example Scheduling a delayed action
89
+ # def on_message(context, message)
90
+ # # Schedule a timer to fire in 30 seconds
91
+ # context.schedule(Time.now + 30)
92
+ # end
93
+ #
94
+ # def on_timer(context, timer)
95
+ # puts "Timer fired for key: #{timer.key}"
96
+ # end
97
+ def schedule(time)
98
+ raise NotImplementedError, "This method is implemented natively in Rust"
99
+ end
100
+
101
+ # Clears all scheduled timers and schedules a new one at the specified time.
102
+ #
103
+ # This is equivalent to calling clear_scheduled followed by schedule, but
104
+ # performed atomically.
105
+ #
106
+ # @param time [Time] When the new timer should fire
107
+ # @return [void]
108
+ # @raise [ArgumentError] If the time is invalid or outside the supported range
109
+ # @raise [RuntimeError] If timer operations fail
110
+ #
111
+ # @example Replacing all timers with a new one
112
+ # def on_message(context, message)
113
+ # # Clear any existing timers and schedule a new one
114
+ # context.clear_and_schedule(Time.now + 60)
115
+ # end
116
+ def clear_and_schedule(time)
117
+ raise NotImplementedError, "This method is implemented natively in Rust"
118
+ end
119
+
120
+ # Unschedules a timer that was scheduled for the specified time.
121
+ #
122
+ # If multiple timers were scheduled for the same time, this will remove one
123
+ # of them. If no timer exists for the specified time, this method does nothing.
124
+ #
125
+ # @param time [Time] The time for which to unschedule the timer
126
+ # @return [void]
127
+ # @raise [ArgumentError] If the time is invalid
128
+ # @raise [RuntimeError] If timer unscheduling fails
129
+ #
130
+ # @example Canceling a specific timer
131
+ # def on_message(context, message)
132
+ # timer_time = Time.now + 30
133
+ # context.schedule(timer_time)
134
+ #
135
+ # # Later, cancel that specific timer
136
+ # context.unschedule(timer_time)
137
+ # end
138
+ def unschedule(time)
139
+ raise NotImplementedError, "This method is implemented natively in Rust"
140
+ end
141
+
142
+ # Clears all scheduled timers.
143
+ #
144
+ # After calling this method, no timers will be scheduled to fire for this
145
+ # message context.
146
+ #
147
+ # @return [void]
148
+ # @raise [RuntimeError] If clearing timers fails
149
+ #
150
+ # @example Canceling all timers
151
+ # def on_message(context, message)
152
+ # context.clear_scheduled
153
+ # end
154
+ def clear_scheduled
155
+ raise NotImplementedError, "This method is implemented natively in Rust"
156
+ end
157
+
158
+ # Returns all currently scheduled timer times.
159
+ #
160
+ # The returned array contains Time objects representing when each scheduled
161
+ # timer will fire. The array may be empty if no timers are scheduled.
162
+ #
163
+ # @return [Array<Time>] Array of scheduled timer times
164
+ # @raise [RuntimeError] If retrieving scheduled times fails
165
+ #
166
+ # @example Checking scheduled timers
167
+ # def on_message(context, message)
168
+ # scheduled_times = context.scheduled
169
+ # puts "#{scheduled_times.length} timers scheduled"
170
+ # scheduled_times.each do |time|
171
+ # puts "Timer will fire at: #{time}"
172
+ # end
173
+ # end
174
+ def scheduled
175
+ raise NotImplementedError, "This method is implemented natively in Rust"
176
+ end
177
+ end
178
+ end