Skip to content

Commit 7283657

Browse files
Fix handling of proxies with symbol names.
- 100% documentation coverage.
1 parent ac66834 commit 7283657

File tree

12 files changed

+311
-33
lines changed

12 files changed

+311
-33
lines changed

lib/async/bus/client.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,11 @@
88

99
module Async
1010
module Bus
11+
# Represents a client that can connect to a server.
1112
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.
1216
def initialize(endpoint = nil, **options)
1317
@endpoint = endpoint || Protocol.local_endpoint
1418
@options = options

lib/async/bus/protocol/connection.rb

Lines changed: 98 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,38 @@
1313

1414
module Async
1515
module Bus
16+
# @namespace
1617
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.
1721
def self.local_endpoint(path = "bus.ipc")
1822
::IO::Endpoint.unix(path)
1923
end
2024

25+
# Represents a connection between client and server for message passing.
2126
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.
2231
def self.client(peer, **options)
2332
self.new(peer, 1, **options)
2433
end
2534

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.
2639
def self.server(peer, **options)
2740
self.new(peer, 2, **options)
2841
end
2942

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.
3048
def initialize(peer, id, wrapper: Wrapper, timeout: nil)
3149
@peer = peer
3250
@id = id
@@ -47,16 +65,20 @@ def initialize(peer, id, wrapper: Wrapper, timeout: nil)
4765
# @attribute [Float] The timeout for transactions.
4866
attr_accessor :timeout
4967

68+
# Flush the packer buffer.
5069
def flush
5170
@packer.flush
5271
end
5372

73+
# Write a message to the connection.
74+
# @parameter message [Object] The message to write.
5475
def write(message)
5576
# $stderr.puts "Writing: #{message.inspect}"
5677
@packer.write(message)
5778
@packer.flush
5879
end
5980

81+
# Close the connection and clean up resources.
6082
def close
6183
@transactions.each do |id, transaction|
6284
transaction.close
@@ -65,23 +87,34 @@ def close
6587
@peer.close
6688
end
6789

90+
# Return a string representation of the connection.
91+
# @returns [String] A string describing the connection.
6892
def inspect
6993
"#<#{self.class} #{@objects.size} objects>"
7094
end
7195

96+
# @attribute [Hash] The bound objects.
7297
attr :objects
98+
99+
# @attribute [ObjectSpace::WeakMap] The proxy cache.
73100
attr :proxies
74101

102+
# @attribute [MessagePack::Unpacker] The message unpacker.
75103
attr :unpacker
104+
105+
# @attribute [MessagePack::Packer] The message packer.
76106
attr :packer
77107

108+
# Get the next transaction ID.
109+
# @returns [Integer] The next transaction ID.
78110
def next_id
79111
id = @id
80112
@id += 2
81113

82114
return id
83115
end
84116

117+
# @attribute [Hash] Active transactions.
85118
attr :transactions
86119

87120
Explicit = Struct.new(:object) do
@@ -96,8 +129,47 @@ def temporary?
96129
end
97130
end
98131

99-
# Bind a local object to a name, such that it could be accessed remotely.
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.
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 will not return objects bound locally, only proxies for remotely bound objects.**
147+
#
148+
# @parameter name [String] The name of the bound object.
149+
# @returns [Object | Proxy] The object or proxy instance for the bound object.
150+
def [](name)
151+
unless proxy = @proxies[name]
152+
proxy = Proxy.new(self, name)
153+
@proxies[name] = proxy
154+
155+
::ObjectSpace.define_finalizer(proxy, finalize(name))
156+
end
157+
158+
return proxy
159+
end
160+
161+
# Explicitly bind an object to a name, such that it could be accessed remotely.
162+
#
163+
# This method is identical to {[]=} but also returns a {Proxy} instance for the bound object which can be passed by reference.
100164
#
165+
# Explicitly bound objects are not garbage collected until the connection is closed.
166+
#
167+
# @example Binding an object to a name and accessing it remotely.
168+
# array_proxy = connection.bind(:items, [1, 2, 3])
169+
# connection[:remote].register(array_proxy)
170+
#
171+
# @parameter name [String] The name to bind the object to.
172+
# @parameter object [Object] The object to bind to the given name.
101173
# @returns [Proxy] A proxy instance for the bound object.
102174
def bind(name, object)
103175
# Bind the object into the local object store (explicitly bound, not temporary):
@@ -107,8 +179,13 @@ def bind(name, object)
107179
return self[name]
108180
end
109181

110-
# Generate a proxy name for an object and bind it.
182+
# Implicitly bind an object with a temporary name, such that it could be accessed remotely.
183+
#
184+
# Implicitly bound objects are garbage collected when the remote end no longer references them.
111185
#
186+
# This method is simliar to {bind} but is designed to be used to generate temporary proxies for objects that are not explicitly bound.
187+
#
188+
# @parameter object [Object] The object to bind to a temporary name.
112189
# @returns [Proxy] A proxy instance for the bound object.
113190
def proxy(object)
114191
name = "<#{object.class}@#{next_id.to_s(16)}>".freeze
@@ -120,9 +197,13 @@ def proxy(object)
120197
return self[name]
121198
end
122199

123-
# Generate a proxy name for an object and bind it, returning just the name.
124-
# Used for serialization when you need the name string, not a Proxy instance.
200+
# Implicitly bind an object with a temporary name, such that it could be accessed remotely.
201+
#
202+
# Implicitly bound objects are garbage collected when the remote end no longer references them.
203+
#
204+
# 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.
125205
#
206+
# @parameter object [Object] The object to bind to a temporary name.
126207
# @returns [String] The name of the bound object.
127208
def proxy_name(object)
128209
name = "<#{object.class}@#{next_id.to_s(16)}>".freeze
@@ -134,38 +215,28 @@ def proxy_name(object)
134215
return name
135216
end
136217

137-
def object(name)
138-
@objects[name]&.object
139-
end
140-
141218
private def finalize(name)
142219
proc do
143220
@finalized.push(name) rescue nil
144221
end
145222
end
146223

147-
def []=(name, object)
148-
@objects[name] = Explicit.new(object)
149-
end
150-
151-
def [](name)
152-
unless proxy = @proxies[name]
153-
proxy = Proxy.new(self, name)
154-
@proxies[name] = proxy
155-
156-
::ObjectSpace.define_finalizer(proxy, finalize(name))
157-
end
158-
159-
return proxy
160-
end
161-
224+
# Create a new transaction.
225+
# @parameter id [Integer] The transaction ID.
226+
# @returns [Transaction] A new transaction.
162227
def transaction!(id = self.next_id)
163228
transaction = Transaction.new(self, id, timeout: @timeout)
164229
@transactions[id] = transaction
165230

166231
return transaction
167232
end
168233

234+
# Invoke a remote procedure.
235+
# @parameter name [Symbol] The name of the remote object.
236+
# @parameter arguments [Array] The arguments to pass.
237+
# @parameter options [Hash] The keyword arguments to pass.
238+
# @yields {|*args| ...} Optional block for yielding operations.
239+
# @returns [Object] The result of the invocation.
169240
def invoke(name, arguments, options = {}, &block)
170241
transaction = self.transaction!
171242

@@ -174,10 +245,14 @@ def invoke(name, arguments, options = {}, &block)
174245
transaction&.close
175246
end
176247

248+
# Send a release message for a named object.
249+
# @parameter name [Symbol] The name of the object to release.
177250
def send_release(name)
178251
self.write(Release.new(name))
179252
end
180253

254+
# Run the connection message loop.
255+
# @parameter parent [Async::Task] The parent task to run under.
181256
def run(parent: Task.current)
182257
finalizer_task = parent.async do
183258
while name = @finalized.pop

lib/async/bus/protocol/invoke.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ module Bus
1111
module Protocol
1212
# Represents a method invocation.
1313
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.
1420
def initialize(id, name, arguments, options, block_given)
1521
@id = id
1622
@name = name
@@ -19,12 +25,23 @@ def initialize(id, name, arguments, options, block_given)
1925
@block_given = block_given
2026
end
2127

28+
# @attribute [Integer] The transaction ID.
2229
attr :id
30+
31+
# @attribute [Symbol] The method name.
2332
attr :name
33+
34+
# @attribute [Array] The positional arguments.
2435
attr :arguments
36+
37+
# @attribute [Hash] The keyword arguments.
2538
attr :options
39+
40+
# @attribute [Boolean] Whether a block was provided.
2641
attr :block_given
2742

43+
# Pack the invocation into a MessagePack packer.
44+
# @parameter packer [MessagePack::Packer] The packer to write to.
2845
def pack(packer)
2946
packer.write(@id)
3047
packer.write(@name)
@@ -43,6 +60,9 @@ def pack(packer)
4360
packer.write(@block_given)
4461
end
4562

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.
4666
def self.unpack(unpacker)
4767
id = unpacker.read
4868
name = unpacker.read

lib/async/bus/protocol/proxy.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,61 @@ def initialize(connection, name)
1919
@name = name
2020
end
2121

22+
# Get the name of the remote object.
23+
# @returns [Symbol] The name of the remote object.
2224
def __name__
2325
@name
2426
end
2527

28+
# Logical negation operator.
29+
# @returns [Object] The result of the negation.
2630
def !
2731
@connection.invoke(@name, [:!])
2832
end
2933

34+
# Equality operator.
35+
# @parameter object [Object] The object to compare with.
36+
# @returns [Boolean] True if equal.
3037
def == object
3138
@connection.invoke(@name, [:==, object])
3239
end
3340

41+
# Inequality operator.
42+
# @parameter object [Object] The object to compare with.
43+
# @returns [Boolean] True if not equal.
3444
def != object
3545
@connection.invoke(@name, [:!=, object])
3646
end
3747

48+
# Forward method calls to the remote object.
49+
# @parameter arguments [Array] The method arguments.
50+
# @parameter options [Hash] The keyword arguments.
51+
# @yields {|*args| ...} Optional block to pass to the method.
52+
# @returns [Object] The result of the method call.
3853
def method_missing(*arguments, **options, &block)
3954
@connection.invoke(@name, arguments, options, &block)
4055
end
4156

57+
# Check if the remote object responds to a method.
58+
# @parameter name [Symbol] The method name to check.
59+
# @parameter include_all [Boolean] Whether to include private methods.
60+
# @returns [Boolean] True if the method exists.
4261
def respond_to?(name, include_all = false)
4362
@connection.invoke(@name, [:respond_to?, name, include_all])
4463
end
4564

65+
# Check if the remote object responds to a missing method.
66+
# @parameter name [Symbol] The method name to check.
67+
# @parameter include_all [Boolean] Whether to include private methods.
68+
# @returns [Boolean] True if the method exists.
4669
def respond_to_missing?(name, include_all = false)
4770
@connection.invoke(@name, [:respond_to?, name, include_all])
4871
end
4972

73+
# Return a string representation of the proxy.
74+
# @returns [String] A string describing the proxy.
5075
def inspect
51-
"#<proxy #{@name}: #{@connection.invoke(@name, [:inspect])}>"
76+
"#<proxy #{@name}>"
5277
end
5378
end
5479
end

lib/async/bus/protocol/release.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,24 @@ module Bus
88
module Protocol
99
# Represents a named object that has been released (no longer available).
1010
class Release
11+
# Initialize a new release message.
12+
# @parameter name [Symbol] The name of the released object.
1113
def initialize(name)
1214
@name = name
1315
end
1416

17+
# @attribute [Symbol] The name of the released object.
1518
attr :name
1619

20+
# Pack the release into a MessagePack packer.
21+
# @parameter packer [MessagePack::Packer] The packer to write to.
1722
def pack(packer)
1823
packer.write(@name)
1924
end
2025

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.
2129
def self.unpack(unpacker)
2230
name = unpacker.read
2331

0 commit comments

Comments
 (0)