local_bus 0.2.0 → 0.3.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.
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