roby 0.8.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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