async-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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/lib/async/bus/client.rb +10 -3
- data/lib/async/bus/protocol/connection.rb +129 -24
- data/lib/async/bus/protocol/invoke.rb +20 -0
- data/lib/async/bus/protocol/proxy.rb +28 -5
- data/lib/async/bus/protocol/release.rb +8 -0
- data/lib/async/bus/protocol/response.rb +12 -0
- data/lib/async/bus/protocol/transaction.rb +37 -2
- data/lib/async/bus/protocol/wrapper.rb +98 -28
- data/lib/async/bus/server.rb +20 -2
- data/lib/async/bus/version.rb +3 -1
- data/readme.md +22 -6
- data/releases.md +11 -0
- data.tar.gz.sig +0 -0
- metadata +1 -1
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: df5f9eee987b7c64e94b521cfdcee2e83bba9657f5155084fb105b02adc9d84d
|
|
4
|
+
data.tar.gz: 76a6ac9c261fd91a3a8f9cc3f20dffe5ee7036a6b1b4a07a64d66170004e39f7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 985a0ac762db79cf5feb87fa3c50aad32bdf627b26f86e015b60d553d146c843023ea7287617834151a8074e23ca87dcc8877f3b323db89994dc04dec7b54ef0
|
|
7
|
+
data.tar.gz: 1f1e8ebda1dd9036a9452a95c410c1232316041600dd51e8b4f820ea99e67f8e3b7dc245f9cf3870ea62d7253cfd85ee1fb7e42ccbaf25248fce4b38086850e0
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/lib/async/bus/client.rb
CHANGED
|
@@ -8,7 +8,11 @@ require "async/queue"
|
|
|
8
8
|
|
|
9
9
|
module Async
|
|
10
10
|
module Bus
|
|
11
|
+
# Represents a client that can connect to a server.
|
|
11
12
|
class Client
|
|
13
|
+
# Initialize a new client.
|
|
14
|
+
# @parameter endpoint [IO::Endpoint] The endpoint to connect to.
|
|
15
|
+
# @parameter options [Hash] Additional options for the connection.
|
|
12
16
|
def initialize(endpoint = nil, **options)
|
|
13
17
|
@endpoint = endpoint || Protocol.local_endpoint
|
|
14
18
|
@options = options
|
|
@@ -59,14 +63,17 @@ module Async
|
|
|
59
63
|
# Automatically reconnects when the connection fails, with random backoff.
|
|
60
64
|
# This is useful for long-running clients that need to maintain a persistent connection.
|
|
61
65
|
#
|
|
62
|
-
# @
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
# @yields {|connection| ...} If a block is given, it will be called with the connection.
|
|
67
|
+
# @returns [Async::Task] The task that runs the client.
|
|
68
|
+
def run(&block)
|
|
69
|
+
Async(transient: true) do |task|
|
|
65
70
|
loop do
|
|
66
71
|
connection = connect!
|
|
67
72
|
|
|
68
73
|
connected_task = task.async do
|
|
69
74
|
connected!(connection)
|
|
75
|
+
|
|
76
|
+
yield(connection) if block_given?
|
|
70
77
|
end
|
|
71
78
|
|
|
72
79
|
connection.run
|
|
@@ -13,20 +13,38 @@ require_relative "response"
|
|
|
13
13
|
|
|
14
14
|
module Async
|
|
15
15
|
module Bus
|
|
16
|
+
# @namespace
|
|
16
17
|
module Protocol
|
|
18
|
+
# Create a local Unix domain socket endpoint.
|
|
19
|
+
# @parameter path [String] The path to the socket file.
|
|
20
|
+
# @returns [IO::Endpoint::Unix] The Unix endpoint.
|
|
17
21
|
def self.local_endpoint(path = "bus.ipc")
|
|
18
22
|
::IO::Endpoint.unix(path)
|
|
19
23
|
end
|
|
20
24
|
|
|
25
|
+
# Represents a connection between client and server for message passing.
|
|
21
26
|
class Connection
|
|
27
|
+
# Create a client-side connection.
|
|
28
|
+
# @parameter peer [IO] The peer connection.
|
|
29
|
+
# @parameter options [Hash] Additional options for the connection.
|
|
30
|
+
# @returns [Connection] A new client connection.
|
|
22
31
|
def self.client(peer, **options)
|
|
23
32
|
self.new(peer, 1, **options)
|
|
24
33
|
end
|
|
25
34
|
|
|
35
|
+
# Create a server-side connection.
|
|
36
|
+
# @parameter peer [IO] The peer connection.
|
|
37
|
+
# @parameter options [Hash] Additional options for the connection.
|
|
38
|
+
# @returns [Connection] A new server connection.
|
|
26
39
|
def self.server(peer, **options)
|
|
27
40
|
self.new(peer, 2, **options)
|
|
28
41
|
end
|
|
29
42
|
|
|
43
|
+
# Initialize a new connection.
|
|
44
|
+
# @parameter peer [IO] The peer connection.
|
|
45
|
+
# @parameter id [Integer] The initial transaction ID.
|
|
46
|
+
# @parameter wrapper [Class] The wrapper class for serialization.
|
|
47
|
+
# @parameter timeout [Float] The timeout for transactions.
|
|
30
48
|
def initialize(peer, id, wrapper: Wrapper, timeout: nil)
|
|
31
49
|
@peer = peer
|
|
32
50
|
@id = id
|
|
@@ -47,16 +65,20 @@ module Async
|
|
|
47
65
|
# @attribute [Float] The timeout for transactions.
|
|
48
66
|
attr_accessor :timeout
|
|
49
67
|
|
|
68
|
+
# Flush the packer buffer.
|
|
50
69
|
def flush
|
|
51
70
|
@packer.flush
|
|
52
71
|
end
|
|
53
72
|
|
|
73
|
+
# Write a message to the connection.
|
|
74
|
+
# @parameter message [Object] The message to write.
|
|
54
75
|
def write(message)
|
|
55
76
|
# $stderr.puts "Writing: #{message.inspect}"
|
|
56
77
|
@packer.write(message)
|
|
57
78
|
@packer.flush
|
|
58
79
|
end
|
|
59
80
|
|
|
81
|
+
# Close the connection and clean up resources.
|
|
60
82
|
def close
|
|
61
83
|
@transactions.each do |id, transaction|
|
|
62
84
|
transaction.close
|
|
@@ -65,16 +87,26 @@ module Async
|
|
|
65
87
|
@peer.close
|
|
66
88
|
end
|
|
67
89
|
|
|
90
|
+
# Return a string representation of the connection.
|
|
91
|
+
# @returns [String] A string describing the connection.
|
|
68
92
|
def inspect
|
|
69
93
|
"#<#{self.class} #{@objects.size} objects>"
|
|
70
94
|
end
|
|
71
95
|
|
|
96
|
+
# @attribute [Hash] The bound objects.
|
|
72
97
|
attr :objects
|
|
98
|
+
|
|
99
|
+
# @attribute [ObjectSpace::WeakMap] The proxy cache.
|
|
73
100
|
attr :proxies
|
|
74
101
|
|
|
102
|
+
# @attribute [MessagePack::Unpacker] The message unpacker.
|
|
75
103
|
attr :unpacker
|
|
104
|
+
|
|
105
|
+
# @attribute [MessagePack::Packer] The message packer.
|
|
76
106
|
attr :packer
|
|
77
107
|
|
|
108
|
+
# Get the next transaction ID.
|
|
109
|
+
# @returns [Integer] The next transaction ID.
|
|
78
110
|
def next_id
|
|
79
111
|
id = @id
|
|
80
112
|
@id += 2
|
|
@@ -82,6 +114,7 @@ module Async
|
|
|
82
114
|
return id
|
|
83
115
|
end
|
|
84
116
|
|
|
117
|
+
# @attribute [Hash] Active transactions.
|
|
85
118
|
attr :transactions
|
|
86
119
|
|
|
87
120
|
Explicit = Struct.new(:object) do
|
|
@@ -96,59 +129,112 @@ module Async
|
|
|
96
129
|
end
|
|
97
130
|
end
|
|
98
131
|
|
|
99
|
-
#
|
|
132
|
+
# Explicitly bind an object to a name, such that it could be accessed remotely.
|
|
133
|
+
#
|
|
134
|
+
# This is the same as {bind} but due to the semantics of the `[]=` operator, it does not return a proxy instance.
|
|
100
135
|
#
|
|
136
|
+
# Explicitly bound objects are not garbage collected until the connection is closed.
|
|
137
|
+
#
|
|
138
|
+
# @parameter name [String] The name to bind the object to.
|
|
139
|
+
# @parameter object [Object] The object to bind to the given name.
|
|
140
|
+
def []=(name, object)
|
|
141
|
+
@objects[name] = Explicit.new(object)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Generate a proxy for a remotely bound object.
|
|
145
|
+
#
|
|
146
|
+
# **This always returns a proxy, even if the object is bound locally.**
|
|
147
|
+
# The object bus is not shared between client and server, so `[]` always
|
|
148
|
+
# returns a proxy to the remote instance.
|
|
149
|
+
#
|
|
150
|
+
# @parameter name [String] The name of the bound object.
|
|
151
|
+
# @returns [Proxy] A proxy instance for the bound object.
|
|
152
|
+
def [](name)
|
|
153
|
+
return proxy_for(name)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Explicitly bind an object to a name, such that it could be accessed remotely.
|
|
157
|
+
#
|
|
158
|
+
# This method is identical to {[]=} but also returns a {Proxy} instance for the bound object which can be passed by reference.
|
|
159
|
+
#
|
|
160
|
+
# Explicitly bound objects are not garbage collected until the connection is closed.
|
|
161
|
+
#
|
|
162
|
+
# @example Binding an object to a name and accessing it remotely.
|
|
163
|
+
# array_proxy = connection.bind(:items, [1, 2, 3])
|
|
164
|
+
# connection[:remote].register(array_proxy)
|
|
165
|
+
#
|
|
166
|
+
# @parameter name [String] The name to bind the object to.
|
|
167
|
+
# @parameter object [Object] The object to bind to the given name.
|
|
101
168
|
# @returns [Proxy] A proxy instance for the bound object.
|
|
102
169
|
def bind(name, object)
|
|
103
170
|
# Bind the object into the local object store (explicitly bound, not temporary):
|
|
104
171
|
@objects[name] = Explicit.new(object)
|
|
105
172
|
|
|
106
|
-
#
|
|
107
|
-
return
|
|
173
|
+
# Always return a proxy for passing by reference, even for locally bound objects:
|
|
174
|
+
return proxy_for(name)
|
|
108
175
|
end
|
|
109
176
|
|
|
110
|
-
#
|
|
177
|
+
# Implicitly bind an object with a temporary name, such that it could be accessed remotely.
|
|
178
|
+
#
|
|
179
|
+
# Implicitly bound objects are garbage collected when the remote end no longer references them.
|
|
111
180
|
#
|
|
181
|
+
# This method is simliar to {bind} but is designed to be used to generate temporary proxies for objects that are not explicitly bound.
|
|
182
|
+
#
|
|
183
|
+
# @parameter object [Object] The object to bind to a temporary name.
|
|
112
184
|
# @returns [Proxy] A proxy instance for the bound object.
|
|
113
185
|
def proxy(object)
|
|
114
|
-
name =
|
|
186
|
+
name = object.__id__
|
|
115
187
|
|
|
116
188
|
# Bind the object into the local object store (temporary):
|
|
117
|
-
@objects[name]
|
|
189
|
+
@objects[name] ||= Implicit.new(object)
|
|
118
190
|
|
|
119
|
-
#
|
|
120
|
-
return
|
|
191
|
+
# Always return a proxy for passing by reference:
|
|
192
|
+
return proxy_for(name)
|
|
121
193
|
end
|
|
122
194
|
|
|
123
|
-
#
|
|
124
|
-
#
|
|
195
|
+
# Implicitly bind an object with a temporary name, such that it could be accessed remotely.
|
|
196
|
+
#
|
|
197
|
+
# Implicitly bound objects are garbage collected when the remote end no longer references them.
|
|
125
198
|
#
|
|
199
|
+
# This method is similar to {proxy} but is designed to be used to generate temporary names for objects that are not explicitly bound during serialization.
|
|
200
|
+
#
|
|
201
|
+
# @parameter object [Object] The object to bind to a temporary name.
|
|
126
202
|
# @returns [String] The name of the bound object.
|
|
127
203
|
def proxy_name(object)
|
|
128
|
-
name =
|
|
204
|
+
name = object.__id__
|
|
129
205
|
|
|
130
206
|
# Bind the object into the local object store (temporary):
|
|
131
|
-
@objects[name]
|
|
207
|
+
@objects[name] ||= Implicit.new(object)
|
|
132
208
|
|
|
133
209
|
# Return the name:
|
|
134
210
|
return name
|
|
135
211
|
end
|
|
136
212
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
213
|
+
# Get an object or proxy for a bound object, handling reverse lookup.
|
|
214
|
+
#
|
|
215
|
+
# If the object is bound locally and the proxy is for this connection, returns the actual object.
|
|
216
|
+
# If the object is bound remotely, or the proxy is from a different connection, returns a proxy.
|
|
217
|
+
# This is used when deserializing proxies to handle round-trip scenarios and avoid name collisions.
|
|
218
|
+
#
|
|
219
|
+
# @parameter name [String] The name of the bound object.
|
|
220
|
+
# @parameter local [Boolean] Whether the proxy is for this connection (from serialization). Defaults to true.
|
|
221
|
+
# @returns [Object | Proxy] The object if bound locally and proxy is for this connection, or a proxy otherwise.
|
|
222
|
+
def proxy_object(name)
|
|
223
|
+
# If the proxy is for this connection and the object is bound locally, return the actual object:
|
|
224
|
+
if entry = @objects[name]
|
|
225
|
+
# This handles round-trip scenarios correctly.
|
|
226
|
+
return entry.object
|
|
144
227
|
end
|
|
228
|
+
|
|
229
|
+
# Otherwise, create a proxy for the remote object:
|
|
230
|
+
return proxy_for(name)
|
|
145
231
|
end
|
|
146
232
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def
|
|
233
|
+
# Get or create a proxy for a named object.
|
|
234
|
+
#
|
|
235
|
+
# @parameter name [String] The name of the object.
|
|
236
|
+
# @returns [Proxy] A proxy instance for the named object.
|
|
237
|
+
private def proxy_for(name)
|
|
152
238
|
unless proxy = @proxies[name]
|
|
153
239
|
proxy = Proxy.new(self, name)
|
|
154
240
|
@proxies[name] = proxy
|
|
@@ -159,6 +245,15 @@ module Async
|
|
|
159
245
|
return proxy
|
|
160
246
|
end
|
|
161
247
|
|
|
248
|
+
private def finalize(name)
|
|
249
|
+
proc do
|
|
250
|
+
@finalized.push(name) rescue nil
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Create a new transaction.
|
|
255
|
+
# @parameter id [Integer] The transaction ID.
|
|
256
|
+
# @returns [Transaction] A new transaction.
|
|
162
257
|
def transaction!(id = self.next_id)
|
|
163
258
|
transaction = Transaction.new(self, id, timeout: @timeout)
|
|
164
259
|
@transactions[id] = transaction
|
|
@@ -166,6 +261,12 @@ module Async
|
|
|
166
261
|
return transaction
|
|
167
262
|
end
|
|
168
263
|
|
|
264
|
+
# Invoke a remote procedure.
|
|
265
|
+
# @parameter name [Symbol] The name of the remote object.
|
|
266
|
+
# @parameter arguments [Array] The arguments to pass.
|
|
267
|
+
# @parameter options [Hash] The keyword arguments to pass.
|
|
268
|
+
# @yields {|*args| ...} Optional block for yielding operations.
|
|
269
|
+
# @returns [Object] The result of the invocation.
|
|
169
270
|
def invoke(name, arguments, options = {}, &block)
|
|
170
271
|
transaction = self.transaction!
|
|
171
272
|
|
|
@@ -174,10 +275,14 @@ module Async
|
|
|
174
275
|
transaction&.close
|
|
175
276
|
end
|
|
176
277
|
|
|
278
|
+
# Send a release message for a named object.
|
|
279
|
+
# @parameter name [Symbol] The name of the object to release.
|
|
177
280
|
def send_release(name)
|
|
178
281
|
self.write(Release.new(name))
|
|
179
282
|
end
|
|
180
283
|
|
|
284
|
+
# Run the connection message loop.
|
|
285
|
+
# @parameter parent [Async::Task] The parent task to run under.
|
|
181
286
|
def run(parent: Task.current)
|
|
182
287
|
finalizer_task = parent.async do
|
|
183
288
|
while name = @finalized.pop
|
|
@@ -11,6 +11,12 @@ module Async
|
|
|
11
11
|
module Protocol
|
|
12
12
|
# Represents a method invocation.
|
|
13
13
|
class Invoke
|
|
14
|
+
# Initialize a new invocation message.
|
|
15
|
+
# @parameter id [Integer] The transaction ID.
|
|
16
|
+
# @parameter name [Symbol] The method name to invoke.
|
|
17
|
+
# @parameter arguments [Array] The positional arguments.
|
|
18
|
+
# @parameter options [Hash] The keyword arguments.
|
|
19
|
+
# @parameter block_given [Boolean] Whether a block was provided.
|
|
14
20
|
def initialize(id, name, arguments, options, block_given)
|
|
15
21
|
@id = id
|
|
16
22
|
@name = name
|
|
@@ -19,12 +25,23 @@ module Async
|
|
|
19
25
|
@block_given = block_given
|
|
20
26
|
end
|
|
21
27
|
|
|
28
|
+
# @attribute [Integer] The transaction ID.
|
|
22
29
|
attr :id
|
|
30
|
+
|
|
31
|
+
# @attribute [Symbol] The method name.
|
|
23
32
|
attr :name
|
|
33
|
+
|
|
34
|
+
# @attribute [Array] The positional arguments.
|
|
24
35
|
attr :arguments
|
|
36
|
+
|
|
37
|
+
# @attribute [Hash] The keyword arguments.
|
|
25
38
|
attr :options
|
|
39
|
+
|
|
40
|
+
# @attribute [Boolean] Whether a block was provided.
|
|
26
41
|
attr :block_given
|
|
27
42
|
|
|
43
|
+
# Pack the invocation into a MessagePack packer.
|
|
44
|
+
# @parameter packer [MessagePack::Packer] The packer to write to.
|
|
28
45
|
def pack(packer)
|
|
29
46
|
packer.write(@id)
|
|
30
47
|
packer.write(@name)
|
|
@@ -43,6 +60,9 @@ module Async
|
|
|
43
60
|
packer.write(@block_given)
|
|
44
61
|
end
|
|
45
62
|
|
|
63
|
+
# Unpack an invocation from a MessagePack unpacker.
|
|
64
|
+
# @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
|
|
65
|
+
# @returns [Invoke] A new invocation instance.
|
|
46
66
|
def self.unpack(unpacker)
|
|
47
67
|
id = unpacker.read
|
|
48
68
|
name = unpacker.read
|
|
@@ -19,36 +19,59 @@ module Async
|
|
|
19
19
|
@name = name
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Get the connection to the remote object.
|
|
23
|
+
# @returns [Connection] The connection to the remote object.
|
|
24
|
+
def __connection__
|
|
25
|
+
@connection
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Get the name of the remote object.
|
|
29
|
+
# @returns [Symbol] The name of the remote object.
|
|
22
30
|
def __name__
|
|
23
31
|
@name
|
|
24
32
|
end
|
|
25
33
|
|
|
34
|
+
# Logical negation operator.
|
|
35
|
+
# @returns [Object] The result of the negation.
|
|
26
36
|
def !
|
|
27
37
|
@connection.invoke(@name, [:!])
|
|
28
38
|
end
|
|
29
39
|
|
|
40
|
+
# Equality operator.
|
|
41
|
+
# @parameter object [Object] The object to compare with.
|
|
42
|
+
# @returns [Boolean] True if equal.
|
|
30
43
|
def == object
|
|
31
44
|
@connection.invoke(@name, [:==, object])
|
|
32
45
|
end
|
|
33
46
|
|
|
47
|
+
# Inequality operator.
|
|
48
|
+
# @parameter object [Object] The object to compare with.
|
|
49
|
+
# @returns [Boolean] True if not equal.
|
|
34
50
|
def != object
|
|
35
51
|
@connection.invoke(@name, [:!=, object])
|
|
36
52
|
end
|
|
37
53
|
|
|
54
|
+
# Forward method calls to the remote object.
|
|
55
|
+
# @parameter arguments [Array] The method arguments.
|
|
56
|
+
# @parameter options [Hash] The keyword arguments.
|
|
57
|
+
# @yields {|*args| ...} Optional block to pass to the method.
|
|
58
|
+
# @returns [Object] The result of the method call.
|
|
38
59
|
def method_missing(*arguments, **options, &block)
|
|
39
60
|
@connection.invoke(@name, arguments, options, &block)
|
|
40
61
|
end
|
|
41
62
|
|
|
63
|
+
# Check if the remote object responds to a method.
|
|
64
|
+
# @parameter name [Symbol] The method name to check.
|
|
65
|
+
# @parameter include_all [Boolean] Whether to include private methods.
|
|
66
|
+
# @returns [Boolean] True if the method exists.
|
|
42
67
|
def respond_to?(name, include_all = false)
|
|
43
68
|
@connection.invoke(@name, [:respond_to?, name, include_all])
|
|
44
69
|
end
|
|
45
70
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
end
|
|
49
|
-
|
|
71
|
+
# Return a string representation of the proxy.
|
|
72
|
+
# @returns [String] A string describing the proxy.
|
|
50
73
|
def inspect
|
|
51
|
-
"#<proxy #{@name}
|
|
74
|
+
"#<proxy #{@name}>"
|
|
52
75
|
end
|
|
53
76
|
end
|
|
54
77
|
end
|
|
@@ -8,16 +8,24 @@ module Async
|
|
|
8
8
|
module Protocol
|
|
9
9
|
# Represents a named object that has been released (no longer available).
|
|
10
10
|
class Release
|
|
11
|
+
# Initialize a new release message.
|
|
12
|
+
# @parameter name [Symbol] The name of the released object.
|
|
11
13
|
def initialize(name)
|
|
12
14
|
@name = name
|
|
13
15
|
end
|
|
14
16
|
|
|
17
|
+
# @attribute [Symbol] The name of the released object.
|
|
15
18
|
attr :name
|
|
16
19
|
|
|
20
|
+
# Pack the release into a MessagePack packer.
|
|
21
|
+
# @parameter packer [MessagePack::Packer] The packer to write to.
|
|
17
22
|
def pack(packer)
|
|
18
23
|
packer.write(@name)
|
|
19
24
|
end
|
|
20
25
|
|
|
26
|
+
# Unpack a release from a MessagePack unpacker.
|
|
27
|
+
# @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
|
|
28
|
+
# @returns [Release] A new release instance.
|
|
21
29
|
def self.unpack(unpacker)
|
|
22
30
|
name = unpacker.read
|
|
23
31
|
|
|
@@ -6,20 +6,32 @@
|
|
|
6
6
|
module Async
|
|
7
7
|
module Bus
|
|
8
8
|
module Protocol
|
|
9
|
+
# Represents a response message from a remote procedure call.
|
|
9
10
|
class Response
|
|
11
|
+
# Initialize a new response message.
|
|
12
|
+
# @parameter id [Integer] The transaction ID.
|
|
13
|
+
# @parameter result [Object] The result value.
|
|
10
14
|
def initialize(id, result)
|
|
11
15
|
@id = id
|
|
12
16
|
@result = result
|
|
13
17
|
end
|
|
14
18
|
|
|
19
|
+
# @attribute [Integer] The transaction ID.
|
|
15
20
|
attr :id
|
|
21
|
+
|
|
22
|
+
# @attribute [Object] The result value.
|
|
16
23
|
attr :result
|
|
17
24
|
|
|
25
|
+
# Pack the response into a MessagePack packer.
|
|
26
|
+
# @parameter packer [MessagePack::Packer] The packer to write to.
|
|
18
27
|
def pack(packer)
|
|
19
28
|
packer.write(@id)
|
|
20
29
|
packer.write(@result)
|
|
21
30
|
end
|
|
22
31
|
|
|
32
|
+
# Unpack a response from a MessagePack unpacker.
|
|
33
|
+
# @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
|
|
34
|
+
# @returns [Response] A new response instance.
|
|
23
35
|
def self.unpack(unpacker)
|
|
24
36
|
id = unpacker.read
|
|
25
37
|
result = unpacker.read
|
|
@@ -8,7 +8,12 @@ require "async/queue"
|
|
|
8
8
|
module Async
|
|
9
9
|
module Bus
|
|
10
10
|
module Protocol
|
|
11
|
+
# Represents a transaction for a remote procedure call.
|
|
11
12
|
class Transaction
|
|
13
|
+
# Initialize a new transaction.
|
|
14
|
+
# @parameter connection [Connection] The connection for this transaction.
|
|
15
|
+
# @parameter id [Integer] The transaction ID.
|
|
16
|
+
# @parameter timeout [Float] The timeout for the transaction.
|
|
12
17
|
def initialize(connection, id, timeout: nil)
|
|
13
18
|
@connection = connection
|
|
14
19
|
@id = id
|
|
@@ -19,14 +24,23 @@ module Async
|
|
|
19
24
|
@accept = nil
|
|
20
25
|
end
|
|
21
26
|
|
|
27
|
+
# @attribute [Connection] The connection for this transaction.
|
|
22
28
|
attr :connection
|
|
29
|
+
|
|
30
|
+
# @attribute [Integer] The transaction ID.
|
|
23
31
|
attr :id
|
|
24
32
|
|
|
33
|
+
# @attribute [Float] The timeout for the transaction.
|
|
25
34
|
attr_accessor :timeout
|
|
26
35
|
|
|
36
|
+
# @attribute [Thread::Queue] The queue of received messages.
|
|
27
37
|
attr :received
|
|
38
|
+
|
|
39
|
+
# @attribute [Object] The accept handler.
|
|
28
40
|
attr :accept
|
|
29
41
|
|
|
42
|
+
# Read a message from the transaction queue.
|
|
43
|
+
# @returns [Object] The next message.
|
|
30
44
|
def read
|
|
31
45
|
if @received.empty?
|
|
32
46
|
@connection.flush
|
|
@@ -35,6 +49,9 @@ module Async
|
|
|
35
49
|
@received.pop(timeout: @timeout)
|
|
36
50
|
end
|
|
37
51
|
|
|
52
|
+
# Write a message to the connection.
|
|
53
|
+
# @parameter message [Object] The message to write.
|
|
54
|
+
# @raises [RuntimeError] If the transaction is closed.
|
|
38
55
|
def write(message)
|
|
39
56
|
if @connection
|
|
40
57
|
@connection.write(message)
|
|
@@ -45,12 +62,14 @@ module Async
|
|
|
45
62
|
|
|
46
63
|
# Push a message to the transaction's received queue.
|
|
47
64
|
# Silently ignores messages if the queue is already closed.
|
|
65
|
+
# @parameter message [Object] The message to push.
|
|
48
66
|
def push(message)
|
|
49
67
|
@received.push(message)
|
|
50
68
|
rescue ClosedQueueError
|
|
51
69
|
# Queue is closed (transaction already finished/closed) - ignore silently.
|
|
52
70
|
end
|
|
53
71
|
|
|
72
|
+
# Close the transaction and clean up resources.
|
|
54
73
|
def close
|
|
55
74
|
if connection = @connection
|
|
56
75
|
@connection = nil
|
|
@@ -61,6 +80,11 @@ module Async
|
|
|
61
80
|
end
|
|
62
81
|
|
|
63
82
|
# Invoke a remote procedure.
|
|
83
|
+
# @parameter name [Symbol] The name of the remote object.
|
|
84
|
+
# @parameter arguments [Array] The positional arguments.
|
|
85
|
+
# @parameter options [Hash] The keyword arguments.
|
|
86
|
+
# @yields {|*args| ...} Optional block for yielding operations.
|
|
87
|
+
# @returns [Object] The result of the invocation.
|
|
64
88
|
def invoke(name, arguments, options, &block)
|
|
65
89
|
Console.debug(self){[name, arguments, options, block]}
|
|
66
90
|
|
|
@@ -79,11 +103,20 @@ module Async
|
|
|
79
103
|
end
|
|
80
104
|
when Error
|
|
81
105
|
raise(response.result)
|
|
106
|
+
when Throw
|
|
107
|
+
# Re-throw the tag and value that was thrown on the server side
|
|
108
|
+
# Throw.result contains [tag, value] array
|
|
109
|
+
tag, value = response.result
|
|
110
|
+
throw(tag, value)
|
|
82
111
|
end
|
|
83
112
|
end
|
|
84
113
|
end
|
|
85
114
|
|
|
86
|
-
# Accept a remote procedure
|
|
115
|
+
# Accept a remote procedure invocation.
|
|
116
|
+
# @parameter object [Object] The object to invoke the method on.
|
|
117
|
+
# @parameter arguments [Array] The positional arguments.
|
|
118
|
+
# @parameter options [Hash] The keyword arguments.
|
|
119
|
+
# @parameter block_given [Boolean] Whether a block was provided.
|
|
87
120
|
def accept(object, arguments, options, block_given)
|
|
88
121
|
if block_given
|
|
89
122
|
result = object.public_send(*arguments, **options) do |*yield_arguments|
|
|
@@ -106,7 +139,9 @@ module Async
|
|
|
106
139
|
|
|
107
140
|
self.write(Return.new(@id, result))
|
|
108
141
|
rescue UncaughtThrowError => error
|
|
109
|
-
|
|
142
|
+
# UncaughtThrowError has both tag and value attributes
|
|
143
|
+
# Store both in the Throw message: result is tag, we'll add value handling
|
|
144
|
+
self.write(Throw.new(@id, [error.tag, error.value]))
|
|
110
145
|
rescue => error
|
|
111
146
|
self.write(Error.new(@id, error))
|
|
112
147
|
end
|
|
@@ -15,66 +15,114 @@ require_relative "../controller"
|
|
|
15
15
|
module Async
|
|
16
16
|
module Bus
|
|
17
17
|
module Protocol
|
|
18
|
+
# Represents a MessagePack factory wrapper for async-bus serialization.
|
|
18
19
|
class Wrapper < MessagePack::Factory
|
|
19
|
-
|
|
20
|
+
# Initialize a new wrapper.
|
|
21
|
+
# @parameter connection [Connection] The connection for proxy resolution.
|
|
22
|
+
# @parameter reference_types [Array(Class)] Types to serialize as proxies.
|
|
23
|
+
def initialize(connection, reference_types: [Controller])
|
|
20
24
|
super()
|
|
21
25
|
|
|
22
|
-
@
|
|
26
|
+
@connection = connection
|
|
23
27
|
@reference_types = reference_types
|
|
24
28
|
|
|
29
|
+
# Store the peer connection for forwarding proxies:
|
|
30
|
+
# When a proxy is forwarded (local=false), it should point back to the sender
|
|
31
|
+
# (the peer connection), not the receiver (this connection).
|
|
32
|
+
@peer_connection = nil
|
|
33
|
+
|
|
25
34
|
# The order here matters.
|
|
26
35
|
|
|
27
36
|
self.register_type(0x00, Invoke, recursive: true,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
packer: ->(invoke, packer){invoke.pack(packer)},
|
|
38
|
+
unpacker: ->(unpacker){Invoke.unpack(unpacker)},
|
|
39
|
+
)
|
|
31
40
|
|
|
32
41
|
[Return, Yield, Error, Next, Throw, Close].each_with_index do |klass, index|
|
|
33
42
|
self.register_type(0x01 + index, klass, recursive: true,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
packer: ->(value, packer){value.pack(packer)},
|
|
44
|
+
unpacker: ->(unpacker){klass.unpack(unpacker)},
|
|
45
|
+
)
|
|
37
46
|
end
|
|
38
47
|
|
|
39
48
|
# Reverse serialize proxies back into proxies:
|
|
40
|
-
# When a Proxy is received,
|
|
41
|
-
self.register_type(0x10, Proxy,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
# When a Proxy is received, use proxy_object to handle reverse lookup
|
|
50
|
+
self.register_type(0x10, Proxy, recursive: true,
|
|
51
|
+
packer: self.method(:pack_proxy),
|
|
52
|
+
unpacker: self.method(:unpack_proxy),
|
|
53
|
+
)
|
|
45
54
|
|
|
46
55
|
self.register_type(0x11, Release, recursive: true,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
packer: ->(release, packer){release.pack(packer)},
|
|
57
|
+
unpacker: ->(unpacker){Release.unpack(unpacker)},
|
|
58
|
+
)
|
|
50
59
|
|
|
51
60
|
self.register_type(0x20, Symbol)
|
|
52
|
-
self.register_type(0x21, Exception,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)
|
|
61
|
+
self.register_type(0x21, Exception, recursive: true,
|
|
62
|
+
packer: self.method(:pack_exception),
|
|
63
|
+
unpacker: self.method(:unpack_exception),
|
|
64
|
+
)
|
|
57
65
|
|
|
58
66
|
self.register_type(0x22, Class,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
packer: ->(klass){klass.name},
|
|
68
|
+
unpacker: ->(name){Object.const_get(name)},
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
reference_packer = self.method(:pack_reference)
|
|
72
|
+
reference_unpacker = self.method(:unpack_reference)
|
|
62
73
|
|
|
63
74
|
# Serialize objects into proxies:
|
|
64
75
|
reference_types&.each_with_index do |klass, index|
|
|
65
|
-
self.register_type(0x30 + index, klass,
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
76
|
+
self.register_type(0x30 + index, klass, recursive: true,
|
|
77
|
+
packer: reference_packer,
|
|
78
|
+
unpacker: reference_unpacker,
|
|
79
|
+
)
|
|
69
80
|
end
|
|
70
81
|
end
|
|
71
82
|
|
|
83
|
+
# Pack a proxy into a MessagePack packer.
|
|
84
|
+
#
|
|
85
|
+
# Validates that the proxy is for this connection and serializes the proxy name.
|
|
86
|
+
# Multi-hop proxy forwarding is not supported, so proxies can only be serialized
|
|
87
|
+
# from the same connection they were created for (round-trip scenarios).
|
|
88
|
+
#
|
|
89
|
+
# @parameter proxy [Proxy] The proxy to serialize.
|
|
90
|
+
# @parameter packer [MessagePack::Packer] The packer to write to.
|
|
91
|
+
# @raises [ArgumentError] If the proxy is from a different connection (multi-hop forwarding not supported).
|
|
92
|
+
def pack_proxy(proxy, packer)
|
|
93
|
+
# Check if the proxy is for this connection:
|
|
94
|
+
if proxy.__connection__ != @connection
|
|
95
|
+
proxy = @connection.proxy(proxy)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
packer.write(proxy.__name__)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Unpack a proxy from a MessagePack unpacker.
|
|
102
|
+
#
|
|
103
|
+
# When deserializing a proxy:
|
|
104
|
+
# - If the object is bound locally, return the actual object (round-trip scenario)
|
|
105
|
+
# - If the object is not found locally, create a proxy pointing to this connection
|
|
106
|
+
# (the proxy was forwarded from another connection and should point back to the sender)
|
|
107
|
+
#
|
|
108
|
+
# @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
|
|
109
|
+
# @returns [Object | Proxy] The actual object if bound locally, or a proxy pointing to this connection.
|
|
110
|
+
def unpack_proxy(unpacker)
|
|
111
|
+
@connection.proxy_object(unpacker.read)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Pack an exception into a MessagePack packer.
|
|
115
|
+
# @parameter exception [Exception] The exception to pack.
|
|
116
|
+
# @parameter packer [MessagePack::Packer] The packer to write to.
|
|
72
117
|
def pack_exception(exception, packer)
|
|
73
118
|
packer.write(exception.class.name)
|
|
74
119
|
packer.write(exception.message)
|
|
75
120
|
packer.write(exception.backtrace)
|
|
76
121
|
end
|
|
77
122
|
|
|
123
|
+
# Unpack an exception from a MessagePack unpacker.
|
|
124
|
+
# @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
|
|
125
|
+
# @returns [Exception] A reconstructed exception.
|
|
78
126
|
def unpack_exception(unpacker)
|
|
79
127
|
klass = unpacker.read
|
|
80
128
|
message = unpacker.read
|
|
@@ -87,6 +135,28 @@ module Async
|
|
|
87
135
|
|
|
88
136
|
return exception
|
|
89
137
|
end
|
|
138
|
+
|
|
139
|
+
# Pack a reference type object (e.g., Controller) into a MessagePack packer.
|
|
140
|
+
#
|
|
141
|
+
# Serializes the object as a proxy by generating a temporary name and writing it to the packer.
|
|
142
|
+
# The object is implicitly bound to the connection with a temporary name.
|
|
143
|
+
#
|
|
144
|
+
# @parameter object [Object] The reference type object to serialize.
|
|
145
|
+
# @parameter packer [MessagePack::Packer] The packer to write to.
|
|
146
|
+
def pack_reference(object, packer)
|
|
147
|
+
packer.write(@connection.proxy_name(object))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Unpack a reference type object from a MessagePack unpacker.
|
|
151
|
+
#
|
|
152
|
+
# Reads a proxy name and returns the corresponding object or proxy.
|
|
153
|
+
# If the object is bound locally, returns the actual object; otherwise returns a proxy.
|
|
154
|
+
#
|
|
155
|
+
# @parameter unpacker [MessagePack::Unpacker] The unpacker to read from.
|
|
156
|
+
# @returns [Object | Proxy] The actual object if bound locally, or a proxy otherwise.
|
|
157
|
+
def unpack_reference(unpacker)
|
|
158
|
+
@connection.proxy_object(unpacker.read)
|
|
159
|
+
end
|
|
90
160
|
end
|
|
91
161
|
end
|
|
92
162
|
end
|
data/lib/async/bus/server.rb
CHANGED
|
@@ -6,19 +6,37 @@
|
|
|
6
6
|
require_relative "protocol/connection"
|
|
7
7
|
require "set"
|
|
8
8
|
|
|
9
|
+
# @namespace
|
|
9
10
|
module Async
|
|
11
|
+
# @namespace
|
|
10
12
|
module Bus
|
|
13
|
+
# Represents a server that accepts async-bus connections.
|
|
11
14
|
class Server
|
|
15
|
+
# Initialize a new server.
|
|
16
|
+
# @parameter endpoint [IO::Endpoint] The endpoint to listen on.
|
|
17
|
+
# @parameter options [Hash] Additional options for connections.
|
|
12
18
|
def initialize(endpoint = nil, **options)
|
|
13
19
|
@endpoint = endpoint || Protocol.local_endpoint
|
|
14
20
|
@options = options
|
|
15
21
|
end
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
# Called when a connection is established.
|
|
24
|
+
# Override this method to perform setup when a connection is established.
|
|
25
|
+
#
|
|
26
|
+
# @parameter connection [Protocol::Connection] The established connection.
|
|
27
|
+
protected def connected!(connection)
|
|
28
|
+
# Do nothing by default.
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Accept incoming connections.
|
|
32
|
+
# @yields {|connection| ...} Block called with each new connection.
|
|
33
|
+
def accept(&block)
|
|
18
34
|
@endpoint.accept do |peer|
|
|
19
35
|
connection = Protocol::Connection.server(peer, **@options)
|
|
20
36
|
|
|
21
|
-
|
|
37
|
+
connected!(connection, &block)
|
|
38
|
+
|
|
39
|
+
yield connection if block_given?
|
|
22
40
|
|
|
23
41
|
connection.run
|
|
24
42
|
ensure
|
data/lib/async/bus/version.rb
CHANGED
data/readme.md
CHANGED
|
@@ -1,23 +1,39 @@
|
|
|
1
1
|
# Async::Bus
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
When building distributed systems or multi-process applications, you need a way for processes to communicate and invoke methods on objects in other processes. `async-bus` provides a lightweight message-passing system for inter-process communication (IPC) using Unix domain sockets, enabling transparent remote procedure calls (RPC) where remote objects feel like local objects.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Use `async-bus` when you need:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- **Inter-process communication**: Connect multiple Ruby processes running on the same machine.
|
|
8
|
+
- **Transparent RPC**: Call methods on remote objects as if they were local.
|
|
9
|
+
- **Type-safe serialization**: Automatically serialize and deserialize Ruby objects using MessagePack.
|
|
10
|
+
- **Asynchronous operations**: Non-blocking message passing built on the Async framework.
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
- Asynchronous Remote Procedure Calls (RPC) with timeouts.
|
|
11
|
-
- Automatic client and server reconnection handling.
|
|
12
|
+
[](https://github.com/socketry/async-bus/actions?workflow=Test)
|
|
12
13
|
|
|
13
14
|
## Usage
|
|
14
15
|
|
|
15
16
|
Please see the [project documentation](https://socketry.github.io/async-bus/) for more details.
|
|
16
17
|
|
|
18
|
+
- [Getting Started](https://socketry.github.io/async-bus/guides/getting-started/index) - This guide explains how to get started with `async-bus` to build asynchronous message-passing systems with transparent remote procedure calls in Ruby.
|
|
19
|
+
|
|
20
|
+
- [Controllers](https://socketry.github.io/async-bus/guides/controllers/index) - This guide explains how to use controllers in `async-bus` to build explicit remote interfaces with pass-by-reference semantics, enabling bidirectional communication and shared state across connections.
|
|
21
|
+
|
|
17
22
|
## Releases
|
|
18
23
|
|
|
19
24
|
Please see the [project releases](https://socketry.github.io/async-bus/releases/index) for all releases.
|
|
20
25
|
|
|
26
|
+
### v0.3.1
|
|
27
|
+
|
|
28
|
+
- `Client#run` now returns an `Async::Task` (as it did in earlier releases).
|
|
29
|
+
|
|
30
|
+
### v0.3.0
|
|
31
|
+
|
|
32
|
+
- Add support for multi-hop proxying.
|
|
33
|
+
- Fix proxying of throw/catch value.
|
|
34
|
+
- `Client#run` now takes a block.
|
|
35
|
+
- `Server#run` delegates to `Server#connected!`.
|
|
36
|
+
|
|
21
37
|
### v0.2.0
|
|
22
38
|
|
|
23
39
|
- Fix handling of temporary objects.
|
data/releases.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Releases
|
|
2
2
|
|
|
3
|
+
## v0.3.1
|
|
4
|
+
|
|
5
|
+
- `Client#run` now returns an `Async::Task` (as it did in earlier releases).
|
|
6
|
+
|
|
7
|
+
## v0.3.0
|
|
8
|
+
|
|
9
|
+
- Add support for multi-hop proxying.
|
|
10
|
+
- Fix proxying of throw/catch value.
|
|
11
|
+
- `Client#run` now takes a block.
|
|
12
|
+
- `Server#run` delegates to `Server#connected!`.
|
|
13
|
+
|
|
3
14
|
## v0.2.0
|
|
4
15
|
|
|
5
16
|
- Fix handling of temporary objects.
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
metadata.gz.sig
CHANGED
|
Binary file
|