roby 0.8.0 → 3.0.0

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 (644) hide show
  1. checksums.yaml +7 -0
  2. data/.deep-cover.rb +3 -0
  3. data/.gitattributes +1 -0
  4. data/.gitignore +24 -0
  5. data/.simplecov +10 -0
  6. data/.travis.yml +17 -0
  7. data/.yardopts +4 -0
  8. data/Gemfile +15 -0
  9. data/README.md +11 -0
  10. data/Rakefile +47 -177
  11. data/benchmark/{alloc_misc.rb → attic/alloc_misc.rb} +2 -2
  12. data/benchmark/{discovery_latency.rb → attic/discovery_latency.rb} +19 -19
  13. data/benchmark/{garbage_collection.rb → attic/garbage_collection.rb} +9 -9
  14. data/benchmark/{genom.rb → attic/genom.rb} +0 -0
  15. data/benchmark/attic/transactions.rb +62 -0
  16. data/benchmark/plan_basic_operations.rb +28 -0
  17. data/benchmark/relations/graph.rb +63 -0
  18. data/benchmark/ruby/identity.rb +18 -0
  19. data/benchmark/ruby/set_intersect_vs_hash_merge.rb +39 -0
  20. data/benchmark/ruby/yield_vs_block.rb +35 -0
  21. data/benchmark/run +5 -0
  22. data/benchmark/synthetic_plan_modifications_with_transactions.rb +79 -0
  23. data/benchmark/transactions.rb +99 -51
  24. data/bin/roby +38 -197
  25. data/bin/roby-display +14 -0
  26. data/bin/roby-log +3 -176
  27. data/doc/guide/{src → attic}/abstraction/achieve_with.page +1 -1
  28. data/doc/guide/{src → attic}/abstraction/forwarding.page +1 -1
  29. data/doc/guide/{src → attic}/abstraction/hierarchy.page +1 -1
  30. data/doc/guide/{src → attic}/abstraction/index.page +1 -1
  31. data/doc/guide/{src → attic}/abstraction/task_models.page +1 -1
  32. data/doc/guide/{overview.rdoc → attic/cycle/api_overview.rdoc} +6 -1
  33. data/doc/guide/{src → attic}/cycle/cycle-overview.png +0 -0
  34. data/doc/guide/{src → attic}/cycle/cycle-overview.svg +0 -0
  35. data/doc/guide/attic/cycle/error_handling.page +98 -0
  36. data/doc/guide/{src → attic}/cycle/error_instantaneous_repair.png +0 -0
  37. data/doc/guide/{src → attic}/cycle/error_instantaneous_repair.svg +0 -0
  38. data/doc/guide/{src/cycle/error_handling.page → attic/cycle/error_sources.page} +46 -89
  39. data/doc/guide/{src → attic}/cycle/garbage_collection.page +1 -1
  40. data/doc/guide/{src → attic}/cycle/index.page +1 -1
  41. data/doc/guide/{src → attic}/cycle/propagation.page +11 -1
  42. data/doc/guide/{src → attic}/cycle/propagation_diamond.png +0 -0
  43. data/doc/guide/{src → attic}/cycle/propagation_diamond.svg +0 -0
  44. data/doc/guide/attic/plans/building_plans.page +89 -0
  45. data/doc/guide/attic/plans/code.page +192 -0
  46. data/doc/guide/{src/basics → attic/plans}/events.page +3 -4
  47. data/doc/guide/attic/plans/index.page +7 -0
  48. data/doc/guide/{plan_modifications.rdoc → attic/plans/plan_modifications.rdoc} +5 -3
  49. data/doc/guide/{src/basics → attic/plans}/plan_objects.page +2 -1
  50. data/doc/guide/attic/plans/querying_plans.page +5 -0
  51. data/doc/guide/{src/basics → attic/plans}/tasks.page +20 -20
  52. data/doc/guide/config.yaml +7 -4
  53. data/doc/guide/ext/extended_menu.rb +29 -0
  54. data/doc/guide/ext/init.rb +6 -0
  55. data/doc/guide/ext/rdoc_links.rb +7 -6
  56. data/doc/guide/src/advanced_concepts/history.page +5 -0
  57. data/doc/guide/src/advanced_concepts/index.page +11 -0
  58. data/doc/guide/src/advanced_concepts/recognizing_patterns.page +83 -0
  59. data/doc/guide/src/advanced_concepts/scheduling.page +87 -0
  60. data/doc/guide/src/advanced_concepts/transactions.page +5 -0
  61. data/doc/guide/src/advanced_concepts/unreachability.page +42 -0
  62. data/doc/guide/src/base.template +96 -0
  63. data/doc/guide/src/basics_shell_header.txt +5 -7
  64. data/doc/guide/src/building/action_coordination.page +96 -0
  65. data/doc/guide/src/building/actions.page +124 -0
  66. data/doc/guide/src/building/file_layout.page +71 -0
  67. data/doc/guide/src/building/index.page +50 -0
  68. data/doc/guide/src/building/patterns.page +86 -0
  69. data/doc/guide/src/building/patterns_forwarding.png +0 -0
  70. data/doc/guide/src/building/patterns_forwarding.svg +277 -0
  71. data/doc/guide/src/building/runtime.page +95 -0
  72. data/doc/guide/src/building/task_models.page +94 -0
  73. data/doc/guide/src/building/tasks.page +284 -0
  74. data/doc/guide/src/concepts/error_handling.page +100 -0
  75. data/doc/guide/src/concepts/exception_propagation.png +0 -0
  76. data/doc/guide/src/concepts/exception_propagation.svg +445 -0
  77. data/doc/guide/src/concepts/execution.page +85 -0
  78. data/doc/guide/src/concepts/execution.png +0 -0
  79. data/doc/guide/src/concepts/execution.svg +573 -0
  80. data/doc/guide/src/concepts/execution_cycle.png +0 -0
  81. data/doc/guide/src/concepts/garbage_collection.page +57 -0
  82. data/doc/guide/src/concepts/index.page +27 -0
  83. data/doc/guide/src/concepts/plans.page +101 -0
  84. data/doc/guide/src/concepts/policy.page +31 -0
  85. data/doc/guide/src/concepts/reactor.page +61 -0
  86. data/doc/guide/src/concepts/simple_plan_example.png +0 -0
  87. data/doc/guide/src/concepts/simple_plan_example.svg +376 -0
  88. data/doc/guide/src/default.template +9 -74
  89. data/doc/guide/src/event_relations/forward.page +71 -0
  90. data/doc/guide/src/event_relations/index.page +12 -0
  91. data/doc/guide/src/event_relations/scheduling_constraints.page +43 -0
  92. data/doc/guide/src/event_relations/signal.page +55 -0
  93. data/doc/guide/src/event_relations/temporal_constraints.page +77 -0
  94. data/doc/guide/src/htmldoc.metainfo +21 -8
  95. data/doc/guide/src/index.page +8 -3
  96. data/doc/guide/src/{introduction/install.page → installation/index.page} +37 -25
  97. data/doc/guide/src/installation/publications.page +14 -0
  98. data/doc/guide/src/{introduction → installation}/videos.page +14 -7
  99. data/doc/guide/src/interacting/index.page +16 -0
  100. data/doc/guide/src/interacting/run.page +33 -0
  101. data/doc/guide/src/interacting/shell.page +95 -0
  102. data/doc/guide/src/plugins/creating_plugins.page +72 -0
  103. data/doc/guide/src/plugins/index.page +27 -5
  104. data/doc/guide/src/plugins/{fault_tolerance.page → standard_plugins/fault_tolerance.page} +2 -2
  105. data/doc/guide/src/plugins/standard_plugins/index.page +11 -0
  106. data/doc/guide/src/plugins/{subsystems.page → standard_plugins/subsystems.page} +2 -2
  107. data/doc/guide/src/style_screen.css +687 -0
  108. data/doc/guide/src/task_relations/dependency.page +107 -0
  109. data/doc/guide/src/task_relations/executed_by.page +77 -0
  110. data/doc/guide/src/task_relations/index.page +12 -0
  111. data/doc/guide/src/task_relations/new_relations.page +119 -0
  112. data/doc/guide/src/task_relations/planned_by.page +46 -0
  113. data/doc/guide/src/tutorial/app.page +117 -0
  114. data/doc/guide/src/{basics → tutorial}/code_examples.page +6 -5
  115. data/doc/guide/src/{basics → tutorial}/dry.page +15 -15
  116. data/doc/guide/src/{basics → tutorial}/errors.page +43 -68
  117. data/doc/guide/src/tutorial/events.page +195 -0
  118. data/doc/guide/src/{basics → tutorial}/hierarchy.page +53 -52
  119. data/doc/guide/src/tutorial/index.page +13 -0
  120. data/doc/guide/src/tutorial/log_replay/goForward_1.png +0 -0
  121. data/doc/guide/src/tutorial/log_replay/goForward_2.png +0 -0
  122. data/doc/guide/src/tutorial/log_replay/goForward_3.png +0 -0
  123. data/doc/guide/src/{basics → tutorial}/log_replay/goForward_4.png +0 -0
  124. data/doc/guide/src/tutorial/log_replay/goForward_5.png +0 -0
  125. data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_1.png +0 -0
  126. data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_2.png +0 -0
  127. data/doc/guide/src/{basics → tutorial}/log_replay/hierarchy_error_3.png +0 -0
  128. data/doc/guide/src/tutorial/log_replay/moveto_code_error.png +0 -0
  129. data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_1.png +0 -0
  130. data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_2.png +0 -0
  131. data/doc/guide/src/{basics → tutorial}/log_replay/plan_repair_3.png +0 -0
  132. data/doc/guide/src/tutorial/log_replay/plan_repair_4.png +0 -0
  133. data/doc/guide/src/tutorial/log_replay/roby_log_main_window.png +0 -0
  134. data/doc/guide/src/{basics → tutorial}/log_replay/roby_log_relation_window.png +0 -0
  135. data/doc/guide/src/{basics → tutorial}/log_replay/roby_replay_event_representation.png +0 -0
  136. data/doc/guide/src/tutorial/relations_display.page +153 -0
  137. data/doc/guide/src/{basics → tutorial}/roby_cycle_overview.png +0 -0
  138. data/doc/guide/src/tutorial/shell.page +121 -0
  139. data/doc/guide/src/{basics → tutorial}/summary.page +1 -1
  140. data/doc/guide/src/tutorial/tasks.page +374 -0
  141. data/lib/roby.rb +102 -47
  142. data/lib/roby/actions.rb +17 -0
  143. data/lib/roby/actions/action.rb +80 -0
  144. data/lib/roby/actions/interface.rb +45 -0
  145. data/lib/roby/actions/library.rb +23 -0
  146. data/lib/roby/actions/models/action.rb +224 -0
  147. data/lib/roby/actions/models/coordination_action.rb +58 -0
  148. data/lib/roby/actions/models/interface.rb +22 -0
  149. data/lib/roby/actions/models/interface_base.rb +294 -0
  150. data/lib/roby/actions/models/library.rb +12 -0
  151. data/lib/roby/actions/models/method_action.rb +90 -0
  152. data/lib/roby/actions/task.rb +114 -0
  153. data/lib/roby/and_generator.rb +125 -0
  154. data/lib/roby/app.rb +2795 -829
  155. data/lib/roby/app/autotest_console_reporter.rb +138 -0
  156. data/lib/roby/app/base.rb +21 -0
  157. data/lib/roby/app/cucumber.rb +2 -0
  158. data/lib/roby/app/cucumber/controller.rb +439 -0
  159. data/lib/roby/app/cucumber/helpers.rb +280 -0
  160. data/lib/roby/app/cucumber/world.rb +32 -0
  161. data/lib/roby/app/debug.rb +136 -0
  162. data/lib/roby/app/gen.rb +2 -0
  163. data/lib/roby/app/rake.rb +178 -38
  164. data/lib/roby/app/robot_config.rb +9 -0
  165. data/lib/roby/app/robot_names.rb +115 -0
  166. data/lib/roby/app/run.rb +3 -2
  167. data/lib/roby/app/scripts.rb +72 -0
  168. data/lib/roby/app/scripts/autotest.rb +173 -0
  169. data/lib/roby/app/scripts/display.rb +2 -0
  170. data/lib/roby/app/scripts/restart.rb +52 -0
  171. data/lib/roby/app/scripts/results.rb +17 -8
  172. data/lib/roby/app/scripts/run.rb +155 -24
  173. data/lib/roby/app/scripts/shell.rb +147 -62
  174. data/lib/roby/app/scripts/test.rb +107 -22
  175. data/lib/roby/app/test_reporter.rb +74 -0
  176. data/lib/roby/app/test_server.rb +159 -0
  177. data/lib/roby/app/vagrant.rb +47 -0
  178. data/lib/roby/backports.rb +16 -0
  179. data/lib/roby/cli/display.rb +190 -0
  180. data/lib/roby/cli/exceptions.rb +17 -0
  181. data/lib/roby/cli/gen/actions/class.rb +5 -0
  182. data/lib/roby/cli/gen/actions/test.rb +6 -0
  183. data/lib/roby/cli/gen/app/.yardopts +6 -0
  184. data/lib/roby/cli/gen/app/README.md +28 -0
  185. data/lib/roby/cli/gen/app/Rakefile +15 -0
  186. data/{app → lib/roby/cli/gen/app}/config/app.yml +29 -39
  187. data/lib/roby/cli/gen/app/models/.gitattributes +1 -0
  188. data/{app → lib/roby/cli/gen/app/scripts}/controllers/.gitattributes +0 -0
  189. data/{app/data/.gitattributes → lib/roby/cli/gen/app/test/.gitignore} +0 -0
  190. data/lib/roby/cli/gen/class/class.rb +6 -0
  191. data/lib/roby/cli/gen/class/test.rb +7 -0
  192. data/lib/roby/cli/gen/helpers.rb +203 -0
  193. data/lib/roby/cli/gen/module/module.rb +5 -0
  194. data/lib/roby/cli/gen/module/test.rb +6 -0
  195. data/lib/roby/cli/gen/roby_app/config/init.rb +17 -0
  196. data/lib/roby/cli/gen/roby_app/config/robots/robot.rb +40 -0
  197. data/lib/roby/cli/gen/task/class.rb +44 -0
  198. data/lib/roby/cli/gen/task/test.rb +6 -0
  199. data/lib/roby/cli/gen_main.rb +120 -0
  200. data/lib/roby/cli/log.rb +276 -0
  201. data/lib/roby/cli/log/flamegraph.html +499 -0
  202. data/lib/roby/cli/log/flamegraph_renderer.rb +88 -0
  203. data/lib/roby/cli/main.rb +153 -0
  204. data/lib/roby/coordination.rb +60 -0
  205. data/lib/roby/coordination/action_script.rb +25 -0
  206. data/lib/roby/coordination/action_state_machine.rb +125 -0
  207. data/lib/roby/coordination/actions.rb +106 -0
  208. data/lib/roby/coordination/base.rb +145 -0
  209. data/lib/roby/coordination/calculus.rb +40 -0
  210. data/lib/roby/coordination/child.rb +28 -0
  211. data/lib/roby/coordination/event.rb +29 -0
  212. data/lib/roby/coordination/fault_handler.rb +25 -0
  213. data/lib/roby/coordination/fault_handling_task.rb +13 -0
  214. data/lib/roby/coordination/fault_response_table.rb +110 -0
  215. data/lib/roby/coordination/models/action_script.rb +64 -0
  216. data/lib/roby/coordination/models/action_state_machine.rb +224 -0
  217. data/lib/roby/coordination/models/actions.rb +191 -0
  218. data/lib/roby/coordination/models/arguments.rb +55 -0
  219. data/lib/roby/coordination/models/base.rb +176 -0
  220. data/lib/roby/coordination/models/capture.rb +86 -0
  221. data/lib/roby/coordination/models/child.rb +35 -0
  222. data/lib/roby/coordination/models/event.rb +41 -0
  223. data/lib/roby/coordination/models/exceptions.rb +42 -0
  224. data/lib/roby/coordination/models/fault_handler.rb +219 -0
  225. data/lib/roby/coordination/models/fault_response_table.rb +77 -0
  226. data/lib/roby/coordination/models/root.rb +22 -0
  227. data/lib/roby/coordination/models/script.rb +283 -0
  228. data/lib/roby/coordination/models/task.rb +184 -0
  229. data/lib/roby/coordination/models/task_from_action.rb +50 -0
  230. data/lib/roby/coordination/models/task_from_as_plan.rb +33 -0
  231. data/lib/roby/coordination/models/task_from_instanciation_object.rb +31 -0
  232. data/lib/roby/coordination/models/task_from_variable.rb +27 -0
  233. data/lib/roby/coordination/models/task_with_dependencies.rb +48 -0
  234. data/lib/roby/coordination/models/variable.rb +32 -0
  235. data/lib/roby/coordination/script.rb +200 -0
  236. data/lib/roby/coordination/script_instruction.rb +12 -0
  237. data/lib/roby/coordination/task.rb +45 -0
  238. data/lib/roby/coordination/task_base.rb +69 -0
  239. data/lib/roby/coordination/task_script.rb +293 -0
  240. data/lib/roby/coordination/task_state_machine.rb +308 -0
  241. data/lib/roby/decision_control.rb +33 -21
  242. data/lib/roby/distributed_object.rb +76 -0
  243. data/lib/roby/droby.rb +17 -0
  244. data/lib/roby/droby/droby_id.rb +6 -0
  245. data/lib/roby/droby/enable.rb +153 -0
  246. data/lib/roby/droby/event_logger.rb +189 -0
  247. data/lib/roby/droby/event_logging.rb +57 -0
  248. data/lib/roby/droby/exceptions.rb +14 -0
  249. data/lib/roby/droby/identifiable.rb +22 -0
  250. data/lib/roby/droby/logfile.rb +141 -0
  251. data/lib/roby/droby/logfile/client.rb +176 -0
  252. data/lib/roby/droby/logfile/file_format.md +97 -0
  253. data/lib/roby/droby/logfile/index.rb +117 -0
  254. data/lib/roby/droby/logfile/reader.rb +139 -0
  255. data/lib/roby/droby/logfile/server.rb +199 -0
  256. data/lib/roby/droby/logfile/writer.rb +114 -0
  257. data/lib/roby/droby/marshal.rb +264 -0
  258. data/lib/roby/droby/marshallable.rb +12 -0
  259. data/lib/roby/droby/null_event_logger.rb +25 -0
  260. data/lib/roby/droby/object_manager.rb +205 -0
  261. data/lib/roby/droby/peer_id.rb +6 -0
  262. data/lib/roby/droby/plan_rebuilder.rb +373 -0
  263. data/lib/roby/droby/rebuilt_plan.rb +160 -0
  264. data/lib/roby/droby/remote_droby_id.rb +6 -0
  265. data/lib/roby/droby/timepoints.rb +205 -0
  266. data/lib/roby/droby/timepoints_ctf.metadata.erb +101 -0
  267. data/lib/roby/droby/timepoints_ctf.rb +125 -0
  268. data/lib/roby/droby/v5.rb +14 -0
  269. data/lib/roby/droby/v5/builtin.rb +120 -0
  270. data/lib/roby/droby/v5/droby_class.rb +45 -0
  271. data/lib/roby/droby/v5/droby_constant.rb +81 -0
  272. data/lib/roby/droby/v5/droby_dump.rb +1026 -0
  273. data/lib/roby/droby/v5/droby_id.rb +44 -0
  274. data/lib/roby/droby/v5/droby_model.rb +82 -0
  275. data/lib/roby/droby/v5/peer_id.rb +10 -0
  276. data/lib/roby/droby/v5/remote_droby_id.rb +42 -0
  277. data/lib/roby/event.rb +79 -957
  278. data/lib/roby/event_constraints.rb +835 -0
  279. data/lib/roby/event_generator.rb +1047 -0
  280. data/lib/roby/event_structure/causal_link.rb +6 -0
  281. data/lib/roby/event_structure/forwarding.rb +6 -0
  282. data/lib/roby/event_structure/precedence.rb +7 -0
  283. data/lib/roby/event_structure/signal.rb +8 -0
  284. data/lib/roby/event_structure/temporal_constraints.rb +640 -0
  285. data/lib/roby/exceptions.rb +446 -152
  286. data/lib/roby/executable_plan.rb +549 -0
  287. data/lib/roby/execution_engine.rb +1997 -950
  288. data/lib/roby/filter_generator.rb +26 -0
  289. data/lib/roby/gui/chronicle_view.rb +225 -0
  290. data/lib/roby/gui/chronicle_widget.rb +925 -0
  291. data/lib/roby/gui/dot_id.rb +11 -0
  292. data/lib/roby/gui/exception_view.rb +44 -0
  293. data/lib/roby/gui/log_display.rb +273 -0
  294. data/lib/roby/gui/model_views.rb +2 -0
  295. data/lib/roby/gui/model_views/action_interface.rb +53 -0
  296. data/lib/roby/gui/model_views/task.rb +47 -0
  297. data/lib/roby/gui/model_views/task.rhtml +41 -0
  298. data/lib/roby/gui/object_info_view.rb +89 -0
  299. data/lib/roby/gui/plan_dot_layout.rb +427 -0
  300. data/lib/roby/gui/plan_rebuilder_widget.rb +357 -0
  301. data/lib/roby/gui/qt4_toMSecsSinceEpoch.rb +8 -0
  302. data/lib/roby/gui/relations_view.rb +278 -0
  303. data/lib/roby/gui/relations_view/relations.ui +139 -0
  304. data/lib/roby/gui/relations_view/relations_canvas.rb +1088 -0
  305. data/lib/roby/gui/relations_view/relations_config.rb +292 -0
  306. data/lib/roby/gui/relations_view/relations_view.ui +53 -0
  307. data/lib/roby/gui/scheduler_view.css +24 -0
  308. data/lib/roby/gui/scheduler_view.rb +46 -0
  309. data/lib/roby/gui/scheduler_view.rhtml +53 -0
  310. data/lib/roby/gui/stepping.rb +93 -0
  311. data/lib/roby/gui/stepping.ui +181 -0
  312. data/lib/roby/gui/styles.rb +81 -0
  313. data/lib/roby/gui/task_display_configuration.rb +42 -0
  314. data/lib/roby/gui/task_state_at.rb +38 -0
  315. data/lib/roby/hooks.rb +26 -0
  316. data/lib/roby/interface.rb +136 -469
  317. data/lib/roby/interface/async.rb +20 -0
  318. data/lib/roby/interface/async/action_monitor.rb +188 -0
  319. data/lib/roby/interface/async/interface.rb +498 -0
  320. data/lib/roby/interface/async/job_monitor.rb +213 -0
  321. data/lib/roby/interface/async/log.rb +238 -0
  322. data/lib/roby/interface/async/new_job_listener.rb +79 -0
  323. data/lib/roby/interface/async/ui_connector.rb +183 -0
  324. data/lib/roby/interface/client.rb +553 -0
  325. data/lib/roby/interface/command.rb +24 -0
  326. data/lib/roby/interface/command_argument.rb +16 -0
  327. data/lib/roby/interface/command_library.rb +92 -0
  328. data/lib/roby/interface/droby_channel.rb +174 -0
  329. data/lib/roby/interface/exceptions.rb +22 -0
  330. data/lib/roby/interface/interface.rb +655 -0
  331. data/lib/roby/interface/job.rb +47 -0
  332. data/lib/roby/interface/rest.rb +10 -0
  333. data/lib/roby/interface/rest/api.rb +29 -0
  334. data/lib/roby/interface/rest/helpers.rb +24 -0
  335. data/lib/roby/interface/rest/server.rb +212 -0
  336. data/lib/roby/interface/server.rb +154 -0
  337. data/lib/roby/interface/shell_client.rb +468 -0
  338. data/lib/roby/interface/shell_subcommand.rb +24 -0
  339. data/lib/roby/interface/subcommand_client.rb +35 -0
  340. data/lib/roby/interface/tcp.rb +168 -0
  341. data/lib/roby/models/arguments.rb +112 -0
  342. data/lib/roby/models/plan_object.rb +83 -0
  343. data/lib/roby/models/task.rb +835 -0
  344. data/lib/roby/models/task_event.rb +62 -0
  345. data/lib/roby/models/task_service.rb +78 -0
  346. data/lib/roby/or_generator.rb +88 -0
  347. data/lib/roby/plan.rb +1751 -864
  348. data/lib/roby/plan_object.rb +611 -0
  349. data/lib/roby/plan_service.rb +200 -0
  350. data/lib/roby/promise.rb +332 -0
  351. data/lib/roby/queries.rb +23 -0
  352. data/lib/roby/queries/and_matcher.rb +32 -0
  353. data/lib/roby/queries/any.rb +27 -0
  354. data/lib/roby/queries/code_error_matcher.rb +58 -0
  355. data/lib/roby/queries/event_generator_matcher.rb +9 -0
  356. data/lib/roby/queries/execution_exception_matcher.rb +165 -0
  357. data/lib/roby/queries/index.rb +165 -0
  358. data/lib/roby/queries/localized_error_matcher.rb +149 -0
  359. data/lib/roby/queries/matcher_base.rb +107 -0
  360. data/lib/roby/queries/none.rb +27 -0
  361. data/lib/roby/queries/not_matcher.rb +30 -0
  362. data/lib/roby/queries/op_matcher.rb +8 -0
  363. data/lib/roby/queries/or_matcher.rb +30 -0
  364. data/lib/roby/queries/plan_object_matcher.rb +363 -0
  365. data/lib/roby/queries/query.rb +188 -0
  366. data/lib/roby/queries/task_event_generator_matcher.rb +86 -0
  367. data/lib/roby/queries/task_matcher.rb +344 -0
  368. data/lib/roby/relations.rb +42 -678
  369. data/lib/roby/relations/bidirectional_directed_adjacency_graph.rb +492 -0
  370. data/lib/roby/relations/directed_relation_support.rb +268 -0
  371. data/lib/roby/relations/event_relation_graph.rb +19 -0
  372. data/lib/roby/relations/fork_merge_visitor.rb +154 -0
  373. data/lib/roby/relations/graph.rb +533 -0
  374. data/lib/roby/relations/models/directed_relation_support.rb +11 -0
  375. data/lib/roby/relations/models/graph.rb +75 -0
  376. data/lib/roby/relations/models/task_relation_graph.rb +18 -0
  377. data/lib/roby/relations/space.rb +380 -0
  378. data/lib/roby/relations/task_relation_graph.rb +20 -0
  379. data/lib/roby/robot.rb +85 -38
  380. data/lib/roby/schedulers/basic.rb +155 -25
  381. data/lib/roby/schedulers/null.rb +20 -0
  382. data/lib/roby/schedulers/reporting.rb +31 -0
  383. data/lib/roby/schedulers/state.rb +129 -0
  384. data/lib/roby/schedulers/temporal.rb +91 -0
  385. data/lib/roby/singletons.rb +87 -0
  386. data/lib/roby/standalone.rb +4 -2
  387. data/lib/roby/standard_errors.rb +405 -82
  388. data/lib/roby/state.rb +6 -3
  389. data/lib/roby/state/conf_model.rb +5 -0
  390. data/lib/roby/state/events.rb +181 -95
  391. data/lib/roby/state/goal_model.rb +77 -0
  392. data/lib/roby/state/open_struct.rb +591 -0
  393. data/lib/roby/state/open_struct_model.rb +68 -0
  394. data/lib/roby/state/pos.rb +45 -45
  395. data/lib/roby/state/shapes.rb +11 -11
  396. data/lib/roby/state/state_model.rb +303 -0
  397. data/lib/roby/state/task.rb +43 -0
  398. data/lib/roby/support.rb +88 -148
  399. data/lib/roby/task.rb +1361 -1750
  400. data/lib/roby/task_arguments.rb +428 -0
  401. data/lib/roby/task_event.rb +127 -0
  402. data/lib/roby/task_event_generator.rb +337 -0
  403. data/lib/roby/task_service.rb +6 -0
  404. data/lib/roby/task_structure/conflicts.rb +104 -0
  405. data/lib/roby/task_structure/dependency.rb +932 -0
  406. data/lib/roby/task_structure/error_handling.rb +118 -0
  407. data/lib/roby/task_structure/executed_by.rb +234 -0
  408. data/lib/roby/task_structure/planned_by.rb +90 -0
  409. data/lib/roby/tasks/aggregator.rb +37 -0
  410. data/lib/roby/tasks/external_process.rb +275 -0
  411. data/lib/roby/tasks/group.rb +27 -0
  412. data/lib/roby/tasks/null.rb +19 -0
  413. data/lib/roby/tasks/parallel.rb +43 -0
  414. data/lib/roby/tasks/sequence.rb +88 -0
  415. data/lib/roby/tasks/simple.rb +21 -0
  416. data/lib/roby/{thread_task.rb → tasks/thread.rb} +50 -24
  417. data/lib/roby/tasks/timeout.rb +17 -0
  418. data/lib/roby/tasks/virtual.rb +55 -0
  419. data/lib/roby/template_plan.rb +7 -0
  420. data/lib/roby/test/aruba_minitest.rb +74 -0
  421. data/lib/roby/test/assertion.rb +16 -0
  422. data/lib/roby/test/assertions.rb +490 -0
  423. data/lib/roby/test/common.rb +368 -591
  424. data/lib/roby/test/dsl.rb +149 -0
  425. data/lib/roby/test/error.rb +18 -0
  426. data/lib/roby/test/event_reporter.rb +83 -0
  427. data/lib/roby/test/execution_expectations.rb +1134 -0
  428. data/lib/roby/test/expect_execution.rb +151 -0
  429. data/lib/roby/test/minitest_helpers.rb +166 -0
  430. data/lib/roby/test/roby_app_helpers.rb +200 -0
  431. data/lib/roby/test/run_planners.rb +155 -0
  432. data/lib/roby/test/self.rb +112 -0
  433. data/lib/roby/test/spec.rb +198 -0
  434. data/lib/roby/test/tasks/empty_task.rb +4 -4
  435. data/lib/roby/test/tasks/goto.rb +28 -27
  436. data/lib/roby/test/teardown_plans.rb +100 -0
  437. data/lib/roby/test/testcase.rb +239 -307
  438. data/lib/roby/test/tools.rb +159 -155
  439. data/lib/roby/test/validate_state_machine.rb +75 -0
  440. data/lib/roby/transaction.rb +1125 -0
  441. data/lib/roby/transaction/event_generator_proxy.rb +63 -0
  442. data/lib/roby/transaction/plan_object_proxy.rb +99 -0
  443. data/lib/roby/transaction/plan_service_proxy.rb +43 -0
  444. data/lib/roby/transaction/proxying.rb +120 -0
  445. data/lib/roby/transaction/task_event_generator_proxy.rb +19 -0
  446. data/lib/roby/transaction/task_proxy.rb +135 -0
  447. data/lib/roby/until_generator.rb +30 -0
  448. data/lib/roby/version.rb +5 -0
  449. data/lib/roby/yard.rb +169 -0
  450. data/lib/yard-roby.rb +1 -0
  451. data/manifest.xml +32 -6
  452. data/roby.gemspec +59 -0
  453. metadata +788 -587
  454. data/Manifest.txt +0 -321
  455. data/NOTES +0 -4
  456. data/README.txt +0 -166
  457. data/TODO.txt +0 -146
  458. data/app/README.txt +0 -24
  459. data/app/Rakefile +0 -8
  460. data/app/config/ROBOT.rb +0 -5
  461. data/app/config/init.rb +0 -33
  462. data/app/config/roby.yml +0 -3
  463. data/app/controllers/ROBOT.rb +0 -2
  464. data/app/planners/ROBOT/main.rb +0 -6
  465. data/app/planners/main.rb +0 -5
  466. data/app/scripts/distributed +0 -3
  467. data/app/scripts/generate/bookmarks +0 -3
  468. data/app/scripts/replay +0 -3
  469. data/app/scripts/results +0 -3
  470. data/app/scripts/run +0 -3
  471. data/app/scripts/server +0 -3
  472. data/app/scripts/shell +0 -3
  473. data/app/scripts/test +0 -3
  474. data/app/tasks/.gitattributes +0 -0
  475. data/app/tasks/ROBOT/.gitattributes +0 -0
  476. data/bin/roby-shell +0 -25
  477. data/doc/guide/src/basics/app.page +0 -139
  478. data/doc/guide/src/basics/index.page +0 -11
  479. data/doc/guide/src/basics/log_replay/goForward_1.png +0 -0
  480. data/doc/guide/src/basics/log_replay/goForward_2.png +0 -0
  481. data/doc/guide/src/basics/log_replay/goForward_3.png +0 -0
  482. data/doc/guide/src/basics/log_replay/goForward_5.png +0 -0
  483. data/doc/guide/src/basics/log_replay/plan_repair_4.png +0 -0
  484. data/doc/guide/src/basics/log_replay/roby_log_main_window.png +0 -0
  485. data/doc/guide/src/basics/relations_display.page +0 -203
  486. data/doc/guide/src/basics/shell.page +0 -102
  487. data/doc/guide/src/default.css +0 -319
  488. data/doc/guide/src/introduction/index.page +0 -29
  489. data/doc/guide/src/introduction/publications.page +0 -14
  490. data/doc/guide/src/relations/dependency.page +0 -89
  491. data/doc/guide/src/relations/index.page +0 -12
  492. data/ext/droby/dump.cc +0 -175
  493. data/ext/droby/extconf.rb +0 -3
  494. data/ext/graph/algorithm.cc +0 -746
  495. data/ext/graph/extconf.rb +0 -7
  496. data/ext/graph/graph.cc +0 -575
  497. data/ext/graph/graph.hh +0 -183
  498. data/ext/graph/iterator_sequence.hh +0 -102
  499. data/ext/graph/undirected_dfs.hh +0 -226
  500. data/ext/graph/undirected_graph.hh +0 -421
  501. data/lib/roby/app/scripts/generate/bookmarks.rb +0 -162
  502. data/lib/roby/app/scripts/replay.rb +0 -31
  503. data/lib/roby/app/scripts/server.rb +0 -18
  504. data/lib/roby/basic_object.rb +0 -151
  505. data/lib/roby/config.rb +0 -14
  506. data/lib/roby/distributed.rb +0 -36
  507. data/lib/roby/distributed/base.rb +0 -448
  508. data/lib/roby/distributed/communication.rb +0 -875
  509. data/lib/roby/distributed/connection_space.rb +0 -616
  510. data/lib/roby/distributed/distributed_object.rb +0 -206
  511. data/lib/roby/distributed/drb.rb +0 -62
  512. data/lib/roby/distributed/notifications.rb +0 -531
  513. data/lib/roby/distributed/peer.rb +0 -555
  514. data/lib/roby/distributed/protocol.rb +0 -529
  515. data/lib/roby/distributed/proxy.rb +0 -343
  516. data/lib/roby/distributed/subscription.rb +0 -311
  517. data/lib/roby/distributed/transaction.rb +0 -498
  518. data/lib/roby/external_process_task.rb +0 -225
  519. data/lib/roby/graph.rb +0 -160
  520. data/lib/roby/log.rb +0 -3
  521. data/lib/roby/log/chronicle.rb +0 -303
  522. data/lib/roby/log/console.rb +0 -74
  523. data/lib/roby/log/data_stream.rb +0 -275
  524. data/lib/roby/log/dot.rb +0 -279
  525. data/lib/roby/log/event_stream.rb +0 -161
  526. data/lib/roby/log/file.rb +0 -396
  527. data/lib/roby/log/gui/basic_display.ui +0 -83
  528. data/lib/roby/log/gui/basic_display_ui.rb +0 -89
  529. data/lib/roby/log/gui/chronicle.rb +0 -26
  530. data/lib/roby/log/gui/chronicle_view.rb +0 -40
  531. data/lib/roby/log/gui/chronicle_view.ui +0 -70
  532. data/lib/roby/log/gui/chronicle_view_ui.rb +0 -90
  533. data/lib/roby/log/gui/data_displays.rb +0 -171
  534. data/lib/roby/log/gui/data_displays.ui +0 -155
  535. data/lib/roby/log/gui/data_displays_ui.rb +0 -146
  536. data/lib/roby/log/gui/notifications.rb +0 -26
  537. data/lib/roby/log/gui/relations.rb +0 -269
  538. data/lib/roby/log/gui/relations.ui +0 -123
  539. data/lib/roby/log/gui/relations_ui.rb +0 -120
  540. data/lib/roby/log/gui/relations_view.rb +0 -185
  541. data/lib/roby/log/gui/relations_view.ui +0 -149
  542. data/lib/roby/log/gui/relations_view_ui.rb +0 -144
  543. data/lib/roby/log/gui/replay.rb +0 -366
  544. data/lib/roby/log/gui/replay_controls.rb +0 -206
  545. data/lib/roby/log/gui/replay_controls.ui +0 -282
  546. data/lib/roby/log/gui/replay_controls_ui.rb +0 -249
  547. data/lib/roby/log/gui/runtime.rb +0 -130
  548. data/lib/roby/log/hooks.rb +0 -186
  549. data/lib/roby/log/logger.rb +0 -203
  550. data/lib/roby/log/notifications.rb +0 -244
  551. data/lib/roby/log/plan_rebuilder.rb +0 -468
  552. data/lib/roby/log/relations.rb +0 -1084
  553. data/lib/roby/log/server.rb +0 -547
  554. data/lib/roby/log/sqlite.rb +0 -47
  555. data/lib/roby/log/timings.rb +0 -233
  556. data/lib/roby/plan-object.rb +0 -371
  557. data/lib/roby/planning.rb +0 -13
  558. data/lib/roby/planning/loops.rb +0 -309
  559. data/lib/roby/planning/model.rb +0 -1012
  560. data/lib/roby/planning/task.rb +0 -180
  561. data/lib/roby/query.rb +0 -655
  562. data/lib/roby/relations/conflicts.rb +0 -67
  563. data/lib/roby/relations/dependency.rb +0 -358
  564. data/lib/roby/relations/ensured.rb +0 -19
  565. data/lib/roby/relations/error_handling.rb +0 -22
  566. data/lib/roby/relations/events.rb +0 -7
  567. data/lib/roby/relations/executed_by.rb +0 -208
  568. data/lib/roby/relations/influence.rb +0 -10
  569. data/lib/roby/relations/planned_by.rb +0 -63
  570. data/lib/roby/state/information.rb +0 -55
  571. data/lib/roby/state/state.rb +0 -367
  572. data/lib/roby/task-operations.rb +0 -186
  573. data/lib/roby/task_index.rb +0 -80
  574. data/lib/roby/test/distributed.rb +0 -230
  575. data/lib/roby/test/tasks/simple_task.rb +0 -23
  576. data/lib/roby/transactions.rb +0 -507
  577. data/lib/roby/transactions/proxy.rb +0 -325
  578. data/plugins/fault_injection/History.txt +0 -4
  579. data/plugins/fault_injection/README.txt +0 -34
  580. data/plugins/fault_injection/Rakefile +0 -12
  581. data/plugins/fault_injection/TODO.txt +0 -0
  582. data/plugins/fault_injection/app.rb +0 -52
  583. data/plugins/fault_injection/fault_injection.rb +0 -89
  584. data/plugins/fault_injection/test/test_fault_injection.rb +0 -78
  585. data/plugins/subsystems/README.txt +0 -37
  586. data/plugins/subsystems/Rakefile +0 -13
  587. data/plugins/subsystems/app.rb +0 -182
  588. data/plugins/subsystems/test/app/README +0 -24
  589. data/plugins/subsystems/test/app/Rakefile +0 -8
  590. data/plugins/subsystems/test/app/config/app.yml +0 -71
  591. data/plugins/subsystems/test/app/config/init.rb +0 -12
  592. data/plugins/subsystems/test/app/config/roby.yml +0 -3
  593. data/plugins/subsystems/test/app/planners/main.rb +0 -20
  594. data/plugins/subsystems/test/app/scripts/distributed +0 -3
  595. data/plugins/subsystems/test/app/scripts/replay +0 -3
  596. data/plugins/subsystems/test/app/scripts/results +0 -3
  597. data/plugins/subsystems/test/app/scripts/run +0 -3
  598. data/plugins/subsystems/test/app/scripts/server +0 -3
  599. data/plugins/subsystems/test/app/scripts/shell +0 -3
  600. data/plugins/subsystems/test/app/scripts/test +0 -3
  601. data/plugins/subsystems/test/app/tasks/services.rb +0 -15
  602. data/plugins/subsystems/test/test_subsystems.rb +0 -78
  603. data/test/distributed/test_communication.rb +0 -195
  604. data/test/distributed/test_connection.rb +0 -284
  605. data/test/distributed/test_execution.rb +0 -378
  606. data/test/distributed/test_mixed_plan.rb +0 -341
  607. data/test/distributed/test_plan_notifications.rb +0 -238
  608. data/test/distributed/test_protocol.rb +0 -525
  609. data/test/distributed/test_query.rb +0 -106
  610. data/test/distributed/test_remote_plan.rb +0 -491
  611. data/test/distributed/test_transaction.rb +0 -466
  612. data/test/mockups/external_process +0 -28
  613. data/test/mockups/tasks.rb +0 -27
  614. data/test/planning/test_loops.rb +0 -432
  615. data/test/planning/test_model.rb +0 -427
  616. data/test/planning/test_task.rb +0 -126
  617. data/test/relations/test_conflicts.rb +0 -42
  618. data/test/relations/test_dependency.rb +0 -324
  619. data/test/relations/test_ensured.rb +0 -38
  620. data/test/relations/test_executed_by.rb +0 -224
  621. data/test/relations/test_planned_by.rb +0 -56
  622. data/test/suite_core.rb +0 -29
  623. data/test/suite_distributed.rb +0 -10
  624. data/test/suite_planning.rb +0 -4
  625. data/test/suite_relations.rb +0 -8
  626. data/test/tasks/test_external_process.rb +0 -126
  627. data/test/tasks/test_thread_task.rb +0 -70
  628. data/test/test_bgl.rb +0 -528
  629. data/test/test_event.rb +0 -969
  630. data/test/test_exceptions.rb +0 -591
  631. data/test/test_execution_engine.rb +0 -987
  632. data/test/test_gui.rb +0 -20
  633. data/test/test_interface.rb +0 -43
  634. data/test/test_log.rb +0 -125
  635. data/test/test_log_server.rb +0 -133
  636. data/test/test_plan.rb +0 -418
  637. data/test/test_query.rb +0 -424
  638. data/test/test_relations.rb +0 -260
  639. data/test/test_state.rb +0 -432
  640. data/test/test_support.rb +0 -16
  641. data/test/test_task.rb +0 -1181
  642. data/test/test_testcase.rb +0 -138
  643. data/test/test_transactions.rb +0 -610
  644. data/test/test_transactions_proxy.rb +0 -216
@@ -1,264 +1,425 @@
1
1
  module Roby
2
- # This class contains all code necessary for the propagation steps during
3
- # execution. This includes event and exception propagation. This
4
- # documentation will first present some useful tools provided by execution
5
- # engines, and will continue by an overview of the implementation of the
6
- # execution engine itself.
7
- #
8
- # == Misc tools
9
- #
10
- # === Block execution queueing
11
- # <em>periodic handlers</em> are code blocks called at the beginning of the
12
- # execution cycle, at the given periodicity (of course rounded to a cycle
13
- # length). They are added by #every and removed by
14
- # #remove_periodic_handler.
15
- #
16
- # === Thread synchronization primitives
17
- # Most direct plan modifications and propagation operations are forbidden
18
- # outside the execution engine's thread, to avoid the need for handling
19
- # asynchronicity. Nonetheless, it is possible that a separate thread has to
20
- # execute some of those operations. To simplify that, the following methods
21
- # are available:
22
- # * #execute blocks the calling thread until the given code
23
- # block is executed by the execution engine. Any exception that is raised
24
- # by the code block is raised back into the original thread and will not
25
- # affect the engine thread.
26
- # * #once queues a block to be executed at the beginning of
27
- # the next execution cycle. Exceptions raised in it _will_ affect the
28
- # execution thread and most likely cause its shutdown.
29
- # * #wait_until(ev) blocks the calling thread until +ev+ is emitted. If +ev+
30
- # becomes unreachable, an UnreachableEvent exception is raised in the
31
- # calling thread.
32
- #
33
- # To simplify the controller development, those tools are available directly
34
- # as singleton methods of the Roby module, which forwards them to the
35
- # main execution engine (Roby.engine). One can for instance do
36
- # Roby.once { puts "start of the execution thread" }
37
- #
38
- # Instead of
39
- # Roby.engine.once { ... }
40
- #
41
- # Or
42
- # engine.once { ... }
43
- #
44
- # Nonetheless, note that it breaks the object-orientation of the system and
45
- # therefore won't work in cases where you want multiple execution engine to
46
- # run in parallel.
47
- #
48
- # == Execution cycle
49
- #
50
- # link:../../images/roby_cycle_overview.png
51
- #
52
- # === Event propagation
53
- # Event propagation is based on three main event relations:
54
- #
55
- # * Signal describes the commands that must be called when an event occurs. The
56
- # signalled event command is called when the signalling events are emitted. If
57
- # more than one event are signalling the same event in the same execution
58
- # cycle, the command will be called only once
59
- # * Forwarding describes the events that must be emitted whenever a source
60
- # event is. It is to be used as a way to define event aliases (for instance
61
- # 'stop' is an alias for 'success'), because a task is stopped when it has
62
- # finished with success. Unlike with signals, if more than one event is
63
- # forwarded to the same event in the same cycle, the target event will be
64
- # emitted as many times as the incoming events.
65
- # * the Precedence relation is a subset of the two preceding relations. It
66
- # represents a partial ordering of the events that must be maintained during
67
- # the propagation stage (i.e. a notion of causality).
68
- #
69
- # In the code, the followin procedure is followed: when a code fragment calls
70
- # EventGenerator#emit or EventGenerator#call, the event is not emitted right
71
- # away. Instead, it is queued in the set of "pending" events through the use of
72
- # #add_event_propagation. The execution engine will then consider
73
- # the pending set of events, choose the appropriate one by following the
74
- # information contained in the Precedence relation and emit or call it. The
75
- # actual call/emission is done through EventGenerator#call_without_propagation
76
- # and EventGenerator#emit_without_propagation. The error checking (i.e. wether
77
- # or not the emission/call is allowed) is done at both steps of propagation,
78
- # because doing it late in the *_without_propagation versions would make the
79
- # system more difficult to debug/test.
80
- #
81
- # === Error handling
82
- # Each user-provided code fragment (i.e. event handlers, event commands,
83
- # polling blocks, ...) are called into a specific error-gathering context.
84
- # Once an exception is caught, it is added to the set of detected errors
85
- # through #add_error. Those errors are handled after the
86
- # event propagation cycle by the #propagate_exceptions
87
- # method. It follows the following steps:
88
- #
89
- # * it removes all exceptions for which a running repair exists
90
- # (#remove_inhibited_exceptions)
91
- #
92
- # * it checks for repairs declared through the
93
- # Roby::TaskStructure::ErrorHandling relation. If one exists, the
94
- # corresponding task is started, adds it to the set of running repairs
95
- # (Plan#add_repair)
96
- #
97
- # For example, the following code fragment declares that +repair_task+
98
- # is a plan repair for all errors involving the +low_battery+ event of the
99
- # +moving+ task
100
- #
101
- # task.event(:moving).handle_with repair_task
102
- #
103
- # * it executes the exception handlers that have been declared for this
104
- # exception by a call to Roby::Task.on_exception. The following code
105
- # fragment defines an exception handler for LowBattery exceptions:
106
- #
107
- # class Moving
108
- # on_exception(LowBattery) { |error| do_something_to_handle_that }
109
- # end
2
+ # Exception wrapper used to report that multiple errors have been raised
3
+ # during a synchronous event processing call.
4
+ #
5
+ # See ExecutionEngine#process_events_synchronous for more information
6
+ class SynchronousEventProcessingMultipleErrors < RuntimeError
7
+ # Exceptions as gathered during propagation with {ExecutionEngine#task_m}
8
+ #
9
+ # @return [Array<ExecutionEngine::PropagationInfo>]
10
+ attr_reader :errors
11
+
12
+ # The set of underlying "real" (i.e. non-Roby) exceptions
13
+ #
14
+ # @return [Array<Exception>]
15
+ def original_exceptions
16
+ errors
17
+ end
18
+
19
+ def initialize(errors)
20
+ @errors = errors
21
+ end
22
+
23
+ def pretty_print(pp)
24
+ pp.text "Got #{errors.size} exceptions and #{original_exceptions.size} sub-exceptions"
25
+ pp.breakable
26
+ pp.seplist(errors.each_with_index) do |e, i|
27
+ Roby.flatten_exception(e).each_with_index do |sub_e, sub_i|
28
+ pp.breakable
29
+ pp.text "[#{i}.#{sub_i}] "
30
+ sub_e.pretty_print(pp)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ # @api private
110
37
  #
111
- # Exception handling is finished whenever an exception handler did not
112
- # call #pass_exception to notify that it cannot handle the given
113
- # exception.
38
+ # The core execution algorithm
114
39
  #
115
- # * if no exception handler is found, or if all of them called
116
- # #pass_exception, then plan-level exception handlers are searched in the
117
- # corresponding Roby::Plan instance. Plan-level exception handlers are
118
- # defined by Plan#on_exception. Alternatively, for the main plan,
119
- # Roby.on_exception can be also used.
40
+ # It is in charge of handling event and exception propagation, as well as
41
+ # running cleanup processes (e.g. garbage collection).
120
42
  #
121
- # * finally, tasks that are still involved in an error are injected into the
122
- # garbage collection process through the +force+ argument of
123
- # #garbage_collect, so that they get killed and removed from the plan.
43
+ # The main method is {#process_events}. When executing a Roby application,
44
+ # it is called periodically by {#event_loop}.
124
45
  #
46
+ # In addition, there is a special "synchronous" propagation mode that is
47
+ # used by {EventGenerator#call} and {EventGenerator#emit}. This mode is used
48
+ # when the event code is not executed within an engine, but from an
49
+ # imperative script, as in unit tests.
125
50
  class ExecutionEngine
126
51
  extend Logger::Hierarchy
127
- extend Logger::Forward
52
+ include Logger::Hierarchy
53
+ include DRoby::EventLogging
54
+
55
+ # Whether this engine should use the OOB GC from the gctools gem
56
+ attr_predicate :use_oob_gc?, true
57
+
58
+ # Whether this engine should trace and log GC-related information
59
+ attr_predicate :profile_gc?, true
60
+
61
+ class << self
62
+ # Whether the engines should use the OOB GC from the gctools gem by
63
+ # default
64
+ #
65
+ # It is enabled in lib/roby.rb if the gctools are installed
66
+ attr_predicate :use_oob_gc?, true
67
+ end
128
68
 
129
69
  # Create an execution engine acting on +plan+, using +control+ as the
130
70
  # decision control object
131
71
  #
132
- # See Roby::Plan and Roby::DecisionControl
133
- def initialize(plan, control)
72
+ # @param [ExecutablePlan] plan the plan on which this engine acts
73
+ # @param [DecisionControl] control the policy object, i.e. the object
74
+ # that embeds policies in cases where multiple reactions would be
75
+ # possible
76
+ # @param [DRoby::EventLogger] event_logger the logger that should be
77
+ # used to trace execution events. It is by default the same than the
78
+ # {#plan}'s. Pass a {DRoby::NullEventLogger} instance to disable event
79
+ # logging for this engine.
80
+ def initialize(plan, control: Roby::DecisionControl.new, event_logger: plan.event_logger)
134
81
  @plan = plan
135
- plan.engine = self
82
+ @event_logger = event_logger
83
+
84
+ @use_oob_gc = ExecutionEngine.use_oob_gc?
85
+
136
86
  @control = control
87
+ @scheduler = Schedulers::Null.new(plan)
88
+ reset_thread_pool
89
+ @thread = Thread.current
137
90
 
91
+ @propagation = nil
138
92
  @propagation_id = 0
93
+ @propagation_exceptions = nil
94
+ @application_exceptions = nil
139
95
  @delayed_events = []
140
- @process_once = Queue.new
141
96
  @event_ordering = Array.new
142
97
  @event_priorities = Hash.new
143
98
  @propagation_handlers = []
99
+ @external_events_handlers = []
144
100
  @at_cycle_end_handlers = Array.new
145
101
  @process_every = Array.new
146
- @waiting_threads = Array.new
102
+ @waiting_work = Concurrent::Array.new
103
+ @emitted_events = Array.new
104
+ @disabled_handlers = Set.new
105
+ @exception_listeners = Array.new
106
+
107
+ @worker_threads_mtx = Mutex.new
108
+ @worker_threads = Array.new
109
+ @once_blocks = Queue.new
110
+
111
+ @pending_exceptions = Hash.new
147
112
 
148
- each_cycle(&ExecutionEngine.method(:call_every))
113
+ each_cycle(&ExecutionEngine.method(:call_every))
149
114
 
150
- @quit = 0
115
+ @quit = 0
151
116
  @allow_propagation = true
152
- @thread = nil
153
- @cycle_index = 0
154
- @cycle_start = Time.now
155
- @cycle_length = 0
156
- @last_stop_count = 0
117
+ @cycle_index = 0
118
+ @cycle_start = Time.now
119
+ @cycle_length = 0.1
120
+ @last_stop_count = 0
157
121
  @finalizers = []
158
122
  @gc_warning = true
159
- end
123
+
124
+ refresh_relations
125
+
126
+ self.display_exceptions = true
127
+ end
128
+
129
+ # Refresh the value of cached relations
130
+ #
131
+ # Some often-used relations are cached at {#initialize}, such as
132
+ # {#dependency_graph} and {#precedence_graph}. Call this when
133
+ # the actual graph objects have changed on the plan
134
+ def refresh_relations
135
+ @dependency_graph = plan.task_relation_graph_for(TaskStructure::Dependency)
136
+ @precedence_graph = plan.event_relation_graph_for(EventStructure::Precedence)
137
+ @signal_graph = plan.event_relation_graph_for(EventStructure::Signal)
138
+ @forward_graph = plan.event_relation_graph_for(EventStructure::Forwarding)
139
+ end
140
+
141
+ # A thread pool on which async work should be executed
142
+ #
143
+ # @see {#promise}
144
+ # @return [Concurrent::CachedThreadPool]
145
+ attr_reader :thread_pool
146
+
147
+ # Cached graph object for {EventStructure::Precedence}
148
+ #
149
+ # This is here for performance reasons, to avoid resolving the same
150
+ # graph over and over
151
+ attr_reader :precedence_graph
152
+
153
+ # Cached graph object for {EventStructure::Signal}
154
+ #
155
+ # This is here for performance reasons, to avoid resolving the same
156
+ # graph over and over
157
+ attr_reader :signal_graph
158
+
159
+ # Cached graph object for {EventStructure::Forward}
160
+ #
161
+ # This is here for performance reasons, to avoid resolving the same
162
+ # graph over and over
163
+ attr_reader :forward_graph
164
+
165
+ # Cached graph object for {TaskStructure::Dependency}
166
+ #
167
+ # This is here for performance reasons, to avoid resolving the same
168
+ # graph over and over
169
+ attr_reader :dependency_graph
160
170
 
161
171
  # The Plan this engine is acting on
162
172
  attr_accessor :plan
173
+ # The underlying {DRoby::EventLogger}
174
+ #
175
+ # It is usually the same than the {#plan}'s. Pass a
176
+ # {DRoby::NullEventLogger} at construction time to disable logging of
177
+ # execution events.
178
+ attr_accessor :event_logger
163
179
  # The DecisionControl object associated with this engine
164
180
  attr_accessor :control
165
181
  # A numeric ID giving the count of the current propagation cycle
166
182
  attr_reader :propagation_id
183
+ # The set of events that have been emitted within the last call to
184
+ # {#process_events} (i.e. the last execution of the event loop)
185
+ #
186
+ # @return [Array<Event>]
187
+ attr_reader :emitted_events
188
+ # The blocks that are currently listening to exceptions
189
+ # @return [Array<#call>]
190
+ attr_reader :exception_listeners
191
+ # Thread-safe queue to push work to the execution engine
192
+ #
193
+ # Do not access directly, use {#once} instead
194
+ #
195
+ # @return [Queue] blocks that should be executed at the beginning of the
196
+ # next execution cycle. It is the only thread safe way to queue work
197
+ # to be executed by the engine
198
+ attr_reader :once_blocks
199
+
200
+ # @api private
201
+ #
202
+ # Internal structure used to store a poll block definition provided to
203
+ # #every or #add_propagation_handler
204
+ #
205
+ # @!macro poll_options
206
+ # @option [Symbol] on_error if :raise (the default), pass exceptions
207
+ # to the caller. If :ignore, do nothing. If :disable, remove the
208
+ # poll block
209
+ class PollBlockDefinition
210
+ ON_ERROR = [:raise, :ignore, :disable]
211
+
212
+ attr_reader :description
213
+ attr_reader :handler
214
+ attr_reader :on_error
215
+ attr_predicate :late?, true
216
+ attr_predicate :once?, true
217
+ attr_predicate :disabled?, true
218
+
219
+ def id; handler.object_id end
220
+
221
+ def initialize(description, handler, on_error: :raise, late: false, once: false)
222
+ if !PollBlockDefinition::ON_ERROR.include?(on_error.to_sym)
223
+ raise ArgumentError, "invalid value '#{on_error} for the :on_error option. Accepted values are #{ON_ERROR.map(&:to_s).join(", ")}"
224
+ end
225
+
226
+ @description, @handler, @on_error, @late, @once =
227
+ description, handler, on_error, late, once
228
+ @disabled = false
229
+ end
167
230
 
168
- @propagation_handlers = []
169
- class << self
170
- # Code blocks that get called at the beginning of each cycle. See
171
- # #add_propagation_handler
231
+ def to_s; "#<PollBlockDefinition: #{description} #{handler} on_error:#{on_error}>" end
232
+
233
+ def call(engine, *args)
234
+ handler.call(*args)
235
+ true
236
+
237
+ rescue Exception => e
238
+ if on_error == :raise
239
+ engine.add_framework_error(e, description)
240
+ return false
241
+ elsif on_error == :disable
242
+ engine.warn "propagation handler #{description} disabled because of the following error"
243
+ Roby.log_exception_with_backtrace(e, engine, :warn)
244
+ return false
245
+ elsif on_error == :ignore
246
+ engine.warn "ignored error from propagation handler #{description}"
247
+ Roby.log_exception_with_backtrace(e, engine, :warn)
248
+ return true
249
+ end
250
+ end
251
+ end
252
+
253
+ # Add/remove propagation handler methods that are shared between the
254
+ # instance and the class
255
+ module PropagationHandlerMethods
256
+ # Code blocks that get called at the beginning of each cycle
257
+ #
258
+ # @return [Array<PollBlockDefinition>]
259
+ attr_reader :external_events_handlers
260
+ # Code blocks that get called during propagation to handle some
261
+ # internal propagation mechanism
262
+ #
263
+ # @return [Array<PollBlockDefinition>]
172
264
  attr_reader :propagation_handlers
173
265
 
174
- # call-seq:
175
- # ExecutionEngine.add_propagation_handler { |plan| ... }
266
+ # @api private
176
267
  #
177
- # The propagation handlers are a set of block objects that have to be
178
- # called at the beginning of every propagation phase for all plans.
179
- # These objects are called in propagation context, which means that the
180
- # events they would call or emit are injected in the propagation
181
- # process itself.
268
+ # Helper method that gets the arguments necessary top create a
269
+ # propagation handler, sanitizes and normalizes them, and returns
270
+ # both the propagation type and the {PollBlockDefinition} object
182
271
  #
183
- # This method adds a new propagation handler. In its first form, the
184
- # argument is the proc object to be added. In the second form, the
185
- # block is taken the handler. In both cases, the method returns a value
186
- # which can be used to remove the propagation handler later. In both
187
- # cases, the block or proc is called with the plan to propagate on
188
- # as argument.
272
+ # @param [:external_events,:propagation] type whether the block should be registered as an
273
+ # :external_events block, processed at the beginning of the cycle,
274
+ # or a :propagation block, processed at each propagation loop.
275
+ # @param [String] description a string describing the block. It will
276
+ # be used when adding timepoints to the event log
277
+ # @param poll_options (see PollBlockDefinition#initialize)
278
+ def create_propagation_handler(type: :external_events, description: 'propagation handler', **poll_options, &block)
279
+ check_arity block, 1
280
+ handler = PollBlockDefinition.new(description, block, **poll_options)
281
+
282
+ if type == :external_events
283
+ if handler.late?
284
+ raise ArgumentError, "only :propagation handlers can be marked as 'late', the external event handlers cannot"
285
+ end
286
+ elsif type != :propagation
287
+ raise ArgumentError, "invalid value for the :type option. Expected :propagation or :external_events, got #{type}"
288
+ end
289
+ return type, handler
290
+ end
291
+
292
+ # The propagation handlers are blocks that should be called at
293
+ # various places during propagation for all plans. These objects
294
+ # are called in propagation context, which means that the events
295
+ # they would call or emit are injected in the propagation process
296
+ # itself.
189
297
  #
190
- # This method sets up global propagation handlers (i.e. to be used for
191
- # all propagation on all plans). For per-plan propagation handlers, see
192
- # ExecutionEngine#add_propagation_handler.
298
+ # @param [:propagation,:external_events] type defines when this block should be called. If
299
+ # :external_events, it is called only once at the beginning of each
300
+ # execution cycle. If :propagation, it is called once at the
301
+ # beginning of each cycle, as well as after each propagation step.
302
+ # The :late option also gives some control over when the handler is
303
+ # called when in propagation mode
304
+ # @option options [Boolean] once (false) if true, this handler will
305
+ # be removed just after its first execution
306
+ # @option options [Boolean] late (false) if true, the handler is
307
+ # called only when there are no events to propagate anymore.
308
+ # @option options [:raise,:ignore,:disable] on_error (:raise)
309
+ # controls what happens when the block raises an exception. If
310
+ # :raise, the error is registered as a framework error. If
311
+ # :ignore, it is completely ignored. If :disable, the handler
312
+ # will be disabled, i.e. not called anymore until #disabled?
193
313
  #
194
- # See also ExecutionEngine.remove_propagation_handler
195
- def add_propagation_handler(proc_obj = nil, &block)
196
- proc_obj ||= block
197
- check_arity proc_obj, 1
198
- propagation_handlers << proc_obj
199
- proc_obj.object_id
314
+ # @return [Object] an ID object that can be passed to
315
+ # {#remove_propagation_handler}
316
+ def add_propagation_handler(type: :external_events, description: 'propagation handler', **poll_options, &block)
317
+ type, handler = create_propagation_handler(type: type, description: description, **poll_options, &block)
318
+ if type == :propagation
319
+ propagation_handlers << handler
320
+ elsif type == :external_events
321
+ external_events_handlers << handler
322
+ end
323
+ handler.id
200
324
  end
201
325
 
202
326
  # This method removes a propagation handler which has been added by
203
- # ExecutionEngine.add_propagation_handler. THe +id+ value is the
204
- # value returned by ExecutionEngine.add_propagation_handler.
327
+ # {#add_propagation_handler}.
328
+ #
329
+ # @param [Object] id the block ID as returned by
330
+ # {#add_propagation_handler}
205
331
  def remove_propagation_handler(id)
206
- propagation_handlers.delete_if { |p| p.object_id == id }
332
+ propagation_handlers.delete_if { |p| p.id == id }
333
+ external_events_handlers.delete_if { |p| p.id == id }
207
334
  nil
208
335
  end
209
- end
210
336
 
211
- # A set of block objects that have to be called at the beginning of every
212
- # propagation phase. These objects are called in propagation context, which
213
- # means that the events they would call or emit are injected in the
214
- # propagation process itself.
215
- attr_reader :propagation_handlers
337
+ # Add a handler that is called at the beginning of the execution cycle
338
+ def at_cycle_begin(description: 'at_cycle_begin', **options, &block)
339
+ add_propagation_handler(description: description, type: :external_events, **options, &block)
340
+ end
341
+
342
+ # Execute the given block at the beginning of each cycle, in propagation
343
+ # context.
344
+ #
345
+ # @return [Object] an ID that can be used to remove the handler using
346
+ # {#remove_propagation_handler}
347
+ def each_cycle(description: 'each_cycle', &block)
348
+ add_propagation_handler(description: description, &block)
349
+ end
350
+ end
216
351
 
217
- # call-seq:
218
- # engine.add_propagation_handler { |plan| ... }
219
- #
220
- # The propagation handlers are a set of block objects that have to be
221
- # called at the beginning of every propagation phase for all plans.
222
- # These objects are called in propagation context, which means that the
223
- # events they would call or emit are injected in the propagation
224
- # process itself.
225
- #
226
- # This method adds a new propagation handler. In its first form, the
227
- # argument is the proc object to be added. In the second form, the
228
- # block is taken the handler. In both cases, the method returns a value
229
- # which can be used to remove the propagation handler later.
230
- #
231
- # See also #remove_propagation_handler
232
- def add_propagation_handler(proc_obj = nil, &block)
233
- proc_obj ||= block
234
- check_arity proc_obj, 1
235
- propagation_handlers << proc_obj
236
- proc_obj.object_id
237
- end
238
-
239
- # This method removes a propagation handler which has been added by
240
- # #add_propagation_handler. THe +id+ value is the value returned by
241
- # #add_propagation_handler. In its first form, the argument is the proc
242
- # object to be added. In the second form, the block is taken the
243
- # handler. In both cases, the method returns a value which can be used
244
- # to remove the propagation handler later.
245
- #
246
- # See also #add_propagation_handler
352
+ @propagation_handlers = Array.new
353
+ @external_events_handlers = Array.new
354
+ extend PropagationHandlerMethods
355
+ include PropagationHandlerMethods
356
+
357
+ # Poll blocks that have been disabled because they raised an exception
358
+ #
359
+ # @return [Array<PollBlockDefinition>]
360
+ attr_reader :disabled_handlers
361
+
247
362
  def remove_propagation_handler(id)
248
- propagation_handlers.delete_if { |p| p.object_id == id }
363
+ disabled_handlers.delete_if { |p| p.id == id }
364
+ super
249
365
  nil
250
366
  end
251
367
 
252
- # call-seq:
253
- # Roby.each_cycle { |plan| ... }
254
- #
255
- # Execute the given block at the beginning of each cycle, in propagation
256
- # context.
257
- #
258
- # The returned value is an ID that can be used to remove the handler using
259
- # #remove_propagation_handler
260
- def each_cycle(&block)
261
- add_propagation_handler(block)
368
+ class JoinAllWaitingWorkTimeout < RuntimeError
369
+ attr_reader :waiting_work
370
+ def initialize(waiting_work)
371
+ @waiting_work = waiting_work.dup
372
+ end
373
+
374
+ def pretty_print(pp)
375
+ pp.text "timed out in #join_all_waiting_work, #{waiting_work.size} promises waiting"
376
+ waiting_work.each do |w|
377
+ pp.breakable
378
+ pp.nest(2) do
379
+ if w.respond_to?(:state)
380
+ pp.text "[state=#{w.state}] "
381
+ end
382
+ w.pretty_print(pp)
383
+ end
384
+ end
385
+ end
386
+ end
387
+
388
+ # Waits for all obligations in {#waiting_work} to finish
389
+ def join_all_waiting_work(timeout: nil)
390
+ return [], PropagationInfo.new if waiting_work.empty?
391
+ deadline = if timeout
392
+ Time.now + timeout
393
+ end
394
+
395
+ finished = Array.new
396
+ propagation_info = PropagationInfo.new
397
+ begin
398
+ framework_errors = gather_framework_errors("#join_all_waiting_work", raise_caught_exceptions: false) do
399
+ next_steps = nil
400
+ event_errors = gather_errors do
401
+ next_steps = gather_propagation do
402
+ finished.concat(process_waiting_work)
403
+ blocks = Array.new
404
+ while !once_blocks.empty?
405
+ blocks << once_blocks.pop.last
406
+ end
407
+ call_poll_blocks(blocks)
408
+ end
409
+ end
410
+
411
+ this_propagation = propagate_events_and_errors(next_steps, event_errors, garbage_collect_pass: false)
412
+ propagation_info.merge(this_propagation)
413
+ end
414
+ propagation_info.add_framework_errors(framework_errors)
415
+
416
+ Thread.pass
417
+ has_scheduled_promises = has_waiting_work?
418
+ if deadline && (Time.now > deadline) && has_scheduled_promises
419
+ raise JoinAllWaitingWorkTimeout.new(waiting_work)
420
+ end
421
+ end while has_waiting_work?
422
+ return finished, propagation_info
262
423
  end
263
424
 
264
425
  # The scheduler is the object which handles non-generic parts of the
@@ -267,10 +428,25 @@ def each_cycle(&block)
267
428
  # events.
268
429
  #
269
430
  # See Schedulers::Basic
270
- attr_accessor :scheduler
431
+ attr_reader :scheduler
432
+
433
+ def scheduler=(scheduler)
434
+ if !scheduler
435
+ raise ArgumentError, "cannot set the scheduler to nil. You can disable the current scheduler with .enabled = false instead, or set it to Schedulers::Null.new"
436
+ end
437
+ @scheduler = scheduler
438
+ end
439
+
440
+ def gathering?
441
+ Roby.warn_deprecated "#gathering? is deprecated, use #in_propagation_context? instead"
442
+ in_propagation_context?
443
+ end
271
444
 
272
- # True if we are currently in the propagation stage
273
- def gathering?; !!@propagation end
445
+ # True if we are within a propagation context (i.e. within event
446
+ # processing)
447
+ def in_propagation_context?
448
+ !!@propagation
449
+ end
274
450
 
275
451
  attr_predicate :allow_propagation
276
452
 
@@ -279,7 +455,7 @@ def gathering?; !!@propagation end
279
455
  attr_reader :propagation_sources
280
456
  # The set of events extracted from #sources
281
457
  def propagation_source_events
282
- result = ValueSet.new
458
+ result = Set.new
283
459
  for ev in @propagation_sources
284
460
  if ev.respond_to?(:generator)
285
461
  result << ev
@@ -290,7 +466,7 @@ def propagation_source_events
290
466
 
291
467
  # The set of generators extracted from #sources
292
468
  def propagation_source_generators
293
- result = ValueSet.new
469
+ result = Set.new
294
470
  for ev in @propagation_sources
295
471
  result << if ev.respond_to?(:generator)
296
472
  ev.generator
@@ -334,10 +510,28 @@ def execute_delayed_events
334
510
  end
335
511
  end
336
512
 
513
+ # Called by EventGenerator when an event became unreachable
514
+ def unreachable_event(event)
515
+ delayed_events.delete_if { |_, _, _, signalled, _| signalled == event }
516
+ end
517
+
518
+ # Called by #plan when a task has been finalized
519
+ def finalized_task(task)
520
+ @pending_exceptions.delete(task)
521
+ end
522
+
337
523
  # Called by #plan when an event has been finalized
338
524
  def finalized_event(event)
339
- event.unreachable!(nil, plan)
340
- delayed_events.delete_if { |_, _, _, signalled, _| signalled == event }
525
+ if @propagation
526
+ @propagation.delete(event)
527
+ end
528
+ event.unreachable!("finalized", plan)
529
+ # since the event is already finalized,
530
+ end
531
+
532
+ # Returns true if some events are queued
533
+ def has_queued_events?
534
+ !@propagation.empty?
341
535
  end
342
536
 
343
537
  # Sets up a propagation context, yielding the block in it. During this
@@ -348,88 +542,152 @@ def finalized_event(event)
348
542
  # where the two +_sources+ are arrays of the form
349
543
  # [[source, context], ...]
350
544
  #
351
- # The method returns the resulting hash. Use #gathering? to know if the
545
+ # The method returns the resulting hash. Use #in_propagation_context? to know if the
352
546
  # current engine is in a propagation context, and #add_event_propagation
353
547
  # to add a new entry to this set.
354
548
  def gather_propagation(initial_set = Hash.new)
355
- raise InternalError, "nested call to #gather_propagation" if gathering?
356
- @propagation = initial_set
549
+ raise InternalError, "nested call to #gather_propagation" if in_propagation_context?
357
550
 
358
- propagation_context(nil) { yield }
551
+ old_allow_propagation, @allow_propagation = @allow_propagation, true
359
552
 
360
- return @propagation
361
- ensure
362
- @propagation = nil
363
- end
553
+ # The ensure clause must NOT apply to the recursive check above.
554
+ # Otherwise, we end up resetting @propagation_exceptions to nil,
555
+ # which wreaks havoc
556
+ begin
557
+ @propagation = initial_set
558
+ @propagation_sources = nil
559
+ @propagation_step_id = 0
364
560
 
365
- # Converts the Exception object +error+ into a Roby::ExecutionException
366
- def self.to_execution_exception(error)
367
- if error.kind_of?(Roby::ExecutionException)
368
- error
369
- else
370
- Roby::ExecutionException.new(error)
561
+ before = @propagation
562
+ propagation_context([]) do
563
+ yield
564
+ end
565
+
566
+ result, @propagation = @propagation, nil
567
+ return result
568
+ ensure
569
+ @propagation = nil
570
+ @allow_propagation = old_allow_propagation
371
571
  end
372
572
  end
373
573
 
374
- # If called in execution context, adds the plan-based error +e+ to be
375
- # handled later in the execution cycle. Otherwise, calls
376
- # #add_framework_error
377
- def add_error(e)
574
+ # Register a LocalizedError for future propagation
575
+ #
576
+ # This method must be called in a error-gathering context (i.e.
577
+ # {#gather_error}.
578
+ #
579
+ # @param [#to_execution_exception] e the exception
580
+ # @raise [NotPropagationContext] raised if called outside
581
+ # {#gather_error}
582
+ def add_error(e, propagate_through: nil)
583
+ plan_exception = e.to_execution_exception
378
584
  if @propagation_exceptions
379
- plan_exception = ExecutionEngine.to_execution_exception(e)
380
- @propagation_exceptions << plan_exception
585
+ @propagation_exceptions << [plan_exception, propagate_through]
381
586
  else
382
- if e.respond_to?(:error) && e.error
383
- add_framework_error(e.error, "error outside error handling")
384
- else
385
- add_framework_error(e, "error outside error handling")
386
- end
587
+ Roby.log_exception_with_backtrace(e, self, :fatal)
588
+ raise NotPropagationContext, "#add_error called outside an error-gathering context (#add_error)"
387
589
  end
388
590
  end
389
591
 
390
- # Yields to the block, calling #add_framework_error if an exception is
391
- # raised
392
- def gather_framework_errors(source)
592
+ # Yields to the block and registers any raised exception using
593
+ # {#add_framework_error}
594
+ #
595
+ # If the method is called within an exception-gathering context (either
596
+ # {#process_events} or {#gather_framework_errors} itself), nothing else
597
+ # is done. Otherwise, {#process_pending_application_exceptions} is
598
+ # called to re-raise any caught exception
599
+ def gather_framework_errors(source, raise_caught_exceptions: true)
600
+ if @application_exceptions
601
+ recursive_error_gathering_context = true
602
+ else
603
+ @application_exceptions = []
604
+ end
605
+
393
606
  yield
607
+
608
+ if !recursive_error_gathering_context && !raise_caught_exceptions
609
+ clear_application_exceptions
610
+ end
394
611
  rescue Exception => e
395
612
  add_framework_error(e, source)
613
+ if !recursive_error_gathering_context && !raise_caught_exceptions
614
+ clear_application_exceptions
615
+ end
616
+ ensure
617
+ if !recursive_error_gathering_context && raise_caught_exceptions
618
+ process_pending_application_exceptions
619
+ end
620
+ end
621
+
622
+ def process_pending_application_exceptions(application_errors = clear_application_exceptions,
623
+ raise_framework_errors: Roby.app.abort_on_application_exception?)
624
+
625
+ # We don't aggregate exceptions, so report them all and raise one
626
+ if display_exceptions?
627
+ application_errors.each do |error, source|
628
+ if !error.kind_of?(Interrupt)
629
+ fatal "Application error in #{source}"
630
+ Roby.log_exception_with_backtrace(error, self, :fatal)
631
+ end
632
+ end
633
+ end
634
+
635
+ error, source = application_errors.find do |error, _|
636
+ raise_framework_errors || error.kind_of?(SignalException)
637
+ end
638
+ if error
639
+ raise error, "in #{source}: #{error.message}", error.backtrace
640
+ end
396
641
  end
397
642
 
398
- # If called in execution context, adds the framework error +error+ to be
399
- # handled later in the execution cycle. Otherwise, either raises the
400
- # error again if Application#abort_on_application_exception is true. IF
401
- # abort_on_application_exception is false, simply displays a warning
643
+ # Registers the given error and a description of its source in the list
644
+ # of application/framework errors
645
+ #
646
+ # It must be called within an exception-gathering context, that is
647
+ # either within {#process_events}, or within {#gather_framework_errors}
648
+ #
649
+ # These errors will terminate the event loop
650
+ #
651
+ # @param [Exception] error
652
+ # @param [Object] source
402
653
  def add_framework_error(error, source)
403
654
  if @application_exceptions
404
655
  @application_exceptions << [error, source]
405
- elsif Roby.app.abort_on_application_exception? || error.kind_of?(SignalException)
406
- raise error, "in #{source}: #{error.message}", error.backtrace
407
656
  else
408
- ExecutionEngine.error "Application error in #{source}"
409
- Roby.format_exception(error).each do |line|
410
- Roby.warn line
411
- end
657
+ Roby.log_exception_with_backtrace(error, self, :fatal)
658
+ raise NotPropagationContext, "#add_framework_error called outside an exception-gathering context"
412
659
  end
413
660
  end
414
661
 
415
662
  # Sets the source_event and source_generator variables according
416
663
  # to +source+. +source+ is the +from+ argument of #add_event_propagation
417
664
  def propagation_context(sources)
418
- raise InternalError, "not in a gathering context in #fire" unless gathering?
665
+ current_sources = @propagation_sources
666
+ raise InternalError, "not in a gathering context in #propagation_context" unless in_propagation_context?
419
667
 
420
- if sources
421
- current_sources = sources
422
- @propagation_sources = sources
423
- else
424
- @propagation_sources = []
425
- end
668
+ @propagation_sources = sources
669
+ yield
670
+ ensure
671
+ @propagation_sources = current_sources
672
+ end
673
+
674
+ def has_propagation_for?(target)
675
+ @propagation && @propagation.has_key?(target)
676
+ end
426
677
 
427
- yield @propagation
678
+ # Queue a signal to be propagated
679
+ def queue_signal(sources, target, context, timespec)
680
+ add_event_propagation(false, sources, target, context, timespec)
681
+ end
428
682
 
429
- ensure
430
- @propagation_sources = sources
683
+ # Queue a forwarding to be propagated
684
+ def queue_forward(sources, target, context, timespec)
685
+ add_event_propagation(true, sources, target, context, timespec)
431
686
  end
432
687
 
688
+ PENDING_PROPAGATION_FORWARD = 1
689
+ PENDING_PROPAGATION_SIGNAL = 2
690
+
433
691
  # Adds a propagation to the next propagation step: it registers a
434
692
  # propagation step to be performed between +source+ and +target+ with
435
693
  # the given +context+. If +is_forward+ is true, the propagation will be
@@ -439,20 +697,122 @@ def propagation_context(sources)
439
697
  # calling the target event.
440
698
  #
441
699
  # See #gather_propagation
442
- def add_event_propagation(is_forward, from, target, context, timespec)
700
+ def add_event_propagation(is_forward, sources, target, context, timespec)
443
701
  if target.plan != plan
444
702
  raise Roby::EventNotExecutable.new(target), "#{target} not in executed plan"
445
703
  end
446
704
 
447
- step = (@propagation[target] ||= [nil, nil])
448
- from = [nil] unless from && !from.empty?
705
+ target.pending(sources.find_all { |ev| ev.kind_of?(Event) })
706
+
707
+ @propagation_step_id += 1
708
+ target_info = (@propagation[target] ||= [@propagation_step_id, [], []])
709
+ step = target_info[is_forward ? PENDING_PROPAGATION_FORWARD : PENDING_PROPAGATION_SIGNAL]
710
+ if sources.empty?
711
+ step << nil << context << timespec
712
+ else
713
+ sources.each do |ev|
714
+ step << ev << context << timespec
715
+ end
716
+ end
717
+ end
718
+
719
+ # Whether a forward matching this signature is currently pending
720
+ def has_pending_forward?(from, to, expected_context)
721
+ if pending = @propagation[to]
722
+ pending[PENDING_PROPAGATION_FORWARD].each_slice(3).any? do |event, context, timespec|
723
+ (from === event.generator) && (expected_context === context)
724
+ end
725
+ end
726
+ end
727
+
728
+ # Whether a signal matching this signature is currently pending
729
+ def has_pending_signal?(from, to, expected_context)
730
+ if pending = @propagation[to]
731
+ pending[PENDING_PROPAGATION_SIGNAL].each_slice(3).any? do |event, context, timespec|
732
+ (from === event.generator) && (expected_context === context)
733
+ end
734
+ end
735
+ end
736
+
737
+ # Helper that calls the propagation handlers in +propagation_handlers+
738
+ # (which are expected to be instances of PollBlockDefinition) and
739
+ # handles the errors according of each handler's policy
740
+ def call_poll_blocks(blocks, late = false)
741
+ blocks.delete_if do |handler|
742
+ if handler.disabled? || (handler.late? ^ late)
743
+ next
744
+ end
745
+
746
+ log_timepoint_group handler.description do
747
+ if !handler.call(self, plan)
748
+ handler.disabled = true
749
+ end
750
+ end
751
+ handler.once?
752
+ end
753
+ end
754
+
755
+ # Dispatch {#once_blocks} to the other handler sets for further
756
+ # processing
757
+ def process_once_blocks
758
+ while !once_blocks.empty?
759
+ type, block = once_blocks.pop
760
+ if type == :external_events
761
+ external_events_handlers << block
762
+ else
763
+ propagation_handlers << block
764
+ end
765
+ end
766
+ end
767
+
768
+ # Gather the events that come out of this plan manager
769
+ def gather_external_events
770
+ process_once_blocks
771
+ gather_framework_errors('delayed events') { execute_delayed_events }
772
+ call_poll_blocks(self.class.external_events_handlers)
773
+ call_poll_blocks(self.external_events_handlers)
774
+ end
775
+
776
+ def call_propagation_handlers
777
+ process_once_blocks
778
+ if scheduler.enabled?
779
+ gather_framework_errors('scheduler') do
780
+ scheduler.initial_events
781
+ log_timepoint 'scheduler'
782
+ end
783
+ end
784
+ call_poll_blocks(self.class.propagation_handlers, false)
785
+ call_poll_blocks(self.propagation_handlers, false)
786
+
787
+ if !has_queued_events?
788
+ call_poll_blocks(self.class.propagation_handlers, true)
789
+ call_poll_blocks(self.propagation_handlers, true)
790
+ end
791
+ end
792
+
793
+ def gathering_errors?
794
+ !!@propagation_exceptions
795
+ end
796
+
797
+ # Executes the given block while gathering errors, and returns the
798
+ # errors that have been declared with #add_error
799
+ #
800
+ # @return [Array<ExecutionException>]
801
+ def gather_errors
802
+ if @propagation_exceptions
803
+ raise InternalError, "recursive call to #gather_errors"
804
+ end
449
805
 
450
- step = if is_forward then (step[0] ||= [])
451
- else (step[1] ||= [])
452
- end
806
+ # The ensure clause must NOT apply to the recursive check above.
807
+ # Otherwise, we end up resetting @propagation_exceptions to nil,
808
+ # which wreaks havoc
809
+ begin
810
+ @propagation_exceptions = []
811
+ yield
812
+ @propagation_exceptions
453
813
 
454
- from.each do |ev|
455
- step << ev << context << timespec
814
+ ensure
815
+ @propagation_exceptions = nil
456
816
  end
457
817
  end
458
818
 
@@ -463,54 +823,99 @@ def add_event_propagation(is_forward, from, target, context, timespec)
463
823
  # events we should consider as already emitted in the following propagation.
464
824
  # +seeds+ si a list of procs which should be called to initiate the propagation
465
825
  # (i.e. build an initial set of events)
466
- def propagate_events(seeds = nil)
467
- if @propagation_exceptions
468
- raise InternalError, "recursive call to propagate_events"
826
+ def event_propagation_phase(initial_events, propagation_info)
827
+ @propagation_id += 1
828
+
829
+ gather_errors do
830
+ next_steps = initial_events
831
+ while !next_steps.empty?
832
+ while !next_steps.empty?
833
+ next_steps = event_propagation_step(next_steps, propagation_info)
834
+ end
835
+ next_steps = gather_propagation { call_propagation_handlers }
836
+ end
837
+ end
838
+ end
839
+
840
+ # Compute errors in plan and handle the results
841
+ def error_handling_phase(events_errors)
842
+ # Do the exception handling phase
843
+ errors = compute_errors(events_errors)
844
+ notify_about_error_handling_results(errors)
845
+
846
+ # nonfatal errors are only notified. Fatal errors (kill_tasks) are
847
+ # handled in the propagation loop during garbage collection. Only
848
+ # the free events errors have to be handled here.
849
+ errors.free_events_errors.each do |exception, generators|
850
+ generators.each { |g| g.unreachable!(exception.exception) }
851
+ end
852
+ return errors
853
+ end
854
+
855
+ # Compute the set of unhandled fatal exceptions
856
+ def compute_kill_tasks_for_unhandled_fatal_errors(fatal_errors)
857
+ kill_tasks = fatal_errors.inject(Set.new) do |tasks, (exception, affected_tasks)|
858
+ tasks.merge(affected_tasks)
469
859
  end
860
+ # Tasks might have been finalized during exception handling, filter
861
+ # those out
862
+ kill_tasks.find_all(&:plan)
863
+ end
470
864
 
471
- @propagation_id = (@propagation_id += 1)
472
- @propagation_exceptions = []
865
+ # Issue the warning message and log notifications related to tasks being
866
+ # killed because of unhandled fatal exceptions
867
+ def notify_about_error_handling_results(errors)
868
+ kill_tasks, fatal_errors, nonfatal_errors, free_events_errors, handled_errors =
869
+ errors.kill_tasks, errors.fatal_errors, errors.nonfatal_errors, errors.free_events_errors, errors.handled_errors
473
870
 
474
- initial_set = []
475
- next_step = gather_propagation do
476
- gather_framework_errors('initial set setup') { yield(initial_set) } if block_given?
477
- gather_framework_errors('distributed events') { Roby::Distributed.process_pending }
478
- gather_framework_errors('delayed events') { execute_delayed_events }
479
- while !process_once.empty?
480
- p = process_once.pop
481
- gather_framework_errors("'once' block #{p}") { p.call }
482
- end
483
- if seeds
484
- for s in seeds
485
- gather_framework_errors("seed #{s}") { s.call }
486
- end
871
+ if !nonfatal_errors.empty?
872
+ if display_exceptions?
873
+ warn "#{nonfatal_errors.size} unhandled non-fatal exceptions"
487
874
  end
488
- if scheduler
489
- gather_framework_errors('scheduler') { scheduler.initial_events }
875
+ nonfatal_errors.each do |exception, tasks|
876
+ notify_exception(EXCEPTION_NONFATAL, exception, tasks)
490
877
  end
491
- for h in self.class.propagation_handlers
492
- gather_framework_errors("propagation handler #{h}") { h.call(plan) }
878
+ end
879
+
880
+ if !handled_errors.empty?
881
+ if display_exceptions?
882
+ warn "#{handled_errors.size} handled errors"
493
883
  end
494
- for h in propagation_handlers
495
- gather_framework_errors("propagation handler #{h}") { h.call(plan) }
884
+ handled_errors.each do |exception, tasks|
885
+ notify_exception(EXCEPTION_HANDLED, exception, tasks)
496
886
  end
497
887
  end
498
888
 
499
- while !next_step.empty?
500
- next_step = event_propagation_step(next_step)
501
- end
502
- @propagation_exceptions
889
+ if !free_events_errors.empty?
890
+ if display_exceptions?
891
+ warn "#{free_events_errors.size} free event exceptions"
892
+ end
893
+ free_events_errors.each do |exception, events|
894
+ notify_exception(EXCEPTION_FREE_EVENT, exception, events)
895
+ end
896
+ end
503
897
 
504
- ensure
505
- @propagation_exceptions = nil
898
+ if !fatal_errors.empty?
899
+ if display_exceptions?
900
+ warn "#{fatal_errors.size} unhandled fatal exceptions, involving #{kill_tasks.size} tasks that will be forcefully killed"
901
+ end
902
+ fatal_errors.each do |exception, tasks|
903
+ notify_exception(EXCEPTION_FATAL, exception, tasks)
904
+ end
905
+ if display_exceptions?
906
+ kill_tasks.each do |task|
907
+ log_pp :warn, task
908
+ end
909
+ end
910
+ end
506
911
  end
507
912
 
508
913
  # Validates +timespec+ as a delay specification. A valid delay
509
914
  # specification is either +nil+ or a hash, in which case two forms are
510
915
  # possible:
511
916
  #
512
- # :at => absolute_time
513
- # :delay => number
917
+ # at: absolute_time
918
+ # delay: number
514
919
  #
515
920
  def self.validate_timespec(timespec)
516
921
  if timespec
@@ -549,22 +954,28 @@ def self.make_delay(timeref, source, target, timespec)
549
954
  def next_event(pending)
550
955
  # this variable is 2 if selected_event is being forwarded, 1 if it
551
956
  # is both forwarded and signalled and 0 if it is only signalled
552
- priority, selected_event = nil
957
+ priority, step_id, selected_event = nil
553
958
  for propagation_step in pending
554
959
  target_event = propagation_step[0]
555
- forwards, signals = *propagation_step[1]
556
- target_priority = if forwards && signals then 1
557
- elsif signals then 0
558
- else 2
960
+ target_step_id, forwards, signals = *propagation_step[1]
961
+ target_priority = if forwards.empty? && signals.empty? then 2
962
+ elsif forwards.empty? then 0
963
+ else 1
559
964
  end
560
965
 
561
966
  do_select = if selected_event
562
- if EventStructure::Precedence.reachable?(selected_event, target_event)
967
+ if precedence_graph.reachable?(selected_event, target_event)
563
968
  false
564
- elsif EventStructure::Precedence.reachable?(target_event, selected_event)
969
+ elsif precedence_graph.reachable?(target_event, selected_event)
970
+ true
971
+ elsif priority < target_priority
565
972
  true
973
+ elsif priority == target_priority
974
+ # If they are of the same priority, handle
975
+ # earlier events first
976
+ step_id > target_step_id
566
977
  else
567
- priority < target_priority
978
+ false
568
979
  end
569
980
  else
570
981
  true
@@ -573,6 +984,7 @@ def next_event(pending)
573
984
  if do_select
574
985
  selected_event = target_event
575
986
  priority = target_priority
987
+ step_id = target_step_id
576
988
  end
577
989
  end
578
990
  [selected_event, *pending.delete(selected_event)]
@@ -595,7 +1007,7 @@ def next_event(pending)
595
1007
  def prepare_propagation(target, is_forward, info)
596
1008
  timeref = Time.now
597
1009
 
598
- source_events, source_generators, context = ValueSet.new, ValueSet.new, []
1010
+ source_events, source_generators, context = Set.new, Set.new, []
599
1011
 
600
1012
  delayed = true
601
1013
  info.each_slice(3) do |src, ctxt, time|
@@ -622,7 +1034,7 @@ def prepare_propagation(target, is_forward, info)
622
1034
  end
623
1035
 
624
1036
  unless delayed
625
- [source_events, source_generators, (context unless context.empty?)]
1037
+ [source_events, source_generators, context]
626
1038
  end
627
1039
  end
628
1040
 
@@ -638,26 +1050,34 @@ def prepare_propagation(target, is_forward, info)
638
1050
  # The method returns the next set of pending emissions and calls, adding
639
1051
  # the forwardings and signals that the propagation of the considered event
640
1052
  # have added.
641
- def event_propagation_step(current_step)
642
- signalled, forward_info, call_info = next_event(current_step)
1053
+ def event_propagation_step(current_step, propagation_info)
1054
+ signalled, step_id, forward_info, call_info = next_event(current_step)
643
1055
 
644
1056
  next_step = nil
645
- if call_info
646
- source_events, source_generators, context = prepare_propagation(signalled, false, call_info)
1057
+ if !call_info.empty?
1058
+ source_events, source_generators, context =
1059
+ prepare_propagation(signalled, false, call_info)
647
1060
  if source_events
648
- for source_ev in source_events
649
- source_ev.generator.signalling(source_ev, signalled)
650
- end
1061
+ log(:generator_propagate_events, false, source_events, signalled)
651
1062
 
652
1063
  if signalled.self_owned?
653
1064
  next_step = gather_propagation(current_step) do
654
- propagation_context(source_events | source_generators) do |result|
1065
+ propagation_context(source_events | source_generators) do
655
1066
  begin
1067
+ propagation_info.add_generator_call(signalled)
656
1068
  signalled.call_without_propagation(context)
657
1069
  rescue Roby::LocalizedError => e
658
- signalled.emit_failed(e)
1070
+ if signalled.command_emitted?
1071
+ add_error(e)
1072
+ else
1073
+ signalled.emit_failed(e)
1074
+ end
659
1075
  rescue Exception => e
660
- signalled.emit_failed(Roby::CommandFailed.new(e, signalled))
1076
+ if signalled.command_emitted?
1077
+ add_error(Roby::CommandFailed.new(e, signalled))
1078
+ else
1079
+ signalled.emit_failed(Roby::CommandFailed.new(e, signalled))
1080
+ end
661
1081
  end
662
1082
  end
663
1083
  end
@@ -666,28 +1086,33 @@ def event_propagation_step(current_step)
666
1086
 
667
1087
  if forward_info
668
1088
  next_step ||= Hash.new
669
- next_step[signalled] ||= []
670
- next_step[signalled][0] ||= []
671
- next_step[signalled][0].concat forward_info
1089
+ target_info = (next_step[signalled] ||= [@propagation_step_id += 1, [], []])
1090
+ target_info[PENDING_PROPAGATION_FORWARD].concat(forward_info)
672
1091
  end
673
1092
 
674
- elsif forward_info
675
- source_events, source_generators, context = prepare_propagation(signalled, true, forward_info)
1093
+ elsif !forward_info.empty?
1094
+ source_events, source_generators, context =
1095
+ prepare_propagation(signalled, true, forward_info)
676
1096
  if source_events
677
- for source_ev in source_events
678
- source_ev.generator.forwarding(source_ev, signalled)
679
- end
1097
+ log(:generator_propagate_events, true, source_events, signalled)
680
1098
 
681
1099
  # If the destination event is not owned, but if the peer is not
682
1100
  # connected, the event is our responsibility now.
683
- if signalled.self_owned? || !signalled.owners.any? { |peer| peer != Roby::Distributed && peer.connected? }
1101
+ if signalled.self_owned? || !signalled.owners.any? { |peer| peer != plan.local_owner && peer.connected? }
684
1102
  next_step = gather_propagation(current_step) do
685
- propagation_context(source_events | source_generators) do |result|
1103
+ propagation_context(source_events | source_generators) do
686
1104
  begin
687
- signalled.emit_without_propagation(context)
1105
+ if event = signalled.emit_without_propagation(context)
1106
+ propagation_info.add_event_emission(event)
1107
+ emitted_events << event
1108
+ end
688
1109
  rescue Roby::LocalizedError => e
1110
+ Roby.warn "Internal Error: #emit_without_propagation emitted a LocalizedError exception. This is unsupported and will become a fatal error in the future. You should usually replace raise with engine.add_error"
1111
+ Roby.display_exception(Roby.logger.io(:warn), e, false)
689
1112
  add_error(e)
690
1113
  rescue Exception => e
1114
+ Roby.warn "Internal Error: #emit_without_propagation emitted an exception. This is unsupported and will become a fatal error in the future. You should create a proper localized error and replace raise with engine.add_error"
1115
+ Roby.display_exception(Roby.logger.io(:warn), e, false)
691
1116
  add_error(Roby::EmissionFailed.new(e, signalled))
692
1117
  end
693
1118
  end
@@ -700,165 +1125,250 @@ def event_propagation_step(current_step)
700
1125
  current_step
701
1126
  end
702
1127
 
703
- # Checks if +error+ is being repaired in the corresponding plan. Note that
704
- # +error+ is supposed to be the original exception, not the corresponding
705
- # ExecutionException object
706
- def remove_inhibited_exceptions(exceptions)
707
- exceptions.find_all do |e, _|
708
- error = e.exception
709
- if !error.respond_to?(:failed_event) ||
710
- !(failure_point = error.failed_event)
711
- true
712
- else
713
- plan.repairs_for(failure_point).empty?
714
- end
1128
+ # Graph visitor that propagates exceptions in the dependency graph
1129
+ class ExceptionPropagationVisitor < Relations::ForkMergeVisitor
1130
+ attr_reader :exception_handler
1131
+ attr_reader :handled_exceptions
1132
+ attr_reader :unhandled_exceptions
1133
+
1134
+ def initialize(graph, object, origin, origin_neighbours = graph.out_neighbours(origin), &exception_handler)
1135
+ super(graph, object, origin, origin_neighbours)
1136
+ @exception_handler = exception_handler
1137
+ @handled_exceptions = Array.new
1138
+ @unhandled_exceptions = Array.new
715
1139
  end
716
- end
717
1140
 
718
- # Removes the set of repairs defined on #plan that are not useful
719
- # anymore, and returns it.
720
- def remove_useless_repairs
721
- finished_repairs = plan.repairs.dup.delete_if { |_, task| task.starting? || task.running? }
722
- for repair in finished_repairs
723
- plan.remove_repair(repair[1])
1141
+ def propagate_object(u, v, obj)
1142
+ raise if u == v
1143
+ if !obj.handled?
1144
+ obj.propagate(u, v)
1145
+ obj
1146
+ end
724
1147
  end
725
1148
 
726
- finished_repairs
727
- end
1149
+ def fork_object(obj)
1150
+ obj.fork
1151
+ end
728
1152
 
729
- # Performs exception propagation for the given ExecutionException objects
730
- # Returns all exceptions which have found no handlers in the task hierarchy
731
- def propagate_exceptions(exceptions)
732
- fatal = [] # the list of exceptions for which no handler has been found
733
-
734
- # Remove finished repairs. Those are still considered during this cycle,
735
- # as it is possible that some actions have been scheduled for the
736
- # beginning of the next cycle through #once
737
- finished_repairs = remove_useless_repairs
738
- # Remove remove exceptions for which a repair exists
739
- exceptions = remove_inhibited_exceptions(exceptions)
740
-
741
- # Install new repairs based on the HandledBy task relation. If a repair
742
- # is installed, remove the exception from the set of errors to handle
743
- exceptions.delete_if do |e, _|
744
- # Check for handled_by relations which would be able to handle +e+
745
- error = e.exception
746
- next unless (failed_event = error.failed_event)
747
- next unless (failed_task = error.failed_task)
748
- next if finished_repairs.has_key?(failed_event)
749
-
750
- failed_generator = error.failed_generator
751
-
752
- repair = failed_task.find_error_handler do |repairing_task, event_set|
753
- event_set.find do |repaired_generator|
754
- repaired_generator = failed_task.event(repaired_generator)
755
-
756
- !repairing_task.finished? &&
757
- (repaired_generator == failed_generator ||
758
- Roby::EventStructure::Forwarding.reachable?(failed_generator, repaired_generator))
759
- end
760
- end
1153
+ def handle_examine_vertex(u)
1154
+ e = vertex_to_object.fetch(u)
1155
+ return if !e
761
1156
 
762
- if repair
763
- plan.add_repair(failed_event, repair)
764
- if repair.pending?
765
- once { repair.start! }
766
- end
767
- true
768
- else
769
- false
1157
+ if e.handled = exception_handler[e, u]
1158
+ handled_exceptions << e
1159
+ elsif out_degree[u] == 0
1160
+ unhandled_exceptions << e
770
1161
  end
771
1162
  end
1163
+ end
772
1164
 
773
- while !exceptions.empty?
774
- by_task = Hash.new { |h, k| h[k] = Array.new }
775
- by_task = exceptions.inject(by_task) do |by_task, (e, parents)|
776
- unless e.task
777
- Roby.log_exception(e.exception, Roby, :fatal)
778
- raise NotImplementedError, "we do not yet handle exceptions from external event generators. Got #{e.exception.full_message}"
779
- end
780
- parents ||= e.task.parent_objects(Roby::TaskStructure::Hierarchy)
781
-
782
- has_parent = false
783
- [*parents].each do |parent|
784
- next if parent.finished?
785
-
786
- if has_parent # we have more than one parent
787
- e = e.fork
1165
+ # The core exception propagation algorithm
1166
+ #
1167
+ # @param [Array<(ExecutionException,Array<Task>)>] exceptions the set of
1168
+ # exceptions to propagate, as well as the parents that towards which
1169
+ # we should propagate them (if empty, all parents)
1170
+ #
1171
+ # @yieldparam [ExecutionException] exception the exception that is being
1172
+ # propagated
1173
+ # @yieldparam [Task,Plan] handling_object the object we want to test
1174
+ # whether it handles the exception or not
1175
+ # @yieldreturn [Boolean] true if the exception is handled, false
1176
+ # otherwise
1177
+ #
1178
+ # @return [Array<(ExecutionException,Array<Task>)>] the set of unhandled
1179
+ # exceptions, as a mapping from an exception description to the set of
1180
+ # tasks that are affected by it
1181
+ def propagate_exception_in_plan(exceptions)
1182
+ propagation_graph = dependency_graph.reverse
1183
+
1184
+ # Propagate the exceptions in the hierarchy
1185
+ handled_unhandled = Array.new
1186
+ exceptions.each do |exception, parents|
1187
+ origin = exception.origin
1188
+ if parents
1189
+ filtered_parents = parents.find_all { |t| t.depends_on?(origin) }
1190
+ if filtered_parents != parents
1191
+ warn "some parents specified for #{exception.exception}(#{exception.exception.class}) are actually not parents of #{origin}, they got filtered out"
1192
+ (parents - filtered_parents).each do |task|
1193
+ warn " #{task}"
788
1194
  end
789
1195
 
790
- parent_exceptions = by_task[parent]
791
- if s = parent_exceptions.find { |s| s.siblings.include?(e) }
792
- s.merge(e)
793
- else parent_exceptions << e
1196
+ if filtered_parents.empty?
1197
+ parents = propagation_graph.out_neighbours(origin)
1198
+ else
1199
+ parents = filtered_parents
794
1200
  end
795
-
796
- has_parent = true
797
1201
  end
1202
+ else
1203
+ parents = propagation_graph.out_neighbours(origin)
1204
+ end
798
1205
 
799
- # Add unhandled exceptions to the fatal set. Merge siblings
800
- # exceptions if possible
801
- unless has_parent
802
- if s = fatal.find { |s| s.siblings.include?(e) }
803
- s.merge(e)
804
- else fatal << e
1206
+ debug do
1207
+ debug "propagating exception "
1208
+ log_pp :debug, exception
1209
+ if !parents.empty?
1210
+ debug " constrained to parents"
1211
+ log_nest(2) do
1212
+ parents.each do |p|
1213
+ log_pp :debug, p
1214
+ end
805
1215
  end
806
1216
  end
807
-
808
- by_task
1217
+ break
809
1218
  end
810
1219
 
811
- parent_trees = by_task.map do |task, _|
812
- [task, task.reverse_generated_subgraph(Roby::TaskStructure::Hierarchy)]
1220
+ visitor = ExceptionPropagationVisitor.new(propagation_graph, exception, origin, parents) do |e, task|
1221
+ yield(e, task)
813
1222
  end
1223
+ visitor.visit
814
1224
 
815
- # Handle the exception in all tasks that are in no other parent trees
816
- new_exceptions = ValueSet.new
817
- by_task.each do |task, task_exceptions|
818
- if parent_trees.find { |t, tree| t != task && tree.include?(task) }
819
- task_exceptions.each { |e| new_exceptions << [e, [task]] }
820
- next
821
- end
1225
+ unhandled = visitor.unhandled_exceptions.inject { |a, b| a.merge(b) }
1226
+ handled = visitor.handled_exceptions.inject { |a, b| a.merge(b) }
1227
+ handled_unhandled << [handled, unhandled]
1228
+ end
822
1229
 
823
- task_exceptions.each do |e|
824
- next if e.handled?
825
- handled = task.handle_exception(e)
826
1230
 
1231
+ exceptions_handled_by = Array.new
1232
+ unhandled_exceptions = Array.new
1233
+ handled_unhandled.each do |handled, e|
1234
+ if e
1235
+ if e.handled = yield(e, plan)
827
1236
  if handled
828
- handled_exception(e, task)
829
- e.handled = true
1237
+ handled_by = (handled.propagation_leafs.to_set << plan)
1238
+ exceptions_handled_by << [handled.merge(e), handled_by]
830
1239
  else
831
- # We do not have the framework to handle concurrent repairs
832
- # For now, the first handler is the one ...
833
- new_exceptions << e
834
- e.trace << task
1240
+ handled = e
1241
+ exceptions_handled_by << [e, [plan].to_set]
1242
+ end
1243
+ else
1244
+ affected_tasks = e.trace.vertices.to_set
1245
+ if handled
1246
+ affected_tasks -= handled.trace.vertices
1247
+ exceptions_handled_by << [handled, handled.propagation_leafs.to_set]
835
1248
  end
1249
+ unhandled_exceptions << [e, affected_tasks]
836
1250
  end
1251
+ else
1252
+ exceptions_handled_by << [handled, handled.propagation_leafs.to_set]
837
1253
  end
1254
+ end
838
1255
 
839
- exceptions = new_exceptions
1256
+ debug do
1257
+ debug "#{unhandled_exceptions.size} unhandled exceptions remain"
1258
+ log_nest(2) do
1259
+ unhandled_exceptions.each do |e, affected_tasks|
1260
+ log_pp :debug, e
1261
+ debug "Affects #{affected_tasks.size} tasks"
1262
+ log_nest(2) do
1263
+ affected_tasks.each do |t|
1264
+ log_pp :debug, t
1265
+ end
1266
+ end
1267
+ end
1268
+ end
1269
+ break
840
1270
  end
1271
+ return unhandled_exceptions, exceptions_handled_by
1272
+ end
1273
+
1274
+ # Propagation exception phase, checking if tasks and/or the main plan
1275
+ # are handling the exceptions
1276
+ #
1277
+ # @param [Array<(ExecutionException,Array<Task>)>] exceptions the set of
1278
+ # exceptions to propagate, as well as the parents that towards which
1279
+ # we should propagate them (if empty, all parents)
1280
+ # @return (see propagate_exception_in_plan)
1281
+ def propagate_exceptions(exceptions)
1282
+ if exceptions.empty?
1283
+ return Array.new, Array.new, Array.new
1284
+ end
1285
+
1286
+ # Remove all exception that are not associated with a task
1287
+ exceptions, free_events_exceptions = exceptions.partition do |e, _|
1288
+ e.origin
1289
+ end
1290
+ # Normalize the free events exceptions
1291
+ free_events_exceptions = free_events_exceptions.map do |e, _|
1292
+ if e.exception.failed_generator.plan
1293
+ [e, Set[e.exception.failed_generator]]
1294
+ end
1295
+ end.compact
1296
+
1297
+ debug "Filtering inhibited exceptions"
1298
+ exceptions = log_nest(2) do
1299
+ non_inhibited, _ = remove_inhibited_exceptions(exceptions)
1300
+ # Reset the trace for the real propagation
1301
+ non_inhibited.map do |e, _|
1302
+ _, propagate_through = exceptions.find { |original_e, _| original_e.exception == e.exception }
1303
+ e.reset_trace
1304
+ [e, propagate_through]
1305
+ end
1306
+ end
1307
+
1308
+ debug "Propagating #{exceptions.size} non-inhibited exceptions"
1309
+ log_nest(2) do
1310
+ # Note that the first half of the method filtered the free
1311
+ # events exceptions out of 'exceptions'
1312
+ unhandled, handled = propagate_exception_in_plan(exceptions) do |e, object|
1313
+ object.handle_exception(e)
1314
+ end
841
1315
 
842
- if !fatal.empty?
843
- Roby::ExecutionEngine.debug do
844
- "remaining fatal exceptions: #{fatal.map(&:exception).map(&:to_s).join(", ")}"
1316
+ return unhandled, free_events_exceptions, handled
1317
+ end
1318
+ end
1319
+
1320
+ # Process the given exceptions to remove the ones that are currently
1321
+ # filtered by the plan repairs
1322
+ #
1323
+ # The returned exceptions are propagated, i.e. their #trace method
1324
+ # contains all the tasks that are affected by the absence of a handling
1325
+ # mechanism
1326
+ #
1327
+ # @param [(ExecutionException,Array<Roby::Task>)] exceptions pairs of
1328
+ # exceptions as well as the "root tasks", i.e. the parents of
1329
+ # origin.task towards which they should be propagated
1330
+ # @return [Array<ExecutionException>] the unhandled exceptions
1331
+ def remove_inhibited_exceptions(exceptions)
1332
+ exceptions = exceptions.find_all do |execution_exception, _|
1333
+ execution_exception.origin.plan
1334
+ end
1335
+
1336
+ propagate_exception_in_plan(exceptions) do |e, object|
1337
+ if has_pending_exception_matching?(e, object)
1338
+ true
1339
+ elsif object.respond_to?(:handles_error?)
1340
+ object.handles_error?(e)
845
1341
  end
846
1342
  end
847
- # Call global exception handlers for exceptions in +fatal+. Return the
848
- # set of still unhandled exceptions
849
- fatal.
850
- find_all { |e| !e.handled? }.
851
- reject { |e| plan.handle_exception(e) }
852
1343
  end
853
1344
 
854
- # A set of proc objects which should be executed at the beginning of the
855
- # next execution cycle.
856
- attr_reader :process_once
1345
+ # Query whether the given exception is inhibited in this plan
1346
+ def inhibited_exception?(exception)
1347
+ unhandled, _ = remove_inhibited_exceptions([exception.to_execution_exception])
1348
+ unhandled.empty?
1349
+ end
857
1350
 
858
1351
  # Schedules +block+ to be called at the beginning of the next execution
859
1352
  # cycle, in propagation context.
860
- def once(&block)
861
- process_once.push block
1353
+ #
1354
+ # @param [#fail] sync a synchronization object that is used to
1355
+ # communicate between the once block and the calling thread. The main
1356
+ # use of this parameter is to make sure that #fail is called if the
1357
+ # execution engine quits
1358
+ # @param (see PropagationHandlerMethods#create_propagation_handler)
1359
+ def once(sync: nil, description: 'once block', type: :external_events, **options, &block)
1360
+ waiting_work << sync if sync
1361
+ once_blocks << create_propagation_handler(description: description, type: type, once: true, **options, &block)
1362
+ end
1363
+
1364
+ # Schedules +block+ to be called once after +delay+ seconds passed, in
1365
+ # the propagation context
1366
+ def delayed(delay, description: 'delayed block', **options, &block)
1367
+ handler = PollBlockDefinition.new(description, block, once: true, **options)
1368
+ once do
1369
+ process_every << [handler, cycle_start, delay]
1370
+ end
1371
+ handler.id
862
1372
  end
863
1373
 
864
1374
  # The set of errors which have been generated outside of the plan's
@@ -866,124 +1376,509 @@ def once(&block)
866
1376
  # down.
867
1377
  attr_reader :application_exceptions
868
1378
  def clear_application_exceptions
1379
+ if !@application_exceptions
1380
+ raise RecursivePropagationContext, "unbalanced call to #clear_application_exceptions"
1381
+ end
1382
+
869
1383
  result, @application_exceptions = @application_exceptions, nil
870
1384
  result
871
1385
  end
872
1386
 
873
- # Abort the control loop because of +exceptions+
874
- def reraise(exceptions)
875
- if exceptions.size == 1
876
- e = exceptions.first
877
- if e.kind_of?(Roby::ExecutionException)
878
- e = e.exception
879
- end
880
- raise e, e.message, e.backtrace
881
- else
882
- raise Aborting.new(exceptions)
883
- end
884
- end
885
-
886
- # Process the pending events. The time at each event loop step
887
- # is saved into +stats+.
888
- def process_events(stats = {:start => Time.now})
889
- @application_exceptions = []
890
-
891
- add_timepoint(stats, :real_start)
892
-
893
- # Gather new events and propagate them
894
- events_errors = begin
895
- old_allow_propagation, @allow_propagation = @allow_propagation, true
896
- propagate_events
897
- ensure @allow_propagation = old_allow_propagation
898
- end
899
- add_timepoint(stats, :events)
900
-
901
- # HACK: events_errors is sometime nil here. It shouldn't
902
- events_errors ||= []
1387
+ # Used during exception propagation to inject new errors in the process
1388
+ #
1389
+ # It shall not be accessed directly. Instead, Plan#add_error should be
1390
+ # called
1391
+ attr_reader :additional_errors
903
1392
 
1393
+ # Compute the set of fatal errors in the current execution state
1394
+ #
1395
+ # @param [Array] events_errors the set of errors gathered during event
1396
+ # propagation
1397
+ # @return [PropagationInfo]
1398
+ def compute_errors(events_errors)
904
1399
  # Generate exceptions from task structure
905
1400
  structure_errors = plan.check_structure
906
- add_timepoint(stats, :structure_check)
1401
+ log_timepoint 'structure_check'
907
1402
 
908
1403
  # Propagate the errors. Note that the plan repairs are taken into
909
- # account in ExecutionEngine.propagate_exceptions drectly. We keep
1404
+ # account in ExecutionEngine.propagate_exceptions directly. We keep
910
1405
  # event and structure errors separate since in the first case there
911
1406
  # is not two-stage handling (all errors that have not been handled
912
1407
  # are fatal), and in the second case we call #check_structure
913
- # again to get the remaining errors
914
- events_errors = propagate_exceptions(events_errors)
915
- propagate_exceptions(structure_errors)
916
- add_timepoint(stats, :exception_propagation)
1408
+ # again to errors that are remaining after the call to the exception
1409
+ # handlers
1410
+ events_errors, free_events_errors, events_handled = propagate_exceptions(events_errors)
1411
+ _, structure_handled = propagate_exceptions(structure_errors)
1412
+ log_timepoint 'exception_propagation'
917
1413
 
918
1414
  # Get the remaining problems in the plan structure, and act on it
919
- fatal_structure_errors = remove_inhibited_exceptions(plan.check_structure)
920
- fatal_errors = fatal_structure_errors.to_a + events_errors
921
- if !fatal_errors.empty?
922
- Roby::ExecutionEngine.info "EE: #{fatal_errors.size} fatal exceptions remaining"
923
- kill_tasks = fatal_errors.inject(ValueSet.new) do |kill_tasks, (error, tasks)|
924
- tasks ||= [*error.origin]
925
- for parent in [*tasks]
926
- new_tasks = parent.reverse_generated_subgraph(Roby::TaskStructure::Hierarchy) - plan.force_gc
927
- if !new_tasks.empty?
928
- fatal_exception(error, new_tasks)
1415
+ structure_errors, structure_inhibited = remove_inhibited_exceptions(plan.check_structure)
1416
+
1417
+ # Partition them by fatal/nonfatal
1418
+ fatal_errors, nonfatal_errors = Array.new, Array.new
1419
+ (structure_errors + events_errors).each do |e, involved_tasks|
1420
+ if e.fatal?
1421
+ fatal_errors << [e, involved_tasks]
1422
+ else
1423
+ nonfatal_errors << [e, involved_tasks]
1424
+ end
1425
+ end
1426
+ kill_tasks = compute_kill_tasks_for_unhandled_fatal_errors(fatal_errors).to_set
1427
+ handled_errors = structure_handled + events_handled
1428
+
1429
+ debug "#{fatal_errors.size} fatal errors found and #{free_events_errors.size} errors involving free events"
1430
+ debug "the fatal errors involve #{kill_tasks.size} non-finalized tasks"
1431
+ return PropagationInfo.new(Set.new, Set.new, kill_tasks, fatal_errors, nonfatal_errors, free_events_errors, handled_errors, structure_inhibited)
1432
+ end
1433
+
1434
+ # Whether this EE has asynchronous waiting work waiting to be processed
1435
+ def has_waiting_work?
1436
+ # Filter out unscheduled promises (promises on which #execute was
1437
+ # not called). If they are unscheduled, we're not waiting on them
1438
+ waiting_work.any? { |w| !w.unscheduled? }
1439
+ end
1440
+
1441
+ # Process asynchronous work registered in {#waiting_work} to clear
1442
+ # completed work and/or handle errors that were not handled by the async
1443
+ # object itself (e.g. a {Promise} without a {Promise#on_error} handler)
1444
+ def process_waiting_work
1445
+ finished, not_finished = waiting_work.partition do |work|
1446
+ work.complete?
1447
+ end
1448
+
1449
+ finished.find_all do |work|
1450
+ work.rejected? && (work.respond_to?(:has_error_handler?) && !work.has_error_handler?)
1451
+ end.each do |work|
1452
+ e = work.reason
1453
+ e.set_backtrace(e.backtrace + caller)
1454
+ add_framework_error(e, work.to_s)
1455
+ end
1456
+
1457
+ @waiting_work = not_finished
1458
+ finished
1459
+ end
1460
+
1461
+ # Gathering of all the errors that happened during an event processing
1462
+ # loop and were not handled
1463
+ PropagationInfo = Struct.new :called_generators, :emitted_events, :kill_tasks, :fatal_errors, :nonfatal_errors, :free_events_errors, :handled_errors, :inhibited_errors, :framework_errors do
1464
+ def initialize(called_generators = Set.new,
1465
+ emitted_events = Set.new,
1466
+ kill_tasks = Set.new,
1467
+ fatal_errors = Array.new,
1468
+ nonfatal_errors = Array.new,
1469
+ free_events_errors = Array.new,
1470
+ handled_errors = Array.new,
1471
+ inhibited_errors = Array.new,
1472
+ framework_errors = Array.new)
1473
+
1474
+ self.called_generators = called_generators .to_set
1475
+ self.emitted_events = emitted_events.to_set
1476
+ self.kill_tasks = kill_tasks.to_set
1477
+ self.fatal_errors = fatal_errors
1478
+ self.nonfatal_errors = nonfatal_errors
1479
+ self.free_events_errors = free_events_errors
1480
+ self.handled_errors = handled_errors
1481
+ self.inhibited_errors = inhibited_errors
1482
+ self.framework_errors = framework_errors
1483
+ end
1484
+
1485
+ def merge(results)
1486
+ self.called_generators.merge(results.called_generators)
1487
+ self.emitted_events.merge(results.emitted_events)
1488
+ self.kill_tasks.merge(results.kill_tasks)
1489
+ self.fatal_errors.concat(results.fatal_errors)
1490
+ self.nonfatal_errors.concat(results.nonfatal_errors)
1491
+ self.free_events_errors.concat(results.free_events_errors)
1492
+ self.handled_errors.concat(results.handled_errors)
1493
+ self.inhibited_errors.concat(results.inhibited_errors)
1494
+ self.framework_errors.concat(results.framework_errors)
1495
+ end
1496
+
1497
+ def add_generator_call(generator)
1498
+ called_generators << generator
1499
+ end
1500
+
1501
+ def add_event_emission(event)
1502
+ emitted_events << event
1503
+ end
1504
+
1505
+ def each_called_generator(&block)
1506
+ called_generators.each(&block)
1507
+ end
1508
+
1509
+ def has_called_generators?
1510
+ !called_generators.empty?
1511
+ end
1512
+
1513
+ def each_emitted_event(&block)
1514
+ emitted_events.each(&block)
1515
+ end
1516
+
1517
+ def has_emitted_events?
1518
+ !emitted_events.empty?
1519
+ end
1520
+
1521
+ # Return the exception objects registered in this result object
1522
+ def exceptions
1523
+ fatal_errors.map(&:first) + nonfatal_errors.map(&:first) + free_events_errors.map(&:first)
1524
+ end
1525
+
1526
+ def each_fatal_error(&block)
1527
+ fatal_errors.each(&block)
1528
+ end
1529
+
1530
+ def has_fatal_errors?
1531
+ !fatal_errors.empty?
1532
+ end
1533
+
1534
+ def each_nonfatal_error(&block)
1535
+ nonfatal_errors.each(&block)
1536
+ end
1537
+
1538
+ def has_nonfatal_errors?
1539
+ !nonfatal_errors.empty?
1540
+ end
1541
+
1542
+ def each_free_events_errors(&block)
1543
+ free_events_errors.each(&block)
1544
+ end
1545
+
1546
+ def has_free_events_errors?
1547
+ !free_events_errors.empty?
1548
+ end
1549
+
1550
+ def each_handled_error(&block)
1551
+ handled_errors.each(&block)
1552
+ end
1553
+
1554
+ def has_handled_errors?
1555
+ !handled_errors.empty?
1556
+ end
1557
+
1558
+ def each_inhibited_error(&block)
1559
+ inhibited_errors.each(&block)
1560
+ end
1561
+
1562
+ def has_inhibited_errors?
1563
+ !inhibited_errors.empty?
1564
+ end
1565
+
1566
+ def each_framework_error(&block)
1567
+ framework_errors.each(&block)
1568
+ end
1569
+
1570
+ def has_framework_errors?
1571
+ !framework_errors.empty?
1572
+ end
1573
+
1574
+ def add_framework_errors(errors)
1575
+ framework_errors.concat(errors)
1576
+ end
1577
+
1578
+ def pretty_print(pp)
1579
+ if !emitted_events.empty?
1580
+ pp.text "received #{emitted_events.size} events:"
1581
+ pp.nest(2) do
1582
+ emitted_events.each do |ev|
1583
+ pp.breakable
1584
+ ev.pretty_print(pp)
1585
+ end
1586
+ end
1587
+ end
1588
+ exceptions = self.exceptions
1589
+ if !exceptions.empty?
1590
+ pp.breakable if !emitted_events.empty?
1591
+ pp.text "#{exceptions.size} unhandled exceptions:"
1592
+ pp.nest(2) do
1593
+ exceptions.map do |e|
1594
+ pp.breakable
1595
+ e.pretty_print(pp)
1596
+ end
1597
+ end
1598
+ end
1599
+ if !handled_errors.empty?
1600
+ pp.breakable if !emitted_events.empty? || !exceptions.empty?
1601
+ pp.text "#{handled_errors.size} handled exceptions:"
1602
+ pp.nest(2) do
1603
+ handled_errors.each do |e, handled_by|
1604
+ pp.breakable
1605
+ e.pretty_print(pp)
1606
+ pp.breakable
1607
+ pp.text "Handled by:"
1608
+ pp.nest(2) do
1609
+ handled_by.each do |t|
1610
+ pp.breakable
1611
+ t.pretty_print(pp)
1612
+ end
1613
+ end
929
1614
  end
930
- kill_tasks.merge(new_tasks)
931
1615
  end
932
- kill_tasks
933
1616
  end
934
- if !kill_tasks.empty?
935
- Roby::ExecutionEngine.info do
936
- Roby::ExecutionEngine.info "EE: will kill the following tasks because of unhandled exceptions:"
937
- kill_tasks.each do |task|
938
- Roby::ExecutionEngine.info " " + task.to_s
1617
+ if !framework_errors.empty?
1618
+ pp.breakable if !emitted_events.empty? || !exceptions.empty? || !handled_errors.empty?
1619
+ pp.text "#{framework_errors.size} framework errors"
1620
+ pp.nest(2) do
1621
+ framework_errors.each do |e, source|
1622
+ pp.breakable
1623
+ pp.text "from #{source}: "
1624
+ e.pretty_print(pp)
939
1625
  end
940
- ""
941
1626
  end
942
1627
  end
943
1628
  end
944
- add_timepoint(stats, :exceptions_fatal)
1629
+ end
1630
+
1631
+ # The methods that setup propagation context in ExecutionEngine must not
1632
+ # be called recursively.
1633
+ #
1634
+ # This exception is thrown if such a recursive call is detected
1635
+ class RecursivePropagationContext < RuntimeError; end
1636
+
1637
+ # Some methods require to be called within a gather_* block. This
1638
+ # exception is raised when they're called outside of it
1639
+ class NotPropagationContext < RuntimeError; end
1640
+
1641
+ # The inside part of the event loop
1642
+ #
1643
+ # It gathers initial events and errors and propagate them
1644
+ #
1645
+ # @return [PropagationInfo] what happened during the propagation
1646
+ # @raise RecursivePropagationContext if called recursively
1647
+ def process_events(raise_framework_errors: Roby.app.abort_on_application_exception?, garbage_collect_pass: true, &caller_block)
1648
+ if @application_exceptions
1649
+ raise RecursivePropagationContext, "recursive call to process_events"
1650
+ end
1651
+ passed_recursive_check = true # to avoid having a almost-method-global ensure block
1652
+ @application_exceptions = []
1653
+ @emitted_events = Array.new
1654
+
1655
+ @thread_pool.send :synchronize do
1656
+ @thread_pool.send(:ns_prune_pool)
1657
+ end
945
1658
 
946
- garbage_collect(kill_tasks)
947
- add_timepoint(stats, :garbage_collect)
1659
+ # Gather new events and propagate them
1660
+ events_errors = nil
1661
+ next_steps = gather_propagation do
1662
+ events_errors = gather_errors do
1663
+ if caller_block
1664
+ yield
1665
+ caller_block = nil
1666
+ end
948
1667
 
949
- application_errors, @application_exceptions =
950
- @application_exceptions, nil
951
- for error, origin in application_errors
952
- add_framework_error(error, origin)
1668
+ if !quitting? || !garbage_collect([])
1669
+ process_waiting_work
1670
+ log_timepoint 'workers'
1671
+ gather_external_events
1672
+ log_timepoint 'external_events'
1673
+ call_propagation_handlers
1674
+ log_timepoint 'propagation_handlers'
1675
+ end
1676
+ end
953
1677
  end
954
1678
 
955
- if Roby.app.abort_on_exception? && !fatal_errors.empty?
956
- reraise(fatal_errors.map { |e, _| e })
1679
+ propagation_info = propagate_events_and_errors(next_steps, events_errors, garbage_collect_pass: garbage_collect_pass)
1680
+ if Roby.app.abort_on_exception? && !all_errors.fatal_errors.empty?
1681
+ raise Aborting.new(propagation_info.each_fatal_error.map(&:exception))
957
1682
  end
1683
+ propagation_info.framework_errors.concat(@application_exceptions)
1684
+ propagation_info
958
1685
 
959
1686
  ensure
960
- @application_exceptions = nil
1687
+ if passed_recursive_check
1688
+ process_pending_application_exceptions(raise_framework_errors: raise_framework_errors)
1689
+ end
961
1690
  end
962
1691
 
963
- # Hook called when a set of tasks is being killed because of an exception
964
- def fatal_exception(error, tasks)
965
- super if defined? super
966
- Roby.format_exception(error.exception).each do |line|
967
- ExecutionEngine.warn line
1692
+ # Tests are using a special mode for propagation, in which everything is
1693
+ # resolved when #emit or #call is called, including error handling. This
1694
+ # mode is implemented using this method
1695
+ #
1696
+ # When errors occur in this mode, the exceptions are raised directly.
1697
+ # This is useful in tests as, this way, we are sure that the exception
1698
+ # will not get overlooked
1699
+ #
1700
+ # If multiple errors are raised in a single call (this is possible due
1701
+ # to Roby's error handling mechanisms), the method will raise
1702
+ # SynchronousEventProcessingMultipleErrors to wrap all the exceptions
1703
+ # into one.
1704
+ def process_events_synchronous(seeds = Hash.new, initial_errors = Array.new, enable_scheduler: false, raise_errors: true)
1705
+ Roby.warn_deprecated "#process_events_synchronous is deprecated, use the expect_execution harness instead"
1706
+
1707
+ if @application_exceptions
1708
+ raise RecursivePropagationContext, "recursive call to process_events"
1709
+ end
1710
+ passed_recursive_check = true # to avoid having a almost-method-global ensure block
1711
+ @application_exceptions = []
1712
+
1713
+ # Save early for the benefit of the 'ensure' block
1714
+ current_scheduler_enabled = scheduler.enabled?
1715
+
1716
+ if (!seeds.empty? || !initial_errors.empty?) && block_given?
1717
+ raise ArgumentError, "cannot provide both seeds/inital errors and a block"
1718
+ elsif block_given?
1719
+ seeds = gather_propagation do
1720
+ initial_errors = gather_errors do
1721
+ yield
1722
+ end
1723
+ end
1724
+ end
1725
+
1726
+ scheduler.enabled = enable_scheduler
1727
+
1728
+ propagation_info = propagate_events_and_errors(seeds, initial_errors, garbage_collect_pass: false)
1729
+ if !propagation_info.kill_tasks.empty?
1730
+ gc_initial_errors = nil
1731
+ gc_seeds = gather_propagation do
1732
+ gc_initial_errors = gather_errors do
1733
+ garbage_collect(propagation_info.kill_tasks)
1734
+ end
1735
+ end
1736
+ gc_errors = propagate_events_and_errors(gc_seeds, gc_initial_errors, garbage_collect_pass: false)
1737
+ propagation_info.merge(gc_errors)
1738
+ end
1739
+
1740
+ if raise_errors
1741
+ propagation_info = propagation_info.exceptions
1742
+ if propagation_info.size == 1
1743
+ raise propagation_info.first
1744
+ elsif !propagation_info.empty?
1745
+ raise SynchronousEventProcessingMultipleErrors.new(propagation_info.map(&:exception))
1746
+ end
1747
+ else
1748
+ propagation_info
1749
+ end
1750
+
1751
+ rescue SynchronousEventProcessingMultipleErrors => e
1752
+ raise SynchronousEventProcessingMultipleErrors.new(e.errors + clear_application_exceptions)
1753
+
1754
+ rescue Exception => e
1755
+ if passed_recursive_check
1756
+ application_exceptions = clear_application_exceptions
1757
+ if !application_exceptions.empty?
1758
+ raise SynchronousEventProcessingMultipleErrors.new(application_exceptions.map(&:first) + [e])
1759
+ else raise e
1760
+ end
1761
+ else
1762
+ raise e
1763
+ end
1764
+
1765
+ ensure
1766
+ if passed_recursive_check && @application_exceptions
1767
+ process_pending_application_exceptions
968
1768
  end
1769
+ scheduler.enabled = current_scheduler_enabled
1770
+ end
1771
+
1772
+
1773
+ # Propagate an initial set of event propagations and errors
1774
+ #
1775
+ # @param [Array] next_steps the next propagations, as returned by
1776
+ # {#gather_propagation}
1777
+ # @param [Array] initial_errors a set of errors that should be
1778
+ # propagated
1779
+ # @param [Boolean] garbage_collect_pass whether the garbage collection
1780
+ # pass should be performed or not. It is used in the tests' codepath
1781
+ # for {EventGenerator#call} and {EventGenerator#emit}.
1782
+ # @return [PropagationInfo] what happened during the propagation
1783
+ # and propagated
1784
+ def propagate_events_and_errors(next_steps, initial_errors, garbage_collect_pass: true)
1785
+ propagation_info = PropagationInfo.new
1786
+ events_errors = initial_errors.dup
1787
+ begin
1788
+ log_timepoint_group 'event_propagation_phase' do
1789
+ events_errors.concat(event_propagation_phase(next_steps, propagation_info))
1790
+ end
1791
+
1792
+ next_steps = gather_propagation do
1793
+ exception_propagation_errors, error_phase_results = nil
1794
+ log_timepoint_group 'error_handling_phase' do
1795
+ exception_propagation_errors = gather_errors do
1796
+ error_phase_results = error_handling_phase(events_errors)
1797
+ end
1798
+ end
1799
+
1800
+ add_exceptions_for_inhibition(error_phase_results.each_fatal_error)
1801
+ propagation_info.merge(error_phase_results)
1802
+ garbage_collection_errors = gather_errors do
1803
+ plan.generate_induced_errors(error_phase_results)
1804
+ if garbage_collect_pass
1805
+ garbage_collect(error_phase_results.kill_tasks)
1806
+ else []
1807
+ end
1808
+ end
1809
+ events_errors = (exception_propagation_errors + garbage_collection_errors)
1810
+ log_timepoint 'garbage_collect'
1811
+ end
1812
+ end while !next_steps.empty? || !events_errors.empty?
1813
+ propagation_info
969
1814
  end
970
1815
 
971
- # Hook called when an exception +e+ has been handled by +task+
972
- def handled_exception(e, task); super if defined? super end
1816
+ # Tests whether there is an exception registered by
1817
+ # {#add_fatal_exceptions_for_inhibition} for a given error and object
1818
+ #
1819
+ # @param [ExecutionException] e
1820
+ # @param [Task,Plan] the handling object
1821
+ def has_pending_exception_matching?(e, object)
1822
+ @pending_exceptions[object] && @pending_exceptions[object].include?([e.exception.class, e.origin])
1823
+ end
1824
+
1825
+ # Register a set of fatal exceptions to ensure that they will be
1826
+ # inhibited in the next exception propagation cycles
1827
+ def add_exceptions_for_inhibition(fatal_errors)
1828
+ fatal_errors.each do |exception, involved_tasks|
1829
+ involved_tasks.each do |t|
1830
+ (@pending_exceptions[t] ||= Set.new) <<
1831
+ [exception.exception.class, exception.origin]
1832
+ end
1833
+ end
1834
+ end
1835
+
1836
+ def unmark_finished_missions_and_permanent_tasks
1837
+ to_unmark = plan.task_index.by_predicate[:finished?] | plan.task_index.by_predicate[:failed?]
1838
+
1839
+ finished_missions = (plan.mission_tasks & to_unmark)
1840
+ # Remove all missions that are finished
1841
+ for finished_mission in finished_missions
1842
+ if !finished_mission.being_repaired?
1843
+ plan.unmark_mission_task(finished_mission)
1844
+ end
1845
+ end
1846
+ finished_permanent = (plan.permanent_tasks & to_unmark)
1847
+ for finished_permanent in (plan.permanent_tasks & to_unmark)
1848
+ if !finished_permanent.being_repaired?
1849
+ plan.unmark_permanent_task(finished_permanent)
1850
+ end
1851
+ end
1852
+ end
973
1853
 
974
1854
  # Kills and removes all unneeded tasks. +force_on+ is a set of task
975
1855
  # whose garbage-collection must be performed, even though those tasks
976
1856
  # are actually useful for the system. This is used to properly kill
977
1857
  # tasks for which errors have been detected.
1858
+ #
1859
+ # @return [Boolean] true if events have been called (thus requiring
1860
+ # some propagation) and false otherwise
978
1861
  def garbage_collect(force_on = nil)
979
1862
  if force_on && !force_on.empty?
980
- ExecutionEngine.info "GC: adding #{force_on.size} tasks in the force_gc set"
981
- plan.force_gc.merge(force_on.to_value_set)
1863
+ info "GC: adding #{force_on.size} tasks in the force_gc set"
1864
+ mismatching_plan = force_on.find_all do |t|
1865
+ if t.plan == self.plan
1866
+ plan.force_gc << t
1867
+ false
1868
+ else
1869
+ true
1870
+ end
1871
+ end
1872
+ if !mismatching_plan.empty?
1873
+ raise ArgumentError, "#{mismatching_plan.map { |t| "#{t}(plan=#{t.plan})" }.join(", ")} have been given to #{self}.garbage_collect, but they are not tasks in #{plan}"
1874
+ end
982
1875
  end
983
1876
 
1877
+ unmark_finished_missions_and_permanent_tasks
1878
+
984
1879
  # The set of tasks for which we queued stop! at this cycle
985
1880
  # #finishing? is false until the next event propagation cycle
986
- finishing = ValueSet.new
1881
+ finishing = Set.new
987
1882
  did_something = true
988
1883
  while did_something
989
1884
  did_something = false
@@ -994,138 +1889,163 @@ def garbage_collect(force_on = nil)
994
1889
 
995
1890
  # Remote tasks are simply removed, regardless of other concerns
996
1891
  for t in remote_tasks
997
- ExecutionEngine.debug { "GC: removing the remote task #{t}" }
998
- plan.remove_object(t)
1892
+ debug { "GC: removing the remote task #{t}" }
1893
+ plan.garbage_task(t)
999
1894
  end
1000
1895
 
1001
1896
  break if local_tasks.empty?
1002
1897
 
1003
- if local_tasks.all? { |t| t.pending? || t.finished? }
1898
+ debug do
1899
+ debug "#{local_tasks.size} tasks are unneeded in this plan"
1004
1900
  local_tasks.each do |t|
1005
- ExecutionEngine.debug { "GC: #{t} is not running, removed" }
1006
- plan.garbage(t)
1007
- plan.remove_object(t)
1901
+ debug " #{t} mission=#{plan.mission_task?(t)} permanent=#{plan.permanent_task?(t)}"
1008
1902
  end
1009
1903
  break
1010
1904
  end
1011
1905
 
1012
- # Mark all root local_tasks as garbage
1013
- roots = nil
1014
- 2.times do |i|
1015
- roots = local_tasks.find_all do |t|
1016
- if t.root?
1017
- plan.garbage(t)
1018
- true
1019
- else
1020
- ExecutionEngine.debug { "GC: ignoring #{t}, it is not root" }
1021
- false
1906
+ if local_tasks.all? { |t| t.pending? || t.finished? }
1907
+ local_tasks.each do |t|
1908
+ debug { "GC: #{t} is not running, removed" }
1909
+ if plan.garbage_task(t)
1910
+ did_something = true
1022
1911
  end
1023
1912
  end
1913
+ break
1914
+ end
1024
1915
 
1025
- break if i == 1 || !roots.empty?
1026
-
1027
- # There is a cycle somewhere. Try to break it by removing
1028
- # weak relations within elements of local_tasks
1029
- ExecutionEngine.debug "cycle found, removing weak relations"
1030
-
1031
- local_tasks.each do |t|
1032
- t.each_graph do |rel|
1033
- rel.remove(t) if rel.weak?
1034
- end
1916
+ # Mark all root local_tasks as garbage.
1917
+ roots = local_tasks.dup
1918
+ plan.each_task_relation_graph do |g|
1919
+ next if !g.root_relation? || g.weak?
1920
+ roots.delete_if do |t|
1921
+ g.each_in_neighbour(t).any? { |p| !p.finished? }
1035
1922
  end
1923
+ break if roots.empty?
1036
1924
  end
1037
1925
 
1038
- (roots.to_value_set - finishing - plan.gc_quarantine).each do |local_task|
1039
- if local_task.pending?
1040
- ExecutionEngine.info "GC: removing pending task #{local_task}"
1041
- plan.remove_object(local_task)
1042
- did_something = true
1926
+ (roots.to_set - finishing).each do |local_task|
1927
+ if local_task.pending?
1928
+ info "GC: removing pending task #{local_task}"
1929
+
1930
+ if plan.garbage_task(local_task)
1931
+ did_something = true
1932
+ end
1933
+ elsif local_task.failed_to_start?
1934
+ info "GC: removing task that failed to start #{local_task}"
1935
+ if plan.garbage_task(local_task)
1936
+ did_something = true
1937
+ end
1043
1938
  elsif local_task.starting?
1044
1939
  # wait for task to be started before killing it
1045
- ExecutionEngine.debug { "GC: #{local_task} is starting" }
1046
- elsif !local_task.running?
1047
- ExecutionEngine.debug { "GC: #{local_task} is not running, removed" }
1048
- plan.remove_object(local_task)
1049
- did_something = true
1940
+ debug { "GC: #{local_task} is starting" }
1941
+ elsif local_task.finished?
1942
+ debug { "GC: #{local_task} is not running, removed" }
1943
+ if plan.garbage_task(local_task)
1944
+ did_something = true
1945
+ end
1050
1946
  elsif !local_task.finishing?
1051
- if local_task.event(:stop).controlable?
1052
- ExecutionEngine.debug { "GC: queueing #{local_task}/stop" }
1947
+ if local_task.quarantined?
1948
+ warn "GC: #{local_task} is running but in quarantine"
1949
+ elsif local_task.event(:stop).controlable?
1950
+ debug { "GC: attempting to stop #{local_task}" }
1053
1951
  if !local_task.respond_to?(:stop!)
1054
- ExecutionEngine.fatal "something fishy: #{local_task}/stop is controlable but there is no #stop! method"
1055
- plan.gc_quarantine << local_task
1952
+ warn "something fishy: #{local_task}/stop is controlable but there is no #stop! method, putting in quarantine"
1953
+ plan.quarantine_task(local_task)
1056
1954
  else
1057
1955
  finishing << local_task
1058
- once do
1059
- ExecutionEngine.info { "GC: stopping #{local_task}" }
1060
- local_task.stop!(nil)
1061
- end
1062
1956
  end
1063
1957
  else
1064
- ExecutionEngine.warn "GC: ignored #{local_task}, it cannot be stopped"
1065
- plan.gc_quarantine << local_task
1958
+ warn "GC: #{local_task} cannot be stopped, putting in quarantine"
1959
+ plan.quarantine_task(local_task)
1066
1960
  end
1067
1961
  elsif local_task.finishing?
1068
- ExecutionEngine.debug { "GC: waiting for #{local_task} to finish" }
1962
+ debug do
1963
+ debug "GC: waiting for #{local_task} to finish"
1964
+ local_task.history.each do |ev|
1965
+ debug "GC: #{ev}"
1966
+ end
1967
+ break
1968
+ end
1069
1969
  else
1070
- ExecutionEngine.warn "GC: ignored #{local_task}"
1970
+ warn "GC: ignored #{local_task}"
1071
1971
  end
1072
1972
  end
1073
1973
  end
1074
1974
 
1975
+ finishing.each do |task|
1976
+ task.stop!
1977
+ end
1978
+
1075
1979
  plan.unneeded_events.each do |event|
1076
- plan.remove_object(event)
1980
+ plan.garbage_event(event)
1077
1981
  end
1078
- end
1079
1982
 
1080
- # Do not sleep or call Thread#pass if there is less that
1081
- # this much time left in the cycle
1082
- SLEEP_MIN_TIME = 0.01
1983
+ !finishing.empty?
1984
+ end
1083
1985
 
1084
- # The priority of the control thread
1085
- THREAD_PRIORITY = 10
1986
+ # Do not sleep or call Thread#pass if there is less that
1987
+ # this much time left in the cycle
1988
+ SLEEP_MIN_TIME = 0.01
1086
1989
 
1087
- # Blocks until at least once execution cycle has been done
1088
- def wait_one_cycle
1089
- current_cycle = execute { cycle_index }
1090
- while current_cycle == execute { cycle_index }
1091
- raise ExecutionQuitError if !running?
1092
- sleep(cycle_length)
1093
- end
1094
- end
1990
+ # Blocks until at least once execution cycle has been done
1991
+ def wait_one_cycle
1992
+ current_cycle = execute { cycle_index }
1993
+ while current_cycle == execute { cycle_index }
1994
+ raise ExecutionQuitError if !running?
1995
+ sleep(cycle_length)
1996
+ end
1997
+ end
1095
1998
 
1096
1999
  # Calls the periodic blocks which should be called
1097
2000
  def self.call_every(plan) # :nodoc:
1098
- engine = plan.engine
2001
+ engine = plan.execution_engine
1099
2002
  now = engine.cycle_start
1100
2003
  length = engine.cycle_length
1101
2004
  engine.process_every.map! do |block, last_call, duration|
1102
- begin
1103
- # Check if the nearest timepoint is the beginning of
1104
- # this cycle or of the next cycle
1105
- if !last_call || (duration - (now - last_call)) < length / 2
1106
- block.call
1107
- last_call = now
2005
+ # Check if the nearest timepoint is the beginning of
2006
+ # this cycle or of the next cycle
2007
+ if !last_call || (duration - (now - last_call)) < length / 2
2008
+ if !block.call(engine, engine.plan)
2009
+ next
1108
2010
  end
1109
- rescue Exception => e
1110
- engine.add_framework_error(e, "#call_every, in #{block}")
2011
+
2012
+ last_call = now
1111
2013
  end
1112
2014
  [block, last_call, duration]
1113
- end
2015
+ end.compact!
1114
2016
  end
1115
2017
 
1116
- # A list of threads which are currently waitiing for the control thread
1117
- # (see for instance Roby.execute)
2018
+ # A list of threaded objects waiting for the control thread
2019
+ #
2020
+ # Objects registered here will be notified them by calling {#fail} when
2021
+ # it quits. In addition, {#join_all_waiting_work} will wait for all
2022
+ # pending jobs to finish.
2023
+ #
2024
+ # Note that all {Concurrent::Obligation} subclasses fit the bill
1118
2025
  #
1119
- # #run will raise ExecutionQuitError on this threads if they
1120
- # are still waiting while the control is quitting
1121
- attr_reader :waiting_threads
2026
+ # @return [Array<#fail,#complete?>]
2027
+ attr_reader :waiting_work
1122
2028
 
1123
2029
  # A set of blocks that are called at each cycle end
1124
2030
  attr_reader :at_cycle_end_handlers
1125
2031
 
1126
- # Call +block+ at the end of the execution cycle
1127
- def at_cycle_end(&block)
1128
- at_cycle_end_handlers << block
2032
+ # Adds a block to be called at the end of each execution cycle
2033
+ #
2034
+ # @return [Object] an object that allows to identify the block so that
2035
+ # it can be removed with {#remove_at_cycle_end}
2036
+ #
2037
+ # @yieldparam [Plan] plan the plan on which this engine runs
2038
+ def at_cycle_end(description: 'at_cycle_end', **options, &block)
2039
+ handler = PollBlockDefinition.new(description, block, **options)
2040
+ at_cycle_end_handlers << handler
2041
+ handler.object_id
2042
+ end
2043
+
2044
+ # Removes a handler added by {#at_cycle_end}
2045
+ #
2046
+ # @param [Object] handler_id the value returned by {#at_cycle_end}
2047
+ def remove_at_cycle_end(handler_id)
2048
+ at_cycle_end_handlers.delete_if { |h| h.object_id == handler_id }
1129
2049
  end
1130
2050
 
1131
2051
  # A set of blocks which are called every cycle
@@ -1136,110 +2056,120 @@ def at_cycle_end(&block)
1136
2056
  #
1137
2057
  # The returned value is the periodic handler ID. It can be passed to
1138
2058
  # #remove_periodic_handler to undefine it.
1139
- def every(duration, &block)
2059
+ def every(duration, description: 'periodic handler', **options, &block)
2060
+ handler = PollBlockDefinition.new(description, block, **options)
2061
+
1140
2062
  once do
1141
- block.call
1142
- process_every << [block, cycle_start, duration]
2063
+ if handler.call(self, plan)
2064
+ process_every << [handler, cycle_start, duration]
2065
+ end
1143
2066
  end
1144
- block.object_id
2067
+ handler.id
1145
2068
  end
1146
2069
 
1147
2070
  # Removes a periodic handler defined by #every. +id+ is the value
1148
2071
  # returned by #every.
1149
2072
  def remove_periodic_handler(id)
1150
2073
  execute do
1151
- process_every.delete_if { |spec| spec[0].object_id == id }
2074
+ process_every.delete_if { |spec| spec[0].id == id }
1152
2075
  end
1153
2076
  end
1154
2077
 
1155
2078
  # The execution thread if there is one running
1156
- attr_accessor :thread
2079
+ attr_accessor :thread
1157
2080
  # True if an execution thread is running
1158
- def running?; !!@thread end
2081
+ attr_predicate :running?, true
1159
2082
 
1160
- # The cycle length in seconds
1161
- attr_reader :cycle_length
2083
+ # The cycle length in seconds
2084
+ attr_reader :cycle_length
1162
2085
 
1163
- # The starting Time of this cycle
1164
- attr_reader :cycle_start
2086
+ # The starting Time of this cycle
2087
+ attr_reader :cycle_start
1165
2088
 
1166
- # The number of this cycle since the beginning
1167
- attr_reader :cycle_index
1168
-
1169
- # True if the current thread is the execution thread of this engine
1170
- #
1171
- # See #outside_control? for a discussion of the use of #inside_control?
1172
- # and #outside_control? when testing the threading context
1173
- def inside_control?
1174
- t = thread
1175
- !t || t == Thread.current
1176
- end
2089
+ # The number of this cycle since the beginning
2090
+ attr_reader :cycle_index
2091
+
2092
+ # True if the current thread is the execution thread of this engine
2093
+ #
2094
+ # See #outside_control? for a discussion of the use of #inside_control?
2095
+ # and #outside_control? when testing the threading context
2096
+ def inside_control?
2097
+ t = thread
2098
+ !t || t == Thread.current
2099
+ end
1177
2100
 
1178
2101
  # True if the current thread is not the execution thread of this
1179
2102
  # engine, or if there is not control thread. When you check the current
1180
2103
  # thread context, always use a negated form. Do not do
1181
- #
1182
- # if Roby.inside_control?
1183
- # ERROR
1184
- # end
1185
- #
1186
- # Do instead
1187
- #
1188
- # if !Roby.outside_control?
1189
- # ERROR
1190
- # end
1191
- #
2104
+ #
2105
+ # if Roby.inside_control?
2106
+ # ERROR
2107
+ # end
2108
+ #
2109
+ # Do instead
2110
+ #
2111
+ # if !Roby.outside_control?
2112
+ # ERROR
2113
+ # end
2114
+ #
1192
2115
  # Since the first form will fail if there is no control thread, while
1193
2116
  # the second form will work. Use the first form only if you require
1194
2117
  # that there actually IS a control thread.
1195
- def outside_control?
1196
- t = thread
1197
- !t || t != Thread.current
1198
- end
1199
-
1200
- # Main event loop. Valid options are
1201
- # cycle:: the cycle duration in seconds (default: 0.1)
1202
- def run(options = {})
1203
- if running?
1204
- raise "there is already a control running in thread #{@thread}"
1205
- end
2118
+ def outside_control?
2119
+ t = thread
2120
+ !t || t != Thread.current
2121
+ end
2122
+
2123
+ class AlreadyRunning < RuntimeError; end
1206
2124
 
1207
- options = validate_options options, :cycle => 0.1
2125
+ # Main event loop. Valid options are
2126
+ # cycle:: the cycle duration in seconds (default: 0.1)
2127
+ def run(cycle: 0.1)
2128
+ if running?
2129
+ raise AlreadyRunning, "#run has already been called"
2130
+ end
2131
+ self.running = true
1208
2132
 
1209
- @quit = 0
1210
2133
  @allow_propagation = false
2134
+ @waiting_work = Concurrent::Array.new
1211
2135
 
1212
- # Start the control thread and wait for @thread to be set
1213
- Roby.condition_variable(true) do |cv, mt|
1214
- mt.synchronize do
1215
- @thread = Thread.new do
1216
- @thread = Thread.current
1217
- @thread.priority = THREAD_PRIORITY
2136
+ @thread = Thread.current
2137
+ @thread.name = "MAIN"
1218
2138
 
1219
- begin
1220
- mt.synchronize { cv.signal }
1221
- @cycle_length = options[:cycle]
1222
- event_loop
1223
-
1224
- ensure
1225
- Roby.synchronize do
1226
- # reset the options only if we are in the control thread
1227
- @thread = nil
1228
- waiting_threads.each do |th|
1229
- th.raise ExecutionQuitError
1230
- end
1231
- finalizers.each { |blk| blk.call rescue nil }
1232
- @quit = 0
1233
- @allow_propagation = true
1234
- end
1235
- end
1236
- end
1237
- cv.wait(mt)
2139
+ @cycle_length = cycle
2140
+ event_loop
2141
+
2142
+ ensure
2143
+ self.running = false
2144
+ @thread = nil
2145
+ waiting_work.delete_if do |w|
2146
+ next(true) if w.complete?
2147
+
2148
+ # rubocop:disable Lint/HandleExceptions
2149
+ begin
2150
+ w.fail ExecutionQuitError
2151
+ Roby.warn "forcefully terminated #{w} on quit"
2152
+ rescue Concurrent::MultipleAssignmentError
2153
+ # Race condition: something completed the promise while
2154
+ # we were trying to make it fail
2155
+ end
2156
+ # rubocop:enable Lint/HandleExceptions
2157
+
2158
+ true
2159
+ end
2160
+ finalizers.each do |blk|
2161
+ begin
2162
+ blk.call
2163
+ rescue Exception => e
2164
+ Roby.warn "finalizer #{blk} failed"
2165
+ Roby.log_exception_with_backtrace(e, Roby, :warn)
1238
2166
  end
1239
2167
  end
1240
- end
2168
+ @quit = 0
2169
+ @allow_propagation = true
2170
+ end
1241
2171
 
1242
- attr_reader :last_stop_count # :nodoc:
2172
+ attr_reader :last_stop_count # :nodoc:
1243
2173
 
1244
2174
  # Sets up the plan for clearing: it discards all missions and undefines
1245
2175
  # all permanent tasks and events.
@@ -1247,268 +2177,270 @@ def run(options = {})
1247
2177
  # Returns nil if the plan is cleared, and the set of remaining tasks
1248
2178
  # otherwise. Note that quaranteened tasks are not counted as remaining,
1249
2179
  # as it is not possible for the execution engine to stop them.
1250
- def clear
1251
- Roby.synchronize do
1252
- plan.missions.dup.each { |t| plan.unmark_mission(t) }
1253
- plan.permanent_tasks.dup.each { |t| plan.unmark_permanent(t) }
1254
- plan.permanent_events.dup.each { |t| plan.unmark_permanent(t) }
1255
- plan.force_gc.merge( plan.known_tasks )
1256
-
1257
- quaranteened_subplan = plan.useful_task_component(nil, ValueSet.new, plan.gc_quarantine.dup)
1258
- remaining = plan.known_tasks - quaranteened_subplan
1259
-
1260
- if remaining.empty?
1261
- # Have to call #garbage_collect one more to make
1262
- # sure that unneeded events are removed as well
1263
- garbage_collect
1264
- # Done cleaning the tasks, clear the remains
1265
- plan.transactions.each do |trsc|
1266
- trsc.discard_transaction if trsc.self_owned?
1267
- end
1268
- plan.clear
1269
- return
1270
- end
1271
-
1272
- if last_stop_count != remaining.size
1273
- if last_stop_count == 0
1274
- ExecutionEngine.info "control quitting. Waiting for #{remaining.size} tasks to finish (#{plan.size} tasks still in plan)"
1275
- ExecutionEngine.info " " + remaining.to_a.join("\n ")
1276
- else
1277
- ExecutionEngine.info "waiting for #{remaining.size} tasks to finish (#{plan.size} tasks still in plan)"
1278
- ExecutionEngine.info " #{remaining.to_a.join("\n ")}"
1279
- end
1280
- if plan.gc_quarantine.size != 0
1281
- ExecutionEngine.info "#{plan.gc_quarantine.size} tasks in quarantine"
1282
- end
1283
- @last_stop_count = remaining.size
1284
- end
1285
- remaining
1286
- end
1287
- end
1288
-
1289
- # How much time remains before the end of the cycle. Updated by
1290
- # #add_timepoint
1291
- attr_reader :remaining_cycle_time
1292
-
1293
- # Adds to the stats the given duration as the expected duration of the
1294
- # +name+ step. The field in +stats+ is named "expected_#{name}".
1295
- def add_expected_duration(stats, name, duration)
1296
- stats[:"expected_#{name}"] = Time.now + duration - stats[:start]
1297
- end
1298
-
1299
- # Adds in +stats+ the current time as a timepoint named +time+, and
1300
- # update #remaining_cycle_time
1301
- def add_timepoint(stats, name)
1302
- stats[:end] = stats[name] = Time.now - stats[:start]
1303
- @remaining_cycle_time = cycle_length - stats[:end]
2180
+ def clear
2181
+ plan.mission_tasks.dup.each { |t| plan.unmark_mission_task(t) }
2182
+ plan.permanent_tasks.dup.each { |t| plan.unmark_permanent_task(t) }
2183
+ plan.permanent_events.dup.each { |t| plan.unmark_permanent_event(t) }
2184
+ plan.force_gc.merge( plan.tasks )
2185
+
2186
+ quaranteened_subplan = plan.compute_useful_tasks(plan.quarantined_tasks)
2187
+ remaining = plan.tasks - quaranteened_subplan
2188
+
2189
+ @pending_exceptions.clear
2190
+
2191
+ if remaining.empty?
2192
+ # Have to call #garbage_collect one more to make
2193
+ # sure that unneeded events are removed as well
2194
+ garbage_collect
2195
+ # Done cleaning the tasks, clear the remains
2196
+ plan.transactions.each do |trsc|
2197
+ trsc.discard_transaction if trsc.self_owned?
2198
+ end
2199
+ plan.clear
2200
+ emitted_events.clear
2201
+ return
2202
+ end
2203
+ remaining
1304
2204
  end
1305
2205
 
1306
2206
  # If set to true, Roby will warn if the GC cannot be controlled by Roby
1307
2207
  attr_predicate :gc_warning?, true
1308
2208
 
2209
+ def issue_quit_progression_warning(remaining)
2210
+ info "Waiting for #{remaining.size} tasks to finish (#{plan.num_tasks} tasks still in plan) and #{waiting_work.size} async work jobs"
2211
+ remaining.each do |task|
2212
+ info " #{task}"
2213
+ end
2214
+ quarantined = remaining.find_all { |t| t.quarantined? }
2215
+ if quarantined.size != 0
2216
+ info "#{quarantined.size} tasks in quarantine"
2217
+ end
2218
+ end
2219
+
2220
+ # How many seconds between two Interrupt before the execution engine's
2221
+ # loop can forcefully quit
2222
+ INTERRUPT_FORCE_EXIT_DEAD_ZONE = 10
2223
+
1309
2224
  # The main event loop. It returns when the execution engine is asked to
1310
2225
  # quit. In general, this does not need to be called direclty: use #run
1311
2226
  # to start the event loop in a separate thread.
1312
- def event_loop
1313
- @last_stop_count = 0
1314
- @cycle_start = Time.now
1315
- @cycle_index = 0
1316
-
1317
- gc_enable_has_argument = begin
1318
- GC.enable(true)
1319
- true
1320
- rescue
1321
- if gc_warning?
1322
- ExecutionEngine.warn "GC.enable does not accept an argument. GC will not be controlled by Roby"
1323
- end
1324
- false
1325
- end
1326
- stats = Hash.new
1327
- if ObjectSpace.respond_to?(:live_objects)
1328
- last_allocated_objects = ObjectSpace.allocated_objects
1329
- end
1330
- last_cpu_time = Process.times
1331
- last_cpu_time = (last_cpu_time.utime + last_cpu_time.stime) * 1000
1332
-
1333
- GC.start
1334
- if gc_enable_has_argument
1335
- already_disabled_gc = GC.disable
1336
- end
1337
- loop do
1338
- begin
1339
- if quitting?
1340
- thread.priority = 0
1341
- begin
1342
- return if forced_exit? || !clear
1343
- rescue Exception => e
1344
- ExecutionEngine.warn "Execution thread failed to clean up"
1345
- Roby.format_exception(e).each do |line|
1346
- ExecutionEngine.warn line
2227
+ def event_loop
2228
+ last_stop_count = 0
2229
+ last_quit_warning = Time.now
2230
+ @cycle_start = Time.now
2231
+ @cycle_index = 0
2232
+
2233
+ force_exit_deadline = nil
2234
+ last_process_times = Process.times
2235
+ last_dump_time = plan.event_logger.dump_time
2236
+
2237
+ loop do
2238
+ begin
2239
+ if profile_gc?
2240
+ GC::Profiler.enable
2241
+ end
2242
+
2243
+ if quitting?
2244
+ if forced_exit?
2245
+ return
2246
+ end
2247
+
2248
+ begin
2249
+ remaining = clear
2250
+ return if !remaining
2251
+
2252
+ if (last_stop_count != remaining.size) || (Time.now - last_quit_warning) > 10
2253
+ if last_stop_count == 0
2254
+ info "Roby quitting ..."
2255
+ end
2256
+
2257
+ issue_quit_progression_warning(remaining)
2258
+ last_quit_warning = Time.now
2259
+ last_stop_count = remaining.size
1347
2260
  end
1348
- return
1349
- end
1350
- end
1351
-
1352
- while Time.now > cycle_start + cycle_length
1353
- @cycle_start += cycle_length
1354
- @cycle_index += 1
1355
- end
1356
- stats[:start] = cycle_start
1357
- stats[:cycle_index] = cycle_index
1358
-
1359
- Roby.synchronize do
1360
- process_events(stats)
2261
+ rescue Exception => e
2262
+ warn "Execution thread failed to clean up"
2263
+ Roby.log_exception_with_backtrace(e, self, :warn, filter: false)
2264
+ return
2265
+ end
1361
2266
  end
1362
2267
 
1363
- @remaining_cycle_time = cycle_length - stats[:end]
1364
-
1365
- # If the ruby interpreter we run on offers a true/false argument to
1366
- # GC.enable, we disabled the GC and just run GC.enable(true) to make
1367
- # it run immediately if needed. Then, we re-disable it just after.
1368
- if gc_enable_has_argument && remaining_cycle_time > SLEEP_MIN_TIME
1369
- GC.enable(true)
1370
- GC.disable
1371
- end
1372
- add_timepoint(stats, :ruby_gc)
1373
-
1374
- # Sleep if there is enough time for it
1375
- if remaining_cycle_time > SLEEP_MIN_TIME
1376
- add_expected_duration(stats, :sleep, remaining_cycle_time)
1377
- sleep(remaining_cycle_time)
1378
- end
1379
- add_timepoint(stats, :sleep)
1380
-
1381
- # Add some statistics and call cycle_end
1382
- if defined? Roby::Log
1383
- stats[:log_queue_size] = Roby::Log.logged_events.size
1384
- end
1385
- stats[:plan_task_count] = plan.known_tasks.size
1386
- stats[:plan_event_count] = plan.free_events.size
1387
- cpu_time = Process.times
1388
- cpu_time = (cpu_time.utime + cpu_time.stime) * 1000
1389
- stats[:cpu_time] = cpu_time - last_cpu_time
1390
- last_cpu_time = cpu_time
1391
-
1392
- if ObjectSpace.respond_to?(:live_objects)
1393
- stats[:object_allocation] = ObjectSpace.allocated_objects - last_allocated_objects
1394
- stats[:live_objects] = ObjectSpace.live_objects
1395
- last_allocated_objects = ObjectSpace.allocated_objects
1396
- end
1397
- if ObjectSpace.respond_to?(:heap_slots)
1398
- stats[:heap_slots] = ObjectSpace.heap_slots
2268
+ log_timepoint_group_start "cycle"
2269
+
2270
+ while Time.now > cycle_start + cycle_length
2271
+ @cycle_start += cycle_length
2272
+ @cycle_index += 1
2273
+ end
2274
+ stats = Hash.new
2275
+ stats[:start] = [cycle_start.tv_sec, cycle_start.tv_usec]
2276
+ stats[:actual_start] = Time.now - cycle_start
2277
+ stats[:cycle_index] = cycle_index
2278
+
2279
+ log_timepoint_group 'process_events' do
2280
+ process_events
1399
2281
  end
1400
2282
 
1401
- stats[:start] = [cycle_start.tv_sec, cycle_start.tv_usec]
2283
+ remaining_cycle_time = cycle_length - (Time.now - cycle_start)
2284
+
2285
+ if use_oob_gc?
2286
+ stats[:pre_oob_gc] = GC.stat
2287
+ GC::OOB.run
2288
+ end
2289
+
2290
+ # Sleep if there is enough time for it
2291
+ if remaining_cycle_time > SLEEP_MIN_TIME
2292
+ sleep(remaining_cycle_time)
2293
+ end
2294
+ log_timepoint 'sleep'
2295
+
2296
+ cycle_end(stats)
2297
+
2298
+ # Log cycle statistics
2299
+ process_times = Process.times
2300
+ dump_time = plan.event_logger.dump_time
2301
+ stats[:log_queue_size] = plan.log_queue_size
2302
+ stats[:plan_task_count] = plan.num_tasks
2303
+ stats[:plan_event_count] = plan.num_free_events
2304
+ stats[:gc] = GC.stat
2305
+ stats[:utime] = process_times.utime - last_process_times.utime
2306
+ stats[:stime] = process_times.stime - last_process_times.stime
2307
+ stats[:dump_time] = dump_time - last_dump_time
1402
2308
  stats[:state] = Roby::State
1403
- cycle_end(stats)
2309
+ stats[:end] = Time.now - cycle_start
2310
+ if profile_gc?
2311
+ stats[:gc_profile_data] = GC::Profiler.raw_data
2312
+ stats[:gc_total_time] = GC::Profiler.total_time
2313
+ else
2314
+ stats[:gc_profile_data] = nil
2315
+ stats[:gc_total_time] = 0
2316
+ end
2317
+ log_flush_cycle :cycle_end, stats
2318
+
2319
+ last_dump_time = dump_time
2320
+ last_process_times = process_times
1404
2321
  stats = Hash.new
1405
2322
 
1406
- @cycle_start += cycle_length
1407
- @cycle_index += 1
2323
+ @cycle_start += cycle_length
2324
+ @cycle_index += 1
2325
+
2326
+ if profile_gc?
2327
+ GC::Profiler.disable
2328
+ end
2329
+
2330
+ rescue Exception => e
2331
+ if e.kind_of?(Interrupt)
2332
+ if quitting?
2333
+ if force_exit_deadline && (force_exit_deadline - Time.now) < 0
2334
+ fatal "Quitting without cleaning up"
2335
+ force_quit
2336
+ else
2337
+ fatal "Still #{Integer(force_exit_deadline - Time.now)}s before interruption will quit without cleaning up"
2338
+ end
2339
+ else
2340
+ fatal "Received interruption request"
2341
+ fatal "Interrupt again in #{INTERRUPT_FORCE_EXIT_DEAD_ZONE}s to quit without cleaning up"
2342
+ quit
2343
+ force_exit_deadline = Time.now + INTERRUPT_FORCE_EXIT_DEAD_ZONE
2344
+ end
2345
+ elsif !quitting?
2346
+ quit
1408
2347
 
1409
- rescue Exception => e
1410
- ExecutionEngine.warn "Execution thread quitting because of unhandled exception"
1411
- Roby.format_exception(e).each do |line|
1412
- ExecutionEngine.warn line
2348
+ fatal "Execution thread quitting because of unhandled exception"
2349
+ Roby.log_exception_with_backtrace(e, self, :fatal)
2350
+ else
2351
+ fatal "Execution thread FORCEFULLY quitting because of unhandled exception"
2352
+ Roby.log_exception_with_backtrace(e, self, :fatal)
2353
+ raise
1413
2354
  end
1414
- quit
1415
- end
1416
- end
2355
+ ensure
2356
+ log_timepoint_group_end "cycle"
2357
+ end
2358
+ end
1417
2359
 
1418
- ensure
1419
- GC.enable if !already_disabled_gc
2360
+ ensure
2361
+ if !plan.tasks.empty?
2362
+ warn "the following tasks are still present in the plan:"
2363
+ plan.tasks.each do |t|
2364
+ warn " #{t}"
2365
+ end
2366
+ end
2367
+ end
1420
2368
 
1421
- if !plan.known_tasks.empty?
1422
- ExecutionEngine.warn "the following tasks are still present in the plan:"
1423
- plan.known_tasks.each do |t|
1424
- ExecutionEngine.warn " #{t}"
1425
- end
1426
- end
1427
- end
2369
+ # Set the cycle_start attribute and increment cycle_index
2370
+ #
2371
+ # This is only used for testing purposes
2372
+ def start_new_cycle(time = Time.now)
2373
+ @cycle_start = time
2374
+ @cycle_index += 1
2375
+ end
1428
2376
 
1429
2377
  # A set of proc objects which are to be called when the execution engine
1430
2378
  # quits.
1431
2379
  attr_reader :finalizers
1432
2380
 
1433
- # True if the control thread is currently quitting
1434
- def quitting?; @quit > 0 end
1435
- # True if the control thread is currently quitting
1436
- def forced_exit?; @quit > 1 end
1437
- # Make control quit
1438
- def quit; @quit += 1 end
1439
-
1440
- # Called at each cycle end
1441
- def cycle_end(stats)
1442
- super if defined? super
1443
-
1444
- at_cycle_end_handlers.each do |handler|
1445
- begin
1446
- handler.call
1447
- rescue Exception => e
1448
- add_framework_error(e, "during cycle end handler #{handler}")
1449
- end
1450
- end
1451
- end
1452
-
1453
- # If the event thread has been started in its own thread,
1454
- # wait for it to terminate
1455
- def join
1456
- thread.join if thread
1457
-
1458
- rescue Interrupt
1459
- Roby.synchronize do
1460
- return unless thread
1461
-
1462
- ExecutionEngine.logger.level = Logger::INFO
1463
- ExecutionEngine.warn "received interruption request"
1464
- quit
1465
- if @quit > 2
1466
- thread.raise Interrupt, "interrupting control thread at user request"
1467
- end
1468
- end
1469
-
1470
- retry
1471
- end
2381
+ # True if the control thread is currently quitting
2382
+ def quitting?; @quit > 0 end
2383
+ # True if the control thread is currently quitting
2384
+ def forced_exit?; @quit > 1 end
2385
+ # Make control quit properly
2386
+ def quit; @quit = 1 end
2387
+ # Force quitting, without cleaning up
2388
+ def force_quit; @quit = 2 end
2389
+
2390
+ # Make a quit EE ready for reuse
2391
+ def reset
2392
+ @quit = 0
2393
+ end
2394
+
2395
+ # Called at each cycle end
2396
+ def cycle_end(stats, raise_framework_errors: Roby.app.abort_on_application_exception?)
2397
+ gather_framework_errors("#cycle_end", raise_caught_exceptions: raise_framework_errors) do
2398
+ call_poll_blocks(at_cycle_end_handlers)
2399
+ end
2400
+ end
1472
2401
 
1473
2402
  # Block until the given block is executed by the execution thread, at
1474
2403
  # the beginning of the event loop, in propagation context. If the block
1475
2404
  # raises, the exception is raised back in the calling thread.
1476
- #
1477
- # This cannot be used in the execution thread itself.
1478
- #
1479
- # If no execution thread is present, yields after having taken
1480
- # Roby.global_lock
1481
- def execute
1482
- if inside_control?
1483
- return Roby.synchronize { yield }
1484
- end
1485
-
1486
- cv = Roby.condition_variable
1487
-
1488
- return_value = nil
1489
- Roby.synchronize do
1490
- if !running?
1491
- raise "control thread not running"
1492
- end
2405
+ def execute(catch: [], type: :external_events)
2406
+ if inside_control?
2407
+ return yield
2408
+ end
1493
2409
 
1494
- caller_thread = Thread.current
1495
- waiting_threads << caller_thread
2410
+ capture_catch = lambda do |symbol, *other|
2411
+ caught = catch(symbol) do
2412
+ if other.empty?
2413
+ return [:ret, yield]
2414
+ else
2415
+ return capture_catch(block, *other)
2416
+ end
2417
+ end
2418
+ [:throw, [symbol, caught]]
2419
+ end
1496
2420
 
1497
- once do
1498
- begin
1499
- return_value = yield
1500
- cv.broadcast
1501
- rescue Exception => e
1502
- caller_thread.raise e, e.message, e.backtrace
1503
- end
1504
- waiting_threads.delete(caller_thread)
1505
- end
1506
- cv.wait(Roby.global_lock)
1507
- end
1508
- return_value
2421
+ ivar = Concurrent::IVar.new
2422
+ once(sync: ivar, type: type) do
2423
+ begin
2424
+ if !catch.empty?
2425
+ result = capture_catch.call(*catch) { yield }
2426
+ ivar.set(result)
2427
+ else
2428
+ ivar.set([:ret, yield])
2429
+ end
2430
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
2431
+ ivar.set([:raise, e])
2432
+ end
2433
+ end
1509
2434
 
1510
- ensure
1511
- Roby.return_condition_variable(cv)
2435
+ mode, value = ivar.value!
2436
+ case mode
2437
+ when :ret
2438
+ return value
2439
+ when :throw
2440
+ throw *value
2441
+ else
2442
+ raise value
2443
+ end
1512
2444
  end
1513
2445
 
1514
2446
  # Stops the current thread until the given even is emitted. If the event
@@ -1518,44 +2450,159 @@ def wait_until(ev)
1518
2450
  raise ThreadMismatch, "cannot use #wait_until in execution threads"
1519
2451
  end
1520
2452
 
1521
- Roby.condition_variable(true) do |cv, mt|
1522
- caller_thread = Thread.current
1523
- # Note: no need to add the caller thread in waiting_threads,
1524
- # since the event will become unreachable if the execution
1525
- # thread quits
2453
+ ivar = Concurrent::IVar.new
2454
+ result = nil
2455
+ once(sync: ivar) do
2456
+ if ev.unreachable?
2457
+ ivar.fail(UnreachableEvent.new(ev, ev.unreachability_reason))
2458
+ else
2459
+ ev.if_unreachable(cancel_at_emission: true) do |reason, event|
2460
+ ivar.fail(UnreachableEvent.new(event, reason)) if !ivar.complete?
2461
+ end
2462
+ ev.once do |ev|
2463
+ ivar.set(result) if !ivar.complete?
2464
+ end
2465
+ begin
2466
+ result = yield if block_given?
2467
+ rescue Exception => e
2468
+ ivar.fail(e)
2469
+ end
2470
+ end
2471
+ end
2472
+ ivar.value!
2473
+ end
2474
+
2475
+ def shutdown
2476
+ killall
2477
+ thread_pool.shutdown
2478
+ end
2479
+
2480
+ def reset_thread_pool
2481
+ if @thread_pool
2482
+ @thread_pool.shutdown
2483
+ end
2484
+ @thread_pool = Concurrent::CachedThreadPool.new(idletime: 10)
2485
+ end
2486
+
2487
+ # Kill all tasks that are currently running in the plan
2488
+ def killall
2489
+ scheduler_enabled = scheduler.enabled?
2490
+
2491
+ plan.permanent_tasks.clear
2492
+ plan.permanent_events.clear
2493
+ plan.mission_tasks.clear
2494
+
2495
+ scheduler.enabled = false
2496
+ quit
2497
+
2498
+ start_new_cycle
2499
+ process_events
2500
+ cycle_end(Hash.new)
2501
+
2502
+ plan.transactions.each do |trsc|
2503
+ trsc.discard_transaction!
2504
+ end
2505
+
2506
+ start_new_cycle
2507
+ Thread.pass
2508
+ process_events
2509
+ cycle_end(Hash.new)
2510
+
2511
+ ensure
2512
+ scheduler.enabled = scheduler_enabled
2513
+ end
2514
+
2515
+ # Exception kind passed to {#on_exception} handlers for non-fatal,
2516
+ # unhandled exceptions
2517
+ EXCEPTION_NONFATAL = :nonfatal
2518
+
2519
+ # Exception kind passed to {#on_exception} handlers for fatal,
2520
+ # unhandled exceptions
2521
+ EXCEPTION_FATAL = :fatal
2522
+
2523
+ # Exception kind passed to {#on_exception} handlers for handled
2524
+ # exceptions
2525
+ EXCEPTION_HANDLED = :handled
2526
+
2527
+ # Exception kind passed to {#on_exception} handlers for free event
2528
+ # exceptions
2529
+ EXCEPTION_FREE_EVENT = :free_event
2530
+
2531
+ # Registers a callback that will be called when exceptions are propagated in the plan
2532
+ #
2533
+ # @yieldparam [Symbol] kind one of {EXCEPTION_NONFATAL},
2534
+ # {EXCEPTION_FATAL}, {EXCEPTION_FREE_EVENT} or {EXCEPTION_HANDLED}
2535
+ # @yieldparam [Roby::ExecutionException] error the exception
2536
+ # @yieldparam [Array<Roby::Task>] tasks the tasks that are involved in this exception
2537
+ #
2538
+ # @return [Object] an ID that can be used as argument to {#remove_exception_listener}
2539
+ def on_exception(description: 'exception listener', on_error: :disable, &block)
2540
+ handler = PollBlockDefinition.new(description, block, on_error: on_error)
2541
+ exception_listeners << handler
2542
+ handler
2543
+ end
2544
+
2545
+ # Controls whether this engine should indiscriminately display all fatal
2546
+ # exceptions
2547
+ #
2548
+ # This is on by default
2549
+ def display_exceptions=(flag)
2550
+ if flag
2551
+ @exception_display_handler ||= on_exception do |kind, error, tasks|
2552
+ level = if kind == EXCEPTION_HANDLED then :debug
2553
+ else :warn
2554
+ end
1526
2555
 
1527
- mt.synchronize do
1528
- once do
1529
- ev.if_unreachable(true) do |reason|
1530
- caller_thread.raise UnreachableEvent.new(ev, reason)
2556
+ send(level) do
2557
+ send(level, "encountered a #{kind} exception")
2558
+ Roby.log_exception_with_backtrace(error.exception, self, level)
2559
+ if kind == EXCEPTION_HANDLED
2560
+ send(level, "the exception was handled by")
2561
+ else
2562
+ send(level, "the exception involved")
1531
2563
  end
1532
- ev.on do
1533
- mt.synchronize { cv.broadcast }
2564
+ tasks.each do |t|
2565
+ send(level, " #{t}")
1534
2566
  end
1535
- yield if block_given?
2567
+ break
1536
2568
  end
1537
- cv.wait(mt)
1538
2569
  end
2570
+ else
2571
+ remove_exception_listener(@exception_display_handler)
2572
+ @exception_display_handler = nil
1539
2573
  end
1540
2574
  end
1541
- end
1542
2575
 
1543
- class << self
1544
- # The ExecutionEngine object which executes Roby.plan
1545
- attr_reader :engine
2576
+ # whether this engine should indiscriminately display all fatal
2577
+ # exceptions
2578
+ def display_exceptions?
2579
+ !!@exception_display_handler
2580
+ end
2581
+
2582
+ # Removes an exception listener registered with {#on_exception}
2583
+ #
2584
+ # @param [Object] the value returned by {#on_exception}
2585
+ # @return [void]
2586
+ def remove_exception_listener(handler)
2587
+ exception_listeners.delete(handler)
2588
+ end
1546
2589
 
1547
- # Sets the engine. This can be done only once
1548
- def engine=(new_engine)
1549
- if engine
1550
- raise ArgumentError, "cannot change the execution engine"
1551
- elsif plan && plan.engine && plan.engine != new_engine
1552
- raise ArgumentError, "must have Roby.engine == Roby.plan.engine"
1553
- elsif control && new_engine.control != control
1554
- raise ArgumentError, "must have Roby.control == Roby.engine.control"
2590
+ # Call to notify the listeners registered with {#on_exception} of the
2591
+ # occurence of an exception
2592
+ def notify_exception(kind, error, involved_objects)
2593
+ log(:exception_notification, plan.droby_id, kind, error, involved_objects)
2594
+ exception_listeners.each do |listener|
2595
+ listener.call(self, kind, error, involved_objects)
1555
2596
  end
2597
+ end
1556
2598
 
1557
- @engine = new_engine
1558
- @control = new_engine.control
2599
+ # Create a promise to execute the given block in a separate thread
2600
+ #
2601
+ # Note that the returned value is a {Roby::Promise}. This means that
2602
+ # callbacks added with #on_success or #rescue will be executed in the
2603
+ # execution engine thread by default.
2604
+ def promise(description: nil, executor: thread_pool, &block)
2605
+ Promise.new(self, executor: executor, description: description, &block)
1559
2606
  end
1560
2607
  end
1561
2608
 
@@ -1563,42 +2610,42 @@ def engine=(new_engine)
1563
2610
  # wait for its completion like Roby.execute does
1564
2611
  #
1565
2612
  # See ExecutionEngine#once
1566
- def self.once; engine.once { yield } end
2613
+ def self.once; execution_engine.once { yield } end
1567
2614
 
1568
2615
  # Make the main engine call +block+ during each propagation step.
1569
2616
  # See ExecutionEngine#each_cycle
1570
- def self.each_cycle(&block); engine.each_cycle(&block) end
2617
+ def self.each_cycle(&block); execution_engine.each_cycle(&block) end
1571
2618
 
1572
2619
  # Install a periodic handler on the main engine
1573
- def self.every(duration, &block); engine.every(duration, &block) end
2620
+ def self.every(duration, options = Hash.new, &block); execution_engine.every(duration, options, &block) end
1574
2621
 
1575
2622
  # True if the current thread is the execution thread of the main engine
1576
2623
  #
1577
2624
  # See ExecutionEngine#inside_control?
1578
- def self.inside_control?; engine.inside_control? end
2625
+ def self.inside_control?; execution_engine.inside_control? end
1579
2626
 
1580
2627
  # True if the current thread is not the execution thread of the main engine
1581
2628
  #
1582
2629
  # See ExecutionEngine#outside_control?
1583
- def self.outside_control?; engine.outside_control? end
2630
+ def self.outside_control?; execution_engine.outside_control? end
1584
2631
 
1585
2632
  # Execute the given block during the event propagation step of the main
1586
2633
  # engine. See ExecutionEngine#execute
1587
2634
  def self.execute
1588
- engine.execute do
2635
+ execution_engine.execute do
1589
2636
  yield
1590
2637
  end
1591
2638
  end
1592
2639
 
1593
2640
  # Blocks until the main engine has executed at least one cycle.
1594
2641
  # See ExecutionEngine#wait_one_cycle
1595
- def self.wait_one_cycle; engine.wait_one_cycle end
2642
+ def self.wait_one_cycle; execution_engine.wait_one_cycle end
1596
2643
 
1597
2644
  # Stops the current thread until the given even is emitted. If the event
1598
2645
  # becomes unreachable, an UnreachableEvent exception is raised.
1599
2646
  #
1600
2647
  # See ExecutionEngine#wait_until
1601
- def self.wait_until(ev, &block); engine.wait_until(ev, &block) end
2648
+ def self.wait_until(ev, &block); execution_engine.wait_until(ev, &block) end
1602
2649
  end
1603
2650
 
1604
2651