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.
Files changed (240) hide show
  1. data/.gitignore +29 -0
  2. data/History.txt +4 -0
  3. data/License-fr.txt +519 -0
  4. data/License.txt +515 -0
  5. data/Manifest.txt +245 -0
  6. data/NOTES +4 -0
  7. data/README.txt +163 -0
  8. data/Rakefile +161 -0
  9. data/TODO.txt +146 -0
  10. data/app/README.txt +24 -0
  11. data/app/Rakefile +8 -0
  12. data/app/config/ROBOT.rb +5 -0
  13. data/app/config/app.yml +91 -0
  14. data/app/config/init.rb +7 -0
  15. data/app/config/roby.yml +3 -0
  16. data/app/controllers/.gitattributes +0 -0
  17. data/app/controllers/ROBOT.rb +2 -0
  18. data/app/data/.gitattributes +0 -0
  19. data/app/planners/ROBOT/main.rb +6 -0
  20. data/app/planners/main.rb +5 -0
  21. data/app/scripts/distributed +3 -0
  22. data/app/scripts/generate/bookmarks +3 -0
  23. data/app/scripts/replay +3 -0
  24. data/app/scripts/results +3 -0
  25. data/app/scripts/run +3 -0
  26. data/app/scripts/server +3 -0
  27. data/app/scripts/shell +3 -0
  28. data/app/scripts/test +3 -0
  29. data/app/tasks/.gitattributes +0 -0
  30. data/app/tasks/ROBOT/.gitattributes +0 -0
  31. data/bin/roby +210 -0
  32. data/bin/roby-log +168 -0
  33. data/bin/roby-shell +25 -0
  34. data/doc/images/event_generalization.png +0 -0
  35. data/doc/images/exception_propagation_1.png +0 -0
  36. data/doc/images/exception_propagation_2.png +0 -0
  37. data/doc/images/exception_propagation_3.png +0 -0
  38. data/doc/images/exception_propagation_4.png +0 -0
  39. data/doc/images/exception_propagation_5.png +0 -0
  40. data/doc/images/replay_handler_error.png +0 -0
  41. data/doc/images/replay_handler_error_0.png +0 -0
  42. data/doc/images/replay_handler_error_1.png +0 -0
  43. data/doc/images/roby_cycle_overview.png +0 -0
  44. data/doc/images/roby_replay_02.png +0 -0
  45. data/doc/images/roby_replay_03.png +0 -0
  46. data/doc/images/roby_replay_04.png +0 -0
  47. data/doc/images/roby_replay_event_representation.png +0 -0
  48. data/doc/images/roby_replay_first_state.png +0 -0
  49. data/doc/images/roby_replay_relations.png +0 -0
  50. data/doc/images/roby_replay_startup.png +0 -0
  51. data/doc/images/task_event_generalization.png +0 -0
  52. data/doc/papers.rdoc +11 -0
  53. data/doc/styles/allison.css +314 -0
  54. data/doc/styles/allison.js +316 -0
  55. data/doc/styles/allison.rb +276 -0
  56. data/doc/styles/jamis.rb +593 -0
  57. data/doc/tutorials/01-GettingStarted.rdoc +86 -0
  58. data/doc/tutorials/02-GoForward.rdoc +220 -0
  59. data/doc/tutorials/03-PlannedPath.rdoc +268 -0
  60. data/doc/tutorials/04-EventPropagation.rdoc +236 -0
  61. data/doc/tutorials/05-ErrorHandling.rdoc +319 -0
  62. data/doc/tutorials/06-Overview.rdoc +40 -0
  63. data/doc/videos.rdoc +69 -0
  64. data/ext/droby/dump.cc +175 -0
  65. data/ext/droby/extconf.rb +3 -0
  66. data/ext/graph/algorithm.cc +746 -0
  67. data/ext/graph/extconf.rb +7 -0
  68. data/ext/graph/graph.cc +529 -0
  69. data/ext/graph/graph.hh +183 -0
  70. data/ext/graph/iterator_sequence.hh +102 -0
  71. data/ext/graph/undirected_dfs.hh +226 -0
  72. data/ext/graph/undirected_graph.hh +421 -0
  73. data/lib/roby.rb +41 -0
  74. data/lib/roby/app.rb +870 -0
  75. data/lib/roby/app/rake.rb +56 -0
  76. data/lib/roby/app/run.rb +14 -0
  77. data/lib/roby/app/scripts/distributed.rb +13 -0
  78. data/lib/roby/app/scripts/generate/bookmarks.rb +162 -0
  79. data/lib/roby/app/scripts/replay.rb +31 -0
  80. data/lib/roby/app/scripts/results.rb +15 -0
  81. data/lib/roby/app/scripts/run.rb +26 -0
  82. data/lib/roby/app/scripts/server.rb +18 -0
  83. data/lib/roby/app/scripts/shell.rb +88 -0
  84. data/lib/roby/app/scripts/test.rb +40 -0
  85. data/lib/roby/basic_object.rb +151 -0
  86. data/lib/roby/config.rb +5 -0
  87. data/lib/roby/control.rb +747 -0
  88. data/lib/roby/decision_control.rb +17 -0
  89. data/lib/roby/distributed.rb +32 -0
  90. data/lib/roby/distributed/base.rb +440 -0
  91. data/lib/roby/distributed/communication.rb +871 -0
  92. data/lib/roby/distributed/connection_space.rb +592 -0
  93. data/lib/roby/distributed/distributed_object.rb +206 -0
  94. data/lib/roby/distributed/drb.rb +62 -0
  95. data/lib/roby/distributed/notifications.rb +539 -0
  96. data/lib/roby/distributed/peer.rb +550 -0
  97. data/lib/roby/distributed/protocol.rb +529 -0
  98. data/lib/roby/distributed/proxy.rb +343 -0
  99. data/lib/roby/distributed/subscription.rb +311 -0
  100. data/lib/roby/distributed/transaction.rb +498 -0
  101. data/lib/roby/event.rb +897 -0
  102. data/lib/roby/exceptions.rb +234 -0
  103. data/lib/roby/executives/simple.rb +30 -0
  104. data/lib/roby/graph.rb +166 -0
  105. data/lib/roby/interface.rb +390 -0
  106. data/lib/roby/log.rb +3 -0
  107. data/lib/roby/log/chronicle.rb +303 -0
  108. data/lib/roby/log/console.rb +72 -0
  109. data/lib/roby/log/data_stream.rb +197 -0
  110. data/lib/roby/log/dot.rb +279 -0
  111. data/lib/roby/log/event_stream.rb +151 -0
  112. data/lib/roby/log/file.rb +340 -0
  113. data/lib/roby/log/gui/basic_display.ui +83 -0
  114. data/lib/roby/log/gui/chronicle.rb +26 -0
  115. data/lib/roby/log/gui/chronicle_view.rb +40 -0
  116. data/lib/roby/log/gui/chronicle_view.ui +70 -0
  117. data/lib/roby/log/gui/data_displays.rb +172 -0
  118. data/lib/roby/log/gui/data_displays.ui +155 -0
  119. data/lib/roby/log/gui/notifications.rb +26 -0
  120. data/lib/roby/log/gui/relations.rb +248 -0
  121. data/lib/roby/log/gui/relations.ui +123 -0
  122. data/lib/roby/log/gui/relations_view.rb +185 -0
  123. data/lib/roby/log/gui/relations_view.ui +149 -0
  124. data/lib/roby/log/gui/replay.rb +327 -0
  125. data/lib/roby/log/gui/replay_controls.rb +200 -0
  126. data/lib/roby/log/gui/replay_controls.ui +259 -0
  127. data/lib/roby/log/gui/runtime.rb +130 -0
  128. data/lib/roby/log/hooks.rb +185 -0
  129. data/lib/roby/log/logger.rb +202 -0
  130. data/lib/roby/log/notifications.rb +244 -0
  131. data/lib/roby/log/plan_rebuilder.rb +470 -0
  132. data/lib/roby/log/relations.rb +1056 -0
  133. data/lib/roby/log/server.rb +550 -0
  134. data/lib/roby/log/sqlite.rb +47 -0
  135. data/lib/roby/log/timings.rb +164 -0
  136. data/lib/roby/plan-object.rb +247 -0
  137. data/lib/roby/plan.rb +762 -0
  138. data/lib/roby/planning.rb +13 -0
  139. data/lib/roby/planning/loops.rb +302 -0
  140. data/lib/roby/planning/model.rb +906 -0
  141. data/lib/roby/planning/task.rb +151 -0
  142. data/lib/roby/propagation.rb +562 -0
  143. data/lib/roby/query.rb +619 -0
  144. data/lib/roby/relations.rb +583 -0
  145. data/lib/roby/relations/conflicts.rb +70 -0
  146. data/lib/roby/relations/ensured.rb +20 -0
  147. data/lib/roby/relations/error_handling.rb +23 -0
  148. data/lib/roby/relations/events.rb +9 -0
  149. data/lib/roby/relations/executed_by.rb +193 -0
  150. data/lib/roby/relations/hierarchy.rb +239 -0
  151. data/lib/roby/relations/influence.rb +10 -0
  152. data/lib/roby/relations/planned_by.rb +63 -0
  153. data/lib/roby/robot.rb +7 -0
  154. data/lib/roby/standard_errors.rb +218 -0
  155. data/lib/roby/state.rb +5 -0
  156. data/lib/roby/state/events.rb +221 -0
  157. data/lib/roby/state/information.rb +55 -0
  158. data/lib/roby/state/pos.rb +110 -0
  159. data/lib/roby/state/shapes.rb +32 -0
  160. data/lib/roby/state/state.rb +353 -0
  161. data/lib/roby/support.rb +92 -0
  162. data/lib/roby/task-operations.rb +182 -0
  163. data/lib/roby/task.rb +1618 -0
  164. data/lib/roby/test/common.rb +399 -0
  165. data/lib/roby/test/distributed.rb +214 -0
  166. data/lib/roby/test/tasks/empty_task.rb +9 -0
  167. data/lib/roby/test/tasks/goto.rb +36 -0
  168. data/lib/roby/test/tasks/simple_task.rb +23 -0
  169. data/lib/roby/test/testcase.rb +519 -0
  170. data/lib/roby/test/tools.rb +160 -0
  171. data/lib/roby/thread_task.rb +87 -0
  172. data/lib/roby/transactions.rb +462 -0
  173. data/lib/roby/transactions/proxy.rb +292 -0
  174. data/lib/roby/transactions/updates.rb +139 -0
  175. data/plugins/fault_injection/History.txt +4 -0
  176. data/plugins/fault_injection/README.txt +37 -0
  177. data/plugins/fault_injection/Rakefile +18 -0
  178. data/plugins/fault_injection/TODO.txt +0 -0
  179. data/plugins/fault_injection/app.rb +52 -0
  180. data/plugins/fault_injection/fault_injection.rb +89 -0
  181. data/plugins/fault_injection/test/test_fault_injection.rb +84 -0
  182. data/plugins/subsystems/README.txt +40 -0
  183. data/plugins/subsystems/Rakefile +18 -0
  184. data/plugins/subsystems/app.rb +171 -0
  185. data/plugins/subsystems/test/app/README +24 -0
  186. data/plugins/subsystems/test/app/Rakefile +8 -0
  187. data/plugins/subsystems/test/app/config/app.yml +71 -0
  188. data/plugins/subsystems/test/app/config/init.rb +9 -0
  189. data/plugins/subsystems/test/app/config/roby.yml +3 -0
  190. data/plugins/subsystems/test/app/planners/main.rb +20 -0
  191. data/plugins/subsystems/test/app/scripts/distributed +3 -0
  192. data/plugins/subsystems/test/app/scripts/replay +3 -0
  193. data/plugins/subsystems/test/app/scripts/results +3 -0
  194. data/plugins/subsystems/test/app/scripts/run +3 -0
  195. data/plugins/subsystems/test/app/scripts/server +3 -0
  196. data/plugins/subsystems/test/app/scripts/shell +3 -0
  197. data/plugins/subsystems/test/app/scripts/test +3 -0
  198. data/plugins/subsystems/test/app/tasks/services.rb +15 -0
  199. data/plugins/subsystems/test/test_subsystems.rb +71 -0
  200. data/test/distributed/test_communication.rb +178 -0
  201. data/test/distributed/test_connection.rb +282 -0
  202. data/test/distributed/test_execution.rb +373 -0
  203. data/test/distributed/test_mixed_plan.rb +341 -0
  204. data/test/distributed/test_plan_notifications.rb +238 -0
  205. data/test/distributed/test_protocol.rb +516 -0
  206. data/test/distributed/test_query.rb +102 -0
  207. data/test/distributed/test_remote_plan.rb +491 -0
  208. data/test/distributed/test_transaction.rb +463 -0
  209. data/test/mockups/tasks.rb +27 -0
  210. data/test/planning/test_loops.rb +380 -0
  211. data/test/planning/test_model.rb +427 -0
  212. data/test/planning/test_task.rb +106 -0
  213. data/test/relations/test_conflicts.rb +42 -0
  214. data/test/relations/test_ensured.rb +38 -0
  215. data/test/relations/test_executed_by.rb +149 -0
  216. data/test/relations/test_hierarchy.rb +158 -0
  217. data/test/relations/test_planned_by.rb +54 -0
  218. data/test/suite_core.rb +24 -0
  219. data/test/suite_distributed.rb +9 -0
  220. data/test/suite_planning.rb +3 -0
  221. data/test/suite_relations.rb +8 -0
  222. data/test/test_bgl.rb +508 -0
  223. data/test/test_control.rb +399 -0
  224. data/test/test_event.rb +894 -0
  225. data/test/test_exceptions.rb +592 -0
  226. data/test/test_interface.rb +37 -0
  227. data/test/test_log.rb +114 -0
  228. data/test/test_log_server.rb +132 -0
  229. data/test/test_plan.rb +584 -0
  230. data/test/test_propagation.rb +210 -0
  231. data/test/test_query.rb +266 -0
  232. data/test/test_relations.rb +180 -0
  233. data/test/test_state.rb +414 -0
  234. data/test/test_support.rb +16 -0
  235. data/test/test_task.rb +938 -0
  236. data/test/test_testcase.rb +122 -0
  237. data/test/test_thread_task.rb +73 -0
  238. data/test/test_transactions.rb +569 -0
  239. data/test/test_transactions_proxy.rb +198 -0
  240. 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