local_bus 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -5,6 +5,8 @@
5
5
  class LocalBus
6
6
  # Wraps a Callable (Proc) and Message intended for asynchronous execution.
7
7
  class Subscriber
8
+ include MonitorMixin
9
+
8
10
  # Custom error class for Subscriber errors
9
11
  class Error < StandardError
10
12
  # Constructor
@@ -24,6 +26,7 @@ class LocalBus
24
26
  # @rbs callable: #call -- the subscriber's callable object
25
27
  # @rbs message: Message -- the message to be processed
26
28
  def initialize(callable, message)
29
+ super()
27
30
  @callable = callable
28
31
  @message = message
29
32
  @id = callable.object_id
@@ -68,21 +71,29 @@ class LocalBus
68
71
  metadata.any?
69
72
  end
70
73
 
71
- # Checks if the subscriber is pending
74
+ # Indicates if the subscriber is pending or unperformed
72
75
  # @rbs return: bool
73
76
  def pending?
74
77
  metadata.empty?
75
78
  end
76
79
 
80
+ # Indicates if the subscriber has errored
81
+ # @rbs return: bool
82
+ def errored?
83
+ !!error
84
+ end
85
+
77
86
  # Performs the subscriber's callable
78
87
  # @rbs return: void
79
88
  def perform
80
- return if performed?
81
-
82
- with_metadata do
83
- @value = callable.call(message)
84
- rescue => cause
85
- @error = Error.new("Invocation failed! #{cause.message}", cause: cause)
89
+ synchronize do
90
+ return if performed?
91
+
92
+ with_metadata do
93
+ @value = callable.call(message)
94
+ rescue => cause
95
+ @error = Error.new("Invocation failed! #{cause.message}", cause: cause)
96
+ end
86
97
  end
87
98
  end
88
99
 
@@ -129,7 +140,7 @@ class LocalBus
129
140
  finished_at: Time.now,
130
141
  duration: Time.now - started_at,
131
142
  latency: Time.now - message.created_at,
132
- message: message
143
+ message: message.to_h
133
144
  }.freeze
134
145
  end
135
146
  end
@@ -3,5 +3,5 @@
3
3
  # rbs_inline: enabled
4
4
 
5
5
  class LocalBus
6
- VERSION = "0.2.0"
6
+ VERSION = "0.3.0"
7
7
  end
data/lib/local_bus.rb CHANGED
@@ -6,20 +6,71 @@ require "zeitwerk"
6
6
  loader = Zeitwerk::Loader.for_gem
7
7
  loader.setup
8
8
 
9
+ require "algorithms"
9
10
  require "async"
10
11
  require "async/barrier"
11
12
  require "async/semaphore"
12
- require "concurrent-ruby"
13
+ require "etc"
13
14
  require "monitor"
14
15
  require "securerandom"
15
16
  require "singleton"
17
+ require "timers"
16
18
 
17
19
  class LocalBus
18
20
  include Singleton
19
21
 
22
+ # Default Bus instance
23
+ # @rbs return: Bus
20
24
  attr_reader :bus
25
+
26
+ # Default Station instance
27
+ # @rbs return: Station
21
28
  attr_reader :station
22
29
 
30
+ class << self
31
+ # Publishes a message via the default Station
32
+ #
33
+ # @rbs topic: String | Symbol -- topic name
34
+ # @rbs priority: Integer -- priority of the message, higher number == higher priority (default: 1)
35
+ # @rbs timeout: Float -- seconds to wait before cancelling
36
+ # @rbs payload: Hash[Symbol, untyped] -- message payload
37
+ # @rbs return: Message
38
+ def publish(...)
39
+ instance.station.publish(...)
40
+ end
41
+
42
+ # Publishes a pre-built message via the default Station
43
+ # @rbs message: Message -- message to publish
44
+ # @rbs return: Message
45
+ def publish_message(...)
46
+ instance.station.publish_message(...)
47
+ end
48
+
49
+ # Subscribe to a topic via the default Station
50
+ # @rbs topic: String -- topic name
51
+ # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
52
+ # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
53
+ # @rbs return: Station
54
+ def subscribe(...)
55
+ instance.station.subscribe(...)
56
+ end
57
+
58
+ # Unsubscribes a callable from a topic via the default Station
59
+ # @rbs topic: String -- topic name
60
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
61
+ # @rbs return: Station
62
+ def unsubscribe(...)
63
+ instance.station.unsubscribe(...)
64
+ end
65
+
66
+ # Unsubscribes all subscribers from a topic and removes the topic via the default Station
67
+ # @rbs topic: String -- topic name
68
+ # @rbs return: Station
69
+ def unsubscribe_all(...)
70
+ instance.station.unsubscribe_all(...)
71
+ end
72
+ end
73
+
23
74
  private
24
75
 
25
76
  def initialize
@@ -0,0 +1,83 @@
1
+ # Generated from lib/local_bus/bus.rb with RBS::Inline
2
+
3
+ class LocalBus
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.
11
+ class Bus
12
+ include MonitorMixin
13
+
14
+ # Constructor
15
+ # @note Creates a new Bus instance with specified max concurrency (i.e. number of tasks that can run in parallel)
16
+ # @rbs concurrency: Integer -- maximum number of concurrent tasks (default: Etc.nprocessors)
17
+ def initialize: (?concurrency: Integer) -> untyped
18
+
19
+ # Maximum number of concurrent tasks that can run in "parallel"
20
+ # @rbs return: Integer
21
+ def concurrency: () -> Integer
22
+
23
+ # Sets the max concurrency
24
+ # @rbs value: Integer -- max number of concurrent tasks that can run in "parallel"
25
+ # @rbs return: Integer -- new concurrency value
26
+ def concurrency=: (Integer value) -> Integer
27
+
28
+ # Registered topics that have subscribers
29
+ # @rbs return: Array[String] -- list of topic names
30
+ def topics: () -> Array[String]
31
+
32
+ # Registered subscriptions
33
+ # @rbs return: Hash[String, Array[callable]] -- mapping of topics to callables
34
+ def subscriptions: () -> Hash[String, Array[callable]]
35
+
36
+ # Subscribes a callable to a topic
37
+ # @rbs topic: String -- topic name
38
+ # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
39
+ # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
40
+ # @rbs return: self
41
+ # @raise [ArgumentError] if neither callable nor block is provided
42
+ def subscribe: (String topic, ?callable: Message) { (Message) -> untyped } -> self
43
+
44
+ # Unsubscribes a callable from a topic
45
+ # @rbs topic: String -- topic name
46
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
47
+ # @rbs return: self
48
+ def unsubscribe: (String topic, callable: Message) -> self
49
+
50
+ # Unsubscribes all subscribers from a topic and removes the topic
51
+ # @rbs topic: String -- topic name
52
+ # @rbs return: self
53
+ def unsubscribe_all: (String topic) -> self
54
+
55
+ # Executes a block and unsubscribes all subscribers from the topic afterwards
56
+ # @rbs topic: String -- topic name
57
+ # @rbs block: (String) -> void -- block to execute (yields the topic)
58
+ def with_topic: (String topic) ?{ (?) -> untyped } -> untyped
59
+
60
+ # Publishes a message
61
+ #
62
+ # @note If subscribers are rapidly created/destroyed mid-publish, there's a theoretical
63
+ # possibility of object_id reuse. However, this is extremely unlikely in practice.
64
+ #
65
+ # * If subscribers are added mid-publish, they will not receive the message
66
+ # * If subscribers are removed mid-publish, they will still receive the message
67
+ #
68
+ # @note If the timeout is exceeded, the task will be cancelled before all subscribers have completed.
69
+ #
70
+ # Check individual Subscribers for possible errors.
71
+ #
72
+ # @rbs topic: String -- topic name
73
+ # @rbs timeout: Float -- seconds to wait for subscribers to process the message before cancelling (default: 60)
74
+ # @rbs payload: Hash -- message payload
75
+ # @rbs return: Message
76
+ def publish: (String topic, ?timeout: Float, **untyped payload) -> Message
77
+
78
+ # Publishes a pre-built message
79
+ # @rbs message: Message -- message to publish
80
+ # @rbs return: Message
81
+ def publish_message: (Message message) -> Message
82
+ end
83
+ end
@@ -0,0 +1,60 @@
1
+ # Generated from lib/local_bus/message.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ # Represents a message in the LocalBus system
5
+ class Message
6
+ # Constructor
7
+ # @note Creates a new Message instance with the given topic and payload
8
+ # @rbs topic: String -- the topic of the message
9
+ # @rbs timeout: Float? -- optional timeout for message processing (in seconds)
10
+ # @rbs payload: Hash -- the message payload
11
+ def initialize: (String topic, ?timeout: Float?, **untyped payload) -> untyped
12
+
13
+ # Metadata for the message
14
+ # @rbs return: Hash[Symbol, untyped]
15
+ attr_reader metadata: untyped
16
+
17
+ # Publication representing the Async barrier and subscribers handling the message
18
+ # @note May be nil if processing hasn't happened yet (e.g. it was published via Station)
19
+ # @rbs return: Publication?
20
+ attr_accessor publication: untyped
21
+
22
+ # Unique identifier for the message
23
+ # @rbs return: String
24
+ def id: () -> String
25
+
26
+ # Message topic
27
+ # @rbs return: String
28
+ def topic: () -> String
29
+
30
+ # Message payload
31
+ # @rbs return: Hash
32
+ def payload: () -> Hash
33
+
34
+ # Time when the message was created or published
35
+ # @rbs return: Time
36
+ def created_at: () -> Time
37
+
38
+ # ID of the thread that created the message
39
+ # @rbs return: Integer
40
+ def thread_id: () -> Integer
41
+
42
+ # Timeout for message processing (in seconds)
43
+ # @rbs return: Float
44
+ def timeout: () -> Float
45
+
46
+ # Blocks and waits for the message to process
47
+ # @rbs interval: Float -- time to wait between checks (default: 0.1)
48
+ # @rbs return: void
49
+ def wait: (?interval: Float) -> void
50
+
51
+ # Blocks and waits for the message process then returns all subscribers
52
+ # @rbs return: Array[Subscriber]
53
+ def subscribers: () -> Array[Subscriber]
54
+
55
+ # Allows pattern matching on message attributes
56
+ # @rbs keys: Array[Symbol] -- keys to extract from the message
57
+ # @rbs return: Hash[Symbol, untyped]
58
+ def deconstruct_keys: (Array[Symbol] keys) -> Hash[Symbol, untyped]
59
+ end
60
+ end
@@ -0,0 +1,20 @@
1
+ # Generated from lib/local_bus/publication.rb with RBS::Inline
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: (Async::Barrier barrier, *untyped subscribers) -> untyped
10
+
11
+ # Blocks and waits for the barrier (i.e. all subscribers to complete)
12
+ # @rbs return: void
13
+ def wait: () -> void
14
+
15
+ # List of Subscribers that are processing a Message
16
+ # @note Blocks until all subscribers complete
17
+ # @rbs return: Array[Subscriber]
18
+ def subscribers: () -> Array[Subscriber]
19
+ end
20
+ end
@@ -0,0 +1,113 @@
1
+ # Generated from lib/local_bus/station.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ # The Station serves as a queuing system for messages, similar to a bus station where passengers wait for their bus.
5
+ #
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.
8
+ #
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.
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.
15
+ class Station
16
+ include MonitorMixin
17
+
18
+ class CapacityError < StandardError
19
+ end
20
+
21
+ # Constructor
22
+ #
23
+ # @note Delays process exit in an attempt to flush the queue to avoid dropping messages.
24
+ # Exit flushing makes a "best effort" to process all messages, but it's not guaranteed.
25
+ # Will not delay process exit when the queue is empty.
26
+ #
27
+ # @rbs bus: Bus -- local message bus (default: Bus.new)
28
+ # @rbs interval: Float -- queue polling interval in seconds (default: 0.01)
29
+ # @rbs limit: Integer -- max queue size (default: 10_000)
30
+ # @rbs threads: Integer -- number of threads to use (default: Etc.nprocessors)
31
+ # @rbs timeout: Float -- seconds to wait for subscribers to process the message before cancelling (default: 60)
32
+ # @rbs wait: Float -- seconds to wait for the queue to flush at process exit (default: 5)
33
+ # @rbs return: void
34
+ def initialize: (?bus: Bus, ?interval: Float, ?limit: Integer, ?threads: Integer, ?timeout: Float, ?wait: Float) -> void
35
+
36
+ # Bus instance
37
+ # @rbs return: Bus
38
+ attr_reader bus: untyped
39
+
40
+ # Queue polling interval in seconds
41
+ # @rbs return: Float
42
+ attr_reader interval: untyped
43
+
44
+ # Max queue size
45
+ # @rbs return: Integer
46
+ attr_reader limit: untyped
47
+
48
+ # Number of threads to use
49
+ # @rbs return: Integer
50
+ attr_reader threads: untyped
51
+
52
+ # Default timeout for message processing (in seconds)
53
+ # @rbs return: Float
54
+ attr_reader timeout: untyped
55
+
56
+ # Starts the station
57
+ # @rbs interval: Float -- queue polling interval in seconds (default: 0.01)
58
+ # @rbs threads: Integer -- number of threads to use (default: self.threads)
59
+ # @rbs return: void
60
+ def start: (?interval: Float, ?threads: Integer) -> void
61
+
62
+ # Stops the station
63
+ # @rbs timeout: Float -- seconds to wait for message processing before killing the thread pool (default: nil)
64
+ # @rbs return: void
65
+ def stop: (?timeout: Float) -> void
66
+
67
+ def stopping?: () -> untyped
68
+
69
+ # Indicates if the station is running
70
+ # @rbs return: bool
71
+ def running?: () -> bool
72
+
73
+ # Indicates if the queue is empty
74
+ # @rbs return: bool
75
+ def empty?: () -> bool
76
+
77
+ # Number of unprocessed messages in the queue
78
+ # @rbs return: Integer
79
+ def count: () -> Integer
80
+
81
+ # Subscribe to a topic
82
+ # @rbs topic: String -- topic name
83
+ # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
84
+ # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
85
+ # @rbs return: self
86
+ def subscribe: () { (Message) -> untyped } -> self
87
+
88
+ # Unsubscribes a callable from a topic
89
+ # @rbs topic: String -- topic name
90
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
91
+ # @rbs return: self
92
+ def unsubscribe: () -> self
93
+
94
+ # Unsubscribes all subscribers from a topic and removes the topic
95
+ # @rbs topic: String -- topic name
96
+ # @rbs return: self
97
+ def unsubscribe_all: () -> self
98
+
99
+ # Publishes a message
100
+ #
101
+ # @rbs topic: String | Symbol -- topic name
102
+ # @rbs priority: Integer -- priority of the message, higher number == higher priority (default: 1)
103
+ # @rbs timeout: Float -- seconds to wait before cancelling
104
+ # @rbs payload: Hash[Symbol, untyped] -- message payload
105
+ # @rbs return: Message
106
+ def publish: (String | Symbol topic, ?priority: Integer, ?timeout: Float, **untyped payload) -> Message
107
+
108
+ # Publishes a pre-built message
109
+ # @rbs message: Message -- message to publish
110
+ # @rbs return: Message
111
+ def publish_message: (Message message, ?priority: untyped) -> Message
112
+ end
113
+ end
@@ -0,0 +1,89 @@
1
+ # Generated from lib/local_bus/subscriber.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ # Wraps a Callable (Proc) and Message intended for asynchronous execution.
5
+ class Subscriber
6
+ include MonitorMixin
7
+
8
+ # Custom error class for Subscriber errors
9
+ class Error < StandardError
10
+ # Constructor
11
+ # @rbs message: String -- error message
12
+ # @rbs cause: StandardError? -- underlying cause of the error
13
+ def initialize: (String message, cause: StandardError?) -> untyped
14
+
15
+ # Underlying cause of the error
16
+ # @rbs return: StandardError?
17
+ attr_reader cause: untyped
18
+ end
19
+
20
+ # Constructor
21
+ # @rbs callable: #call -- the subscriber's callable object
22
+ # @rbs message: Message -- the message to be processed
23
+ def initialize: (untyped callable, Message message) -> untyped
24
+
25
+ # Unique identifier for the subscriber
26
+ # @rbs return: String
27
+ attr_reader id: untyped
28
+
29
+ # Source location of the callable
30
+ # @rbs return: Array[String, Integer]?
31
+ attr_reader source_location: untyped
32
+
33
+ # Callable object -- Proc, lambda, etc. (must respond to #call)
34
+ # @rbs return: #call
35
+ attr_reader callable: untyped
36
+
37
+ # Error if the subscriber fails (available after performing)
38
+ # @rbs return: Error?
39
+ attr_reader error: untyped
40
+
41
+ # Message for the subscriber to process
42
+ # @rbs return: Message
43
+ attr_reader message: untyped
44
+
45
+ # Metadata for the subscriber (available after performing)
46
+ # @rbs return: Hash[Symbol, untyped]
47
+ attr_reader metadata: untyped
48
+
49
+ # Value returned by the callable (available after performing)
50
+ # @rbs return: untyped
51
+ attr_reader value: untyped
52
+
53
+ # Indicates if the subscriber has been performed
54
+ # @rbs return: bool
55
+ def performed?: () -> bool
56
+
57
+ # Indicates if the subscriber is pending or unperformed
58
+ # @rbs return: bool
59
+ def pending?: () -> bool
60
+
61
+ # Indicates if the subscriber has errored
62
+ # @rbs return: bool
63
+ def errored?: () -> bool
64
+
65
+ # Performs the subscriber's callable
66
+ # @rbs return: void
67
+ def perform: () -> void
68
+
69
+ # Handles timeout for the subscriber
70
+ # @rbs cause: StandardError -- the cause of the timeout
71
+ # @rbs return: void
72
+ def timeout: (StandardError cause) -> void
73
+
74
+ # Returns the subscriber's data as a hash
75
+ # @rbs return: Hash[Symbol, untyped]
76
+ def to_h: () -> Hash[Symbol, untyped]
77
+
78
+ # Allows pattern matching on subscriber attributes
79
+ # @rbs keys: Array[Symbol] -- keys to extract from the subscriber
80
+ # @rbs return: Hash[Symbol, untyped]
81
+ def deconstruct_keys: (Array[Symbol] keys) -> Hash[Symbol, untyped]
82
+
83
+ private
84
+
85
+ # Captures metadata for the subscriber's performance
86
+ # @rbs return: void
87
+ def with_metadata: () -> void
88
+ end
89
+ end
@@ -0,0 +1,5 @@
1
+ # Generated from lib/local_bus/version.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ VERSION: ::String
5
+ end
@@ -0,0 +1,49 @@
1
+ # Generated from lib/local_bus.rb with RBS::Inline
2
+
3
+ class LocalBus
4
+ include Singleton
5
+
6
+ # Default Bus instance
7
+ # @rbs return: Bus
8
+ attr_reader bus: untyped
9
+
10
+ # Default Station instance
11
+ # @rbs return: Station
12
+ attr_reader station: untyped
13
+
14
+ # Publishes a message via the default Station
15
+ #
16
+ # @rbs topic: String | Symbol -- topic name
17
+ # @rbs priority: Integer -- priority of the message, higher number == higher priority (default: 1)
18
+ # @rbs timeout: Float -- seconds to wait before cancelling
19
+ # @rbs payload: Hash[Symbol, untyped] -- message payload
20
+ # @rbs return: Message
21
+ def self.publish: () -> Message
22
+
23
+ # Publishes a pre-built message via the default Station
24
+ # @rbs message: Message -- message to publish
25
+ # @rbs return: Message
26
+ def self.publish_message: () -> Message
27
+
28
+ # Subscribe to a topic via the default Station
29
+ # @rbs topic: String -- topic name
30
+ # @rbs callable: (Message) -> untyped -- callable that will process messages published to the topic
31
+ # @rbs &block: (Message) -> untyped -- alternative way to provide a callable
32
+ # @rbs return: Station
33
+ def self.subscribe: () { (Message) -> untyped } -> Station
34
+
35
+ # Unsubscribes a callable from a topic via the default Station
36
+ # @rbs topic: String -- topic name
37
+ # @rbs callable: (Message) -> untyped -- subscriber that should no longer receive messages
38
+ # @rbs return: Station
39
+ def self.unsubscribe: () -> Station
40
+
41
+ # Unsubscribes all subscribers from a topic and removes the topic via the default Station
42
+ # @rbs topic: String -- topic name
43
+ # @rbs return: Station
44
+ def self.unsubscribe_all: () -> Station
45
+
46
+ private
47
+
48
+ def initialize: () -> untyped
49
+ end