class Roby::Plan
A plan object manages a collection of tasks and events.
Attributes
A set of structure checking procedures that must be performed on all plans
@yieldparam [Plan] the plan @yieldreturn [Array<(to_execution_exception,Array<Task>)>] a list
of exceptions, and the tasks toward which these exceptions should be propagated. If the list of tasks is nil, all parents of the exception's origin will be selected
The list of fault response tables that are currently globally active on this plan
The event logger
The graphs that make event relations, formatted as required by {Relations::DirectedRelationSupport#relation_graphs}
The list of events that are not included in a task
The observer object that reacts to relation changes
The Peer ID of the local owner (i.e. of the local process / execution engine)
The set of the robot's missions @see add_mission_task
unmark_mission_task
The list of events that are kept outside GC. Do not change that set directly, use permanent and auto instead.
The set of tasks that are kept around “just in case” @see add_permanent_task
unmark_permanent_task
The set of PlanService
instances that are defined on this plan
The set of blocks that should be called to check the structure of the plan.
@yieldparam [Plan] the plan @yieldreturn [Array<(to_execution_exception,Array<Task>)>] a list
of exceptions, and the tasks toward which these exceptions should be propagated. If the list of tasks is nil, all parents of the exception's origin will be selected
The set of events that are defined by tasks
The task index for this plan. This is a {Queries::Index} object which allows efficient resolving of queries.
The graphs that make task relations, formatted as required by {Relations::DirectedRelationSupport#relation_graphs}
The list of tasks that are included in this plan
The set of transactions which are built on top of this plan
A set of pair of task matching objects and blocks defining this plan's triggers
See {#add_trigger}
Public Class Methods
# File lib/roby/plan.rb, line 1309 def self.can_gc?(task) if task.starting? then true # wait for the task to be started before deciding ... elsif task.running? && !task.finishing? task.event(:stop).controlable? else true end end
Get all missions that have failed
# File lib/roby/plan.rb, line 1548 def self.check_failed_missions(plan) result = Array.new for task in plan.mission_tasks result << MissionFailedError.new(task) if task.failed? end for task in plan.permanent_tasks result << PermanentTaskError.new(task) if task.failed? end result end
# File lib/roby/plan.rb, line 111 def self.instanciate_relation_graphs(graph_observer: nil) task_relation_graphs = Relations::Space.new_relation_graph_mapping Task.all_relation_spaces.each do |space| task_relation_graphs.merge!( space.instanciate(observer: graph_observer)) end event_relation_graphs = Relations::Space.new_relation_graph_mapping EventGenerator.all_relation_spaces.each do |space| event_relation_graphs.merge!( space.instanciate(observer: graph_observer)) end return task_relation_graphs, event_relation_graphs end
# File lib/roby/plan.rb, line 70 def initialize(graph_observer: nil, event_logger: DRoby::NullEventLogger.new) @local_owner = DRoby::PeerID.new('local') @mission_tasks = Set.new @permanent_tasks = Set.new @permanent_events = Set.new @tasks = Set.new @free_events = Set.new @task_events = Set.new @transactions = Set.new @fault_response_tables = Array.new @triggers = [] @plan_services = Hash.new self.event_logger = event_logger @active_fault_response_tables = Array.new @task_index = Roby::Queries::Index.new @graph_observer = graph_observer create_relations super() end
Public Instance Methods
Returns object
if object is a plan object from this plan, or if it has no plan yet (in which case it is added to the plan first). Otherwise, raises ArgumentError.
This method is provided for consistency with Transaction#[]
# File lib/roby/plan.rb, line 1296 def [](object, create = true) if object.plan == self object elsif !object.finalized? && object.plan.template? add(object) object elsif object.finalized? && create raise ArgumentError, "#{object} is has been finalized, and can't be reused" else raise ArgumentError, "#{object} is not from #{self}" end end
Adds the subplan of the given tasks and events into the plan.
That means that it adds the listed tasks/events and the task/events that are reachable through any relations).
# File lib/roby/plan.rb, line 923 def add(objects) is_scalar = objects.respond_to?(:each) objects = normalize_add_arguments(objects) plans = Set.new objects.each do |plan_object| p = plan_object.plan next if p == self if plan_object.removed_at raise ArgumentError, "cannot add #{plan_object} in #{self}, it has been removed from the plan" elsif !p raise InternalError, "there seem to be an inconsistency, #{plan_object}#plan is nil but #removed_at is not set" elsif p.empty? raise InternalError, "there seem to be an inconsistency, #{plan_object} is associated with #{p} but #{p} is empty" elsif !p.template? raise ModelViolation, "cannot add #{plan_object} in #{self}, it is already included in #{p}" end plans << p end plans.each do |p| merge!(p) end if is_scalar objects.first else objects end end
@deprecated use {#add_mission_task} instead
# File lib/roby/plan.rb, line 469 def add_mission(task) Roby.warn_deprecated "#add_mission is deprecated, use #add_mission_task instead" add_mission_task(task) end
Add a task to the plan's set of missions
A mission represents the system's overall goal. As such a mission task and all its dependencies are protected against the garbage collection mechanisms, and the emission of a mission's failed event causes a MissionFailedError
exception to be generated.
Note that this method should be used to add the task to the plan and mark it as mission, and to mark an already added task as mission as well.
@see mission_task? unmark_mission_task
# File lib/roby/plan.rb, line 503 def add_mission_task(task) task = normalize_add_arguments([task]).first return if mission_tasks.include?(task) add([task]) mission_tasks << task task.mission = true if task.self_owned? notify_task_status_change(task, :mission) task end
@deprecated use {#add_permanent_task} or {#add_permanent_event} instead
# File lib/roby/plan.rb, line 538 def add_permanent(object) Roby.warn_deprecated "#add_permanent is deprecated, use either #add_permanent_task or #add_permanent_event instead" object = normalize_add_arguments([object]).first if object.respond_to?(:to_task) add_permanent_task(object) else add_permanent_event(object) end object end
Mark an event as permanent, optionally adding to the plan
Permanent events are protected against garbage collection
# File lib/roby/plan.rb, line 609 def add_permanent_event(event) event = normalize_add_arguments([event]).first return if permanent_events.include?(event) add([event]) permanent_events << event notify_event_status_change(event, :permanent) event end
Mark a task as permanent, optionally adding to the plan
Permanent tasks are protected against garbage collection. Like missions, failure of a permanent task will generate a plan exception {PermanentTaskError}. Unlike missions, this exception is non-fatal.
# File lib/roby/plan.rb, line 578 def add_permanent_task(task) task = normalize_add_arguments([task]).first return if permanent_tasks.include?(task) add([task]) permanent_tasks << task notify_task_status_change(task, :permanent) task end
Register a new plan service on this plan
# File lib/roby/plan.rb, line 757 def add_plan_service(service) if service.task.plan != self raise "trying to register a plan service on #{self} for #{service.task}, which is included in #{service.task.plan}" end set = (plan_services[service.task] ||= Set.new) if !set.include?(service) set << service end self end
Add a trigger
This registers a notification: the given block will be called for each new task that match the given query object. It yields right away for the tasks that are already in the plan
@param [#===] query_object the object against which tasks are tested.
Tasks for which #=== returns true are yield to the block
@yieldparam [Roby::Task] task the task that matched the query object @return [Object] an ID object that can be used in {#remove_trigger}
# File lib/roby/plan.rb, line 1001 def add_trigger(query_object, &block) tr = Trigger.new(query_object, block) triggers << tr tr.each(self) do |t| tr.call(t) end tr end
Hook called when a new transaction has been built on top of this plan
# File lib/roby/plan.rb, line 1032 def added_transaction(trsc) end
# File lib/roby/plan.rb, line 865 def apply_replacement_operations(new_relations, removed_relations) removed_relations.each do |graph, parent, child| graph.remove_relation(parent, child) end new_relations.each do |graph, parent, child, info| graph.add_relation(parent, child, info) end end
# File lib/roby/plan.rb, line 280 def apply_triggers_matches(matches) matches.each do |trigger, matched_tasks| matched_tasks.each do |t| trigger.call(t) end end end
# File lib/roby/plan.rb, line 1581 def call_structure_check_handler(handler) handler.call(self) end
Perform the structure checking step by calling the procs registered in {#structure_checks} and {Plan.structure_checks}
@return [Hash<ExecutionException,Array<Roby::Task>,nil>
# File lib/roby/plan.rb, line 1589 def check_structure # Do structure checking and gather the raised exceptions exceptions = Hash.new for prc in (Plan.structure_checks + structure_checks) new_exceptions = call_structure_check_handler(prc) next unless new_exceptions format_exception_set(exceptions, new_exceptions) end exceptions end
Remove all tasks
# File lib/roby/plan.rb, line 1444 def clear tasks, @tasks = @tasks, Set.new free_events, @free_events = @free_events, Set.new clear! remaining = tasks.find_all do |t| if executable? && t.running? true else finalize_task(t) false end end if !remaining.empty? Roby.warn "#{remaining.size} tasks remaining after clearing the plan as they are still running" remaining.each do |t| Roby.warn " #{t}" end end free_events.each do |e| finalize_event(e) end self end
# File lib/roby/plan.rb, line 1427 def clear! each_task_relation_graph do |g| g.clear end each_event_relation_graph do |g| g.clear end @free_events.clear @mission_tasks.clear @tasks.clear @permanent_tasks.clear @permanent_events.clear @task_index.clear @task_events.clear end
# File lib/roby/plan.rb, line 826 def compute_subplan_replacement(mappings, relation_graphs, child_objects: true) new_relations, removed_relations = Array.new, Array.new relation_graphs.each do |graph| next if graph.strong? resolved_mappings = Hash.new mappings.each do |obj, (mapped_obj, mapped_obj_resolver)| next if !mapped_obj && !mapped_obj_resolver graph.each_in_neighbour(obj) do |parent| next if mappings.has_key?(parent) if !graph.copy_on_replace? removed_relations << [graph, parent, obj] end if !mapped_obj mapped_obj = mapped_obj_resolver.call(obj) resolved_mappings[obj] = mapped_obj end new_relations << [graph, parent, mapped_obj, graph.edge_info(parent, obj)] end next if !child_objects graph.each_out_neighbour(obj) do |child| next if mappings.has_key?(child) if !graph.copy_on_replace? removed_relations << [graph, obj, child] end if !mapped_obj mapped_obj = mapped_obj_resolver.call(obj) resolved_mappings[obj] = mapped_obj end new_relations << [graph, mapped_obj, child, graph.edge_info(obj, child)] end end mappings.merge!(resolved_mappings) end return new_relations, removed_relations end
@api private
Compute the set of events that are “useful” to the plan.
It contains every event that is connected to an event in {#permanent_events} or to an event on a task in the plan
@return [Set<EventGenerator>]
# File lib/roby/plan.rb, line 1170 def compute_useful_free_events # Quick path for a very common case return Set.new if free_events.empty? graphs = each_event_relation_graph. find_all { |g| g.root_relation? && !g.weak? } seen = Set.new result = permanent_events.dup pending_events = free_events.to_a while !pending_events.empty? # This basically computes the subplan that contains "seed" and # determines if it is useful or not seed = pending_events.shift next if seen.include?(seed) visitors = Array.new graphs.each do |g| visitors << [g, UsefulFreeEventVisitor.new(g, task_events, permanent_events), [seed].to_set] visitors << [g.reverse, UsefulFreeEventVisitor.new(g.reverse, task_events, permanent_events), [seed].to_set] end component = [seed].to_set has_pending_seeds = true while has_pending_seeds has_pending_seeds = false visitors.each do |graph, visitor, seeds| next if seeds.empty? new_seeds = Array.new seeds.each do |vertex| if !visitor.finished_vertex?(vertex) && graph.has_vertex?(vertex) graph.depth_first_visit(vertex, visitor) { |v| new_seeds << v } end end if !new_seeds.empty? has_pending_seeds = true component.merge(new_seeds) visitors.each { |g, _, s| s.merge(new_seeds) if g != graph } end seeds.clear end end seen.merge(component) if visitors.any? { |_, v, _| v.useful? } result.merge(component) end end result end
@api private
Compute the subplan that is useful for a given set of tasks
@param [Set<Roby::Task>] seeds the root “useful” tasks @param [Array<Relations::BidirectionalDirectedAdjancencyGraph>] graphs the
graphs through which "usefulness" is propagated
# File lib/roby/plan.rb, line 1056 def compute_useful_tasks(seeds, graphs: default_useful_task_graphs) seeds = seeds.to_set visitors = graphs.map do |g| [g, RGL::DFSVisitor.new(g), seeds.dup] end result = seeds.dup has_pending_seeds = true while has_pending_seeds has_pending_seeds = false visitors.each do |graph, visitor, seeds| next if seeds.empty? new_seeds = Array.new seeds.each do |vertex| if !visitor.finished_vertex?(vertex) && graph.has_vertex?(vertex) graph.depth_first_visit(vertex, visitor) do |v| yield(v) if block_given? new_seeds << v end end end if !new_seeds.empty? has_pending_seeds = true result.merge(new_seeds) visitors.each { |g, _, s| s.merge(new_seeds) if g != graph } end seeds.clear end end result end
# File lib/roby/plan.rb, line 386 def copy_relation_graphs_to(copy, mappings) each_task_relation_graph do |graph| target_graph = copy.task_relation_graph_for(graph.class) graph.each_edge do |parent, child| target_graph.add_edge( mappings[parent], mappings[child], graph.edge_info(parent, child)) end end each_event_relation_graph do |graph| target_graph = copy.event_relation_graph_for(graph.class) graph.each_edge do |parent, child| target_graph.add_edge( mappings[parent], mappings[child], graph.edge_info(parent, child)) end end end
@deprecated use {#merge} instead
# File lib/roby/plan.rb, line 219 def copy_to(copy) copy.merge(self) end
# File lib/roby/plan.rb, line 95 def create_relations @task_relation_graphs, @event_relation_graphs = self.class.instanciate_relation_graphs(graph_observer: graph_observer) @structure_checks = Array.new each_relation_graph do |graph| if graph.respond_to?(:check_structure) structure_checks << graph.method(:check_structure) end end end
# File lib/roby/plan.rb, line 126 def dedupe(source) @task_relation_graphs.each do |relation, graph| if relation != graph graph.dedupe(source.task_relation_graph_for(relation)) end end @event_relation_graphs.each do |relation, graph| if relation != graph graph.dedupe(source.event_relation_graph_for(relation)) end end end
# File lib/roby/plan.rb, line 332 def deep_copy plan = Roby::Plan.new mappings = deep_copy_to(plan) return plan, mappings end
Copies this plan's state (tasks, events and their relations) into the provided plan
It returns the mapping from the plan objects in self
to the plan objects in copy
. For instance, if t
is a task in plan
, then
mapping = plan.copy_to(copy) mapping[t] => corresponding task in +copy+
# File lib/roby/plan.rb, line 346 def deep_copy_to(copy) mappings = Hash.new do |h, k| if !self.include?(k) raise InternalError, "#{k} is listed in a relation, but is not included in the corresponding plan #{self}" else raise InternalError, "#{k} is an object in #{self} for which no mapping has been created in #{copy}" end end # First create a copy of all the tasks tasks.each do |t| new_t = t.dup mappings[t] = new_t t.each_event do |ev| new_ev = ev.dup new_ev.instance_variable_set :@task, new_t new_t.bound_events[ev.symbol] = new_ev mappings[ev] = new_ev end copy.register_task(new_t) new_t.each_event do |ev| copy.register_event(ev) end end free_events.each do |e| new_e = e.dup mappings[e] = new_e copy.register_event(new_e) end mission_tasks.each { |t| copy.add_mission_task(mappings[t]) } permanent_tasks.each { |t| copy.add_permanent_task(mappings[t]) } permanent_events.each { |e| copy.add_permanent_event(mappings[e]) } copy_relation_graphs_to(copy, mappings) mappings end
@api private
Default set of graphs that should be discovered by {#compute_useful_tasks}
# File lib/roby/plan.rb, line 1045 def default_useful_task_graphs each_task_relation_graph.find_all { |g| g.root_relation? && !g.weak? } end
# File lib/roby/plan.rb, line 200 def dup new_plan = Plan.new copy_to(new_plan) new_plan end
Enumerate the graph objects that contain this plan's event relation information
@yieldparam [Relations::EventRelationGraph] graph
# File lib/roby/plan.rb, line 172 def each_event_relation_graph return enum_for(__method__) if !block_given? event_relation_graphs.each do |k, v| yield(v) if k == v end end
Enumerate object identities along the transaction stack
The enumeration starts with the deepest transaction and stops at the topmost plan where the object is not a transaction proxy.
@param [PlanObject] object @yieldparam [PlanObject] object the object's identity at the
given level of the stack. Note that the last element is guaranteed to not be a transaction proxy.
# File lib/roby/plan.rb, line 1819 def each_object_in_transaction_stack(object) return enum_for(__method__, object) if !block_given? current_plan = self while true yield(current_plan, object) return if !object.transaction_proxy? current_plan = current_plan.plan object = object.__getobj__ end nil end
Enumerate the graph objects that contain this plan's relation information
@yieldparam [Relations::Graph] graph
# File lib/roby/plan.rb, line 155 def each_relation_graph(&block) return enum_for(__method__) if !block_given? each_event_relation_graph(&block) each_task_relation_graph(&block) end
Iterates on all tasks
@yieldparam [Task] task
# File lib/roby/plan.rb, line 1286 def each_task return enum_for(__method__) if !block_given? @tasks.each { |t| yield(t) } end
Enumerate the graph objects that contain this plan's task relation information
@yieldparam [Relations::TaskRelationGraph] graph
# File lib/roby/plan.rb, line 188 def each_task_relation_graph return enum_for(__method__) if !block_given? task_relation_graphs.each do |k, v| yield(v) if k == v end end
# File lib/roby/plan.rb, line 654 def edit if block_given? yield end end
Returns true if there is no task in this plan
# File lib/roby/plan.rb, line 1282 def empty?; @tasks.empty? && @free_events.empty? end
Resolves an event graph object from the graph class (i.e. the graph model)
# File lib/roby/plan.rb, line 180 def event_relation_graph_for(model) event_relation_graphs.fetch(model) end
Check that this is an executable plan. This is always true for plain Plan
objects and false for transcations
# File lib/roby/plan.rb, line 62 def executable?; false end
Calls the given block in the execution thread of this plan's engine. If there is no engine attached to this plan, yields immediately
# File lib/roby/plan.rb, line 214 def execute(&block) yield end
# File lib/roby/plan.rb, line 1367 def finalize_event(event, timestamp = nil) verify_plan_object_finalization_sanity(event) if (event.plan != self) && has_free_event?(event) raise ArgumentError, "#{event} is included in #{self} but #plan == #{event.plan}" end # Remove relations first. This is needed by transaction since # removing relations may need wrapping some new event, and in # that case these new event will be discovered as well event.clear_relations finalized_event(event) event.finalized!(timestamp) end
# File lib/roby/plan.rb, line 1340 def finalize_task(task, timestamp = nil) verify_plan_object_finalization_sanity(task) if (task.plan != self) && has_task?(task) raise ArgumentError, "#{task} is included in #{self} but #plan == #{task.plan}" end if services = plan_services.delete(task) services.each(&:finalized!) end # Remove relations first. This is needed by transaction since # removing relations may need wrapping some new task, and in # that case these new task will be discovered as well task.clear_relations(remove_internal: true) task.mission = false for ev in task.bound_events.each_value finalized_event(ev) end finalized_task(task) for ev in task.bound_events.each_value ev.finalized!(timestamp) end task.finalized!(timestamp) end
Hook called when event
has been removed from this plan
# File lib/roby/plan.rb, line 1483 def finalized_event(event) log(:finalized_event, droby_id, event) return unless event.root_object? for trsc in transactions next unless trsc.proxying? if proxy = trsc.find_local_object_for_event(event) trsc.finalized_plan_event(proxy) end end end
Hook called when task
has been removed from this plan
# File lib/roby/plan.rb, line 1472 def finalized_task(task) for trsc in transactions next unless trsc.proxying? if proxy = trsc.find_local_object_for_task(task) trsc.finalized_plan_task(proxy) end end log(:finalized_task, droby_id, task) end
Find all the defined plan services for a given task
# File lib/roby/plan.rb, line 789 def find_all_plan_services(task) plan_services[task] || Array.new end
Starts a local query on this plan.
Unlike find_tasks
, when applied on a transaction, it will only match tasks that are already in the transaction.
See find_global_tasks for a local query.
# File lib/roby/plan.rb, line 1700 def find_local_tasks(*args, &block) query = find_tasks(*args, &block) query.local_scope query end
Finds a single difference between this plan and the other plan, using the provided mappings to map objects from self to object in other_plan
# File lib/roby/plan.rb, line 1622 def find_plan_difference(other_plan, mappings) all_self_objects = tasks | free_events | task_events all_other_objects = (other_plan.tasks | other_plan.free_events | other_plan.task_events) all_mapped_objects = all_self_objects.map do |obj| if !mappings.has_key?(obj) return [:new_object, obj] end mappings[obj] end.to_set if all_mapped_objects != all_other_objects return [:removed_objects, all_other_objects - all_mapped_objects] elsif mission_tasks.map { |m| mappings[m] }.to_set != other_plan.mission_tasks return [:missions_differ] elsif permanent_tasks.map { |p| mappings[p] }.to_set != other_plan.permanent_tasks return [:permanent_tasks_differ] elsif permanent_events.map { |p| mappings[p] }.to_set != other_plan.permanent_events return [:permanent_events_differ] end each_task_relation_graph do |graph| other_graph = other_plan.task_relation_graph_for(graph.class) if diff = graph.find_edge_difference(other_graph, mappings) return [graph.class] + diff end end each_event_relation_graph do |graph| other_graph = other_plan.event_relation_graph_for(graph.class) if diff = graph.find_edge_difference(other_graph, mappings) return [graph.class] + diff end end nil end
If at least one plan service is defined for task
, returns one of them. Otherwise, returns nil.
# File lib/roby/plan.rb, line 795 def find_plan_service(task) if set = plan_services[task] set.find { true } end end
Returns a Query object that applies on this plan.
This is equivalent to
Roby::Query.new(self)
Additionally, the model
and args
options are passed to Query#which_fullfills. For example:
plan.find_tasks(Tasks::SimpleTask, id: 20)
is equivalent to
Roby::Query.new(self).which_fullfills(Tasks::SimpleTask, id: 20)
The returned query is applied on the global scope by default. This means that, if it is applied on a transaction, it will match tasks that are in the underlying plans but not yet in the transaction, import the matches in the transaction and return the new proxies.
See find_local_tasks
for a local query.
# File lib/roby/plan.rb, line 1686 def find_tasks(model = nil, args = nil) q = Queries::Query.new(self) if model || args q.which_fullfills(model, args) end q end
# File lib/roby/plan.rb, line 274 def find_triggers_matches(plan) triggers.map do |tr| [tr, tr.each(plan).to_a] end end
# File lib/roby/plan.rb, line 672 def force_replace(from, to) handle_force_replace(from, to) do from.replace_subplan_by(to) end end
# File lib/roby/plan.rb, line 666 def force_replace_task(from, to) handle_force_replace(from, to) do from.replace_by(to) end end
@api private
Normalize the value returned by one of the {#structure_checks}, by computing the list of propagation parents if they were not specified in the return value
@param [Hash] result @param [Array,Hash] new
# File lib/roby/plan.rb, line 1568 def format_exception_set(result, new) [*new].each do |error, tasks| roby_exception = error.to_execution_exception if !tasks if error.kind_of?(RelationFailedError) tasks = [error.parent] end end result[roby_exception] = tasks end result end
# File lib/roby/plan.rb, line 678 def handle_force_replace(from, to) if !from.plan raise ArgumentError, "#{from} has been removed from plan, cannot use as source in a replacement" elsif !to.plan raise ArgumentError, "#{to} has been removed from plan, cannot use as target in a replacement" elsif from.plan != self raise ArgumentError, "trying to replace #{from} but its plan is #{from.plan}, expected #{self}" elsif to.plan.template? add(to) elsif to.plan != self raise ArgumentError, "trying to replace #{to} but its plan is #{to.plan}, expected #{self}" elsif from == to return end # Swap the subplans of +from+ and +to+ yield(from, to) if mission_task?(from) add_mission_task(to) replaced(from, to) unmark_mission_task(from) elsif permanent_task?(from) add_permanent_task(to) replaced(from, to) unmark_permanent_task(from) else add(to) replaced(from, to) end end
Tests whether a free event is present in this plan
# File lib/roby/plan.rb, line 1265 def has_free_event?(generator) free_events.include?(generator) end
Tests whether a task is present in the plan
# File lib/roby/plan.rb, line 487 def has_task?(task) tasks.include?(task) end
Tests whether a task event is present in this plan
# File lib/roby/plan.rb, line 1260 def has_task_event?(generator) task_events.include?(generator) end
Creates a new transaction and yields it. Ensures that the transaction is discarded if the block returns without having committed it.
# File lib/roby/plan.rb, line 1022 def in_transaction yield(trsc = Transaction.new(self)) ensure if trsc && !trsc.finalized? trsc.discard_transaction end end
Tests whether a task is useful for another one task
It is O(N) where N is the number of edges in the combined task relation graphs. If you have to do a lot of tests with the same task, compute the set of useful tasks with {Plan#compute_useful_tasks}
@param reference_task the reference task @param task the task whose usefulness is being tested @return [Boolean]
# File lib/roby/plan.rb, line 1801 def in_useful_subplan?(reference_task, task) compute_useful_tasks([task]) do |useful_t| if useful_t == self return true end end return false end
@deprecated use the more specific {#has_task?}, {#has_free_event?} or
{#has_task_event?} instead
# File lib/roby/plan.rb, line 1271 def include?(object) Roby.warn_deprecated "Plan#include? is deprecated, use one of the more specific #has_task? #has_task_event? and #has_free_event?" has_free_event?(object) || has_task_event?(object) || has_task?(object) end
# File lib/roby/plan.rb, line 1114 def local_tasks task_index.self_owned end
# File lib/roby/plan.rb, line 1091 def locally_useful_roots(with_transactions: true) # Create the set of tasks which must be kept as-is seeds = @mission_tasks | @permanent_tasks if with_transactions for trsc in transactions seeds.merge trsc.proxy_tasks.keys.to_set end end seeds end
# File lib/roby/plan.rb, line 1102 def locally_useful_tasks compute_useful_tasks(locally_useful_roots) end
Merges the content of a plan into self
It is assumed that self and plan do not intersect.
Unlike {#merge!}, it does not update its argument, neither update the plan objects to point to self afterwards
@param [Roby::Plan] plan the plan to merge into self
# File lib/roby/plan.rb, line 296 def merge(plan) return if plan == self trigger_matches = find_triggers_matches(plan) merging_plan(plan) merge_base(plan) merge_relation_graphs(plan) merged_plan(plan) apply_triggers_matches(trigger_matches) end
Moves the content of other_plan into self, and clears other_plan
It is assumed that other_plan and plan do not intersect
Unlike {#merge}, it ensures that all plan objects have their {PlanObject#plan} attribute properly updated, and it cleans plan
@param [Roby::Plan] plan the plan to merge into self
# File lib/roby/plan.rb, line 315 def merge!(plan) return if plan == self tasks, events = plan.tasks.dup, plan.free_events.dup tasks.each { |t| t.plan = self } events.each { |e| e.plan = self } merge(plan) end
# File lib/roby/plan.rb, line 223 def merge_base(plan) free_events.merge(plan.free_events) mission_tasks.merge(plan.mission_tasks) tasks.merge(plan.tasks) permanent_tasks.merge(plan.permanent_tasks) permanent_events.merge(plan.permanent_events) task_index.merge(plan.task_index) task_events.merge(plan.task_events) end
# File lib/roby/plan.rb, line 233 def merge_relation_graphs(plan) # Now merge the relation graphs # # Since task_relation_graphs contains both Class<Graph>=>Graph and # Graph=>Graph, we merge only the graphs for which # self.task_relation_graphs has an entry (i.e. Class<Graph>) and # ignore the rest plan.task_relation_graphs.each do |rel_id, rel| next if rel_id == rel next if !(this_rel = task_relation_graphs.fetch(rel_id, nil)) this_rel.merge(rel) end plan.event_relation_graphs.each do |rel_id, rel| next if rel_id == rel next if !(this_rel = event_relation_graphs.fetch(rel_id, nil)) this_rel.merge(rel) end end
# File lib/roby/plan.rb, line 258 def merge_transaction(transaction, merged_graphs, added, removed, updated) merging_plan(transaction) merge_base(transaction) replace_relation_graphs(merged_graphs) merged_plan(transaction) end
# File lib/roby/plan.rb, line 265 def merge_transaction!(transaction, merged_graphs, added, removed, updated) # Note: Task#plan= updates its bound events tasks, events = transaction.tasks.dup, transaction.free_events.dup tasks.each { |t| t.plan = self } events.each { |e| e.plan = self } merge_transaction(transaction, merged_graphs, added, removed, updated) end
Hook called when a {#merge} has been performed
# File lib/roby/plan.rb, line 329 def merged_plan(plan) end
Hook called just before performing a {#merge}
# File lib/roby/plan.rb, line 325 def merging_plan(plan) end
@deprecated use {#mission_task?} instead
# File lib/roby/plan.rb, line 475 def mission?(task) Roby.warn_deprecated "#mission? is deprecated, use #mission_task? instead" mission_task?(task) end
Checks if a task is part of the plan's missions
@see add_mission_task
unmark_mission_task
# File lib/roby/plan.rb, line 516 def mission_task?(task) @mission_tasks.include?(task.to_task) end
Change the actual task a given plan service is representing
# File lib/roby/plan.rb, line 780 def move_plan_service(service, new_task) return if new_task == service.task remove_plan_service(service) service.task = new_task add_plan_service(service) end
@api private
Normalize an validate the arguments to {#add} into a list of plan objects
# File lib/roby/plan.rb, line 426 def normalize_add_arguments(objects) if !objects.respond_to?(:each) objects = [objects] end objects.map do |o| if o.respond_to?(:as_plan) then o.as_plan elsif o.respond_to?(:to_event) then o.to_event elsif o.respond_to?(:to_task) then o.to_task else raise ArgumentError, "found #{o || 'nil'} which is neither a task nor an event" end end end
@api private
Perform notifications related to the status change of an event
# File lib/roby/plan.rb, line 650 def notify_event_status_change(event, status) log(:event_status_change, event, status) end
@api private
Perform notifications related to the status change of a task
# File lib/roby/plan.rb, line 640 def notify_task_status_change(task, status) if services = plan_services[task] services.each { |s| s.notify_task_status_change(status) } end log(:task_status_change, task, status) end
The number of events, both free and task events
# File lib/roby/plan.rb, line 1250 def num_events task_events.size + free_events.size end
The number of events that are not task events
# File lib/roby/plan.rb, line 1245 def num_free_events free_events.size end
The number of tasks
# File lib/roby/plan.rb, line 1240 def num_tasks tasks.size end
True if this plan owns the given object, i.e. if all the owners of the object are also owners of the plan.
# File lib/roby/plan.rb, line 662 def owns?(object) (object.owners - owners).empty? end
@deprecated use {#permanent_task?} or {#permanent_event?} instead
# File lib/roby/plan.rb, line 562 def permanent?(object) Roby.warn_deprecated "#permanent? is deprecated, use either #permanent_task? or #permanent_event?" if object.respond_to?(:to_task) permanent_task?(object) elsif object.respond_to?(:to_event) permanent_event?(object) else raise ArgumentError, "expected a task or event and got #{object}" end end
True if the given event is registered as a permanent event on self
# File lib/roby/plan.rb, line 619 def permanent_event?(generator) @permanent_events.include?(generator) end
True if the given task is registered as a permanent task on self
# File lib/roby/plan.rb, line 588 def permanent_task?(task) @permanent_tasks.include?(task) end
# File lib/roby/plan.rb, line 1118 def quarantined_tasks tasks.find_all(&:quarantined?) end
If this plan is a toplevel plan, returns self. If it is a transaction, returns the underlying plan
# File lib/roby/plan.rb, line 442 def real_plan ret = self while ret.respond_to?(:plan) ret = ret.plan end ret end
Replace task
with a fresh copy of itself.
The new task takes the place of the old one in the plan: any relation that was going to/from task
or one of its events is removed, and the corresponding one is created, but this time involving the newly created task.
# File lib/roby/plan.rb, line 1500 def recreate(task) new_task = task.create_fresh_copy replace_task(task, new_task) new_task end
# File lib/roby/plan.rb, line 107 def refresh_relations create_relations end
@api private
Registers a task object in this plan
It is for Roby
internal usage only, for the creation of template plans. Use {#add}.
# File lib/roby/plan.rb, line 904 def register_event(event) event.plan = self if event.root_object? free_events << event else task_events << event end end
@api private
Registers a task object in this plan
It is for Roby
internal usage only, for the creation of template plans. Use {#add}.
# File lib/roby/plan.rb, line 891 def register_task(task) task.plan = self tasks << task task_index.add(task) task_events.merge(task.each_event) end
# File lib/roby/plan.rb, line 1122 def remote_tasks if local_tasks = task_index.self_owned tasks - local_tasks else tasks end end
Remove a fault response table that has been added with {#use_fault_response_table}
@overload remove_fault_response_table
(table)
@param [Coordination::FaultResponseTable] table the table that should be removed. This is the return value of {#use_fault_response_table}
@overload remove_fault_response_table
(table_model)
Removes all the tables whose model is the given table model @param [Model<Coordination::FaultResponseTable>] table_model
@return [void] @see use_fault_response_table
# File lib/roby/plan.rb, line 1783 def remove_fault_response_table(table_model) active_fault_response_tables.delete_if do |t| if (table_model.kind_of?(Class) && t.kind_of?(table_model)) || t == table_model t.removed! true end end end
# File lib/roby/plan.rb, line 1401 def remove_free_event(event, timestamp = Time.now) if !@free_events.delete?(event) raise ArgumentError, "#{event} is not a free event of #{self}" end remove_free_event!(event, timestamp) end
# File lib/roby/plan.rb, line 1408 def remove_free_event!(event, timestamp = Time.now) @free_events.delete(event) @permanent_events.delete(event) finalize_event(event, timestamp) self end
@deprecated use {#remove_task} or {#remove_free_event} instead
# File lib/roby/plan.rb, line 1416 def remove_object(object, timestamp = Time.now) Roby.warn_deprecated "#remove_object is deprecated, use either #remove_task or #remove_free_event" if has_task?(object) remove_task(object, timestamp) elsif has_free_event?(object) remove_free_event(object, timestamp) else raise ArgumentError, "#{object} is neither a task nor a free event of #{self}" end end
Deregisters a plan service from this plan
# File lib/roby/plan.rb, line 770 def remove_plan_service(service) if set = plan_services[service.task] set.delete(service) if set.empty? plan_services.delete(service.task) end end end
# File lib/roby/plan.rb, line 1381 def remove_task(task, timestamp = Time.now) if !@tasks.delete?(task) raise ArgumentError, "#{task} is not a task of #{self}" end remove_task!(task, timestamp) end
# File lib/roby/plan.rb, line 1388 def remove_task!(task, timestamp = Time.now) @tasks.delete(task) @mission_tasks.delete(task) @permanent_tasks.delete(task) @task_index.remove(task) for ev in task.bound_events.each_value @task_events.delete(ev) end finalize_task(task, timestamp) self end
Removes the transaction trsc
from the list of known transactions built on this plan
# File lib/roby/plan.rb, line 1037 def remove_transaction(trsc) transactions.delete(trsc) end
Removes a trigger
@param [Object] trigger the trigger to be removed. This is the return value of
the corresponding {#add_trigger} call
@return [void]
# File lib/roby/plan.rb, line 1015 def remove_trigger(trigger) triggers.delete(trigger) nil end
Replace from
by to
in the plan, in all relations in which from
and its events are /children/. It therefore replaces the subplan generated by from
(i.e. from
and all the tasks/events that can be reached by following the task and event relations) by the subplan generated by to
.
See also replace_task
# File lib/roby/plan.rb, line 750 def replace(from, to) handle_replace(from, to) do from.replace_subplan_by(to) end end
# File lib/roby/plan.rb, line 252 def replace_relation_graphs(merged_graphs) merged_graphs.each do |self_g, new_g| self_g.replace(new_g) end end
Replace subgraphs by another in the plan
It copies relations that are not within the keys in task_mappings and event_mappings to the corresponding task/events. The targets might be nil, in which case the relations involving the source will be simply ignored.
If needed, instead of providing an object as target, one can provide a resolver object which will be called with call and the source, The resolver should be given as a second element of a pair, e.g.
source => [nil, #call]
# File lib/roby/plan.rb, line 814 def replace_subplan(task_mappings, event_mappings, task_children: true, event_children: true) new_relations, removed_relations = compute_subplan_replacement(task_mappings, each_task_relation_graph, child_objects: task_children) apply_replacement_operations(new_relations, removed_relations) new_relations, removed_relations = compute_subplan_replacement(event_mappings, each_event_relation_graph, child_objects: event_children) apply_replacement_operations(new_relations, removed_relations) end
Replace the task from
by to
in all relations from
is part of (including events).
See also replace
# File lib/roby/plan.rb, line 737 def replace_task(from, to) handle_replace(from, to) do from.replace_by(to) end end
Hook called when replacing_task
has replaced replaced_task
in this plan
# File lib/roby/plan.rb, line 875 def replaced(replaced_task, replacing_task) # Make the PlanService object follow the replacement if services = plan_services.delete(replaced_task) services.each do |srv| srv.task = replacing_task (plan_services[replacing_task] ||= Set.new) << srv end end end
Creates a new planning pattern replacing the given task and its current planner
@param [Roby::Task] task the task that needs to be replanned @return [Roby::Task] the new planning pattern
# File lib/roby/plan.rb, line 1511 def replan(task) if !task.planning_task return task.create_fresh_copy end planner = replan(old_planner = task.planning_task) planned = task.create_fresh_copy planned.abstract = true planned.planned_by planner replace(task, planned) planned end
# File lib/roby/plan.rb, line 1730 def root_in_query?(result_set, task, graph) graph.depth_first_visit(task) do |v| return false if v != task && result_set.include?(v) end true end
True if this plan is root in the plan hierarchy
# File lib/roby/plan.rb, line 451 def root_plan? true end
Compares this plan to other_plan
, mappings providing the mapping from task/Events in self
to task/events in other_plan
# File lib/roby/plan.rb, line 1661 def same_plan?(other_plan, mappings) !find_plan_difference(other_plan, mappings) end
If this object is the main plan, checks if we are subscribed to the whole remote plan
# File lib/roby/plan.rb, line 42 def sibling_on?(peer) if Roby.plan == self then peer.remote_plan else super end end
Count of tasks in this plan
# File lib/roby/plan.rb, line 1277 def size Roby.warn_deprecated "Plan#size is deprecated, use #num_tasks instead" @tasks.size end
Run a garbage collection pass. This is 'static', as it does not care about the task's state: it will simply remove *from the plan* any task that is not useful *in the context of the plan*.
This is mainly useful for static tests, and for transactions
Do not use it on executed plans.
# File lib/roby/plan.rb, line 1608 def static_garbage_collect if block_given? for t in unneeded_tasks yield(t) end else for t in unneeded_tasks remove_task(t) end end end
Resolves a task graph object from the graph class (i.e. the graph model)
# File lib/roby/plan.rb, line 196 def task_relation_graph_for(model) task_relation_graphs.fetch(model) end
A template plan is meant to be injected in another plan
When a {PlanObject} is included in a template plan, adding relations to other tasks causes the plans to merge as needed. Doing the same operation with plain plans causes an error
@see TemplatePlan
# File lib/roby/plan.rb, line 58 def template?; false end
Returns the set of stacked transaction
@return [Array] the list of plans in the transaction stack, the first
element being the most-nested transaction and the last element the underlying real plan (equal to {#real_plan})
# File lib/roby/plan.rb, line 460 def transaction_stack plan_chain = [self] while plan_chain.last.respond_to?(:plan) plan_chain << plan_chain.last.plan end plan_chain end
@deprecated use {#unmark_mission_task} instead
# File lib/roby/plan.rb, line 481 def unmark_mission(task) Roby.warn_deprecated "#unmark_mission is deprecated, use #unmark_mission_task instead" unmark_mission_task(task) end
Removes a task from the plan's missions
It does not remove the task from the plan. In a plan that is being executed, it is done by garbage collection. In a static plan, it can either be done with {#static_garbage_collect} or directly by calling {#remove_task} or {#remove_free_event}
@see add_mission_task
mission_task?
# File lib/roby/plan.rb, line 528 def unmark_mission_task(task) task = task.to_task return if !@mission_tasks.include?(task) @mission_tasks.delete(task) task.mission = false if task.self_owned? notify_task_status_change(task, :normal) self end
@deprecated use {#unmark_permanent_task} or {#unmark_permanent_event} instead
# File lib/roby/plan.rb, line 550 def unmark_permanent(object) Roby.warn_deprecated "#unmark_permanent is deprecated, use either #unmark_permanent_task or #unmark_permanent_event" if object.respond_to?(:to_task) unmark_permanent_task(object) elsif object.respond_to?(:to_event) unmark_permanent_event(object) else raise ArgumentError, "expected a task or event and got #{object}" end end
Removes a task from the set of permanent tasks
This does not remove the event from the plan. In plans being executed, the removal will be done by garabage collection. In plans used as data structures, either use {#static_garbage_collect} or remove the event directly with {#remove_task} or {#remove_free_event}
@see add_permanent_event
permanent_event?
# File lib/roby/plan.rb, line 631 def unmark_permanent_event(event) if @permanent_events.delete?(event.to_event) notify_event_status_change(event, :normal) end end
Removes a task from the set of permanent tasks
This does not remove the event from the plan. In plans being executed, the removal will be done by garabage collection. In plans used as data structures, either use {#static_garbage_collect} or remove the event directly with {#remove_task} or {#remove_free_event}
@see add_permanent_event
permanent_event?
# File lib/roby/plan.rb, line 600 def unmark_permanent_task(task) if @permanent_tasks.delete?(task.to_task) notify_task_status_change(task, :normal) end end
The set of events that can be removed from the plan
# File lib/roby/plan.rb, line 1229 def unneeded_events useful_events = self.useful_events result = (free_events - useful_events) result.delete_if do |ev| transactions.any? { |trsc| trsc.find_local_object_for_event(ev) } end result end
# File lib/roby/plan.rb, line 1110 def unneeded_tasks tasks - useful_tasks end
Enables a fault response table on this plan
@param [Model<Coordination::FaultResponseTable>] table_model the fault
response table model
@param [Hash] arguments the arguments that should be passed to the
created table
@return [Coordination::FaultResponseTable] the fault response table
that got added to this plan. It can be removed using {#remove_fault_response_table}
@return [void] @see remove_fault_response_table
# File lib/roby/plan.rb, line 1761 def use_fault_response_table(table_model, arguments = Hash.new) table = table_model.new(self, arguments) table.attach_to(self) active_fault_response_tables << table table end
Computes the set of events that are useful in the plan Events are 'useful' when they are chained to a task.
# File lib/roby/plan.rb, line 1224 def useful_events compute_useful_free_events end
Computes the set of useful tasks and checks that task
is in it. This is quite slow. It is here for debugging purposes. Do not use it in production code
# File lib/roby/plan.rb, line 1133 def useful_task?(task) tasks.include?(task) && !unneeded_tasks.include?(task) end
# File lib/roby/plan.rb, line 1106 def useful_tasks(with_transactions: true) compute_useful_tasks(locally_useful_roots(with_transactions: with_transactions)) end
Verifies that all graphs that should be acyclic are
# File lib/roby/plan.rb, line 405 def validate_graphs(graphs) # Make a topological sort of the graphs seen = Set.new Relations.each_graph_topologically(graphs) do |g| if seen.include?(g) next elsif !g.dag? next end if !g.acyclic? raise Relations::CycleFoundError, "#{g.class} has cycles" end seen << g seen.merge(g.recursive_subsets) end end
@api private
Perform sanity checks on a plan object that will be finalized
# File lib/roby/plan.rb, line 1320 def verify_plan_object_finalization_sanity(object) if !object.root_object? raise ArgumentError, "cannot remove #{object} which is a non-root object" elsif object.plan != self if !object.plan if object.removed_at if PlanObject.debug_finalization_place? raise ArgumentError, "#{object} has already been removed from its plan\n" + "Removed at\n #{object.removed_at.join("\n ")}" else raise ArgumentError, "#{object} has already been removed from its plan. Set PlanObject.debug_finalization_place to true to get the backtrace of where (in the code) the object got finalized" end else raise ArgumentError, "#{object} has never been included in this plan" end end raise ArgumentError, "#{object} is not in #{self}: #plan == #{object.plan}" end end