roby 0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +29 -0
- data/History.txt +4 -0
- data/License-fr.txt +519 -0
- data/License.txt +515 -0
- data/Manifest.txt +245 -0
- data/NOTES +4 -0
- data/README.txt +163 -0
- data/Rakefile +161 -0
- data/TODO.txt +146 -0
- data/app/README.txt +24 -0
- data/app/Rakefile +8 -0
- data/app/config/ROBOT.rb +5 -0
- data/app/config/app.yml +91 -0
- data/app/config/init.rb +7 -0
- data/app/config/roby.yml +3 -0
- data/app/controllers/.gitattributes +0 -0
- data/app/controllers/ROBOT.rb +2 -0
- data/app/data/.gitattributes +0 -0
- data/app/planners/ROBOT/main.rb +6 -0
- data/app/planners/main.rb +5 -0
- data/app/scripts/distributed +3 -0
- data/app/scripts/generate/bookmarks +3 -0
- data/app/scripts/replay +3 -0
- data/app/scripts/results +3 -0
- data/app/scripts/run +3 -0
- data/app/scripts/server +3 -0
- data/app/scripts/shell +3 -0
- data/app/scripts/test +3 -0
- data/app/tasks/.gitattributes +0 -0
- data/app/tasks/ROBOT/.gitattributes +0 -0
- data/bin/roby +210 -0
- data/bin/roby-log +168 -0
- data/bin/roby-shell +25 -0
- data/doc/images/event_generalization.png +0 -0
- data/doc/images/exception_propagation_1.png +0 -0
- data/doc/images/exception_propagation_2.png +0 -0
- data/doc/images/exception_propagation_3.png +0 -0
- data/doc/images/exception_propagation_4.png +0 -0
- data/doc/images/exception_propagation_5.png +0 -0
- data/doc/images/replay_handler_error.png +0 -0
- data/doc/images/replay_handler_error_0.png +0 -0
- data/doc/images/replay_handler_error_1.png +0 -0
- data/doc/images/roby_cycle_overview.png +0 -0
- data/doc/images/roby_replay_02.png +0 -0
- data/doc/images/roby_replay_03.png +0 -0
- data/doc/images/roby_replay_04.png +0 -0
- data/doc/images/roby_replay_event_representation.png +0 -0
- data/doc/images/roby_replay_first_state.png +0 -0
- data/doc/images/roby_replay_relations.png +0 -0
- data/doc/images/roby_replay_startup.png +0 -0
- data/doc/images/task_event_generalization.png +0 -0
- data/doc/papers.rdoc +11 -0
- data/doc/styles/allison.css +314 -0
- data/doc/styles/allison.js +316 -0
- data/doc/styles/allison.rb +276 -0
- data/doc/styles/jamis.rb +593 -0
- data/doc/tutorials/01-GettingStarted.rdoc +86 -0
- data/doc/tutorials/02-GoForward.rdoc +220 -0
- data/doc/tutorials/03-PlannedPath.rdoc +268 -0
- data/doc/tutorials/04-EventPropagation.rdoc +236 -0
- data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
- data/doc/tutorials/06-Overview.rdoc +40 -0
- data/doc/videos.rdoc +69 -0
- data/ext/droby/dump.cc +175 -0
- data/ext/droby/extconf.rb +3 -0
- data/ext/graph/algorithm.cc +746 -0
- data/ext/graph/extconf.rb +7 -0
- data/ext/graph/graph.cc +529 -0
- data/ext/graph/graph.hh +183 -0
- data/ext/graph/iterator_sequence.hh +102 -0
- data/ext/graph/undirected_dfs.hh +226 -0
- data/ext/graph/undirected_graph.hh +421 -0
- data/lib/roby.rb +41 -0
- data/lib/roby/app.rb +870 -0
- data/lib/roby/app/rake.rb +56 -0
- data/lib/roby/app/run.rb +14 -0
- data/lib/roby/app/scripts/distributed.rb +13 -0
- data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
- data/lib/roby/app/scripts/replay.rb +31 -0
- data/lib/roby/app/scripts/results.rb +15 -0
- data/lib/roby/app/scripts/run.rb +26 -0
- data/lib/roby/app/scripts/server.rb +18 -0
- data/lib/roby/app/scripts/shell.rb +88 -0
- data/lib/roby/app/scripts/test.rb +40 -0
- data/lib/roby/basic_object.rb +151 -0
- data/lib/roby/config.rb +5 -0
- data/lib/roby/control.rb +747 -0
- data/lib/roby/decision_control.rb +17 -0
- data/lib/roby/distributed.rb +32 -0
- data/lib/roby/distributed/base.rb +440 -0
- data/lib/roby/distributed/communication.rb +871 -0
- data/lib/roby/distributed/connection_space.rb +592 -0
- data/lib/roby/distributed/distributed_object.rb +206 -0
- data/lib/roby/distributed/drb.rb +62 -0
- data/lib/roby/distributed/notifications.rb +539 -0
- data/lib/roby/distributed/peer.rb +550 -0
- data/lib/roby/distributed/protocol.rb +529 -0
- data/lib/roby/distributed/proxy.rb +343 -0
- data/lib/roby/distributed/subscription.rb +311 -0
- data/lib/roby/distributed/transaction.rb +498 -0
- data/lib/roby/event.rb +897 -0
- data/lib/roby/exceptions.rb +234 -0
- data/lib/roby/executives/simple.rb +30 -0
- data/lib/roby/graph.rb +166 -0
- data/lib/roby/interface.rb +390 -0
- data/lib/roby/log.rb +3 -0
- data/lib/roby/log/chronicle.rb +303 -0
- data/lib/roby/log/console.rb +72 -0
- data/lib/roby/log/data_stream.rb +197 -0
- data/lib/roby/log/dot.rb +279 -0
- data/lib/roby/log/event_stream.rb +151 -0
- data/lib/roby/log/file.rb +340 -0
- data/lib/roby/log/gui/basic_display.ui +83 -0
- data/lib/roby/log/gui/chronicle.rb +26 -0
- data/lib/roby/log/gui/chronicle_view.rb +40 -0
- data/lib/roby/log/gui/chronicle_view.ui +70 -0
- data/lib/roby/log/gui/data_displays.rb +172 -0
- data/lib/roby/log/gui/data_displays.ui +155 -0
- data/lib/roby/log/gui/notifications.rb +26 -0
- data/lib/roby/log/gui/relations.rb +248 -0
- data/lib/roby/log/gui/relations.ui +123 -0
- data/lib/roby/log/gui/relations_view.rb +185 -0
- data/lib/roby/log/gui/relations_view.ui +149 -0
- data/lib/roby/log/gui/replay.rb +327 -0
- data/lib/roby/log/gui/replay_controls.rb +200 -0
- data/lib/roby/log/gui/replay_controls.ui +259 -0
- data/lib/roby/log/gui/runtime.rb +130 -0
- data/lib/roby/log/hooks.rb +185 -0
- data/lib/roby/log/logger.rb +202 -0
- data/lib/roby/log/notifications.rb +244 -0
- data/lib/roby/log/plan_rebuilder.rb +470 -0
- data/lib/roby/log/relations.rb +1056 -0
- data/lib/roby/log/server.rb +550 -0
- data/lib/roby/log/sqlite.rb +47 -0
- data/lib/roby/log/timings.rb +164 -0
- data/lib/roby/plan-object.rb +247 -0
- data/lib/roby/plan.rb +762 -0
- data/lib/roby/planning.rb +13 -0
- data/lib/roby/planning/loops.rb +302 -0
- data/lib/roby/planning/model.rb +906 -0
- data/lib/roby/planning/task.rb +151 -0
- data/lib/roby/propagation.rb +562 -0
- data/lib/roby/query.rb +619 -0
- data/lib/roby/relations.rb +583 -0
- data/lib/roby/relations/conflicts.rb +70 -0
- data/lib/roby/relations/ensured.rb +20 -0
- data/lib/roby/relations/error_handling.rb +23 -0
- data/lib/roby/relations/events.rb +9 -0
- data/lib/roby/relations/executed_by.rb +193 -0
- data/lib/roby/relations/hierarchy.rb +239 -0
- data/lib/roby/relations/influence.rb +10 -0
- data/lib/roby/relations/planned_by.rb +63 -0
- data/lib/roby/robot.rb +7 -0
- data/lib/roby/standard_errors.rb +218 -0
- data/lib/roby/state.rb +5 -0
- data/lib/roby/state/events.rb +221 -0
- data/lib/roby/state/information.rb +55 -0
- data/lib/roby/state/pos.rb +110 -0
- data/lib/roby/state/shapes.rb +32 -0
- data/lib/roby/state/state.rb +353 -0
- data/lib/roby/support.rb +92 -0
- data/lib/roby/task-operations.rb +182 -0
- data/lib/roby/task.rb +1618 -0
- data/lib/roby/test/common.rb +399 -0
- data/lib/roby/test/distributed.rb +214 -0
- data/lib/roby/test/tasks/empty_task.rb +9 -0
- data/lib/roby/test/tasks/goto.rb +36 -0
- data/lib/roby/test/tasks/simple_task.rb +23 -0
- data/lib/roby/test/testcase.rb +519 -0
- data/lib/roby/test/tools.rb +160 -0
- data/lib/roby/thread_task.rb +87 -0
- data/lib/roby/transactions.rb +462 -0
- data/lib/roby/transactions/proxy.rb +292 -0
- data/lib/roby/transactions/updates.rb +139 -0
- data/plugins/fault_injection/History.txt +4 -0
- data/plugins/fault_injection/README.txt +37 -0
- data/plugins/fault_injection/Rakefile +18 -0
- data/plugins/fault_injection/TODO.txt +0 -0
- data/plugins/fault_injection/app.rb +52 -0
- data/plugins/fault_injection/fault_injection.rb +89 -0
- data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
- data/plugins/subsystems/README.txt +40 -0
- data/plugins/subsystems/Rakefile +18 -0
- data/plugins/subsystems/app.rb +171 -0
- data/plugins/subsystems/test/app/README +24 -0
- data/plugins/subsystems/test/app/Rakefile +8 -0
- data/plugins/subsystems/test/app/config/app.yml +71 -0
- data/plugins/subsystems/test/app/config/init.rb +9 -0
- data/plugins/subsystems/test/app/config/roby.yml +3 -0
- data/plugins/subsystems/test/app/planners/main.rb +20 -0
- data/plugins/subsystems/test/app/scripts/distributed +3 -0
- data/plugins/subsystems/test/app/scripts/replay +3 -0
- data/plugins/subsystems/test/app/scripts/results +3 -0
- data/plugins/subsystems/test/app/scripts/run +3 -0
- data/plugins/subsystems/test/app/scripts/server +3 -0
- data/plugins/subsystems/test/app/scripts/shell +3 -0
- data/plugins/subsystems/test/app/scripts/test +3 -0
- data/plugins/subsystems/test/app/tasks/services.rb +15 -0
- data/plugins/subsystems/test/test_subsystems.rb +71 -0
- data/test/distributed/test_communication.rb +178 -0
- data/test/distributed/test_connection.rb +282 -0
- data/test/distributed/test_execution.rb +373 -0
- data/test/distributed/test_mixed_plan.rb +341 -0
- data/test/distributed/test_plan_notifications.rb +238 -0
- data/test/distributed/test_protocol.rb +516 -0
- data/test/distributed/test_query.rb +102 -0
- data/test/distributed/test_remote_plan.rb +491 -0
- data/test/distributed/test_transaction.rb +463 -0
- data/test/mockups/tasks.rb +27 -0
- data/test/planning/test_loops.rb +380 -0
- data/test/planning/test_model.rb +427 -0
- data/test/planning/test_task.rb +106 -0
- data/test/relations/test_conflicts.rb +42 -0
- data/test/relations/test_ensured.rb +38 -0
- data/test/relations/test_executed_by.rb +149 -0
- data/test/relations/test_hierarchy.rb +158 -0
- data/test/relations/test_planned_by.rb +54 -0
- data/test/suite_core.rb +24 -0
- data/test/suite_distributed.rb +9 -0
- data/test/suite_planning.rb +3 -0
- data/test/suite_relations.rb +8 -0
- data/test/test_bgl.rb +508 -0
- data/test/test_control.rb +399 -0
- data/test/test_event.rb +894 -0
- data/test/test_exceptions.rb +592 -0
- data/test/test_interface.rb +37 -0
- data/test/test_log.rb +114 -0
- data/test/test_log_server.rb +132 -0
- data/test/test_plan.rb +584 -0
- data/test/test_propagation.rb +210 -0
- data/test/test_query.rb +266 -0
- data/test/test_relations.rb +180 -0
- data/test/test_state.rb +414 -0
- data/test/test_support.rb +16 -0
- data/test/test_task.rb +938 -0
- data/test/test_testcase.rb +122 -0
- data/test/test_thread_task.rb +73 -0
- data/test/test_transactions.rb +569 -0
- data/test/test_transactions_proxy.rb +198 -0
- metadata +570 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
module Roby
|
|
2
|
+
module Distributed
|
|
3
|
+
# Error raised when a connection attempt failed on the given neighbour
|
|
4
|
+
class ConnectionFailed < RuntimeError
|
|
5
|
+
attr_reader :neighbour
|
|
6
|
+
|
|
7
|
+
def initialize(neighbour)
|
|
8
|
+
@neighbour = neighbour
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Peer
|
|
13
|
+
class << self
|
|
14
|
+
private :new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# ConnectionToken objects are used to sort out concurrent
|
|
18
|
+
# connections, i.e. cases where two peers are trying to initiate a
|
|
19
|
+
# connection with each other at the same time.
|
|
20
|
+
#
|
|
21
|
+
# When this situation appears, each peer compares its own token
|
|
22
|
+
# with the one sent by the remote peer. The greatest token wins and
|
|
23
|
+
# is considered the initiator of the connection.
|
|
24
|
+
#
|
|
25
|
+
# See #initiate_connection
|
|
26
|
+
class ConnectionToken
|
|
27
|
+
attr_reader :time, :value
|
|
28
|
+
def initialize
|
|
29
|
+
@time = Time.now
|
|
30
|
+
@value = rand
|
|
31
|
+
end
|
|
32
|
+
def <=>(other)
|
|
33
|
+
result = (time <=> other.time)
|
|
34
|
+
if result == 0
|
|
35
|
+
value <=> other.value
|
|
36
|
+
else
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
include Comparable
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# A value indicating the current status of the connection. It can
|
|
44
|
+
# be one of :connected, :disconnecting, :disconnected
|
|
45
|
+
attr_reader :connection_state
|
|
46
|
+
|
|
47
|
+
# Connect to +neighbour+ and return the corresponding peer. It is a
|
|
48
|
+
# blocking method, so it is an error to call it from within the control thread
|
|
49
|
+
def self.connect(neighbour)
|
|
50
|
+
Roby.condition_variable(true) do |cv, mutex|
|
|
51
|
+
peer = nil
|
|
52
|
+
mutex.synchronize do
|
|
53
|
+
thread = initiate_connection(Distributed.state, neighbour) do |peer|
|
|
54
|
+
return peer unless thread
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
mutex.unlock
|
|
59
|
+
thread.value
|
|
60
|
+
rescue Exception => e
|
|
61
|
+
connection_space.synchronize do
|
|
62
|
+
connection_space.pending_connections.delete(neighbour.remote_id)
|
|
63
|
+
end
|
|
64
|
+
raise ConnectionFailed.new(neighbour), e.message
|
|
65
|
+
ensure
|
|
66
|
+
mutex.lock
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Start connecting to +neighbour+ in an another thread and yield
|
|
73
|
+
# the corresponding Peer object. This is safe to call if we have
|
|
74
|
+
# already connected to +neighbour+, in which case the already
|
|
75
|
+
# existing peer is returned.
|
|
76
|
+
#
|
|
77
|
+
# The Peer object is yield from within the control thread, only
|
|
78
|
+
# when the :ready event of the peer's ConnectionTask has been
|
|
79
|
+
# emitted
|
|
80
|
+
#
|
|
81
|
+
# Returns the connection thread
|
|
82
|
+
def self.initiate_connection(connection_space, neighbour, &block)
|
|
83
|
+
connection_space.synchronize do
|
|
84
|
+
if peer = connection_space.peers[neighbour.remote_id]
|
|
85
|
+
# already connected
|
|
86
|
+
yield(peer) if block_given?
|
|
87
|
+
return
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
local_token = ConnectionToken.new
|
|
91
|
+
call = [:connect, local_token,
|
|
92
|
+
connection_space.name,
|
|
93
|
+
connection_space.remote_id,
|
|
94
|
+
Distributed.format(Roby::State)]
|
|
95
|
+
send_connection_request(connection_space, neighbour, call, local_token, &block)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.abort_connection_thread(connection_space, remote_id, lock = true)
|
|
100
|
+
if lock
|
|
101
|
+
connection_space.synchronize do
|
|
102
|
+
abort_connection_thread(connection_space, remote_id, false)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
connection_space.pending_connections.delete(remote_id)
|
|
107
|
+
if peer = connection_space.peers[remote_id]
|
|
108
|
+
begin
|
|
109
|
+
connection_space.mutex.unlock
|
|
110
|
+
peer.disconnected(:aborted)
|
|
111
|
+
ensure
|
|
112
|
+
connection_space.mutex.lock
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.send_connection_thread(connection_space, neighbour, call, local_token, &block)
|
|
118
|
+
remote_id = neighbour.remote_id
|
|
119
|
+
Thread.current.abort_on_exception = false
|
|
120
|
+
|
|
121
|
+
begin
|
|
122
|
+
socket = TCPSocket.new(remote_id.uri, remote_id.ref)
|
|
123
|
+
rescue Errno::ECONNRESET, Errno::ECONNREFUSED
|
|
124
|
+
abort_connection_thread(connection_space, remote_id)
|
|
125
|
+
return
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
begin
|
|
129
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
130
|
+
Distributed.debug "#{call[0]}: #{neighbour} on #{socket.peer_info}"
|
|
131
|
+
|
|
132
|
+
# Send the connection request
|
|
133
|
+
call = Marshal.dump(call)
|
|
134
|
+
socket.write [call.size].pack("N")
|
|
135
|
+
socket.write call
|
|
136
|
+
|
|
137
|
+
reply_size = socket.read(4)
|
|
138
|
+
if !reply_size
|
|
139
|
+
raise "peer disconnected"
|
|
140
|
+
end
|
|
141
|
+
reply = Marshal.load(socket.read(*reply_size.unpack("N")))
|
|
142
|
+
rescue Errno::ECONNRESET, Errno::ENOTCONN
|
|
143
|
+
abort_connection_thread(connection_space, remote_id)
|
|
144
|
+
return
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
connection_space.synchronize do
|
|
148
|
+
connection_space.pending_connections.delete(remote_id)
|
|
149
|
+
m = reply.shift
|
|
150
|
+
Roby::Distributed.debug "remote peer #{m}"
|
|
151
|
+
|
|
152
|
+
# if the remote peer is also connecting, and if its
|
|
153
|
+
# token is better than our own, m will be nil and thus
|
|
154
|
+
# the thread will finish without doing anything
|
|
155
|
+
|
|
156
|
+
case m
|
|
157
|
+
when :connected
|
|
158
|
+
peer = new(connection_space, socket, *reply)
|
|
159
|
+
when :reconnected
|
|
160
|
+
peer = connection_space.peers[remote_id]
|
|
161
|
+
peer.reconnected(socket)
|
|
162
|
+
when :aborted
|
|
163
|
+
abort_connection_thread(connection_space, remote_id, false)
|
|
164
|
+
return
|
|
165
|
+
when :already_connecting, :already_connected
|
|
166
|
+
peer = connection_space.peers[remote_id]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
yield(peer) if peer && block_given?
|
|
170
|
+
peer
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Generic handling of connection/reconnection initiated by this side
|
|
175
|
+
def self.send_connection_request(connection_space, neighbour, call, local_token, &block) # :nodoc:
|
|
176
|
+
remote_id = neighbour.remote_id
|
|
177
|
+
token, connecting_thread = connection_space.pending_connections[remote_id]
|
|
178
|
+
if token
|
|
179
|
+
# we are already connecting to the peer, check the connection token
|
|
180
|
+
peer = begin
|
|
181
|
+
connection_space.mutex.unlock
|
|
182
|
+
connecting_thread.value
|
|
183
|
+
ensure
|
|
184
|
+
connection_space.mutex.lock
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
if token < local_token
|
|
188
|
+
if !peer
|
|
189
|
+
raise "something went wrong during connection: got nil peer with better token"
|
|
190
|
+
end
|
|
191
|
+
yield(peer) if block_given?
|
|
192
|
+
return
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
connecting_thread = Thread.new do
|
|
198
|
+
send_connection_thread(connection_space, neighbour, call, local_token, &block)
|
|
199
|
+
end
|
|
200
|
+
connection_space.pending_connections[remote_id] = [local_token, connecting_thread]
|
|
201
|
+
connecting_thread
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Create a Peer object for a connection attempt on the server
|
|
205
|
+
# socket There is nothing to do here. The remote peer is supposed
|
|
206
|
+
# to send us a #connect message, after which we can assume that the
|
|
207
|
+
# connection is up
|
|
208
|
+
def self.connection_request(connection_space, socket)
|
|
209
|
+
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
|
|
210
|
+
|
|
211
|
+
# read connection info from +socket+
|
|
212
|
+
info_size = *socket.read(4).unpack("N")
|
|
213
|
+
m, remote_token, remote_name, remote_id, remote_state =
|
|
214
|
+
Marshal.load(socket.read(info_size))
|
|
215
|
+
|
|
216
|
+
Distributed.debug "connection attempt from #{socket}: #{m} #{remote_name} #{remote_id}"
|
|
217
|
+
|
|
218
|
+
connection_space.synchronize do
|
|
219
|
+
# Now check the connection status
|
|
220
|
+
if old_peer = connection_space.aborted_connections.delete(remote_id)
|
|
221
|
+
reply = [:aborted]
|
|
222
|
+
elsif m == :connect && peer = connection_space.peers[remote_id]
|
|
223
|
+
reply = [:already_connected]
|
|
224
|
+
else
|
|
225
|
+
token, connecting_thread = connection_space.pending_connections[remote_id]
|
|
226
|
+
if token && token < remote_token
|
|
227
|
+
if connecting_thread
|
|
228
|
+
begin
|
|
229
|
+
connection_space.mutex.unlock
|
|
230
|
+
connecting_thread.join
|
|
231
|
+
ensure
|
|
232
|
+
connection_space.mutex.lock
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
reply = [:already_connecting]
|
|
236
|
+
elsif m == :reconnect
|
|
237
|
+
peer = connection_space.peers[remote_id]
|
|
238
|
+
peer.reconnected(socket)
|
|
239
|
+
reply = [:reconnected]
|
|
240
|
+
else
|
|
241
|
+
peer = new(connection_space, socket, remote_name, remote_id, remote_state)
|
|
242
|
+
reply = [:connected, connection_space.name,
|
|
243
|
+
connection_space.remote_id,
|
|
244
|
+
Distributed.format(Roby::State)]
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
Distributed.debug "connection attempt from #{socket}: #{reply[0]}"
|
|
249
|
+
reply = Marshal.dump(reply)
|
|
250
|
+
socket.write [reply.size].pack("N")
|
|
251
|
+
socket.write reply
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Reconnect to the given peer after the socket closed
|
|
256
|
+
def reconnect
|
|
257
|
+
local_token = ConnectionToken.new
|
|
258
|
+
|
|
259
|
+
connection_space.synchronize do
|
|
260
|
+
call = [:reconnect, local_token, connection_space.name, connection_space.remote_id]
|
|
261
|
+
Peer.send_connection_request(connection_space, self, call, local_token)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Called when we managed to reconnect to our peer. +socket+ is the new communication socket
|
|
266
|
+
def reconnected(socket)
|
|
267
|
+
Roby::Distributed.debug "new socket for #{self}: #{socket.peer_info}"
|
|
268
|
+
connection_space.pending_sockets << [socket, self]
|
|
269
|
+
@socket = socket
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Normal disconnection procedure.
|
|
273
|
+
#
|
|
274
|
+
# The procedure is as follows:
|
|
275
|
+
# * we set the connection state as 'disconnecting'. This disables all
|
|
276
|
+
# notifications for this peer (see for instance
|
|
277
|
+
# Distributed.each_subscribed_peer)
|
|
278
|
+
# * we queue the :disconnected message
|
|
279
|
+
#
|
|
280
|
+
# At this point, we are waiting for the remote peer to do the same:
|
|
281
|
+
# send us 'disconnected'. When we receive that message, we put the
|
|
282
|
+
# connection into the disconnected state and all transmission is
|
|
283
|
+
# forbidden. We make the transmission thread quit then, and the
|
|
284
|
+
# 'failed' event is emitted on the ConnectionTask task
|
|
285
|
+
#
|
|
286
|
+
# Note that once the connection leaves the connected state, the only
|
|
287
|
+
# messages allowed by #queue_call are 'completed' and 'disconnected'
|
|
288
|
+
def disconnect
|
|
289
|
+
synchronize do
|
|
290
|
+
Roby::Distributed.info "disconnecting from #{self}"
|
|
291
|
+
@connection_state = :disconnecting
|
|
292
|
+
end
|
|
293
|
+
queue_call false, :disconnect
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# +error+ has been raised while we were processing +msg+(*+args+)
|
|
297
|
+
# This error cannot be recovered, and the connection to the peer
|
|
298
|
+
# will be closed.
|
|
299
|
+
#
|
|
300
|
+
# This sends the PeerServer#fatal_error message to our peer
|
|
301
|
+
def fatal_error(error, msg, args)
|
|
302
|
+
synchronize do
|
|
303
|
+
Roby::Distributed.fatal "fatal error '#{error.message}' while processing #{msg}(#{args.join(", ")})"
|
|
304
|
+
Roby::Distributed.fatal Roby.filter_backtrace(error.backtrace).join("\n ")
|
|
305
|
+
@connection_state = :disconnecting
|
|
306
|
+
end
|
|
307
|
+
queue_call false, :fatal_error, [error, msg, args]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Called when the peer acknowledged the fact that we disconnected
|
|
311
|
+
def disconnected(event = :failed) # :nodoc:
|
|
312
|
+
Roby::Distributed.info "#{remote_name} disconnected (#{event})"
|
|
313
|
+
|
|
314
|
+
connection_space.synchronize do
|
|
315
|
+
Distributed.peers.delete(remote_id)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
synchronize do
|
|
319
|
+
@connection_state = :disconnected
|
|
320
|
+
|
|
321
|
+
if @send_thread && @send_thread != Thread.current
|
|
322
|
+
begin
|
|
323
|
+
@send_queue.clear
|
|
324
|
+
@send_queue.push nil
|
|
325
|
+
mutex.unlock
|
|
326
|
+
@send_thread.join
|
|
327
|
+
ensure
|
|
328
|
+
mutex.lock
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
@send_thread = nil
|
|
332
|
+
|
|
333
|
+
proxies.each_value do |obj|
|
|
334
|
+
obj.remote_siblings.delete(self)
|
|
335
|
+
end
|
|
336
|
+
proxies.clear
|
|
337
|
+
removing_proxies.clear
|
|
338
|
+
|
|
339
|
+
socket.close unless socket.closed?
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
Roby.once do
|
|
343
|
+
task.emit(event)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Call to disconnect outside of the normal protocol.
|
|
348
|
+
def disconnected!
|
|
349
|
+
connection_space.synchronize do
|
|
350
|
+
connection_space.aborted_connections[remote_id] = self
|
|
351
|
+
end
|
|
352
|
+
disconnected(:aborted)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Returns true if the connection has been established. See also #link_alive?
|
|
356
|
+
def connected?; connection_state == :connected end
|
|
357
|
+
# Returns true if the we disconnected on our side but the peer did not
|
|
358
|
+
# acknowledge it yet
|
|
359
|
+
def disconnecting?; connection_state == :disconnecting end
|
|
360
|
+
# Returns true if the connection with this peer has been removed
|
|
361
|
+
def disconnected?; connection_state == :disconnected end
|
|
362
|
+
|
|
363
|
+
# Mark the link as dead regardless of the last neighbour discovery. This
|
|
364
|
+
# will be reset during the next neighbour discovery
|
|
365
|
+
def link_dead!; @dead = true end
|
|
366
|
+
|
|
367
|
+
# Disables the sending part of the communication link. It is an
|
|
368
|
+
# accumulator: if #disable_tx is called twice, then TX will be
|
|
369
|
+
# reenabled only when #enable_tx is also called twice.
|
|
370
|
+
def disable_tx; @disabled_tx += 1 end
|
|
371
|
+
# Enables the sending part of the communication link. It is an
|
|
372
|
+
# accumulator: if #enable_tx is called twice, then TX will be
|
|
373
|
+
# disabled only when #disable_tx is also called twice.
|
|
374
|
+
def enable_tx; @disabled_tx -= 1 end
|
|
375
|
+
# True if TX is currently disabled
|
|
376
|
+
def disabled_tx?; @disabled_tx > 0 end
|
|
377
|
+
# Disables the receiving part of the communication link. It is an
|
|
378
|
+
# accumulator: if #disable_rx is called twice, then RX will be
|
|
379
|
+
# reenabled only when #enable_rx is also called twice.
|
|
380
|
+
def disable_rx; @disabled_rx += 1 end
|
|
381
|
+
# Enables the receiving part of the communication link. It is an
|
|
382
|
+
# accumulator: if #enable_rx is called twice, then RX will be
|
|
383
|
+
# disabled only when #disable_rx is also called twice.
|
|
384
|
+
def enable_rx; @disabled_rx -= 1 end
|
|
385
|
+
# True if RX is currently disabled
|
|
386
|
+
def disabled_rx?; @disabled_rx > 0 end
|
|
387
|
+
|
|
388
|
+
# Checks if the connection is currently alive, i.e. if we can send
|
|
389
|
+
# data on the link. This does not mean that we currently have no
|
|
390
|
+
# interaction with the peer: it only means that we cannot currently
|
|
391
|
+
# communicate with it.
|
|
392
|
+
def link_alive?
|
|
393
|
+
return false if socket.closed? || @dead || @disabled_tx > 0
|
|
394
|
+
return false unless !remote_id || connection_space.neighbours.find { |n| n.remote_id == remote_id }
|
|
395
|
+
true
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
class PeerServer
|
|
401
|
+
# Message received when an error occured on the remote side, if
|
|
402
|
+
# this error cannot be recovered.
|
|
403
|
+
def fatal_error(error, msg, args)
|
|
404
|
+
Distributed.fatal "remote reports #{peer.local_object(error)} while processing #{msg}(#{args.join(", ")})"
|
|
405
|
+
disconnect
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Message received when our peer is closing the connection
|
|
409
|
+
def disconnect
|
|
410
|
+
peer.disconnected
|
|
411
|
+
nil
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Error raised when a communication callback is queueing another
|
|
416
|
+
# communication callback
|
|
417
|
+
class RecursiveCallbacksError < RuntimeError; end
|
|
418
|
+
# Error raised when a callback has failed.
|
|
419
|
+
class CallbackProcessingError < RuntimeError; end
|
|
420
|
+
|
|
421
|
+
CallSpec = Struct.new :is_callback,
|
|
422
|
+
:method, :formatted_args, :original_args,
|
|
423
|
+
:on_completion, :trace, :waiting_thread,
|
|
424
|
+
:message_id
|
|
425
|
+
|
|
426
|
+
# The specification of a call in Peer#send_queue and Peer#completion_queue. Note
|
|
427
|
+
# that only the #is_callback, #method and #formatted_args are sent to the remote
|
|
428
|
+
# PeerServer#demux method
|
|
429
|
+
#
|
|
430
|
+
# * is_callback is a boolean flag indicating if this call has been
|
|
431
|
+
# queued while the PeerServer object was processing a remote request
|
|
432
|
+
# * <tt>method</tt> is the method name to call on the remote PeerServer object
|
|
433
|
+
# * <tt>formatted_args</tt> is the arguments formatted by
|
|
434
|
+
# Distributed.format. Arguments are formatted right away, since we
|
|
435
|
+
# want the marshalled arguments to reflect objects state at the
|
|
436
|
+
# time of the call, not at the time they are sent
|
|
437
|
+
# * +original_args+ is the arguments not yet formatted. They are
|
|
438
|
+
# kept here to protect involved object from Ruby's GC until the
|
|
439
|
+
# call is completed.
|
|
440
|
+
# * +on_completion+ is a proc object which will be called when the
|
|
441
|
+
# method has successfully been processed by the remote object, with
|
|
442
|
+
# the returned value as argument$
|
|
443
|
+
# * trace is the location (as returned by Kernel#caller) from which
|
|
444
|
+
# the call has been queued. It is mainly used for debugging
|
|
445
|
+
# purposes
|
|
446
|
+
# * if +thread+ is not nil, it is the thread which is waiting for
|
|
447
|
+
# the call to complete. If the call is aborted, the error will be
|
|
448
|
+
# raised in the waiting thread
|
|
449
|
+
class CallSpec
|
|
450
|
+
alias :callback? :is_callback
|
|
451
|
+
|
|
452
|
+
def to_s
|
|
453
|
+
args = formatted_args.map do |arg|
|
|
454
|
+
if arg.kind_of?(DRbObject) then arg.inspect
|
|
455
|
+
else arg.to_s
|
|
456
|
+
end
|
|
457
|
+
end
|
|
458
|
+
"#{method}(#{args.join(", ")})"
|
|
459
|
+
end
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Called in PeerServer messages handlers to completely ignore the
|
|
463
|
+
# message which is currently being processed
|
|
464
|
+
def self.ignore!
|
|
465
|
+
throw :ignore_this_call
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
class PeerServer
|
|
469
|
+
# True the current thread is processing a remote request
|
|
470
|
+
attr_predicate :processing?, true
|
|
471
|
+
# True if the current thread is processing a remote request, and if it is a callback
|
|
472
|
+
attr_predicate :processing_callback?, true
|
|
473
|
+
# True if we have already queued a +completed+ message for the message being processed
|
|
474
|
+
attr_predicate :queued_completion?, true
|
|
475
|
+
# The ID of the message we are currently processing
|
|
476
|
+
attr_accessor :current_message_id
|
|
477
|
+
|
|
478
|
+
# Message received when the first half of a synchro point is
|
|
479
|
+
# reached. See Peer#synchro_point.
|
|
480
|
+
def synchro_point
|
|
481
|
+
peer.transmit(:done_synchro_point)
|
|
482
|
+
nil
|
|
483
|
+
end
|
|
484
|
+
# Message received when the synchro point is finished.
|
|
485
|
+
def done_synchro_point; end
|
|
486
|
+
|
|
487
|
+
# Message received to describe a group of consecutive calls that
|
|
488
|
+
# have been completed, when all those calls return nil. This is
|
|
489
|
+
# simply an optimization of the communication protocol, as most
|
|
490
|
+
# remote calls return nil.
|
|
491
|
+
#
|
|
492
|
+
# +from_id+ is the ID of the first call of the group and +to_id+
|
|
493
|
+
# the last. Both are included in the group.
|
|
494
|
+
def completion_group(from_id, to_id)
|
|
495
|
+
for id in (from_id..to_id)
|
|
496
|
+
completed(nil, nil, id)
|
|
497
|
+
end
|
|
498
|
+
nil
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
# Message received when a given call, identified by its ID, has
|
|
502
|
+
# been processed on the remote peer. +result+ is the value
|
|
503
|
+
# returned by the method, +error+ an exception object (if an error
|
|
504
|
+
# occured).
|
|
505
|
+
def completed(result, error, id)
|
|
506
|
+
call_spec = peer.completion_queue.pop
|
|
507
|
+
if call_spec.message_id != id
|
|
508
|
+
result = Exception.exception("something fishy: ID mismatch in completion queue (#{call_spec.message_id} != #{id}")
|
|
509
|
+
error = true
|
|
510
|
+
call_spec = nil
|
|
511
|
+
end
|
|
512
|
+
if error
|
|
513
|
+
if call_spec && thread = call_spec.waiting_thread
|
|
514
|
+
result = peer.local_object(result)
|
|
515
|
+
thread.raise result
|
|
516
|
+
else
|
|
517
|
+
Roby::Distributed.fatal "fatal error in communication with #{peer}: #{result.full_message}"
|
|
518
|
+
Roby::Distributed.fatal "disconnecting ..."
|
|
519
|
+
if peer.connected?
|
|
520
|
+
peer.disconnect
|
|
521
|
+
else
|
|
522
|
+
peer.disconnected!
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
elsif call_spec
|
|
527
|
+
peer.call_attached_block(call_spec, result)
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
nil
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# Queue a completion message for our peer. This is usually done
|
|
534
|
+
# automatically in #demux, but it is useful to do it manually in
|
|
535
|
+
# certain conditions, for instance in PeerServer#execute
|
|
536
|
+
#
|
|
537
|
+
# In #execute, the control thread -> RX thread context switch is
|
|
538
|
+
# not immediate. Therefore, it is possible that events are queued
|
|
539
|
+
# by the control thread while the #completed message is not.
|
|
540
|
+
# #completed! both queues the message *and* makes sure that #demux
|
|
541
|
+
# won't.
|
|
542
|
+
def completed!(result, error)
|
|
543
|
+
if queued_completion?
|
|
544
|
+
raise "already queued the completed message"
|
|
545
|
+
else
|
|
546
|
+
Distributed.debug { "done, returns #{'error ' if error}#{result || 'nil'} in completed!" }
|
|
547
|
+
self.queued_completion = true
|
|
548
|
+
peer.queue_call false, :completed, [result, error, current_message_id]
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
# call-seq:
|
|
553
|
+
# execute { ... }
|
|
554
|
+
#
|
|
555
|
+
# Executes the given block in the control thread and return when the block
|
|
556
|
+
# has finished its execution. This method can be called only when serving
|
|
557
|
+
# a remote call.
|
|
558
|
+
def execute
|
|
559
|
+
if !processing?
|
|
560
|
+
return yield
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
Roby.execute do
|
|
564
|
+
error = nil
|
|
565
|
+
begin
|
|
566
|
+
result = yield
|
|
567
|
+
rescue Exception => error
|
|
568
|
+
end
|
|
569
|
+
completed!(error || result, !!error, peer.current_message_id)
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
class Peer
|
|
575
|
+
# The main synchronization mutex to access the peer. See also
|
|
576
|
+
# Peer#synchronize
|
|
577
|
+
attr_reader :mutex
|
|
578
|
+
def synchronize; @mutex.synchronize { yield } end
|
|
579
|
+
|
|
580
|
+
# The transmission thread
|
|
581
|
+
attr_reader :send_thread
|
|
582
|
+
# The queue which holds all calls to the remote peer. Calls are
|
|
583
|
+
# saved as CallSpec objects
|
|
584
|
+
attr_reader :send_queue
|
|
585
|
+
# The queue of calls that have been sent to our peer, but for which
|
|
586
|
+
# a +completed+ message has not been received. This is a queue of
|
|
587
|
+
# CallSpec objects
|
|
588
|
+
attr_reader :completion_queue
|
|
589
|
+
# The cycle data which is being gathered before queueing it into #send_queue
|
|
590
|
+
attr_reader :current_cycle
|
|
591
|
+
|
|
592
|
+
@@message_id = 0
|
|
593
|
+
|
|
594
|
+
# Checks that +object+ is marshallable. If +object+ is a
|
|
595
|
+
# collection, it will check that each of its elements is
|
|
596
|
+
# marshallable first. This is automatically called for all
|
|
597
|
+
# messages if DEBUG_MARSHALLING is set to true.
|
|
598
|
+
def check_marshallable(object, stack = ValueSet.new)
|
|
599
|
+
if !object.kind_of?(DRbObject) && object.respond_to?(:each) && !object.kind_of?(String)
|
|
600
|
+
if stack.include?(object)
|
|
601
|
+
Roby.warn "recursive marshalling of #{obj}"
|
|
602
|
+
raise "recursive marshalling"
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
stack << object
|
|
606
|
+
begin
|
|
607
|
+
object.each do |obj|
|
|
608
|
+
marshalled = begin
|
|
609
|
+
check_marshallable(obj, stack)
|
|
610
|
+
rescue Exception
|
|
611
|
+
raise TypeError, "cannot dump #{obj}(#{obj.class}): #{$!.message}"
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
if Marshal.load(marshalled).kind_of?(DRb::DRbUnknown)
|
|
616
|
+
raise TypeError, "cannot load #{obj}(#{obj.class})"
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
ensure
|
|
620
|
+
stack.delete(object)
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
Marshal.dump(object)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
# This set of calls mark the end of a cycle. When one of these is
|
|
627
|
+
# encountered, the calls gathered in #current_cycle are moved into
|
|
628
|
+
# #send_queue
|
|
629
|
+
CYCLE_END_CALLS = [:connect, :disconnect, :fatal_error, :state_update]
|
|
630
|
+
|
|
631
|
+
attr_predicate :sync?, true
|
|
632
|
+
|
|
633
|
+
# Add a CallSpec object in #send_queue. Do not use that method
|
|
634
|
+
# directly, but use #transmit and #call instead.
|
|
635
|
+
#
|
|
636
|
+
# The message to be sent is m(*args). +on_completion+ is either
|
|
637
|
+
# nil or a block object which should be called once the message has
|
|
638
|
+
# been processed by our remote peer. +waiting_thread+ is a Thread
|
|
639
|
+
# object of a thread waiting for the message to be processed.
|
|
640
|
+
# #raise will be called on it if an error has occured during the
|
|
641
|
+
# remote processing.
|
|
642
|
+
#
|
|
643
|
+
# If +is_callback+ is true, it means that the message is being
|
|
644
|
+
# queued during the processing of another message. In that case, we
|
|
645
|
+
# will receive the completion message only when all callbacks have
|
|
646
|
+
# also been processed. Queueing callbacks while processing another
|
|
647
|
+
# callback is forbidden and the communication layer raises
|
|
648
|
+
# RecursiveCallbacksError if it happens.
|
|
649
|
+
#
|
|
650
|
+
# #queueing allow to queue normal messages when they would have
|
|
651
|
+
# been marked as callbacks.
|
|
652
|
+
def queue_call(is_callback, m, args = [], on_completion = nil, waiting_thread = nil)
|
|
653
|
+
# Do some sanity checks
|
|
654
|
+
if !m.respond_to?(:to_sym)
|
|
655
|
+
raise ArgumentError, "method argument should be a symbol, was #{m.class}"
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
# Check the connection state
|
|
659
|
+
if (disconnecting? && m != :disconnect && m != :fatal_error) || disconnected?
|
|
660
|
+
raise DisconnectedError, "cannot queue #{m}(#{args.join(", ")}), we are not currently connected to #{remote_name}"
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Marshal DRoby-dumped objects now, since the object may be
|
|
664
|
+
# modified between now and the time it is sent
|
|
665
|
+
formatted_args = Distributed.format(args, self)
|
|
666
|
+
|
|
667
|
+
if Roby::Distributed::DEBUG_MARSHALLING
|
|
668
|
+
check_marshallable(formatted_args)
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
call_spec = CallSpec.new(is_callback,
|
|
672
|
+
m, formatted_args, args,
|
|
673
|
+
on_completion, caller(2), waiting_thread)
|
|
674
|
+
|
|
675
|
+
synchronize do
|
|
676
|
+
# No return message for 'completed' (of course)
|
|
677
|
+
if call_spec.method != :completed
|
|
678
|
+
@@message_id += 1
|
|
679
|
+
call_spec.message_id = @@message_id
|
|
680
|
+
completion_queue << call_spec
|
|
681
|
+
|
|
682
|
+
elsif !current_cycle.empty? && !(args[0] || args[1])
|
|
683
|
+
# Try to merge empty completed messages
|
|
684
|
+
last_call = current_cycle.last
|
|
685
|
+
last_method, last_args = last_call[1], last_call[2]
|
|
686
|
+
|
|
687
|
+
case last_method
|
|
688
|
+
when :completed
|
|
689
|
+
if !(last_args[0] || last_args[1])
|
|
690
|
+
Distributed.debug "merging two completion messages"
|
|
691
|
+
current_cycle.pop
|
|
692
|
+
call_spec.method = :completion_group
|
|
693
|
+
call_spec.formatted_args = [last_args[2], args[2]]
|
|
694
|
+
end
|
|
695
|
+
when :completion_group
|
|
696
|
+
Distributed.debug "extending a completion group"
|
|
697
|
+
current_cycle.pop
|
|
698
|
+
call_spec.method = :completion_group
|
|
699
|
+
call_spec.formatted_args = [last_args[0], args[2]]
|
|
700
|
+
end
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
Distributed.debug { "#{call_spec.is_callback ? 'adding callback' : 'queueing'} [#{call_spec.message_id}]#{remote_name}.#{call_spec.method}" }
|
|
704
|
+
current_cycle << [call_spec.is_callback, call_spec.method, call_spec.formatted_args, !waiting_thread, call_spec.message_id]
|
|
705
|
+
if sync? || CYCLE_END_CALLS.include?(m)
|
|
706
|
+
send_queue << current_cycle
|
|
707
|
+
@current_cycle = Array.new
|
|
708
|
+
end
|
|
709
|
+
end
|
|
710
|
+
end
|
|
711
|
+
|
|
712
|
+
# If #transmit calls are done in the block given to #queueing, they
|
|
713
|
+
# will queue the call normally, instead of marking it as callback
|
|
714
|
+
def queueing
|
|
715
|
+
old_processing = local_server.processing?
|
|
716
|
+
|
|
717
|
+
local_server.processing = false
|
|
718
|
+
yield
|
|
719
|
+
|
|
720
|
+
ensure
|
|
721
|
+
local_server.processing = old_processing
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# call-seq:
|
|
725
|
+
# peer.transmit(method, arg1, arg2, ...) { |ret| ... }
|
|
726
|
+
#
|
|
727
|
+
# Asynchronous call to the remote host. If a block is given, it is
|
|
728
|
+
# called in the communication thread when the call succeeds, with
|
|
729
|
+
# the returned value as argument.
|
|
730
|
+
def transmit(m, *args, &block)
|
|
731
|
+
is_callback = Roby.inside_control? && local_server.processing?
|
|
732
|
+
if is_callback && local_server.processing_callback?
|
|
733
|
+
raise RecursiveCallbacksError, "cannot queue callback #{m}(#{args.join(", ")}) while serving one"
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
queue_call is_callback, m, args, block
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# call-seq:
|
|
740
|
+
# peer.call(method, arg1, arg2) => result
|
|
741
|
+
#
|
|
742
|
+
# Calls a method synchronously and returns the value returned by
|
|
743
|
+
# the remote server. If we disconnect before this call is
|
|
744
|
+
# processed, raises DisconnectedError. If the remote server returns
|
|
745
|
+
# an exception, this exception is raised in the calling thread as
|
|
746
|
+
# well.
|
|
747
|
+
#
|
|
748
|
+
# Note that it is forbidden to use this method in control or
|
|
749
|
+
# communication threads, as it would make the application deadlock
|
|
750
|
+
def call(m, *args, &block)
|
|
751
|
+
if !Roby.outside_control? || Roby::Control.taken_mutex?
|
|
752
|
+
raise "cannot use Peer#call in control thread or while taking the Roby::Control mutex"
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
result = nil
|
|
756
|
+
Roby.condition_variable(true) do |cv, mt|
|
|
757
|
+
mt.synchronize do
|
|
758
|
+
Distributed.debug do
|
|
759
|
+
"calling #{remote_name}.#{m}"
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
callback = Proc.new do |return_value|
|
|
763
|
+
mt.synchronize do
|
|
764
|
+
result = return_value
|
|
765
|
+
block.call(return_value) if block
|
|
766
|
+
cv.broadcast
|
|
767
|
+
end
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
queue_call false, m, args, callback, Thread.current
|
|
771
|
+
cv.wait(mt)
|
|
772
|
+
end
|
|
773
|
+
end
|
|
774
|
+
|
|
775
|
+
result
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
# Main loop of the thread which communicates with the remote peer
|
|
779
|
+
def communication_loop
|
|
780
|
+
Thread.current.priority = 2
|
|
781
|
+
id = 0
|
|
782
|
+
data = nil
|
|
783
|
+
buffer = StringIO.new(" " * 8, 'w')
|
|
784
|
+
|
|
785
|
+
loop do
|
|
786
|
+
data ||= send_queue.shift
|
|
787
|
+
return if disconnected?
|
|
788
|
+
|
|
789
|
+
# Wait for the link to be alive before sending anything
|
|
790
|
+
while !link_alive?
|
|
791
|
+
return if disconnected?
|
|
792
|
+
connection_space.wait_next_discovery
|
|
793
|
+
end
|
|
794
|
+
return if disconnected?
|
|
795
|
+
|
|
796
|
+
buffer.truncate(8)
|
|
797
|
+
buffer.seek(8)
|
|
798
|
+
Marshal.dump(data, buffer)
|
|
799
|
+
buffer.string[0, 8] = [id += 1, buffer.size - 8].pack("NN")
|
|
800
|
+
|
|
801
|
+
begin
|
|
802
|
+
size = buffer.string.size
|
|
803
|
+
Roby::Distributed.debug { "sending #{size}B to #{self}" }
|
|
804
|
+
stats.tx += size
|
|
805
|
+
socket.write(buffer.string)
|
|
806
|
+
|
|
807
|
+
data = nil
|
|
808
|
+
rescue Errno::EPIPE
|
|
809
|
+
@dead = true
|
|
810
|
+
# communication error, retry sending the data (or, if we are disconnected, return)
|
|
811
|
+
end
|
|
812
|
+
end
|
|
813
|
+
|
|
814
|
+
rescue Interrupt
|
|
815
|
+
rescue Exception
|
|
816
|
+
Distributed.fatal do
|
|
817
|
+
"While sending #{data.inspect}\n" +
|
|
818
|
+
"Communication thread dies with\n#{$!.full_message}"
|
|
819
|
+
end
|
|
820
|
+
|
|
821
|
+
disconnected!
|
|
822
|
+
|
|
823
|
+
ensure
|
|
824
|
+
Distributed.info "communication thread quitting for #{self}. Rx: #{stats.rx}B, Tx: #{stats.tx}B"
|
|
825
|
+
calls = []
|
|
826
|
+
while !completion_queue.empty?
|
|
827
|
+
calls << completion_queue.shift
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
calls.each do |call_spec|
|
|
831
|
+
next unless call_spec
|
|
832
|
+
if thread = call_spec.waiting_thread
|
|
833
|
+
thread.raise DisconnectedError
|
|
834
|
+
end
|
|
835
|
+
end
|
|
836
|
+
|
|
837
|
+
Distributed.info "communication thread quit for #{self}"
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
# Formats an error message because +error+ has been reported by +call+
|
|
841
|
+
def report_remote_error(call, error)
|
|
842
|
+
error_message = error.full_message { |msg| msg !~ /drb\/[\w+]\.rb/ }
|
|
843
|
+
if call
|
|
844
|
+
"#{remote_name} reports an error on #{call}:\n#{error_message}\n" +
|
|
845
|
+
"call was initiated by\n #{call.trace.join("\n ")}"
|
|
846
|
+
else
|
|
847
|
+
"#{remote_name} reports an error on:\n#{error_message}"
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
# Calls the completion block that has been given to #transmit when
|
|
852
|
+
# +call+ is completed (the +on_completion+ parameter of
|
|
853
|
+
# #queue_call). A remote call is completed when it has been
|
|
854
|
+
# processed remotely *and* the callbacks returned by the remote
|
|
855
|
+
# server (if any) have been processed as well. +result+ is the
|
|
856
|
+
# value returned by the remote server.
|
|
857
|
+
def call_attached_block(call, result)
|
|
858
|
+
if block = call.on_completion
|
|
859
|
+
begin
|
|
860
|
+
Roby::Distributed.debug "calling completion block #{block} for #{call}"
|
|
861
|
+
block.call(result)
|
|
862
|
+
rescue Exception => e
|
|
863
|
+
Roby.application_error(:droby_callbacks, block, e)
|
|
864
|
+
end
|
|
865
|
+
end
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def synchro_point; call(:synchro_point) end
|
|
869
|
+
end
|
|
870
|
+
end
|
|
871
|
+
end
|