roby 0.7.3 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +7 -5
- data/Manifest.txt +91 -16
- data/README.txt +24 -24
- data/Rakefile +92 -64
- data/app/config/app.yml +42 -43
- data/app/config/init.rb +26 -0
- data/benchmark/alloc_misc.rb +123 -0
- data/benchmark/discovery_latency.rb +67 -0
- data/benchmark/garbage_collection.rb +48 -0
- data/benchmark/genom.rb +31 -0
- data/benchmark/transactions.rb +62 -0
- data/bin/roby +1 -1
- data/bin/roby-log +16 -6
- data/doc/guide/.gitignore +2 -0
- data/doc/guide/config.yaml +34 -0
- data/doc/guide/ext/init.rb +14 -0
- data/doc/guide/ext/previous_next.rb +40 -0
- data/doc/guide/ext/rdoc_links.rb +33 -0
- data/doc/guide/index.rdoc +16 -0
- data/doc/guide/overview.rdoc +62 -0
- data/doc/guide/plan_modifications.rdoc +67 -0
- data/doc/guide/src/abstraction/achieve_with.page +8 -0
- data/doc/guide/src/abstraction/forwarding.page +8 -0
- data/doc/guide/src/abstraction/hierarchy.page +19 -0
- data/doc/guide/src/abstraction/index.page +28 -0
- data/doc/guide/src/abstraction/task_models.page +13 -0
- data/doc/guide/src/basics.template +6 -0
- data/doc/guide/src/basics/app.page +139 -0
- data/doc/guide/src/basics/code_examples.page +33 -0
- data/doc/guide/src/basics/dry.page +69 -0
- data/doc/guide/src/basics/errors.page +443 -0
- data/doc/guide/src/basics/events.page +179 -0
- data/doc/guide/src/basics/hierarchy.page +275 -0
- data/doc/guide/src/basics/index.page +11 -0
- data/doc/guide/src/basics/log_replay/goForward_1.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_2.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_3.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_4.png +0 -0
- data/doc/guide/src/basics/log_replay/goForward_5.png +0 -0
- data/doc/guide/src/basics/log_replay/hierarchy_error_1.png +0 -0
- data/doc/guide/src/basics/log_replay/hierarchy_error_2.png +0 -0
- data/doc/guide/src/basics/log_replay/hierarchy_error_3.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_1.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_2.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_3.png +0 -0
- data/doc/guide/src/basics/log_replay/plan_repair_4.png +0 -0
- data/doc/guide/src/basics/log_replay/roby_log_main_window.png +0 -0
- data/doc/guide/src/basics/log_replay/roby_log_relation_window.png +0 -0
- data/doc/guide/src/basics/log_replay/roby_replay_event_representation.png +0 -0
- data/doc/guide/src/basics/plan_objects.page +71 -0
- data/doc/guide/src/basics/relations_display.page +203 -0
- data/doc/guide/src/basics/roby_cycle_overview.png +0 -0
- data/doc/guide/src/basics/shell.page +102 -0
- data/doc/guide/src/basics/summary.page +32 -0
- data/doc/guide/src/basics/tasks.page +357 -0
- data/doc/guide/src/basics_shell_header.txt +16 -0
- data/doc/guide/src/cycle/cycle-overview.png +0 -0
- data/doc/guide/src/cycle/cycle-overview.svg +208 -0
- data/doc/guide/src/cycle/error_handling.page +168 -0
- data/doc/guide/src/cycle/error_instantaneous_repair.png +0 -0
- data/doc/guide/src/cycle/error_instantaneous_repair.svg +1224 -0
- data/doc/guide/src/cycle/garbage_collection.page +10 -0
- data/doc/guide/src/cycle/index.page +23 -0
- data/doc/guide/src/cycle/propagation.page +154 -0
- data/doc/guide/src/cycle/propagation_diamond.png +0 -0
- data/doc/guide/src/cycle/propagation_diamond.svg +1279 -0
- data/doc/guide/src/default.css +319 -0
- data/doc/guide/src/default.template +74 -0
- data/doc/guide/src/htmldoc.metainfo +20 -0
- data/doc/guide/src/htmldoc.virtual +18 -0
- data/doc/guide/src/images/bodybg.png +0 -0
- data/doc/guide/src/images/contbg.png +0 -0
- data/doc/guide/src/images/footerbg.png +0 -0
- data/doc/guide/src/images/gradient1.png +0 -0
- data/doc/guide/src/images/gradient2.png +0 -0
- data/doc/guide/src/index.page +7 -0
- data/doc/guide/src/introduction/index.page +29 -0
- data/doc/guide/src/introduction/install.page +133 -0
- data/doc/{papers.rdoc → guide/src/introduction/publications.page} +5 -2
- data/doc/{videos.rdoc → guide/src/introduction/videos.page} +4 -2
- data/doc/guide/src/plugins/fault_tolerance.page +44 -0
- data/doc/guide/src/plugins/index.page +11 -0
- data/doc/guide/src/plugins/subsystems.page +45 -0
- data/doc/guide/src/relations/dependency.page +89 -0
- data/doc/guide/src/relations/index.page +12 -0
- data/doc/misc/update_github +24 -0
- data/doc/tutorials/02-GoForward.rdoc +3 -3
- data/ext/graph/graph.cc +46 -0
- data/lib/roby.rb +57 -22
- data/lib/roby/app.rb +132 -112
- data/lib/roby/app/plugins/rake.rb +21 -0
- data/lib/roby/app/rake.rb +0 -7
- data/lib/roby/app/run.rb +1 -1
- data/lib/roby/app/scripts/distributed.rb +1 -2
- data/lib/roby/app/scripts/generate/bookmarks.rb +1 -1
- data/lib/roby/app/scripts/results.rb +2 -1
- data/lib/roby/app/scripts/run.rb +6 -2
- data/lib/roby/app/scripts/shell.rb +11 -11
- data/lib/roby/config.rb +1 -1
- data/lib/roby/decision_control.rb +62 -3
- data/lib/roby/distributed.rb +4 -0
- data/lib/roby/distributed/base.rb +8 -0
- data/lib/roby/distributed/communication.rb +12 -8
- data/lib/roby/distributed/connection_space.rb +61 -44
- data/lib/roby/distributed/distributed_object.rb +1 -1
- data/lib/roby/distributed/notifications.rb +22 -30
- data/lib/roby/distributed/peer.rb +13 -8
- data/lib/roby/distributed/proxy.rb +5 -5
- data/lib/roby/distributed/subscription.rb +4 -4
- data/lib/roby/distributed/transaction.rb +3 -3
- data/lib/roby/event.rb +176 -110
- data/lib/roby/exceptions.rb +12 -4
- data/lib/roby/execution_engine.rb +1604 -0
- data/lib/roby/external_process_task.rb +225 -0
- data/lib/roby/graph.rb +0 -6
- data/lib/roby/interface.rb +221 -137
- data/lib/roby/log/console.rb +5 -3
- data/lib/roby/log/data_stream.rb +94 -16
- data/lib/roby/log/dot.rb +8 -8
- data/lib/roby/log/event_stream.rb +13 -3
- data/lib/roby/log/file.rb +43 -18
- data/lib/roby/log/gui/basic_display_ui.rb +89 -0
- data/lib/roby/log/gui/chronicle_view_ui.rb +90 -0
- data/lib/roby/log/gui/data_displays.rb +4 -5
- data/lib/roby/log/gui/data_displays_ui.rb +146 -0
- data/lib/roby/log/gui/relations.rb +18 -18
- data/lib/roby/log/gui/relations_ui.rb +120 -0
- data/lib/roby/log/gui/relations_view_ui.rb +144 -0
- data/lib/roby/log/gui/replay.rb +41 -13
- data/lib/roby/log/gui/replay_controls.rb +3 -0
- data/lib/roby/log/gui/replay_controls.ui +133 -110
- data/lib/roby/log/gui/replay_controls_ui.rb +249 -0
- data/lib/roby/log/hooks.rb +19 -18
- data/lib/roby/log/logger.rb +7 -6
- data/lib/roby/log/notifications.rb +4 -4
- data/lib/roby/log/plan_rebuilder.rb +20 -22
- data/lib/roby/log/relations.rb +44 -16
- data/lib/roby/log/server.rb +1 -4
- data/lib/roby/log/timings.rb +88 -19
- data/lib/roby/plan-object.rb +135 -11
- data/lib/roby/plan.rb +408 -224
- data/lib/roby/planning/loops.rb +32 -25
- data/lib/roby/planning/model.rb +157 -51
- data/lib/roby/planning/task.rb +47 -20
- data/lib/roby/query.rb +128 -92
- data/lib/roby/relations.rb +254 -136
- data/lib/roby/relations/conflicts.rb +6 -9
- data/lib/roby/relations/dependency.rb +358 -0
- data/lib/roby/relations/ensured.rb +0 -1
- data/lib/roby/relations/error_handling.rb +0 -1
- data/lib/roby/relations/events.rb +0 -2
- data/lib/roby/relations/executed_by.rb +26 -11
- data/lib/roby/relations/planned_by.rb +14 -14
- data/lib/roby/robot.rb +46 -0
- data/lib/roby/schedulers/basic.rb +34 -0
- data/lib/roby/standalone.rb +4 -0
- data/lib/roby/standard_errors.rb +21 -15
- data/lib/roby/state/events.rb +5 -4
- data/lib/roby/support.rb +107 -6
- data/lib/roby/task-operations.rb +23 -19
- data/lib/roby/task.rb +522 -148
- data/lib/roby/task_index.rb +80 -0
- data/lib/roby/test/common.rb +283 -44
- data/lib/roby/test/distributed.rb +53 -37
- data/lib/roby/test/testcase.rb +9 -204
- data/lib/roby/test/tools.rb +3 -3
- data/lib/roby/transactions.rb +154 -111
- data/lib/roby/transactions/proxy.rb +40 -7
- data/manifest.xml +20 -0
- data/plugins/fault_injection/README.txt +0 -3
- data/plugins/fault_injection/Rakefile +2 -8
- data/plugins/fault_injection/app.rb +1 -1
- data/plugins/fault_injection/fault_injection.rb +3 -3
- data/plugins/fault_injection/test/test_fault_injection.rb +19 -25
- data/plugins/subsystems/README.txt +0 -3
- data/plugins/subsystems/Rakefile +2 -7
- data/plugins/subsystems/app.rb +27 -16
- data/plugins/subsystems/test/app/config/init.rb +3 -0
- data/plugins/subsystems/test/app/planners/main.rb +1 -1
- data/plugins/subsystems/test/app/tasks/services.rb +1 -1
- data/plugins/subsystems/test/test_subsystems.rb +23 -16
- data/test/distributed/test_communication.rb +32 -15
- data/test/distributed/test_connection.rb +28 -26
- data/test/distributed/test_execution.rb +59 -54
- data/test/distributed/test_mixed_plan.rb +34 -34
- data/test/distributed/test_plan_notifications.rb +26 -26
- data/test/distributed/test_protocol.rb +57 -48
- data/test/distributed/test_query.rb +11 -7
- data/test/distributed/test_remote_plan.rb +71 -71
- data/test/distributed/test_transaction.rb +50 -47
- data/test/mockups/external_process +28 -0
- data/test/planning/test_loops.rb +163 -119
- data/test/planning/test_model.rb +3 -3
- data/test/planning/test_task.rb +27 -7
- data/test/relations/test_conflicts.rb +3 -3
- data/test/relations/test_dependency.rb +324 -0
- data/test/relations/test_ensured.rb +2 -2
- data/test/relations/test_executed_by.rb +94 -19
- data/test/relations/test_planned_by.rb +11 -9
- data/test/suite_core.rb +6 -3
- data/test/suite_distributed.rb +1 -0
- data/test/suite_planning.rb +1 -0
- data/test/suite_relations.rb +2 -2
- data/test/tasks/test_external_process.rb +126 -0
- data/test/{test_thread_task.rb → tasks/test_thread_task.rb} +17 -20
- data/test/test_bgl.rb +21 -1
- data/test/test_event.rb +229 -155
- data/test/test_exceptions.rb +79 -80
- data/test/test_execution_engine.rb +987 -0
- data/test/test_gui.rb +1 -1
- data/test/test_interface.rb +11 -5
- data/test/test_log.rb +18 -7
- data/test/test_log_server.rb +1 -0
- data/test/test_plan.rb +229 -395
- data/test/test_query.rb +193 -35
- data/test/test_relations.rb +88 -8
- data/test/test_state.rb +55 -37
- data/test/test_support.rb +1 -1
- data/test/test_task.rb +371 -218
- data/test/test_testcase.rb +32 -16
- data/test/test_transactions.rb +211 -170
- data/test/test_transactions_proxy.rb +37 -19
- metadata +169 -71
- data/.gitignore +0 -29
- data/doc/styles/allison.css +0 -314
- data/doc/styles/allison.js +0 -316
- data/doc/styles/allison.rb +0 -276
- data/doc/styles/jamis.rb +0 -593
- data/lib/roby/control.rb +0 -746
- data/lib/roby/executives/simple.rb +0 -30
- data/lib/roby/propagation.rb +0 -562
- data/lib/roby/relations/hierarchy.rb +0 -239
- data/lib/roby/transactions/updates.rb +0 -139
- data/test/relations/test_hierarchy.rb +0 -158
- data/test/test_control.rb +0 -399
- data/test/test_propagation.rb +0 -210
data/lib/roby/exceptions.rb
CHANGED
@@ -130,10 +130,10 @@ module Roby
|
|
130
130
|
handler.call(self, exception_object)
|
131
131
|
return true
|
132
132
|
rescue Exception => e
|
133
|
-
if
|
134
|
-
|
133
|
+
if !kind_of?(PlanObject)
|
134
|
+
engine.add_framework_error(e, 'global exception handling')
|
135
135
|
else
|
136
|
-
|
136
|
+
engine.add_error(FailedExceptionHandler.new(e, self, exception_object))
|
137
137
|
end
|
138
138
|
end
|
139
139
|
end
|
@@ -144,7 +144,15 @@ module Roby
|
|
144
144
|
end
|
145
145
|
|
146
146
|
RX_IN_FRAMEWORK = /^((?:\s*\(druby:\/\/.+\)\s*)?#{Regexp.quote(ROBY_LIB_DIR)}\/)/
|
147
|
-
def self.filter_backtrace(original_backtrace)
|
147
|
+
def self.filter_backtrace(original_backtrace = nil)
|
148
|
+
if !original_backtrace && block_given?
|
149
|
+
begin
|
150
|
+
return yield
|
151
|
+
rescue Exception => e
|
152
|
+
raise e, e.message, filter_backtrace(e.backtrace)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
148
156
|
if Roby.app.filter_backtraces? && original_backtrace
|
149
157
|
app_dir = if defined? APP_DIR then Regexp.quote(APP_DIR) end
|
150
158
|
|
@@ -0,0 +1,1604 @@
|
|
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
|
110
|
+
#
|
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.
|
114
|
+
#
|
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.
|
120
|
+
#
|
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.
|
124
|
+
#
|
125
|
+
class ExecutionEngine
|
126
|
+
extend Logger::Hierarchy
|
127
|
+
extend Logger::Forward
|
128
|
+
|
129
|
+
# Create an execution engine acting on +plan+, using +control+ as the
|
130
|
+
# decision control object
|
131
|
+
#
|
132
|
+
# See Roby::Plan and Roby::DecisionControl
|
133
|
+
def initialize(plan, control)
|
134
|
+
@plan = plan
|
135
|
+
plan.engine = self
|
136
|
+
@control = control
|
137
|
+
|
138
|
+
@propagation_id = 0
|
139
|
+
@delayed_events = []
|
140
|
+
@process_once = Queue.new
|
141
|
+
@event_ordering = Array.new
|
142
|
+
@event_priorities = Hash.new
|
143
|
+
@propagation_handlers = []
|
144
|
+
@at_cycle_end_handlers = Array.new
|
145
|
+
@process_every = Array.new
|
146
|
+
@waiting_threads = Array.new
|
147
|
+
|
148
|
+
each_cycle(&ExecutionEngine.method(:call_every))
|
149
|
+
|
150
|
+
@quit = 0
|
151
|
+
@allow_propagation = true
|
152
|
+
@thread = nil
|
153
|
+
@cycle_index = 0
|
154
|
+
@cycle_start = Time.now
|
155
|
+
@cycle_length = 0
|
156
|
+
@last_stop_count = 0
|
157
|
+
@finalizers = []
|
158
|
+
@gc_warning = true
|
159
|
+
end
|
160
|
+
|
161
|
+
# The Plan this engine is acting on
|
162
|
+
attr_accessor :plan
|
163
|
+
# The DecisionControl object associated with this engine
|
164
|
+
attr_accessor :control
|
165
|
+
# A numeric ID giving the count of the current propagation cycle
|
166
|
+
attr_reader :propagation_id
|
167
|
+
|
168
|
+
@propagation_handlers = []
|
169
|
+
class << self
|
170
|
+
# Code blocks that get called at the beginning of each cycle. See
|
171
|
+
# #add_propagation_handler
|
172
|
+
attr_reader :propagation_handlers
|
173
|
+
|
174
|
+
# call-seq:
|
175
|
+
# ExecutionEngine.add_propagation_handler { |plan| ... }
|
176
|
+
#
|
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.
|
182
|
+
#
|
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.
|
189
|
+
#
|
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.
|
193
|
+
#
|
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
|
200
|
+
end
|
201
|
+
|
202
|
+
# 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.
|
205
|
+
def remove_propagation_handler(id)
|
206
|
+
propagation_handlers.delete_if { |p| p.object_id == id }
|
207
|
+
nil
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
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
|
216
|
+
|
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
|
247
|
+
def remove_propagation_handler(id)
|
248
|
+
propagation_handlers.delete_if { |p| p.object_id == id }
|
249
|
+
nil
|
250
|
+
end
|
251
|
+
|
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)
|
262
|
+
end
|
263
|
+
|
264
|
+
# The scheduler is the object which handles non-generic parts of the
|
265
|
+
# propagation cycle. For now, its #initial_events method is called at
|
266
|
+
# the beginning of each propagation cycle and can call or emit a set of
|
267
|
+
# events.
|
268
|
+
#
|
269
|
+
# See Schedulers::Basic
|
270
|
+
attr_accessor :scheduler
|
271
|
+
|
272
|
+
# True if we are currently in the propagation stage
|
273
|
+
def gathering?; !!@propagation end
|
274
|
+
|
275
|
+
attr_predicate :allow_propagation
|
276
|
+
|
277
|
+
# The set of source events for the current propagation action. This is a
|
278
|
+
# mix of EventGenerator and Event objects.
|
279
|
+
attr_reader :propagation_sources
|
280
|
+
# The set of events extracted from #sources
|
281
|
+
def propagation_source_events
|
282
|
+
result = ValueSet.new
|
283
|
+
for ev in @propagation_sources
|
284
|
+
if ev.respond_to?(:generator)
|
285
|
+
result << ev
|
286
|
+
end
|
287
|
+
end
|
288
|
+
result
|
289
|
+
end
|
290
|
+
|
291
|
+
# The set of generators extracted from #sources
|
292
|
+
def propagation_source_generators
|
293
|
+
result = ValueSet.new
|
294
|
+
for ev in @propagation_sources
|
295
|
+
result << if ev.respond_to?(:generator)
|
296
|
+
ev.generator
|
297
|
+
else
|
298
|
+
ev
|
299
|
+
end
|
300
|
+
end
|
301
|
+
result
|
302
|
+
end
|
303
|
+
|
304
|
+
# The set of pending delayed events. This is an array of the form
|
305
|
+
#
|
306
|
+
# [[time, is_forward, source, target, context], ...]
|
307
|
+
#
|
308
|
+
# See #add_event_delay for more information
|
309
|
+
attr_reader :delayed_events
|
310
|
+
|
311
|
+
# Adds a propagation step to be performed when the current time is
|
312
|
+
# greater than +time+. The propagation step is a signal if +is_forward+
|
313
|
+
# is false and a forward otherwise.
|
314
|
+
#
|
315
|
+
# This method should not be called directly. Use #add_event_propagation
|
316
|
+
# with the appropriate +timespec+ argument.
|
317
|
+
#
|
318
|
+
# See also #delayed_events and #execute_delayed_events
|
319
|
+
def add_event_delay(time, is_forward, source, target, context)
|
320
|
+
delayed_events << [time, is_forward, source, target, context]
|
321
|
+
end
|
322
|
+
|
323
|
+
# Adds the events in +delayed_events+ whose time has passed into the
|
324
|
+
# propagation. This must be called in propagation context.
|
325
|
+
#
|
326
|
+
# See #add_event_delay and #delayed_events
|
327
|
+
def execute_delayed_events
|
328
|
+
reftime = Time.now
|
329
|
+
delayed_events.delete_if do |time, forward, source, signalled, context|
|
330
|
+
if time <= reftime
|
331
|
+
add_event_propagation(forward, [source], signalled, context, nil)
|
332
|
+
true
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
# Called by #plan when an event has been finalized
|
338
|
+
def finalized_event(event)
|
339
|
+
event.unreachable!(nil, plan)
|
340
|
+
delayed_events.delete_if { |_, _, _, signalled, _| signalled == event }
|
341
|
+
end
|
342
|
+
|
343
|
+
# Sets up a propagation context, yielding the block in it. During this
|
344
|
+
# propagation stage, all calls to #emit and #call are stored in an
|
345
|
+
# internal hash of the form:
|
346
|
+
# target => [forward_sources, signal_sources]
|
347
|
+
#
|
348
|
+
# where the two +_sources+ are arrays of the form
|
349
|
+
# [[source, context], ...]
|
350
|
+
#
|
351
|
+
# The method returns the resulting hash. Use #gathering? to know if the
|
352
|
+
# current engine is in a propagation context, and #add_event_propagation
|
353
|
+
# to add a new entry to this set.
|
354
|
+
def gather_propagation(initial_set = Hash.new)
|
355
|
+
raise InternalError, "nested call to #gather_propagation" if gathering?
|
356
|
+
@propagation = initial_set
|
357
|
+
|
358
|
+
propagation_context(nil) { yield }
|
359
|
+
|
360
|
+
return @propagation
|
361
|
+
ensure
|
362
|
+
@propagation = nil
|
363
|
+
end
|
364
|
+
|
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)
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
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)
|
378
|
+
if @propagation_exceptions
|
379
|
+
plan_exception = ExecutionEngine.to_execution_exception(e)
|
380
|
+
@propagation_exceptions << plan_exception
|
381
|
+
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
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
# Yields to the block, calling #add_framework_error if an exception is
|
391
|
+
# raised
|
392
|
+
def gather_framework_errors(source)
|
393
|
+
yield
|
394
|
+
rescue Exception => e
|
395
|
+
add_framework_error(e, source)
|
396
|
+
end
|
397
|
+
|
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
|
402
|
+
def add_framework_error(error, source)
|
403
|
+
if @application_exceptions
|
404
|
+
@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
|
+
else
|
408
|
+
ExecutionEngine.error "Application error in #{source}"
|
409
|
+
Roby.format_exception(error).each do |line|
|
410
|
+
Roby.warn line
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Sets the source_event and source_generator variables according
|
416
|
+
# to +source+. +source+ is the +from+ argument of #add_event_propagation
|
417
|
+
def propagation_context(sources)
|
418
|
+
raise InternalError, "not in a gathering context in #fire" unless gathering?
|
419
|
+
|
420
|
+
if sources
|
421
|
+
current_sources = sources
|
422
|
+
@propagation_sources = sources
|
423
|
+
else
|
424
|
+
@propagation_sources = []
|
425
|
+
end
|
426
|
+
|
427
|
+
yield @propagation
|
428
|
+
|
429
|
+
ensure
|
430
|
+
@propagation_sources = sources
|
431
|
+
end
|
432
|
+
|
433
|
+
# Adds a propagation to the next propagation step: it registers a
|
434
|
+
# propagation step to be performed between +source+ and +target+ with
|
435
|
+
# the given +context+. If +is_forward+ is true, the propagation will be
|
436
|
+
# a forwarding, otherwise it is a signal.
|
437
|
+
#
|
438
|
+
# If +timespec+ is not nil, it defines a delay to be applied before
|
439
|
+
# calling the target event.
|
440
|
+
#
|
441
|
+
# See #gather_propagation
|
442
|
+
def add_event_propagation(is_forward, from, target, context, timespec)
|
443
|
+
if target.plan != plan
|
444
|
+
raise Roby::EventNotExecutable.new(target), "#{target} not in executed plan"
|
445
|
+
end
|
446
|
+
|
447
|
+
step = (@propagation[target] ||= [nil, nil])
|
448
|
+
from = [nil] unless from && !from.empty?
|
449
|
+
|
450
|
+
step = if is_forward then (step[0] ||= [])
|
451
|
+
else (step[1] ||= [])
|
452
|
+
end
|
453
|
+
|
454
|
+
from.each do |ev|
|
455
|
+
step << ev << context << timespec
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
# Calls its block in a #gather_propagation context and propagate events
|
460
|
+
# that have been called and/or emitted by the block
|
461
|
+
#
|
462
|
+
# If a block is given, it is called with the initial set of events: the
|
463
|
+
# events we should consider as already emitted in the following propagation.
|
464
|
+
# +seeds+ si a list of procs which should be called to initiate the propagation
|
465
|
+
# (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"
|
469
|
+
end
|
470
|
+
|
471
|
+
@propagation_id = (@propagation_id += 1)
|
472
|
+
@propagation_exceptions = []
|
473
|
+
|
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
|
487
|
+
end
|
488
|
+
if scheduler
|
489
|
+
gather_framework_errors('scheduler') { scheduler.initial_events }
|
490
|
+
end
|
491
|
+
for h in self.class.propagation_handlers
|
492
|
+
gather_framework_errors("propagation handler #{h}") { h.call(plan) }
|
493
|
+
end
|
494
|
+
for h in propagation_handlers
|
495
|
+
gather_framework_errors("propagation handler #{h}") { h.call(plan) }
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
while !next_step.empty?
|
500
|
+
next_step = event_propagation_step(next_step)
|
501
|
+
end
|
502
|
+
@propagation_exceptions
|
503
|
+
|
504
|
+
ensure
|
505
|
+
@propagation_exceptions = nil
|
506
|
+
end
|
507
|
+
|
508
|
+
# Validates +timespec+ as a delay specification. A valid delay
|
509
|
+
# specification is either +nil+ or a hash, in which case two forms are
|
510
|
+
# possible:
|
511
|
+
#
|
512
|
+
# :at => absolute_time
|
513
|
+
# :delay => number
|
514
|
+
#
|
515
|
+
def self.validate_timespec(timespec)
|
516
|
+
if timespec
|
517
|
+
timespec = validate_options timespec, [:delay, :at]
|
518
|
+
end
|
519
|
+
end
|
520
|
+
|
521
|
+
# Returns a Time object which represents the absolute point in time
|
522
|
+
# referenced by +timespec+ in the context of delaying a propagation
|
523
|
+
# between +source+ and +target+.
|
524
|
+
#
|
525
|
+
# See validate_timespec for more information
|
526
|
+
def self.make_delay(timeref, source, target, timespec)
|
527
|
+
if delay = timespec[:delay] then timeref + delay
|
528
|
+
elsif at = timespec[:at] then at
|
529
|
+
else
|
530
|
+
raise ArgumentError, "invalid timespec #{timespec}"
|
531
|
+
end
|
532
|
+
end
|
533
|
+
|
534
|
+
# The topological ordering of events w.r.t. the Precedence relation.
|
535
|
+
# This gets updated on-demand when the event relations change.
|
536
|
+
attr_reader :event_ordering
|
537
|
+
# The event => index hash which give the propagation priority for each
|
538
|
+
# event
|
539
|
+
attr_reader :event_priorities
|
540
|
+
|
541
|
+
# call-seq:
|
542
|
+
# next_event(pending) => event, propagation_info
|
543
|
+
#
|
544
|
+
# Determines the event in +current_step+ which should be signalled now.
|
545
|
+
# Removes it from the set and returns the event and the associated
|
546
|
+
# propagation information.
|
547
|
+
#
|
548
|
+
# See #gather_propagation for the format of the returned # +propagation_info+
|
549
|
+
def next_event(pending)
|
550
|
+
# this variable is 2 if selected_event is being forwarded, 1 if it
|
551
|
+
# is both forwarded and signalled and 0 if it is only signalled
|
552
|
+
priority, selected_event = nil
|
553
|
+
for propagation_step in pending
|
554
|
+
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
|
559
|
+
end
|
560
|
+
|
561
|
+
do_select = if selected_event
|
562
|
+
if EventStructure::Precedence.reachable?(selected_event, target_event)
|
563
|
+
false
|
564
|
+
elsif EventStructure::Precedence.reachable?(target_event, selected_event)
|
565
|
+
true
|
566
|
+
else
|
567
|
+
priority < target_priority
|
568
|
+
end
|
569
|
+
else
|
570
|
+
true
|
571
|
+
end
|
572
|
+
|
573
|
+
if do_select
|
574
|
+
selected_event = target_event
|
575
|
+
priority = target_priority
|
576
|
+
end
|
577
|
+
end
|
578
|
+
[selected_event, *pending.delete(selected_event)]
|
579
|
+
end
|
580
|
+
|
581
|
+
# call-seq:
|
582
|
+
# prepare_propagation(target, is_forward, info) => source_events, source_generators, context
|
583
|
+
# prepare_propagation(target, is_forward, info) => nil
|
584
|
+
#
|
585
|
+
# Parses the propagation information +info+ in the context of a
|
586
|
+
# signalling if +is_forward+ is true and a forwarding otherwise.
|
587
|
+
# +target+ is the target event.
|
588
|
+
#
|
589
|
+
# The method adds the appropriate delayed events using #add_event_delay,
|
590
|
+
# and returns either nil if no propagation is to be performed, or the
|
591
|
+
# propagation source events, generators and context.
|
592
|
+
#
|
593
|
+
# The format of +info+ is the same as the hash values described in
|
594
|
+
# #gather_propagation.
|
595
|
+
def prepare_propagation(target, is_forward, info)
|
596
|
+
timeref = Time.now
|
597
|
+
|
598
|
+
source_events, source_generators, context = ValueSet.new, ValueSet.new, []
|
599
|
+
|
600
|
+
delayed = true
|
601
|
+
info.each_slice(3) do |src, ctxt, time|
|
602
|
+
if time && (delay = ExecutionEngine.make_delay(timeref, src, target, time))
|
603
|
+
add_event_delay(delay, is_forward, src, target, ctxt)
|
604
|
+
next
|
605
|
+
end
|
606
|
+
|
607
|
+
delayed = false
|
608
|
+
|
609
|
+
# Merge identical signals. Needed because two different event handlers
|
610
|
+
# can both call #emit, and two signals are set up
|
611
|
+
if src
|
612
|
+
if src.respond_to?(:generator)
|
613
|
+
source_events << src
|
614
|
+
source_generators << src.generator
|
615
|
+
else
|
616
|
+
source_generators << src
|
617
|
+
end
|
618
|
+
end
|
619
|
+
if ctxt
|
620
|
+
context.concat ctxt
|
621
|
+
end
|
622
|
+
end
|
623
|
+
|
624
|
+
unless delayed
|
625
|
+
[source_events, source_generators, (context unless context.empty?)]
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
|
630
|
+
# Propagate one step
|
631
|
+
#
|
632
|
+
# +current_step+ describes all pending emissions and calls.
|
633
|
+
#
|
634
|
+
# This method calls ExecutionEngine.next_event to get the description of the
|
635
|
+
# next event to call. If there are signals going to this event, they are
|
636
|
+
# processed and the forwardings will be treated in the next step.
|
637
|
+
#
|
638
|
+
# The method returns the next set of pending emissions and calls, adding
|
639
|
+
# the forwardings and signals that the propagation of the considered event
|
640
|
+
# have added.
|
641
|
+
def event_propagation_step(current_step)
|
642
|
+
signalled, forward_info, call_info = next_event(current_step)
|
643
|
+
|
644
|
+
next_step = nil
|
645
|
+
if call_info
|
646
|
+
source_events, source_generators, context = prepare_propagation(signalled, false, call_info)
|
647
|
+
if source_events
|
648
|
+
for source_ev in source_events
|
649
|
+
source_ev.generator.signalling(source_ev, signalled)
|
650
|
+
end
|
651
|
+
|
652
|
+
if signalled.self_owned?
|
653
|
+
next_step = gather_propagation(current_step) do
|
654
|
+
propagation_context(source_events | source_generators) do |result|
|
655
|
+
begin
|
656
|
+
signalled.call_without_propagation(context)
|
657
|
+
rescue Roby::LocalizedError => e
|
658
|
+
signalled.emit_failed(e)
|
659
|
+
rescue Exception => e
|
660
|
+
signalled.emit_failed(Roby::CommandFailed.new(e, signalled))
|
661
|
+
end
|
662
|
+
end
|
663
|
+
end
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
if forward_info
|
668
|
+
next_step ||= Hash.new
|
669
|
+
next_step[signalled] ||= []
|
670
|
+
next_step[signalled][0] ||= []
|
671
|
+
next_step[signalled][0].concat forward_info
|
672
|
+
end
|
673
|
+
|
674
|
+
elsif forward_info
|
675
|
+
source_events, source_generators, context = prepare_propagation(signalled, true, forward_info)
|
676
|
+
if source_events
|
677
|
+
for source_ev in source_events
|
678
|
+
source_ev.generator.forwarding(source_ev, signalled)
|
679
|
+
end
|
680
|
+
|
681
|
+
# If the destination event is not owned, but if the peer is not
|
682
|
+
# connected, the event is our responsibility now.
|
683
|
+
if signalled.self_owned? || !signalled.owners.any? { |peer| peer != Roby::Distributed && peer.connected? }
|
684
|
+
next_step = gather_propagation(current_step) do
|
685
|
+
propagation_context(source_events | source_generators) do |result|
|
686
|
+
begin
|
687
|
+
signalled.emit_without_propagation(context)
|
688
|
+
rescue Roby::LocalizedError => e
|
689
|
+
add_error(e)
|
690
|
+
rescue Exception => e
|
691
|
+
add_error(Roby::EmissionFailed.new(e, signalled))
|
692
|
+
end
|
693
|
+
end
|
694
|
+
end
|
695
|
+
end
|
696
|
+
end
|
697
|
+
end
|
698
|
+
|
699
|
+
current_step.merge!(next_step) if next_step
|
700
|
+
current_step
|
701
|
+
end
|
702
|
+
|
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
|
715
|
+
end
|
716
|
+
end
|
717
|
+
|
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])
|
724
|
+
end
|
725
|
+
|
726
|
+
finished_repairs
|
727
|
+
end
|
728
|
+
|
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
|
761
|
+
|
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
|
770
|
+
end
|
771
|
+
end
|
772
|
+
|
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
|
788
|
+
end
|
789
|
+
|
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
|
794
|
+
end
|
795
|
+
|
796
|
+
has_parent = true
|
797
|
+
end
|
798
|
+
|
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
|
805
|
+
end
|
806
|
+
end
|
807
|
+
|
808
|
+
by_task
|
809
|
+
end
|
810
|
+
|
811
|
+
parent_trees = by_task.map do |task, _|
|
812
|
+
[task, task.reverse_generated_subgraph(Roby::TaskStructure::Hierarchy)]
|
813
|
+
end
|
814
|
+
|
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
|
822
|
+
|
823
|
+
task_exceptions.each do |e|
|
824
|
+
next if e.handled?
|
825
|
+
handled = task.handle_exception(e)
|
826
|
+
|
827
|
+
if handled
|
828
|
+
handled_exception(e, task)
|
829
|
+
e.handled = true
|
830
|
+
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
|
835
|
+
end
|
836
|
+
end
|
837
|
+
end
|
838
|
+
|
839
|
+
exceptions = new_exceptions
|
840
|
+
end
|
841
|
+
|
842
|
+
if !fatal.empty?
|
843
|
+
Roby::ExecutionEngine.debug do
|
844
|
+
"remaining fatal exceptions: #{fatal.map(&:exception).map(&:to_s).join(", ")}"
|
845
|
+
end
|
846
|
+
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
|
+
end
|
853
|
+
|
854
|
+
# A set of proc objects which should be executed at the beginning of the
|
855
|
+
# next execution cycle.
|
856
|
+
attr_reader :process_once
|
857
|
+
|
858
|
+
# Schedules +block+ to be called at the beginning of the next execution
|
859
|
+
# cycle, in propagation context.
|
860
|
+
def once(&block)
|
861
|
+
process_once.push block
|
862
|
+
end
|
863
|
+
|
864
|
+
# The set of errors which have been generated outside of the plan's
|
865
|
+
# control. For now, those errors cause the whole controller to shut
|
866
|
+
# down.
|
867
|
+
attr_reader :application_exceptions
|
868
|
+
def clear_application_exceptions
|
869
|
+
result, @application_exceptions = @application_exceptions, nil
|
870
|
+
result
|
871
|
+
end
|
872
|
+
|
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 ||= []
|
903
|
+
|
904
|
+
# Generate exceptions from task structure
|
905
|
+
structure_errors = plan.check_structure
|
906
|
+
add_timepoint(stats, :structure_check)
|
907
|
+
|
908
|
+
# Propagate the errors. Note that the plan repairs are taken into
|
909
|
+
# account in ExecutionEngine.propagate_exceptions drectly. We keep
|
910
|
+
# event and structure errors separate since in the first case there
|
911
|
+
# is not two-stage handling (all errors that have not been handled
|
912
|
+
# 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)
|
917
|
+
|
918
|
+
# 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)
|
929
|
+
end
|
930
|
+
kill_tasks.merge(new_tasks)
|
931
|
+
end
|
932
|
+
kill_tasks
|
933
|
+
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
|
939
|
+
end
|
940
|
+
""
|
941
|
+
end
|
942
|
+
end
|
943
|
+
end
|
944
|
+
add_timepoint(stats, :exceptions_fatal)
|
945
|
+
|
946
|
+
garbage_collect(kill_tasks)
|
947
|
+
add_timepoint(stats, :garbage_collect)
|
948
|
+
|
949
|
+
application_errors, @application_exceptions =
|
950
|
+
@application_exceptions, nil
|
951
|
+
for error, origin in application_errors
|
952
|
+
add_framework_error(error, origin)
|
953
|
+
end
|
954
|
+
|
955
|
+
if Roby.app.abort_on_exception? && !fatal_errors.empty?
|
956
|
+
reraise(fatal_errors.map { |e, _| e })
|
957
|
+
end
|
958
|
+
|
959
|
+
ensure
|
960
|
+
@application_exceptions = nil
|
961
|
+
end
|
962
|
+
|
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
|
968
|
+
end
|
969
|
+
end
|
970
|
+
|
971
|
+
# Hook called when an exception +e+ has been handled by +task+
|
972
|
+
def handled_exception(e, task); super if defined? super end
|
973
|
+
|
974
|
+
# Kills and removes all unneeded tasks. +force_on+ is a set of task
|
975
|
+
# whose garbage-collection must be performed, even though those tasks
|
976
|
+
# are actually useful for the system. This is used to properly kill
|
977
|
+
# tasks for which errors have been detected.
|
978
|
+
def garbage_collect(force_on = nil)
|
979
|
+
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)
|
982
|
+
end
|
983
|
+
|
984
|
+
# The set of tasks for which we queued stop! at this cycle
|
985
|
+
# #finishing? is false until the next event propagation cycle
|
986
|
+
finishing = ValueSet.new
|
987
|
+
did_something = true
|
988
|
+
while did_something
|
989
|
+
did_something = false
|
990
|
+
|
991
|
+
tasks = plan.unneeded_tasks | plan.force_gc
|
992
|
+
local_tasks = plan.local_tasks & tasks
|
993
|
+
remote_tasks = tasks - local_tasks
|
994
|
+
|
995
|
+
# Remote tasks are simply removed, regardless of other concerns
|
996
|
+
for t in remote_tasks
|
997
|
+
ExecutionEngine.debug { "GC: removing the remote task #{t}" }
|
998
|
+
plan.remove_object(t)
|
999
|
+
end
|
1000
|
+
|
1001
|
+
break if local_tasks.empty?
|
1002
|
+
|
1003
|
+
if local_tasks.all? { |t| t.pending? || t.finished? }
|
1004
|
+
local_tasks.each do |t|
|
1005
|
+
ExecutionEngine.debug { "GC: #{t} is not running, removed" }
|
1006
|
+
plan.garbage(t)
|
1007
|
+
plan.remove_object(t)
|
1008
|
+
end
|
1009
|
+
break
|
1010
|
+
end
|
1011
|
+
|
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
|
1022
|
+
end
|
1023
|
+
end
|
1024
|
+
|
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
|
1035
|
+
end
|
1036
|
+
end
|
1037
|
+
|
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
|
1043
|
+
elsif local_task.starting?
|
1044
|
+
# 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
|
1050
|
+
elsif !local_task.finishing?
|
1051
|
+
if local_task.event(:stop).controlable?
|
1052
|
+
ExecutionEngine.debug { "GC: queueing #{local_task}/stop" }
|
1053
|
+
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
|
1056
|
+
else
|
1057
|
+
finishing << local_task
|
1058
|
+
once do
|
1059
|
+
ExecutionEngine.info { "GC: stopping #{local_task}" }
|
1060
|
+
local_task.stop!(nil)
|
1061
|
+
end
|
1062
|
+
end
|
1063
|
+
else
|
1064
|
+
ExecutionEngine.warn "GC: ignored #{local_task}, it cannot be stopped"
|
1065
|
+
plan.gc_quarantine << local_task
|
1066
|
+
end
|
1067
|
+
elsif local_task.finishing?
|
1068
|
+
ExecutionEngine.debug { "GC: waiting for #{local_task} to finish" }
|
1069
|
+
else
|
1070
|
+
ExecutionEngine.warn "GC: ignored #{local_task}"
|
1071
|
+
end
|
1072
|
+
end
|
1073
|
+
end
|
1074
|
+
|
1075
|
+
plan.unneeded_events.each do |event|
|
1076
|
+
plan.remove_object(event)
|
1077
|
+
end
|
1078
|
+
end
|
1079
|
+
|
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
|
1083
|
+
|
1084
|
+
# The priority of the control thread
|
1085
|
+
THREAD_PRIORITY = 10
|
1086
|
+
|
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
|
1095
|
+
|
1096
|
+
# Calls the periodic blocks which should be called
|
1097
|
+
def self.call_every(plan) # :nodoc:
|
1098
|
+
engine = plan.engine
|
1099
|
+
now = engine.cycle_start
|
1100
|
+
length = engine.cycle_length
|
1101
|
+
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
|
1108
|
+
end
|
1109
|
+
rescue Exception => e
|
1110
|
+
engine.add_framework_error(e, "#call_every, in #{block}")
|
1111
|
+
end
|
1112
|
+
[block, last_call, duration]
|
1113
|
+
end
|
1114
|
+
end
|
1115
|
+
|
1116
|
+
# A list of threads which are currently waitiing for the control thread
|
1117
|
+
# (see for instance Roby.execute)
|
1118
|
+
#
|
1119
|
+
# #run will raise ExecutionQuitError on this threads if they
|
1120
|
+
# are still waiting while the control is quitting
|
1121
|
+
attr_reader :waiting_threads
|
1122
|
+
|
1123
|
+
# A set of blocks that are called at each cycle end
|
1124
|
+
attr_reader :at_cycle_end_handlers
|
1125
|
+
|
1126
|
+
# Call +block+ at the end of the execution cycle
|
1127
|
+
def at_cycle_end(&block)
|
1128
|
+
at_cycle_end_handlers << block
|
1129
|
+
end
|
1130
|
+
|
1131
|
+
# A set of blocks which are called every cycle
|
1132
|
+
attr_reader :process_every
|
1133
|
+
|
1134
|
+
# Call +block+ every +duration+ seconds. Note that +duration+ is round
|
1135
|
+
# up to the cycle size (time between calls is *at least* duration)
|
1136
|
+
#
|
1137
|
+
# The returned value is the periodic handler ID. It can be passed to
|
1138
|
+
# #remove_periodic_handler to undefine it.
|
1139
|
+
def every(duration, &block)
|
1140
|
+
once do
|
1141
|
+
block.call
|
1142
|
+
process_every << [block, cycle_start, duration]
|
1143
|
+
end
|
1144
|
+
block.object_id
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
# Removes a periodic handler defined by #every. +id+ is the value
|
1148
|
+
# returned by #every.
|
1149
|
+
def remove_periodic_handler(id)
|
1150
|
+
execute do
|
1151
|
+
process_every.delete_if { |spec| spec[0].object_id == id }
|
1152
|
+
end
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
# The execution thread if there is one running
|
1156
|
+
attr_accessor :thread
|
1157
|
+
# True if an execution thread is running
|
1158
|
+
def running?; !!@thread end
|
1159
|
+
|
1160
|
+
# The cycle length in seconds
|
1161
|
+
attr_reader :cycle_length
|
1162
|
+
|
1163
|
+
# The starting Time of this cycle
|
1164
|
+
attr_reader :cycle_start
|
1165
|
+
|
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
|
1177
|
+
|
1178
|
+
# True if the current thread is not the execution thread of this
|
1179
|
+
# engine, or if there is not control thread. When you check the current
|
1180
|
+
# 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
|
+
#
|
1192
|
+
# Since the first form will fail if there is no control thread, while
|
1193
|
+
# the second form will work. Use the first form only if you require
|
1194
|
+
# 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
|
1206
|
+
|
1207
|
+
options = validate_options options, :cycle => 0.1
|
1208
|
+
|
1209
|
+
@quit = 0
|
1210
|
+
@allow_propagation = false
|
1211
|
+
|
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
|
1218
|
+
|
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)
|
1238
|
+
end
|
1239
|
+
end
|
1240
|
+
end
|
1241
|
+
|
1242
|
+
attr_reader :last_stop_count # :nodoc:
|
1243
|
+
|
1244
|
+
# Sets up the plan for clearing: it discards all missions and undefines
|
1245
|
+
# all permanent tasks and events.
|
1246
|
+
#
|
1247
|
+
# Returns nil if the plan is cleared, and the set of remaining tasks
|
1248
|
+
# otherwise. Note that quaranteened tasks are not counted as remaining,
|
1249
|
+
# 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]
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
# If set to true, Roby will warn if the GC cannot be controlled by Roby
|
1307
|
+
attr_predicate :gc_warning?, true
|
1308
|
+
|
1309
|
+
# The main event loop. It returns when the execution engine is asked to
|
1310
|
+
# quit. In general, this does not need to be called direclty: use #run
|
1311
|
+
# 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
|
1347
|
+
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)
|
1361
|
+
end
|
1362
|
+
|
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
|
1399
|
+
end
|
1400
|
+
|
1401
|
+
stats[:start] = [cycle_start.tv_sec, cycle_start.tv_usec]
|
1402
|
+
stats[:state] = Roby::State
|
1403
|
+
cycle_end(stats)
|
1404
|
+
stats = Hash.new
|
1405
|
+
|
1406
|
+
@cycle_start += cycle_length
|
1407
|
+
@cycle_index += 1
|
1408
|
+
|
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
|
1413
|
+
end
|
1414
|
+
quit
|
1415
|
+
end
|
1416
|
+
end
|
1417
|
+
|
1418
|
+
ensure
|
1419
|
+
GC.enable if !already_disabled_gc
|
1420
|
+
|
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
|
1428
|
+
|
1429
|
+
# A set of proc objects which are to be called when the execution engine
|
1430
|
+
# quits.
|
1431
|
+
attr_reader :finalizers
|
1432
|
+
|
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
|
1472
|
+
|
1473
|
+
# Block until the given block is executed by the execution thread, at
|
1474
|
+
# the beginning of the event loop, in propagation context. If the block
|
1475
|
+
# 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
|
1493
|
+
|
1494
|
+
caller_thread = Thread.current
|
1495
|
+
waiting_threads << caller_thread
|
1496
|
+
|
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
|
1509
|
+
|
1510
|
+
ensure
|
1511
|
+
Roby.return_condition_variable(cv)
|
1512
|
+
end
|
1513
|
+
|
1514
|
+
# Stops the current thread until the given even is emitted. If the event
|
1515
|
+
# becomes unreachable, an UnreachableEvent exception is raised.
|
1516
|
+
def wait_until(ev)
|
1517
|
+
if inside_control?
|
1518
|
+
raise ThreadMismatch, "cannot use #wait_until in execution threads"
|
1519
|
+
end
|
1520
|
+
|
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
|
1526
|
+
|
1527
|
+
mt.synchronize do
|
1528
|
+
once do
|
1529
|
+
ev.if_unreachable(true) do |reason|
|
1530
|
+
caller_thread.raise UnreachableEvent.new(ev, reason)
|
1531
|
+
end
|
1532
|
+
ev.on do
|
1533
|
+
mt.synchronize { cv.broadcast }
|
1534
|
+
end
|
1535
|
+
yield if block_given?
|
1536
|
+
end
|
1537
|
+
cv.wait(mt)
|
1538
|
+
end
|
1539
|
+
end
|
1540
|
+
end
|
1541
|
+
end
|
1542
|
+
|
1543
|
+
class << self
|
1544
|
+
# The ExecutionEngine object which executes Roby.plan
|
1545
|
+
attr_reader :engine
|
1546
|
+
|
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"
|
1555
|
+
end
|
1556
|
+
|
1557
|
+
@engine = new_engine
|
1558
|
+
@control = new_engine.control
|
1559
|
+
end
|
1560
|
+
end
|
1561
|
+
|
1562
|
+
# Execute the given block in the main plan's propagation context, but don't
|
1563
|
+
# wait for its completion like Roby.execute does
|
1564
|
+
#
|
1565
|
+
# See ExecutionEngine#once
|
1566
|
+
def self.once; engine.once { yield } end
|
1567
|
+
|
1568
|
+
# Make the main engine call +block+ during each propagation step.
|
1569
|
+
# See ExecutionEngine#each_cycle
|
1570
|
+
def self.each_cycle(&block); engine.each_cycle(&block) end
|
1571
|
+
|
1572
|
+
# Install a periodic handler on the main engine
|
1573
|
+
def self.every(duration, &block); engine.every(duration, &block) end
|
1574
|
+
|
1575
|
+
# True if the current thread is the execution thread of the main engine
|
1576
|
+
#
|
1577
|
+
# See ExecutionEngine#inside_control?
|
1578
|
+
def self.inside_control?; engine.inside_control? end
|
1579
|
+
|
1580
|
+
# True if the current thread is not the execution thread of the main engine
|
1581
|
+
#
|
1582
|
+
# See ExecutionEngine#outside_control?
|
1583
|
+
def self.outside_control?; engine.outside_control? end
|
1584
|
+
|
1585
|
+
# Execute the given block during the event propagation step of the main
|
1586
|
+
# engine. See ExecutionEngine#execute
|
1587
|
+
def self.execute
|
1588
|
+
engine.execute do
|
1589
|
+
yield
|
1590
|
+
end
|
1591
|
+
end
|
1592
|
+
|
1593
|
+
# Blocks until the main engine has executed at least one cycle.
|
1594
|
+
# See ExecutionEngine#wait_one_cycle
|
1595
|
+
def self.wait_one_cycle; engine.wait_one_cycle end
|
1596
|
+
|
1597
|
+
# Stops the current thread until the given even is emitted. If the event
|
1598
|
+
# becomes unreachable, an UnreachableEvent exception is raised.
|
1599
|
+
#
|
1600
|
+
# See ExecutionEngine#wait_until
|
1601
|
+
def self.wait_until(ev, &block); engine.wait_until(ev, &block) end
|
1602
|
+
end
|
1603
|
+
|
1604
|
+
|