local_bus 0.2.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
data/lib/local_bus/bus.rb CHANGED
@@ -1,47 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rbs_inline: enabled
4
-
5
3
  class LocalBus
6
- # Local in-process single threaded "message bus" with non-blocking I/O
4
+ # The Bus acts as a direct transport mechanism for messages, akin to placing a passenger directly onto a bus.
5
+ # When a message is published to the Bus, it is immediately delivered to all subscribers, ensuring prompt execution of tasks.
6
+ # This is achieved through non-blocking I/O operations, which allow the Bus to handle multiple tasks efficiently without blocking the main thread.
7
+ #
8
+ # @note While the Bus uses asynchronous operations to optimize performance,
9
+ # the actual processing of a message may still experience slight delays due to I/O wait times from prior messages.
10
+ # This means that while the Bus aims for immediate processing, the nature of asynchronous operations can introduce some latency.
7
11
  class Bus
8
12
  include MonitorMixin
9
13
 
10
14
  # Constructor
11
15
  # @note Creates a new Bus instance with specified max concurrency (i.e. number of tasks that can run in parallel)
12
- # @rbs max_concurrency: Integer -- maximum number of concurrent tasks (default: Concurrent.processor_count)
13
- def initialize(max_concurrency: Concurrent.processor_count)
16
+ # @rbs concurrency: Integer -- maximum number of concurrent tasks (default: Etc.nprocessors)
17
+ def initialize(concurrency: Etc.nprocessors)
14
18
  super()
15
- @max_concurrency = max_concurrency.to_i
16
- @subscriptions = Concurrent::Hash.new do |hash, key|
17
- hash[key] = Concurrent::Set.new
19
+ @concurrency = concurrency.to_i
20
+ @subscriptions = Hash.new do |hash, key|
21
+ hash[key] = Set.new
18
22
  end
19
23
  end
20
24
 
21
25
  # Maximum number of concurrent tasks that can run in "parallel"
22
26
  # @rbs return: Integer
23
- def max_concurrency
24
- synchronize { @max_concurrency }
27
+ def concurrency
28
+ synchronize { @concurrency }
25
29
  end
26
30
 
27
31
  # Sets the max concurrency
28
32
  # @rbs value: Integer -- max number of concurrent tasks that can run in "parallel"
29
33
  # @rbs return: Integer -- new concurrency value
30
- def max_concurrency=(value)
31
- synchronize { @max_concurrency = value.to_i }
34
+ def concurrency=(value)
35
+ synchronize { @concurrency = value.to_i }
32
36
  end
33
37
 
34
38
  # Registered topics that have subscribers
35
39
  # @rbs return: Array[String] -- list of topic names
36
40
  def topics
37
- @subscriptions.keys
41
+ synchronize { @subscriptions.keys }
38
42
  end
39
43
 
40
44
  # Registered subscriptions
41
45
  # @rbs return: Hash[String, Array[callable]] -- mapping of topics to callables
42
46
  def subscriptions
43
- @subscriptions.each_with_object({}) do |(topic, callables), memo|
44
- memo[topic] = callables.to_a
47
+ synchronize do
48
+ @subscriptions.each_with_object({}) do |(topic, callables), memo|
49
+ memo[topic] = callables.to_a
50
+ end
45
51
  end
46
52
  end
47
53
 
@@ -54,7 +60,7 @@ class LocalBus
54
60
  def subscribe(topic, callable: nil, &block)
55
61
  callable ||= block
56
62
  raise ArgumentError, "Subscriber must respond to #call" unless callable.respond_to?(:call, false)
57
- @subscriptions[topic.to_s].add callable
63
+ synchronize { @subscriptions[topic.to_s].add callable }
58
64
  self
59
65
  end
60
66
 
@@ -64,8 +70,10 @@ class LocalBus
64
70
  # @rbs return: self
65
71
  def unsubscribe(topic, callable:)
66
72
  topic = topic.to_s
67
- @subscriptions[topic].delete callable
68
- @subscriptions.delete(topic) if @subscriptions[topic].empty?
73
+ synchronize do
74
+ @subscriptions[topic].delete callable
75
+ @subscriptions.delete(topic) if @subscriptions[topic].empty?
76
+ end
69
77
  self
70
78
  end
71
79
 
@@ -74,8 +82,10 @@ class LocalBus
74
82
  # @rbs return: self
75
83
  def unsubscribe_all(topic)
76
84
  topic = topic.to_s
77
- @subscriptions[topic].clear
78
- @subscriptions.delete topic
85
+ synchronize do
86
+ @subscriptions[topic].clear
87
+ @subscriptions.delete topic
88
+ end
79
89
  self
80
90
  end
81
91
 
@@ -88,7 +98,7 @@ class LocalBus
88
98
  unsubscribe_all topic
89
99
  end
90
100
 
91
- # Publishes a message to a topic
101
+ # Publishes a message
92
102
  #
93
103
  # @note If subscribers are rapidly created/destroyed mid-publish, there's a theoretical
94
104
  # possibility of object_id reuse. However, this is extremely unlikely in practice.
@@ -98,21 +108,27 @@ class LocalBus
98
108
  #
99
109
  # @note If the timeout is exceeded, the task will be cancelled before all subscribers have completed.
100
110
  #
101
- # Check the Subscriber for any errors.
111
+ # Check individual Subscribers for possible errors.
102
112
  #
103
113
  # @rbs topic: String -- topic name
104
- # @rbs timeout: Float -- seconds to wait before cancelling (default: 300)
114
+ # @rbs timeout: Float -- seconds to wait for subscribers to process the message before cancelling (default: 60)
105
115
  # @rbs payload: Hash -- message payload
106
- # @rbs return: Array[Subscriber] -- list of performed subscribers (empty if no subscribers)
107
- def publish(topic, timeout: 300, **payload)
116
+ # @rbs return: Message
117
+ def publish(topic, timeout: 60, **payload)
118
+ publish_message Message.new(topic, timeout: timeout.to_f, **payload)
119
+ end
120
+
121
+ # Publishes a pre-built message
122
+ # @rbs message: Message -- message to publish
123
+ # @rbs return: Message
124
+ def publish_message(message)
108
125
  barrier = Async::Barrier.new
109
- message = Message.new(topic, timeout: timeout, **payload)
110
126
  subscribers = subscriptions.fetch(message.topic, []).map { Subscriber.new _1, message }
111
127
 
112
128
  if subscribers.any?
113
129
  Sync do |task|
114
- task.with_timeout timeout.to_f do
115
- semaphore = Async::Semaphore.new(max_concurrency, parent: barrier)
130
+ task.with_timeout message.timeout do
131
+ semaphore = Async::Semaphore.new(concurrency, parent: barrier)
116
132
 
117
133
  subscribers.each do |subscriber|
118
134
  semaphore.async do
@@ -129,7 +145,8 @@ class LocalBus
129
145
  end
130
146
  end
131
147
 
132
- Pledge.new(barrier, *subscribers)
148
+ message.publication = Publication.new(barrier, *subscribers)
149
+ message
133
150
  end
134
151
  end
135
152
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rbs_inline: enabled
4
-
5
3
  class LocalBus
6
4
  # Represents a message in the LocalBus system
7
5
  class Message
@@ -11,50 +9,82 @@ class LocalBus
11
9
  # @rbs timeout: Float? -- optional timeout for message processing (in seconds)
12
10
  # @rbs payload: Hash -- the message payload
13
11
  def initialize(topic, timeout: nil, **payload)
14
- @id = SecureRandom.uuid_v7
15
- @topic = topic.to_s.freeze
16
- @payload = payload.transform_keys(&:to_sym).freeze
17
- @created_at = Time.now
18
- @thread_id = Thread.current.object_id
19
- @timeout = timeout.to_f
20
12
  @metadata ||= {
21
- id: id,
22
- topic: topic,
23
- payload: payload,
24
- created_at: created_at,
25
- thread_id: thread_id,
26
- timeout: timeout
13
+ id: SecureRandom.uuid_v7,
14
+ topic: topic.to_s.freeze,
15
+ payload: payload.transform_keys(&:to_sym).freeze,
16
+ created_at: Time.now,
17
+ thread_id: Thread.current.object_id,
18
+ timeout: timeout.to_f
27
19
  }.freeze
28
- freeze
29
20
  end
30
21
 
22
+ # Metadata for the message
23
+ # @rbs return: Hash[Symbol, untyped]
24
+ attr_reader :metadata
25
+
26
+ # Publication representing the Async barrier and subscribers handling the message
27
+ # @note May be nil if processing hasn't happened yet (e.g. it was published via Station)
28
+ # @rbs return: Publication?
29
+ attr_accessor :publication
30
+
31
31
  # Unique identifier for the message
32
32
  # @rbs return: String
33
- attr_reader :id
33
+ def id
34
+ metadata[:id]
35
+ end
34
36
 
35
37
  # Message topic
36
38
  # @rbs return: String
37
- attr_reader :topic
39
+ def topic
40
+ metadata[:topic]
41
+ end
38
42
 
39
43
  # Message payload
40
44
  # @rbs return: Hash
41
- attr_reader :payload
45
+ def payload
46
+ metadata[:payload]
47
+ end
42
48
 
43
49
  # Time when the message was created or published
44
50
  # @rbs return: Time
45
- attr_reader :created_at
51
+ def created_at
52
+ metadata[:created_at]
53
+ end
46
54
 
47
55
  # ID of the thread that created the message
48
56
  # @rbs return: Integer
49
- attr_reader :thread_id
57
+ def thread_id
58
+ metadata[:thread_id]
59
+ end
50
60
 
51
61
  # Timeout for message processing (in seconds)
52
62
  # @rbs return: Float
53
- attr_reader :timeout
63
+ def timeout
64
+ metadata[:timeout]
65
+ end
54
66
 
55
- # Metadata for the message
56
- # @rbs return: Hash[Symbol, untyped]
57
- attr_reader :metadata
67
+ # Blocks and waits for the message to process
68
+ # @rbs interval: Float -- time to wait between checks (default: 0.1)
69
+ # @rbs return: void
70
+ def wait(interval: 0.1)
71
+ @timers ||= Timers::Group.new.tap { _1.every(interval) {} }
72
+ loop do
73
+ break if publication
74
+ @timers.wait
75
+ end
76
+ publication&.wait
77
+ ensure
78
+ @timers&.cancel
79
+ @timers = nil
80
+ end
81
+
82
+ # Blocks and waits for the message process then returns all subscribers
83
+ # @rbs return: Array[Subscriber]
84
+ def subscribers
85
+ wait
86
+ publication.subscribers
87
+ end
58
88
 
59
89
  # Converts the message to a hash
60
90
  # @rbs return: Hash[Symbol, untyped]
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ class LocalBus
4
+ # Wraps an Async::Barrier and a list of Subscribers that are processing a Message.
5
+ class Publication
6
+ # Constructor
7
+ # @rbs barrier: Async::Barrier -- barrier used to wait for all subscribers
8
+ # @rbs subscribers: Array[Subscriber]
9
+ def initialize(barrier, *subscribers)
10
+ @barrier = barrier
11
+ @subscribers = subscribers
12
+ end
13
+
14
+ # Blocks and waits for the barrier (i.e. all subscribers to complete)
15
+ # @rbs return: void
16
+ def wait
17
+ @barrier.wait
18
+ self
19
+ end
20
+
21
+ # List of Subscribers that are processing a Message
22
+ # @note Blocks until all subscribers complete
23
+ # @rbs return: Array[Subscriber]
24
+ def subscribers
25
+ wait
26
+ @subscribers
27
+ end
28
+ end
29
+ end
@@ -1,123 +1,138 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # rbs_inline: enabled
4
- # rubocop:disable Lint/MissingCopEnableDirective
5
- # rubocop:disable Style/ArgumentsForwarding
6
-
7
3
  class LocalBus
8
- # An in-process message queuing system that buffers and publishes messages to Bus.
9
- # This class acts as an intermediary, queuing messages internally before publishing them to the Bus.
10
- #
11
- # @note Station shares the same interface as Bus and is thus a message bus.
12
- # The key difference is that Stations are multi-threaded and will not block the main thread.
4
+ # The Station serves as a queuing system for messages, similar to a bus station where passengers wait for their bus.
13
5
  #
14
- # Three fallback policies are supported:
15
- # 1. `abort` - Raises an exception and discards the task when the queue is full (default)
16
- # 2. `discard` - Discards the task when the queue is full
17
- # 3. `caller_runs` - Executes the task on the calling thread when the queue is full,
18
- # This effectively jumps the queue (and blocks the main thread) but ensures the task is performed
6
+ # When a message is published to the Station, it is queued and processed at a later time, allowing for deferred execution.
7
+ # This is particularly useful for tasks that can be handled later.
19
8
  #
20
- # IMPORTANT: Be sure to release resources like database connections in subscribers when publishing via Station.
9
+ # The Station employs a thread pool to manage message processing, enabling high concurrency and efficient resource utilization.
10
+ # Messages can also be prioritized, ensuring that higher-priority tasks are processed first.
21
11
  #
12
+ # @note: While the Station provides a robust mechanism for background processing,
13
+ # it's important to understand that the exact timing of message processing is not controlled by the publisher,
14
+ # and messages will be processed as resources become available.
22
15
  class Station
23
16
  include MonitorMixin
24
17
 
25
- class TimeoutError < StandardError; end
26
-
27
- # Default options for Concurrent::FixedThreadPool (can be overridden via the constructor)
28
- # @see https://ruby-concurrency.github.io/concurrent-ruby/1.3.4/Concurrent/ThreadPoolExecutor.html
29
- THREAD_POOL_OPTIONS = {
30
- max_queue: 5_000, # max number of pending tasks allowed in the queue
31
- fallback_policy: :caller_runs # Options: :abort, :discard, :caller_runs
32
- }.freeze
18
+ class CapacityError < StandardError; end
33
19
 
34
20
  # Constructor
21
+ #
22
+ # @note Delays process exit in an attempt to flush the queue to avoid dropping messages.
23
+ # Exit flushing makes a "best effort" to process all messages, but it's not guaranteed.
24
+ # Will not delay process exit when the queue is empty.
25
+ #
35
26
  # @rbs bus: Bus -- local message bus (default: Bus.new)
36
- # @rbs max_threads: Integer -- number of max_threads (default: Concurrent.processor_count)
37
- # @rbs default_timeout: Float -- seconds to wait for a future to complete
38
- # @rbs shutdown_timeout: Float -- seconds to wait for all futures to complete on process exit
39
- # @rbs options: Hash[Symbol, untyped] -- Concurrent::FixedThreadPool options
27
+ # @rbs interval: Float -- queue polling interval in seconds (default: 0.1)
28
+ # @rbs limit: Integer -- max queue size (default: 10_000)
29
+ # @rbs threads: Integer -- number of threads to use (default: Etc.nprocessors)
30
+ # @rbs timeout: Float -- seconds to wait for subscribers to process the message before cancelling (default: 60)
31
+ # @rbs wait: Float -- seconds to wait for the queue to flush at process exit (default: 5)
40
32
  # @rbs return: void
41
- def initialize(
42
- bus: Bus.new,
43
- max_threads: Concurrent.processor_count,
44
- default_timeout: 0,
45
- shutdown_timeout: 8,
46
- **options
47
- )
33
+ def initialize(bus: Bus.new, interval: 0.1, limit: 10_000, threads: Etc.nprocessors, timeout: 60, wait: 5)
48
34
  super()
49
35
  @bus = bus
50
- @max_threads = [2, max_threads].max.to_i
51
- @default_timeout = default_timeout.to_f
52
- @shutdown_timeout = shutdown_timeout.to_f
53
- @shutdown = Concurrent::AtomicBoolean.new(false)
54
- start(**options)
36
+ @interval = interval.to_f
37
+ @interval = 0.1 unless @interval.positive?
38
+ @limit = limit.to_i.positive? ? limit.to_i : 10_000
39
+ @threads = [threads.to_i, 1].max
40
+ @timeout = timeout.to_f
41
+ @queue = Containers::PriorityQueue.new
42
+ at_exit { stop timeout: [wait.to_f, 1].max }
43
+ start
55
44
  end
56
45
 
57
46
  # Bus instance
58
47
  # @rbs return: Bus
59
48
  attr_reader :bus
60
49
 
61
- # Number of threads used to process messages
50
+ # Queue polling interval in seconds
51
+ # @rbs return: Float
52
+ attr_reader :interval
53
+
54
+ # Max queue size
62
55
  # @rbs return: Integer
63
- attr_reader :max_threads
56
+ attr_reader :limit
64
57
 
65
- # Default timeout for message processing (in seconds)
66
- # @rbs return: Float
67
- attr_reader :default_timeout
58
+ # Number of threads to use
59
+ # @rbs return: Integer
60
+ attr_reader :threads
68
61
 
69
- # Timeout for graceful shutdown (in seconds)
62
+ # Default timeout for message processing (in seconds)
70
63
  # @rbs return: Float
71
- attr_reader :shutdown_timeout
64
+ attr_reader :timeout
72
65
 
73
- # Starts the broker
74
- # @rbs options: Hash[Symbol, untyped] -- Concurrent::FixedThreadPool options
66
+ # Starts the station
67
+ # @rbs interval: Float -- queue polling interval in seconds (default: self.interval)
68
+ # @rbs threads: Integer -- number of threads to use (default: self.threads)
75
69
  # @rbs return: void
76
- def start(**options)
70
+ def start(interval: self.interval, threads: self.threads)
71
+ interval = 0.1 unless interval.positive?
72
+ threads = [threads.to_i, 1].max
73
+
77
74
  synchronize do
78
- return if running?
75
+ return if running? || stopping?
76
+
77
+ timers = Timers::Group.new
78
+ @pool = []
79
+ threads.times do
80
+ @pool << Thread.new do
81
+ Thread.current.report_on_exception = true
82
+ timers.every interval do
83
+ message = synchronize { @queue.pop unless @queue.empty? || stopping? }
84
+ bus.send :publish_message, message if message
85
+ end
79
86
 
80
- start_shutdown_handler
81
- @pool = Concurrent::FixedThreadPool.new(max_threads, THREAD_POOL_OPTIONS.merge(options))
82
- enable_safe_shutdown on: ["HUP", "INT", "QUIT", "TERM"]
87
+ loop do
88
+ timers.wait
89
+ break if stopping?
90
+ end
91
+ ensure
92
+ timers.cancel
93
+ end
94
+ end
83
95
  end
84
96
  end
85
97
 
86
- # Stops the broker
87
- # @rbs timeout: Float -- seconds to wait for all futures to complete
98
+ # Stops the station
99
+ # @rbs timeout: Float -- seconds to wait for message processing before killing the thread pool (default: nil)
88
100
  # @rbs return: void
89
- def stop(timeout: shutdown_timeout)
90
- return unless @shutdown.make_true # Ensure we only stop once
91
-
101
+ def stop(timeout: nil)
92
102
  synchronize do
93
- if running?
94
- # First try graceful shutdown
95
- pool.shutdown
96
-
97
- # If graceful shutdown fails, force termination
98
- pool.kill unless pool.wait_for_termination(timeout)
99
-
100
- @pool = nil
101
- end
102
- rescue
103
- nil # ignore errors during shutdown
103
+ return unless running?
104
+ return if stopping?
105
+ @stopping = true
104
106
  end
105
107
 
106
- # Clean up shutdown handler
107
- if @shutdown_thread&.alive?
108
- @shutdown_queue&.close
109
- @shutdown_thread&.join timeout
108
+ @pool&.each do |thread|
109
+ timeout.is_a?(Numeric) ? thread.join(timeout) : thread.join
110
110
  end
111
+ ensure
112
+ @stopping = false
113
+ @pool = nil
114
+ end
111
115
 
112
- @shutdown_thread = nil
113
- @shutdown_queue = nil
114
- @shutdown_completed&.set
116
+ def stopping?
117
+ synchronize { !!@stopping }
115
118
  end
116
119
 
117
- # Indicates if the broker is running
120
+ # Indicates if the station is running
118
121
  # @rbs return: bool
119
122
  def running?
120
- synchronize { pool&.running? }
123
+ synchronize { !!@pool }
124
+ end
125
+
126
+ # Indicates if the queue is empty
127
+ # @rbs return: bool
128
+ def empty?
129
+ synchronize { @queue.empty? }
130
+ end
131
+
132
+ # Number of unprocessed messages in the queue
133
+ # @rbs return: Integer
134
+ def count
135
+ synchronize { @queue.size }
121
136
  end
122
137
 
123
138
  # Subscribe to a topic
@@ -125,103 +140,46 @@ class LocalBus
125
140
  # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
126
141
  # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
127
142
  # @rbs return: self
128
- def subscribe(topic, callable: nil, &block)
129
- bus.subscribe(topic, callable: callable || block)
143
+ def subscribe(...)
144
+ bus.subscribe(...)
130
145
  self
131
146
  end
132
147
 
133
- # Unsubscribe from a topic
148
+ # Unsubscribes a callable from a topic
134
149
  # @rbs topic: String -- topic name
150
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
135
151
  # @rbs return: self
136
- def unsubscribe(topic)
137
- bus.unsubscribe(topic)
152
+ def unsubscribe(...)
153
+ bus.unsubscribe(...)
138
154
  self
139
155
  end
140
156
 
141
157
  # Unsubscribes all subscribers from a topic and removes the topic
142
158
  # @rbs topic: String -- topic name
143
159
  # @rbs return: self
144
- def unsubscribe_all(topic)
145
- bus.unsubscribe_all topic
160
+ def unsubscribe_all(...)
161
+ bus.unsubscribe_all(...)
146
162
  self
147
163
  end
148
164
 
149
- # Publishes a message to Bus on a separate thread keeping the main thread free for additional work.
150
- #
151
- # @note This allows you to publish messages when performing operations like handling web requests
152
- # without blocking the main thread and slowing down the response.
153
- #
154
- # @see https://ruby-concurrency.github.io/concurrent-ruby/1.3.4/Concurrent/Promises/Future.html
165
+ # Publishes a message
155
166
  #
156
167
  # @rbs topic: String | Symbol -- topic name
168
+ # @rbs priority: Integer -- priority of the message, higher number == higher priority (default: 1)
157
169
  # @rbs timeout: Float -- seconds to wait before cancelling
158
170
  # @rbs payload: Hash[Symbol, untyped] -- message payload
159
- # @rbs return: Concurrent::Promises::Future
160
- def publish(topic, timeout: default_timeout, **payload)
161
- timeout = timeout.to_f
162
-
163
- future = Concurrent::Promises.future_on(pool) do
164
- case timeout
165
- in 0 then bus.publish(topic, **payload).value
166
- else bus.publish(topic, timeout: timeout, **payload).value
167
- end
168
- end
169
-
170
- # ensure calls to future.then use the thread pool
171
- executor = pool
172
- future.singleton_class.define_method :then do |&block|
173
- future.then_on(executor, &block)
174
- end
175
-
176
- future
171
+ # @rbs return: Message
172
+ def publish(topic, priority: 1, timeout: self.timeout, **payload)
173
+ publish_message Message.new(topic, timeout: timeout, **payload), priority: priority
177
174
  end
178
175
 
179
- private
180
-
181
- # Thread pool used for asynchronous operations
182
- # @rbs return: Concurrent::FixedThreadPool
183
- attr_reader :pool
184
-
185
- # Starts the shutdown handler thread
186
- # @rbs return: void
187
- def start_shutdown_handler
188
- return if @shutdown.true?
189
-
190
- @shutdown_queue = Queue.new
191
- @shutdown_completed = Concurrent::Event.new
192
- @shutdown_thread = Thread.new do
193
- catch :shutdown do
194
- loop do
195
- signal = @shutdown_queue.pop # blocks until something is available
196
- throw :shutdown if @shutdown_queue.closed?
197
-
198
- stop # initiate shutdown sequence
199
-
200
- # Re-raise the signal to let the process terminate
201
- if signal
202
- # Remove our trap handler before re-raising
203
- trap signal, "DEFAULT"
204
- Process.kill signal, Process.pid
205
- end
206
- rescue ThreadError, ClosedQueueError
207
- break # queue was closed, exit gracefully
208
- end
209
- end
210
- @shutdown_completed.set
211
- end
212
- end
213
-
214
- # Enables safe shutdown on process exit by trapping specified signals
215
- # @rbs on: Array[String] -- signals to trap
216
- # @rbs return: void
217
- def enable_safe_shutdown(on:)
218
- at_exit { stop }
219
- on.each do |signal|
220
- trap signal do
221
- @shutdown_queue.push signal unless @shutdown.true?
222
- rescue
223
- nil
224
- end
176
+ # Publishes a pre-built message
177
+ # @rbs message: Message -- message to publish
178
+ # @rbs return: Message
179
+ def publish_message(message, priority: 1)
180
+ synchronize do
181
+ raise CapacityError, "Station is at capacity! (limit: #{limit})" if @queue.size >= limit
182
+ @queue.push message, priority
225
183
  end
226
184
  end
227
185
  end