class Roby::ExecutablePlan

A plan that can be used for execution

While {Plan} maintains the plan data structure itself, this class provides excution-related services such as exceptions and GC-related methods

Attributes

connection_space[RW]

The ConnectionSpace object which handles this plan. The role of this object is to sharing with other Roby plan managers

exception_handlers[R]

The list of plan-wide exception handlers

@return [Array<(===, call)>]

execution_engine[RW]

The ExecutionEngine object which handles this plan. The role of this object is to provide the event propagation, error propagation and garbage collection mechanisms for the execution.

@return [ExecutionEngine]

force_gc[R]

A set of tasks which are useful (and as such would not been garbage collected), but we want to GC anyway

@return [Set<Roby::Task>]

Public Class Methods

new(event_logger: DRoby::NullEventLogger.new) click to toggle source
Calls superclass method Roby::Plan::new
# File lib/roby/executable_plan.rb, line 43
def initialize(event_logger: DRoby::NullEventLogger.new)
    super(graph_observer: self, event_logger: event_logger)

    @execution_engine = ExecutionEngine.new(self)
    @force_gc    = Set.new
    @exception_handlers = Array.new
    on_exception LocalizedError do |plan, error|
        plan.default_localized_error_handling(error)
    end
end

Public Instance Methods

added_edge(parent, child, relations, info) click to toggle source

@api private

Hook called after a new edge has been added in this plan

@param [Object] parent the child object @param [Object] child the child object @param [Array<Class<Relations::Graph>>] relations the graphs in which an edge

has been added

@param [Object] info the associated edge info that applies to

relations.first
# File lib/roby/executable_plan.rb, line 213
def added_edge(parent, child, relations, info)
    relations.each do |rel|
        if rel == Roby::EventStructure::Precedence
            execution_engine.event_ordering.clear
        end

        if name = rel.child_name
            parent.send("added_#{rel.child_name}", child, info)
            child.send("added_#{rel.child_name}_parent", parent, info)
        end
    end

    log(:added_edge, parent, child, relations, info)
end
adding_edge(parent, child, relations, info) click to toggle source

@api private

Hook called before an edge gets added to this plan

If an exception is raised, the edge will not be added

@param [Object] parent the child object @param [Object] child the child object @param [Array<Class<Relations::Graph>>] relations the graphs in which an edge

has been added

@param [Object] info the associated edge info that applies to

relations.first
# File lib/roby/executable_plan.rb, line 173
def adding_edge(parent, child, relations, info)
    if !parent.read_write? || !child.read_write?
        raise OwnershipError, "cannot remove a relation between two objects we don't own"
    elsif parent.garbage?
        raise ReusingGarbage, "attempting to reuse #{parent} which is marked as garbage"
    elsif child.garbage?
        raise ReusingGarbage, "attempting to reuse #{child} which is marked as garbage"
    end

    if last_dag = relations.find_all(&:dag?).last
        if child.relation_graph_for(last_dag).reachable?(child, parent)
            raise Relations::CycleFoundError, "adding an edge from #{parent} to #{child} would create a cycle in #{last_dag}"
        end
    end

    relations.each do |rel|
        if name = rel.child_name
            parent.send("adding_#{rel.child_name}", child, info)
            child.send("adding_#{rel.child_name}_parent", parent, info)
        end
    end

    for trsc in transactions
        next unless trsc.proxying?
        if (parent_proxy = trsc[parent, create: false]) && (child_proxy = trsc[child, create: false])
            trsc.adding_plan_relation(parent_proxy, child_proxy, relations, info) 
        end
    end
end
call_structure_check_handler(handler) click to toggle source

@api private

Called by {ExecutionEngine} to verify the plan's internal structure

Calls superclass method Roby::Plan#call_structure_check_handler
# File lib/roby/executable_plan.rb, line 542
def call_structure_check_handler(handler)
    super
rescue Exception => e
    execution_engine.add_framework_error(e, 'structure checking')
end
clear() click to toggle source

Clear the plan

Calls superclass method Roby::Plan#clear
# File lib/roby/executable_plan.rb, line 525
def clear
    super
    @force_gc.clear
end
control() click to toggle source

The DecisionControl object which is associated with this plan. This object's role is to handle the conflicts that can occur during event propagation.

# File lib/roby/executable_plan.rb, line 30
def control; execution_engine.control end
default_localized_error_handling(error) click to toggle source

@api private

Default toplevel error handling for {LocalizedError}

It activates fault handlers, and adds {MissionFailedError} / {PermanentTaskError}

# File lib/roby/executable_plan.rb, line 93
def default_localized_error_handling(error)
    matching_handlers = Array.new
    active_fault_response_tables.each do |table|
        table.find_all_matching_handlers(error).each do |handler|
            matching_handlers << [table, handler]
        end
    end
    handlers = matching_handlers.sort_by { |_, handler| handler.priority }

    while !handlers.empty?
        table, handler = handlers.shift
        if handler
            begin
                handler.activate(error, table.arguments)
                return
            rescue Exception => e
                Robot.warn "ignored exception handler #{handler} because of exception"
                Roby.log_exception_with_backtrace(e, Robot, :warn)
            end
        end
    end

    pass_exception
end
each_exception_handler(&block) click to toggle source

Iterate over the plan-wide exception handlers

@yieldparam [#===] matcher an object that allows to match an

{ExecutionException}

@yieldparam [#call] handler the exception handler, which will be

called with the plan and the {ExecutionException} object as argument
# File lib/roby/executable_plan.rb, line 494
def each_exception_handler(&block)
    exception_handlers.each(&block)
end
emit_relation_change_hook(parent, child, rel, *args, prefix: nil) click to toggle source

@api private

Helper for {#updating_edge_info} and {#updated_edge_info}

# File lib/roby/executable_plan.rb, line 307
def emit_relation_change_hook(parent, child, rel, *args, prefix: nil)
    if name = rel.child_name
        parent.send("#{prefix}_#{rel.child_name}", child, *args)
        child.send("#{prefix}_#{rel.child_name}_parent", parent, *args)
    end
end
emit_relation_graph_merge_hooks(graph, prefix: nil) click to toggle source

@api private

Calls the added_* hook methods for all edges in a relation graph

It is a helper for {#merged_plan}

# File lib/roby/executable_plan.rb, line 319
def emit_relation_graph_merge_hooks(graph, prefix: nil)
    rel = graph.class
    if rel.child_name
        added_child_hook  = "#{prefix}_#{rel.child_name}"
        added_parent_hook = "#{added_child_hook}_parent"
        graph.each_edge do |parent, child, info|
            parent.send(added_child_hook, child, info)
            child.send(added_parent_hook, parent, info)
        end
    end
end
emit_relation_graph_transaction_application_hooks(list, prefix: nil) click to toggle source

@api private

Calls the added_ and adding_ hooks for modifications originating from a transaction that involve tasks originally from the plan

# File lib/roby/executable_plan.rb, line 335
def emit_relation_graph_transaction_application_hooks(list, prefix: nil)
    hooks = Hash.new
    list.each do |graph, parent, child, *args|
        if !hooks.has_key?(graph)
            rel = graph.class
            if rel.child_name
                parent_hook = "#{prefix}_#{rel.child_name}"
                child_hook  = "#{parent_hook}_parent"
                hooks[graph] = [parent_hook, child_hook]
            else
                hooks[graph] = nil
            end
        end

        parent_hook, child_hook = hooks[graph]
        next if !child_hook

        parent.send(parent_hook, child, *args)
        child.send(child_hook, parent, *args)
    end
end
engine() click to toggle source

@deprecated use {#execution_engine} instead

# File lib/roby/executable_plan.rb, line 22
def engine
    Roby.warn_deprecated "Plan#engine is deprecated, use #execution_engine instead"
    execution_engine
end
event_logger=(logger) click to toggle source
Calls superclass method
# File lib/roby/executable_plan.rb, line 82
def event_logger=(logger)
    super
    log :register_executable_plan, droby_id
end
executable?() click to toggle source

Check that this is an executable plan

This always returns true for {ExecutablePlan}

# File lib/roby/executable_plan.rb, line 75
def executable?; true end
execute(&block) click to toggle source

Calls the given block in the execution thread of this plan's engine. If there is no engine attached to this plan, yields immediately

See ExecutionEngine#execute

# File lib/roby/executable_plan.rb, line 141
def execute(&block)
    execution_engine.execute(&block)
end
finalized_event(event) click to toggle source

@api private

Hook called when an event is finalized

Calls superclass method Roby::Plan#finalized_event
# File lib/roby/executable_plan.rb, line 156
def finalized_event(event)
    execution_engine.finalized_event(event)
    super
end
finalized_task(task) click to toggle source

@api private

Hook called when a task is finalized

Calls superclass method Roby::Plan#finalized_task
# File lib/roby/executable_plan.rb, line 148
def finalized_task(task)
    execution_engine.finalized_task(task)
    super
end
garbage_event(event) click to toggle source

Called to handle a free event that should be garbage-collected

What actually happens to the event is controlled by {PlanObject#can_finalize?}. If the event can be finalized, it is (i.e. removed from the plan, after having triggered all relevant log events/hooks). Otherwise, its relations are cleared and the task is left in the plan

@return [Boolean] true if the plan got modified, and false otherwise.

In practice, it will return false only for events that had no
relations and that cannot be finalized.
# File lib/roby/executable_plan.rb, line 476
def garbage_event(event)
    log(:garbage_event, droby_id, event)
    if event.can_finalize?
        remove_free_event(event)
        true
    else
        event.clear_relations(remove_strong: false)
    end
end
garbage_task(task) click to toggle source

@api private

Called to handle a task that should be garbage-collected

What actually happens to the task is controlled by {PlanObject#can_finalize?}.

If the task can be finalized, it is removed from the plan, after having triggered all relevant log events/hooks.

Otherwise, it is isolated from the rest of the plan. Its relations and the relations of its events are cleared and the task is left in the plan. In the latter case, the task is marked as non-reusable.

Always check {Task#reusable?} before using a task present in an {ExecutablePlan} in a new structure.

@param [Task] task the task that is being garbage-collected @return [Boolean] true if the plan got modified, and false otherwise.

In practice, it will return false only if the task cannot be
finalized *and* has external relations.
# File lib/roby/executable_plan.rb, line 453
def garbage_task(task)
    log(:garbage_task, droby_id, task, task.can_finalize?)

    if task.can_finalize?
        remove_task(task)
        true
    else
        task.garbage!
        task.clear_relations(remove_internal: false, remove_strong: false)
    end
end
generate_induced_errors(error_phase_results) click to toggle source
# File lib/roby/executable_plan.rb, line 118
def generate_induced_errors(error_phase_results)
    error_phase_results.each_fatal_error do |execution_exception, tasks|
        # MissionFailedError and PermanentTaskError are not propagated,
        # so tasks == [origin] and we should not re-add an error
        if execution_exception.exception.kind_of?(MissionFailedError) ||
                execution_exception.exception.kind_of?(PermanentTaskError)
            next
        end

        tasks.each do |t|
            if mission_task?(t)
                add_error(MissionFailedError.new(t, execution_exception.exception), propagate_through: [])
            elsif permanent_task?(t)
                add_error(PermanentTaskError.new(t, execution_exception.exception), propagate_through: [])
            end
        end
    end
end
merge_transaction(transaction, merged_graphs, added, removed, updated) click to toggle source

@api private

Applies modification information extracted from a transaction. This is used by {Transaction#commit_transaction}

Calls superclass method Roby::Plan#merge_transaction
# File lib/roby/executable_plan.rb, line 361
def merge_transaction(transaction, merged_graphs, added, removed, updated)
    added.each do |_, parent, child, _|
        if parent.garbage?
            raise ReusingGarbage, "attempting to reuse #{parent} which is marked as garbage"
        elsif child.garbage?
            raise ReusingGarbage, "attempting to reuse #{child} which is marked as garbage"
        end
    end

    emit_relation_graph_transaction_application_hooks(added, prefix: 'adding')
    emit_relation_graph_transaction_application_hooks(removed, prefix: 'removing')
    emit_relation_graph_transaction_application_hooks(updated, prefix: 'updating')

    super

    precedence_graph = event_relation_graph_for(EventStructure::Precedence)
    precedence_edge_count = precedence_graph.num_edges
    emit_relation_graph_transaction_application_hooks(added, prefix: 'added')
    if precedence_edge_count != precedence_graph.num_edges
        execution_engine.event_ordering.clear
    end
    emit_relation_graph_transaction_application_hooks(removed, prefix: 'removed')
    if precedence_edge_count != precedence_graph.num_edges
        execution_engine.event_ordering.clear
    end
    emit_relation_graph_transaction_application_hooks(updated, prefix: 'updated')

    added.each do |graph, parent, child, info|
        log(:added_edge, parent, child, [graph.class], info)
    end
    removed.each do |graph, parent, child|
        log(:removed_edge, parent, child, [graph.class])
    end
    updated.each do |graph, parent, child, info|
        log(:updated_edge_info, parent, child, graph.class, info)
    end
end
merged_plan(plan) click to toggle source

@api private

Emits the added_* hooks when a plan gets merged in self

Calls superclass method Roby::Plan#merged_plan
# File lib/roby/executable_plan.rb, line 415
def merged_plan(plan)
    if !plan.event_relation_graph_for(EventStructure::Precedence).empty?
        execution_engine.event_ordering.clear
    end

    plan.each_task_relation_graph do |graph|
        emit_relation_graph_merge_hooks(graph, prefix: 'added')
    end
    plan.each_event_relation_graph do |graph|
        emit_relation_graph_merge_hooks(graph, prefix: 'added')
    end

    super

    log(:merged_plan, droby_id, plan)
end
merging_plan(plan) click to toggle source

@api private

Emits the adding_* hooks when a plan gets merged in self

Calls superclass method Roby::Plan#merging_plan
# File lib/roby/executable_plan.rb, line 402
def merging_plan(plan)
    plan.each_task_relation_graph do |graph|
        emit_relation_graph_merge_hooks(graph, prefix: 'adding')
    end
    plan.each_event_relation_graph do |graph|
        emit_relation_graph_merge_hooks(graph, prefix: 'adding')
    end
    super
end
on_exception(matcher, &handler) click to toggle source

Register a new exception handler

@param [#===,#to_execution_exception_matcher] matcher

an object that matches exceptions for which the handler should be
called. Exception classes can be used directly. If more advanced
matching is needed, use .match to convert an exception class into
{Queries::LocalizedErrorMatcher} or one of its subclasses.

@yieldparam [ExecutablePlan] plan the plan in which the exception

happened

@yieldparam [ExecutionException] exception the exception that is being handled

# File lib/roby/executable_plan.rb, line 509
def on_exception(matcher, &handler)
    check_arity(handler, 2)
    exception_handlers.unshift [matcher.to_execution_exception_matcher, handler]
end
quarantine_task(task) click to toggle source

@api private

Put the given task in quarantine. In practice, it means that all the event relations of that task's events are removed, as well as its children. Then, the task is marked as quarantined with {Task#quarantined?} and the engine will not attempt to garbage-collect it anymore

This is used as a last resort, when the task cannot be stopped/GCed by normal means.

# File lib/roby/executable_plan.rb, line 64
def quarantine_task(task)
    log(:quarantined_task, droby_id, task)

    task.quarantined!
    task.clear_relations(remove_internal: false, remove_strong: false)
    self
end
refresh_relations() click to toggle source
Calls superclass method Roby::Plan#refresh_relations
# File lib/roby/executable_plan.rb, line 77
def refresh_relations
    super
    execution_engine.refresh_relations
end
remove_task(object, timestamp = nil) click to toggle source

Actually remove a task from the plan

Calls superclass method Roby::Plan#remove_task
# File lib/roby/executable_plan.rb, line 515
def remove_task(object, timestamp = nil)
    if object.respond_to?(:running?) && object.running? && object.self_owned?
        raise ArgumentError, "attempting to remove #{object}, which is a running task, from an executable plan"
    end

    super
    @force_gc.delete(object)
end
removed_edge(parent, child, relations) click to toggle source

@api private

Hook called after an edge has been removed from this plan

@param [Object] parent the child object @param [Object] child the child object @param [Array<Class<Relations::Graph>>] relations the graphs in which an edge

has been removed
# File lib/roby/executable_plan.rb, line 293
def removed_edge(parent, child, relations)
    relations.each do |rel|
        if name = rel.child_name
            parent.send("removed_#{rel.child_name}", child)
            child.send("removed_#{rel.child_name}_parent", parent)
        end
    end

    log(:removed_edge, parent, child, relations)
end
removing_edge(parent, child, relations) click to toggle source

@api private

Hook called before an edge gets removed from this plan

If an exception is raised, the edge will not be removed

@param [Object] parent the parent object @param [Object] child the child object @param [Array<Class<Relations::Graph>>] relations the graphs in which an edge

is being removed
# File lib/roby/executable_plan.rb, line 265
def removing_edge(parent, child, relations)
    unless parent.read_write? || child.child.read_write?
        raise OwnershipError, "cannot remove a relation between two objects we don't own"
    end

    relations.each do |rel|
        if name = rel.child_name
            parent.send("removing_#{rel.child_name}", child)
            child.send("removing_#{rel.child_name}_parent", parent)
        end
    end

    for trsc in transactions
        next unless trsc.proxying?
        if (parent_proxy = trsc[parent, create: false]) && (child_proxy = trsc[child, create: false])
            trsc.removing_plan_relation(parent_proxy, child_proxy, relations) 
        end
    end
end
respawn(task) click to toggle source

Replace task with a fresh copy of itself and start it.

See recreate for details about the new task.

# File lib/roby/executable_plan.rb, line 533
def respawn(task)
    new = recreate(task)
    execution_engine.once { new.start!(nil) }
    new
end
updated_edge_info(parent, child, relation, info) click to toggle source

@api private

Hook called when the edge information of an existing edge has been updated

@param parent the edge parent object @param child the edge child object @param [Class<Relations::Graph>] relation the relation graph ID @param [Object] info the new edge info

# File lib/roby/executable_plan.rb, line 250
def updated_edge_info(parent, child, relation, info)
    emit_relation_change_hook(parent, child, relation, info, prefix: 'updated')
    log(:updated_edge_info, parent, child, relation, info)
end
updating_edge_info(parent, child, relation, info) click to toggle source

@api private

Hook called to announce that the edge information of an existing edge will be updated

@param parent the edge parent object @param child the edge child object @param [Class<Relations::Graph>] relation the relation graph ID @param [Object] info the new edge info

# File lib/roby/executable_plan.rb, line 237
def updating_edge_info(parent, child, relation, info)
    emit_relation_change_hook(parent, child, relation, info, prefix: 'updating')
end