local_bus 0.2.0 → 0.3.0
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 +4 -4
- data/README.md +269 -179
- data/lib/local_bus/bus.rb +46 -27
- data/lib/local_bus/message.rb +54 -22
- data/lib/local_bus/publication.rb +31 -0
- data/lib/local_bus/station.rb +112 -151
- data/lib/local_bus/subscriber.rb +19 -8
- data/lib/local_bus/version.rb +1 -1
- data/lib/local_bus.rb +52 -1
- data/sig/generated/local_bus/bus.rbs +83 -0
- data/sig/generated/local_bus/message.rbs +60 -0
- data/sig/generated/local_bus/publication.rbs +20 -0
- data/sig/generated/local_bus/station.rbs +113 -0
- data/sig/generated/local_bus/subscriber.rbs +89 -0
- data/sig/generated/local_bus/version.rbs +5 -0
- data/sig/generated/local_bus.rbs +49 -0
- metadata +25 -18
- data/lib/local_bus/pledge.rb +0 -43
data/lib/local_bus/bus.rb
CHANGED
@@ -3,45 +3,53 @@
|
|
3
3
|
# rbs_inline: enabled
|
4
4
|
|
5
5
|
class LocalBus
|
6
|
-
#
|
6
|
+
# The Bus acts as a direct transport mechanism for messages, akin to placing a passenger directly onto a bus.
|
7
|
+
# When a message is published to the Bus, it is immediately delivered to all subscribers, ensuring prompt execution of tasks.
|
8
|
+
# This is achieved through non-blocking I/O operations, which allow the Bus to handle multiple tasks efficiently without blocking the main thread.
|
9
|
+
#
|
10
|
+
# @note While the Bus uses asynchronous operations to optimize performance,
|
11
|
+
# the actual processing of a message may still experience slight delays due to I/O wait times from prior messages.
|
12
|
+
# This means that while the Bus aims for immediate processing, the nature of asynchronous operations can introduce some latency.
|
7
13
|
class Bus
|
8
14
|
include MonitorMixin
|
9
15
|
|
10
16
|
# Constructor
|
11
17
|
# @note Creates a new Bus instance with specified max concurrency (i.e. number of tasks that can run in parallel)
|
12
|
-
# @rbs
|
13
|
-
def initialize(
|
18
|
+
# @rbs concurrency: Integer -- maximum number of concurrent tasks (default: Etc.nprocessors)
|
19
|
+
def initialize(concurrency: Etc.nprocessors)
|
14
20
|
super()
|
15
|
-
@
|
16
|
-
@subscriptions =
|
17
|
-
hash[key] =
|
21
|
+
@concurrency = concurrency.to_i
|
22
|
+
@subscriptions = Hash.new do |hash, key|
|
23
|
+
hash[key] = Set.new
|
18
24
|
end
|
19
25
|
end
|
20
26
|
|
21
27
|
# Maximum number of concurrent tasks that can run in "parallel"
|
22
28
|
# @rbs return: Integer
|
23
|
-
def
|
24
|
-
synchronize { @
|
29
|
+
def concurrency
|
30
|
+
synchronize { @concurrency }
|
25
31
|
end
|
26
32
|
|
27
33
|
# Sets the max concurrency
|
28
34
|
# @rbs value: Integer -- max number of concurrent tasks that can run in "parallel"
|
29
35
|
# @rbs return: Integer -- new concurrency value
|
30
|
-
def
|
31
|
-
synchronize { @
|
36
|
+
def concurrency=(value)
|
37
|
+
synchronize { @concurrency = value.to_i }
|
32
38
|
end
|
33
39
|
|
34
40
|
# Registered topics that have subscribers
|
35
41
|
# @rbs return: Array[String] -- list of topic names
|
36
42
|
def topics
|
37
|
-
@subscriptions.keys
|
43
|
+
synchronize { @subscriptions.keys }
|
38
44
|
end
|
39
45
|
|
40
46
|
# Registered subscriptions
|
41
47
|
# @rbs return: Hash[String, Array[callable]] -- mapping of topics to callables
|
42
48
|
def subscriptions
|
43
|
-
|
44
|
-
|
49
|
+
synchronize do
|
50
|
+
@subscriptions.each_with_object({}) do |(topic, callables), memo|
|
51
|
+
memo[topic] = callables.to_a
|
52
|
+
end
|
45
53
|
end
|
46
54
|
end
|
47
55
|
|
@@ -54,7 +62,7 @@ class LocalBus
|
|
54
62
|
def subscribe(topic, callable: nil, &block)
|
55
63
|
callable ||= block
|
56
64
|
raise ArgumentError, "Subscriber must respond to #call" unless callable.respond_to?(:call, false)
|
57
|
-
@subscriptions[topic.to_s].add callable
|
65
|
+
synchronize { @subscriptions[topic.to_s].add callable }
|
58
66
|
self
|
59
67
|
end
|
60
68
|
|
@@ -64,8 +72,10 @@ class LocalBus
|
|
64
72
|
# @rbs return: self
|
65
73
|
def unsubscribe(topic, callable:)
|
66
74
|
topic = topic.to_s
|
67
|
-
|
68
|
-
|
75
|
+
synchronize do
|
76
|
+
@subscriptions[topic].delete callable
|
77
|
+
@subscriptions.delete(topic) if @subscriptions[topic].empty?
|
78
|
+
end
|
69
79
|
self
|
70
80
|
end
|
71
81
|
|
@@ -74,8 +84,10 @@ class LocalBus
|
|
74
84
|
# @rbs return: self
|
75
85
|
def unsubscribe_all(topic)
|
76
86
|
topic = topic.to_s
|
77
|
-
|
78
|
-
|
87
|
+
synchronize do
|
88
|
+
@subscriptions[topic].clear
|
89
|
+
@subscriptions.delete topic
|
90
|
+
end
|
79
91
|
self
|
80
92
|
end
|
81
93
|
|
@@ -88,7 +100,7 @@ class LocalBus
|
|
88
100
|
unsubscribe_all topic
|
89
101
|
end
|
90
102
|
|
91
|
-
# Publishes a message
|
103
|
+
# Publishes a message
|
92
104
|
#
|
93
105
|
# @note If subscribers are rapidly created/destroyed mid-publish, there's a theoretical
|
94
106
|
# possibility of object_id reuse. However, this is extremely unlikely in practice.
|
@@ -98,21 +110,27 @@ class LocalBus
|
|
98
110
|
#
|
99
111
|
# @note If the timeout is exceeded, the task will be cancelled before all subscribers have completed.
|
100
112
|
#
|
101
|
-
# Check
|
113
|
+
# Check individual Subscribers for possible errors.
|
102
114
|
#
|
103
115
|
# @rbs topic: String -- topic name
|
104
|
-
# @rbs timeout: Float -- seconds to wait before cancelling (default:
|
116
|
+
# @rbs timeout: Float -- seconds to wait for subscribers to process the message before cancelling (default: 60)
|
105
117
|
# @rbs payload: Hash -- message payload
|
106
|
-
# @rbs return:
|
107
|
-
def publish(topic, timeout:
|
118
|
+
# @rbs return: Message
|
119
|
+
def publish(topic, timeout: 60, **payload)
|
120
|
+
publish_message Message.new(topic, timeout: timeout.to_f, **payload)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Publishes a pre-built message
|
124
|
+
# @rbs message: Message -- message to publish
|
125
|
+
# @rbs return: Message
|
126
|
+
def publish_message(message)
|
108
127
|
barrier = Async::Barrier.new
|
109
|
-
message = Message.new(topic, timeout: timeout, **payload)
|
110
128
|
subscribers = subscriptions.fetch(message.topic, []).map { Subscriber.new _1, message }
|
111
129
|
|
112
130
|
if subscribers.any?
|
113
131
|
Sync do |task|
|
114
|
-
task.with_timeout timeout
|
115
|
-
semaphore = Async::Semaphore.new(
|
132
|
+
task.with_timeout message.timeout do
|
133
|
+
semaphore = Async::Semaphore.new(concurrency, parent: barrier)
|
116
134
|
|
117
135
|
subscribers.each do |subscriber|
|
118
136
|
semaphore.async do
|
@@ -129,7 +147,8 @@ class LocalBus
|
|
129
147
|
end
|
130
148
|
end
|
131
149
|
|
132
|
-
|
150
|
+
message.publication = Publication.new(barrier, *subscribers)
|
151
|
+
message
|
133
152
|
end
|
134
153
|
end
|
135
154
|
end
|
data/lib/local_bus/message.rb
CHANGED
@@ -11,50 +11,82 @@ class LocalBus
|
|
11
11
|
# @rbs timeout: Float? -- optional timeout for message processing (in seconds)
|
12
12
|
# @rbs payload: Hash -- the message payload
|
13
13
|
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
14
|
@metadata ||= {
|
21
|
-
id:
|
22
|
-
topic: topic,
|
23
|
-
payload: payload,
|
24
|
-
created_at:
|
25
|
-
thread_id:
|
26
|
-
timeout: timeout
|
15
|
+
id: SecureRandom.uuid_v7,
|
16
|
+
topic: topic.to_s.freeze,
|
17
|
+
payload: payload.transform_keys(&:to_sym).freeze,
|
18
|
+
created_at: Time.now,
|
19
|
+
thread_id: Thread.current.object_id,
|
20
|
+
timeout: timeout.to_f
|
27
21
|
}.freeze
|
28
|
-
freeze
|
29
22
|
end
|
30
23
|
|
24
|
+
# Metadata for the message
|
25
|
+
# @rbs return: Hash[Symbol, untyped]
|
26
|
+
attr_reader :metadata
|
27
|
+
|
28
|
+
# Publication representing the Async barrier and subscribers handling the message
|
29
|
+
# @note May be nil if processing hasn't happened yet (e.g. it was published via Station)
|
30
|
+
# @rbs return: Publication?
|
31
|
+
attr_accessor :publication
|
32
|
+
|
31
33
|
# Unique identifier for the message
|
32
34
|
# @rbs return: String
|
33
|
-
|
35
|
+
def id
|
36
|
+
metadata[:id]
|
37
|
+
end
|
34
38
|
|
35
39
|
# Message topic
|
36
40
|
# @rbs return: String
|
37
|
-
|
41
|
+
def topic
|
42
|
+
metadata[:topic]
|
43
|
+
end
|
38
44
|
|
39
45
|
# Message payload
|
40
46
|
# @rbs return: Hash
|
41
|
-
|
47
|
+
def payload
|
48
|
+
metadata[:payload]
|
49
|
+
end
|
42
50
|
|
43
51
|
# Time when the message was created or published
|
44
52
|
# @rbs return: Time
|
45
|
-
|
53
|
+
def created_at
|
54
|
+
metadata[:created_at]
|
55
|
+
end
|
46
56
|
|
47
57
|
# ID of the thread that created the message
|
48
58
|
# @rbs return: Integer
|
49
|
-
|
59
|
+
def thread_id
|
60
|
+
metadata[:thread_id]
|
61
|
+
end
|
50
62
|
|
51
63
|
# Timeout for message processing (in seconds)
|
52
64
|
# @rbs return: Float
|
53
|
-
|
65
|
+
def timeout
|
66
|
+
metadata[:timeout]
|
67
|
+
end
|
54
68
|
|
55
|
-
#
|
56
|
-
# @rbs
|
57
|
-
|
69
|
+
# Blocks and waits for the message to process
|
70
|
+
# @rbs interval: Float -- time to wait between checks (default: 0.1)
|
71
|
+
# @rbs return: void
|
72
|
+
def wait(interval: 0.1)
|
73
|
+
@timers ||= Timers::Group.new.tap { _1.every(interval) {} }
|
74
|
+
loop do
|
75
|
+
break if publication
|
76
|
+
@timers.wait
|
77
|
+
end
|
78
|
+
publication&.wait
|
79
|
+
ensure
|
80
|
+
@timers&.cancel
|
81
|
+
@timers = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# Blocks and waits for the message process then returns all subscribers
|
85
|
+
# @rbs return: Array[Subscriber]
|
86
|
+
def subscribers
|
87
|
+
wait
|
88
|
+
publication.subscribers
|
89
|
+
end
|
58
90
|
|
59
91
|
# Converts the message to a hash
|
60
92
|
# @rbs return: Hash[Symbol, untyped]
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rbs_inline: enabled
|
4
|
+
|
5
|
+
class LocalBus
|
6
|
+
# Wraps an Async::Barrier and a list of Subscribers that are processing a Message.
|
7
|
+
class Publication
|
8
|
+
# Constructor
|
9
|
+
# @rbs barrier: Async::Barrier -- barrier used to wait for all subscribers
|
10
|
+
# @rbs subscribers: Array[Subscriber]
|
11
|
+
def initialize(barrier, *subscribers)
|
12
|
+
@barrier = barrier
|
13
|
+
@subscribers = subscribers
|
14
|
+
end
|
15
|
+
|
16
|
+
# Blocks and waits for the barrier (i.e. all subscribers to complete)
|
17
|
+
# @rbs return: void
|
18
|
+
def wait
|
19
|
+
@barrier.wait
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# List of Subscribers that are processing a Message
|
24
|
+
# @note Blocks until all subscribers complete
|
25
|
+
# @rbs return: Array[Subscriber]
|
26
|
+
def subscribers
|
27
|
+
wait
|
28
|
+
@subscribers
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/local_bus/station.rb
CHANGED
@@ -5,119 +5,137 @@
|
|
5
5
|
# rubocop:disable Style/ArgumentsForwarding
|
6
6
|
|
7
7
|
class LocalBus
|
8
|
-
#
|
9
|
-
# This class acts as an intermediary, queuing messages internally before publishing them to the Bus.
|
8
|
+
# The Station serves as a queuing system for messages, similar to a bus station where passengers wait for their bus.
|
10
9
|
#
|
11
|
-
#
|
12
|
-
#
|
10
|
+
# When a message is published to the Station, it is queued and processed at a later time, allowing for deferred execution.
|
11
|
+
# This is particularly useful for tasks that can be handled later.
|
13
12
|
#
|
14
|
-
#
|
15
|
-
#
|
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
|
19
|
-
#
|
20
|
-
# IMPORTANT: Be sure to release resources like database connections in subscribers when publishing via Station.
|
13
|
+
# The Station employs a thread pool to manage message processing, enabling high concurrency and efficient resource utilization.
|
14
|
+
# Messages can also be prioritized, ensuring that higher-priority tasks are processed first.
|
21
15
|
#
|
16
|
+
# @note: While the Station provides a robust mechanism for background processing,
|
17
|
+
# it's important to understand that the exact timing of message processing is not controlled by the publisher,
|
18
|
+
# and messages will be processed as resources become available.
|
22
19
|
class Station
|
23
20
|
include MonitorMixin
|
24
21
|
|
25
|
-
class
|
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
|
22
|
+
class CapacityError < StandardError; end
|
33
23
|
|
34
24
|
# Constructor
|
25
|
+
#
|
26
|
+
# @note Delays process exit in an attempt to flush the queue to avoid dropping messages.
|
27
|
+
# Exit flushing makes a "best effort" to process all messages, but it's not guaranteed.
|
28
|
+
# Will not delay process exit when the queue is empty.
|
29
|
+
#
|
35
30
|
# @rbs bus: Bus -- local message bus (default: Bus.new)
|
36
|
-
# @rbs
|
37
|
-
# @rbs
|
38
|
-
# @rbs
|
39
|
-
# @rbs
|
31
|
+
# @rbs interval: Float -- queue polling interval in seconds (default: 0.01)
|
32
|
+
# @rbs limit: Integer -- max queue size (default: 10_000)
|
33
|
+
# @rbs threads: Integer -- number of threads to use (default: Etc.nprocessors)
|
34
|
+
# @rbs timeout: Float -- seconds to wait for subscribers to process the message before cancelling (default: 60)
|
35
|
+
# @rbs wait: Float -- seconds to wait for the queue to flush at process exit (default: 5)
|
40
36
|
# @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
|
-
)
|
37
|
+
def initialize(bus: Bus.new, interval: 0.01, limit: 10_000, threads: Etc.nprocessors, timeout: 60, wait: 5)
|
48
38
|
super()
|
49
39
|
@bus = bus
|
50
|
-
@
|
51
|
-
@
|
52
|
-
@
|
53
|
-
@
|
54
|
-
|
40
|
+
@interval = [interval.to_f, 0.01].max
|
41
|
+
@limit = limit.to_i.positive? ? limit.to_i : 10_000
|
42
|
+
@threads = [threads.to_i, 1].max
|
43
|
+
@timeout = timeout.to_f
|
44
|
+
@queue = Containers::PriorityQueue.new
|
45
|
+
at_exit { stop timeout: [wait.to_f, 1].max }
|
46
|
+
start
|
55
47
|
end
|
56
48
|
|
57
49
|
# Bus instance
|
58
50
|
# @rbs return: Bus
|
59
51
|
attr_reader :bus
|
60
52
|
|
61
|
-
#
|
53
|
+
# Queue polling interval in seconds
|
54
|
+
# @rbs return: Float
|
55
|
+
attr_reader :interval
|
56
|
+
|
57
|
+
# Max queue size
|
62
58
|
# @rbs return: Integer
|
63
|
-
attr_reader :
|
59
|
+
attr_reader :limit
|
64
60
|
|
65
|
-
#
|
66
|
-
# @rbs return:
|
67
|
-
attr_reader :
|
61
|
+
# Number of threads to use
|
62
|
+
# @rbs return: Integer
|
63
|
+
attr_reader :threads
|
68
64
|
|
69
|
-
#
|
65
|
+
# Default timeout for message processing (in seconds)
|
70
66
|
# @rbs return: Float
|
71
|
-
attr_reader :
|
67
|
+
attr_reader :timeout
|
72
68
|
|
73
|
-
# Starts the
|
74
|
-
# @rbs
|
69
|
+
# Starts the station
|
70
|
+
# @rbs interval: Float -- queue polling interval in seconds (default: 0.01)
|
71
|
+
# @rbs threads: Integer -- number of threads to use (default: self.threads)
|
75
72
|
# @rbs return: void
|
76
|
-
def start(
|
73
|
+
def start(interval: self.interval, threads: self.threads)
|
74
|
+
interval = [interval.to_f, 0.01].max
|
75
|
+
threads = [threads.to_i, 1].max
|
76
|
+
|
77
77
|
synchronize do
|
78
|
-
return if running?
|
78
|
+
return if running? || stopping?
|
79
|
+
|
80
|
+
timers = Timers::Group.new
|
81
|
+
@pool = []
|
82
|
+
threads.times do
|
83
|
+
@pool << Thread.new do
|
84
|
+
Thread.current.report_on_exception = true
|
85
|
+
timers.every interval do
|
86
|
+
message = synchronize { @queue.pop unless @queue.empty? || stopping? }
|
87
|
+
bus.send :publish_message, message if message
|
88
|
+
end
|
79
89
|
|
80
|
-
|
81
|
-
|
82
|
-
|
90
|
+
loop do
|
91
|
+
timers.wait
|
92
|
+
break if stopping?
|
93
|
+
end
|
94
|
+
ensure
|
95
|
+
timers.cancel
|
96
|
+
end
|
97
|
+
end
|
83
98
|
end
|
84
99
|
end
|
85
100
|
|
86
|
-
# Stops the
|
87
|
-
# @rbs timeout: Float -- seconds to wait for
|
101
|
+
# Stops the station
|
102
|
+
# @rbs timeout: Float -- seconds to wait for message processing before killing the thread pool (default: nil)
|
88
103
|
# @rbs return: void
|
89
|
-
def stop(timeout:
|
90
|
-
return unless @shutdown.make_true # Ensure we only stop once
|
91
|
-
|
104
|
+
def stop(timeout: nil)
|
92
105
|
synchronize do
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
106
|
+
return unless running?
|
107
|
+
return if stopping?
|
108
|
+
@stopping = true
|
104
109
|
end
|
105
110
|
|
106
|
-
|
107
|
-
|
108
|
-
@shutdown_queue&.close
|
109
|
-
@shutdown_thread&.join timeout
|
111
|
+
@pool&.each do |thread|
|
112
|
+
timeout.is_a?(Numeric) ? thread.join(timeout) : thread.join
|
110
113
|
end
|
114
|
+
ensure
|
115
|
+
@stopping = false
|
116
|
+
@pool = nil
|
117
|
+
end
|
111
118
|
|
112
|
-
|
113
|
-
|
114
|
-
@shutdown_completed&.set
|
119
|
+
def stopping?
|
120
|
+
synchronize { !!@stopping }
|
115
121
|
end
|
116
122
|
|
117
|
-
# Indicates if the
|
123
|
+
# Indicates if the station is running
|
118
124
|
# @rbs return: bool
|
119
125
|
def running?
|
120
|
-
synchronize { pool
|
126
|
+
synchronize { !!@pool }
|
127
|
+
end
|
128
|
+
|
129
|
+
# Indicates if the queue is empty
|
130
|
+
# @rbs return: bool
|
131
|
+
def empty?
|
132
|
+
synchronize { @queue.empty? }
|
133
|
+
end
|
134
|
+
|
135
|
+
# Number of unprocessed messages in the queue
|
136
|
+
# @rbs return: Integer
|
137
|
+
def count
|
138
|
+
synchronize { @queue.size }
|
121
139
|
end
|
122
140
|
|
123
141
|
# Subscribe to a topic
|
@@ -125,103 +143,46 @@ class LocalBus
|
|
125
143
|
# @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
|
126
144
|
# @rbs &block: (Message) -> untyped -- alternative way to provide a callable
|
127
145
|
# @rbs return: self
|
128
|
-
def subscribe(
|
129
|
-
bus.subscribe(
|
146
|
+
def subscribe(...)
|
147
|
+
bus.subscribe(...)
|
130
148
|
self
|
131
149
|
end
|
132
150
|
|
133
|
-
#
|
151
|
+
# Unsubscribes a callable from a topic
|
134
152
|
# @rbs topic: String -- topic name
|
153
|
+
# @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
|
135
154
|
# @rbs return: self
|
136
|
-
def unsubscribe(
|
137
|
-
bus.unsubscribe(
|
155
|
+
def unsubscribe(...)
|
156
|
+
bus.unsubscribe(...)
|
138
157
|
self
|
139
158
|
end
|
140
159
|
|
141
160
|
# Unsubscribes all subscribers from a topic and removes the topic
|
142
161
|
# @rbs topic: String -- topic name
|
143
162
|
# @rbs return: self
|
144
|
-
def unsubscribe_all(
|
145
|
-
bus.unsubscribe_all
|
163
|
+
def unsubscribe_all(...)
|
164
|
+
bus.unsubscribe_all(...)
|
146
165
|
self
|
147
166
|
end
|
148
167
|
|
149
|
-
# Publishes a message
|
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
|
168
|
+
# Publishes a message
|
155
169
|
#
|
156
170
|
# @rbs topic: String | Symbol -- topic name
|
171
|
+
# @rbs priority: Integer -- priority of the message, higher number == higher priority (default: 1)
|
157
172
|
# @rbs timeout: Float -- seconds to wait before cancelling
|
158
173
|
# @rbs payload: Hash[Symbol, untyped] -- message payload
|
159
|
-
# @rbs return:
|
160
|
-
def publish(topic, timeout:
|
161
|
-
timeout
|
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
|
177
|
-
end
|
178
|
-
|
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
|
174
|
+
# @rbs return: Message
|
175
|
+
def publish(topic, priority: 1, timeout: self.timeout, **payload)
|
176
|
+
publish_message Message.new(topic, timeout: timeout, **payload), priority: priority
|
212
177
|
end
|
213
178
|
|
214
|
-
#
|
215
|
-
# @rbs
|
216
|
-
# @rbs return:
|
217
|
-
def
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
@shutdown_queue.push signal unless @shutdown.true?
|
222
|
-
rescue
|
223
|
-
nil
|
224
|
-
end
|
179
|
+
# Publishes a pre-built message
|
180
|
+
# @rbs message: Message -- message to publish
|
181
|
+
# @rbs return: Message
|
182
|
+
def publish_message(message, priority: 1)
|
183
|
+
synchronize do
|
184
|
+
raise CapacityError, "Station is at capacity! (limit: #{limit})" if @queue.size >= limit
|
185
|
+
@queue.push message, priority
|
225
186
|
end
|
226
187
|
end
|
227
188
|
end
|