Class: Roby::Plan

Inherits:
DistributedObject show all
Extended by:
Logger::Forward, Logger::Hierarchy
Includes:
DRoby::EventLogging, DRoby::Identifiable, DRoby::V5::PlanDumper, GUI::GraphvizPlan, GUI::RelationsCanvasPlan
Defined in:
lib/roby/plan.rb,
lib/roby.rb,
lib/roby/event_structure/temporal_constraints.rb,
lib/roby/droby/enable.rb

Overview

A plan object manages a collection of tasks and events.

Defined Under Namespace

Classes: ReplacementFilter, Trigger, UsefulFreeEventVisitor

Constant Summary

Constants included from GUI::RelationsCanvasPlan

GUI::RelationsCanvasPlan::PLAN_STROKE_WIDTH

Class Attribute Summary collapse

Instance Attribute Summary collapse

Attributes included from GUI::RelationsCanvasPlan

#depth, #max_depth

Attributes included from GUI::GraphvizPlan

#depth, #layout_level

Attributes inherited from DistributedObject

#local_owner_id, #owners

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DRoby::V5::PlanDumper

#droby_dump

Methods included from DRoby::Identifiable

#droby_id, #initialize_copy

Methods included from GUI::RelationsCanvasPlan

#display, #display_create, #display_name, #display_parent

Methods included from GUI::GraphvizPlan

#all_events, #apply_layout, #compute_depth, #each_displayed_relation, #each_edge, #each_layout_relation, #layout_relations, #relations_to_dot, #to_dot

Methods included from DRoby::EventLogging

#log, #log_flush_cycle, #log_queue_size, #log_timepoint, #log_timepoint_group, #log_timepoint_group_end, #log_timepoint_group_start

Methods inherited from DistributedObject

#add_owner, #clear_owners, #initialize_copy, #owned_by?, #remove_owner

Constructor Details

#initialize(graph_observer: nil, event_logger: DRoby::NullEventLogger.new) ⇒ Plan

Returns a new instance of Plan.



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/roby/plan.rb', line 88

def initialize(graph_observer: nil, event_logger: DRoby::NullEventLogger.new)
    @local_owner = DRoby::PeerID.new("local")

    @tasks = Set.new
    @free_events = Set.new
    @task_events = Set.new
    @transactions = Set.new
    @fault_response_tables = []
    @triggers = []

    @plan_services = {}

    self.event_logger = event_logger
    @active_fault_response_tables = []
    @task_index = Roby::Queries::Index.new

    @graph_observer = graph_observer
    create_relations
    create_null_relations

    super()
end

Class Attribute Details

.structure_checks {|the| ... } ⇒ Object (readonly)

A set of structure checking procedures that must be performed on all plans

Yield Parameters:

  • the (Plan)

    plan

Yield Returns:

  • (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



1782
1783
1784
# File 'lib/roby/plan.rb', line 1782

def structure_checks
  @structure_checks
end

Instance Attribute Details

#active_fault_response_tablesObject (readonly)

The list of fault response tables that are currently globally active on this plan



1946
1947
1948
# File 'lib/roby/plan.rb', line 1946

def active_fault_response_tables
  @active_fault_response_tables
end

#event_loggerObject

The event logger



83
84
85
# File 'lib/roby/plan.rb', line 83

def event_logger
  @event_logger
end

#event_relation_graphsObject (readonly)

The graphs that make event relations, formatted as required by Relations::DirectedRelationSupport#relation_graphs



182
183
184
# File 'lib/roby/plan.rb', line 182

def event_relation_graphs
  @event_relation_graphs
end

#free_eventsObject (readonly)

The list of events that are not included in a task



36
37
38
# File 'lib/roby/plan.rb', line 36

def free_events
  @free_events
end

#graph_observerObject (readonly)

The observer object that reacts to relation changes



86
87
88
# File 'lib/roby/plan.rb', line 86

def graph_observer
  @graph_observer
end

#local_ownerObject

The Peer ID of the local owner (i.e. of the local process / execution engine)



12
13
14
# File 'lib/roby/plan.rb', line 12

def local_owner
  @local_owner
end

#null_event_relation_graphsObject (readonly)

A set of empty graphs that match #event_relation_graphs

Used for finalized events



187
188
189
# File 'lib/roby/plan.rb', line 187

def null_event_relation_graphs
  @null_event_relation_graphs
end

#null_task_relation_graphsObject (readonly)

A set of empty graphs that match #task_relation_graphs

Used for finalized tasks



176
177
178
# File 'lib/roby/plan.rb', line 176

def null_task_relation_graphs
  @null_task_relation_graphs
end

#plan_servicesObject (readonly)

The set of PlanService instances that are defined on this plan



63
64
65
# File 'lib/roby/plan.rb', line 63

def plan_services
  @plan_services
end

#structure_checks {|the| ... } ⇒ Object (readonly)

The set of blocks that should be called to check the structure of the plan.

Yield Parameters:

  • the (Plan)

    plan

Yield Returns:

  • (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



1771
1772
1773
# File 'lib/roby/plan.rb', line 1771

def structure_checks
  @structure_checks
end

#task_eventsObject (readonly)

The set of events that are defined by #tasks



21
22
23
# File 'lib/roby/plan.rb', line 21

def task_events
  @task_events
end

#task_indexObject (readonly)

The task index for this plan. This is a Queries::Index object which allows efficient resolving of queries.



16
17
18
# File 'lib/roby/plan.rb', line 16

def task_index
  @task_index
end

#task_relation_graphsObject (readonly)

The graphs that make task relations, formatted as required by Relations::DirectedRelationSupport#relation_graphs



171
172
173
# File 'lib/roby/plan.rb', line 171

def task_relation_graphs
  @task_relation_graphs
end

#tasksObject (readonly)

The list of tasks that are included in this plan



19
20
21
# File 'lib/roby/plan.rb', line 19

def tasks
  @tasks
end

#transactionsObject (readonly)

The set of transactions which are built on top of this plan



51
52
53
# File 'lib/roby/plan.rb', line 51

def transactions
  @transactions
end

#triggersObject (readonly)

A set of pair of task matching objects and blocks defining this plan’s triggers

See #add_trigger



48
49
50
# File 'lib/roby/plan.rb', line 48

def triggers
  @triggers
end

Class Method Details

.can_gc?(task) ⇒ Boolean

Returns:

  • (Boolean)


1538
1539
1540
1541
1542
1543
1544
1545
1546
# File 'lib/roby/plan.rb', line 1538

def self.can_gc?(task)
    if task.starting?
        true # wait for the task to be started before deciding ...
    elsif task.running? && !task.finishing?
        task.event(:stop).controlable?
    else
        true
    end
end

.check_failed_missions(plan) ⇒ Object

Get all missions that have failed



1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
# File 'lib/roby/plan.rb', line 1786

def self.check_failed_missions(plan)
    result = []
    plan.mission_tasks.each do |task|
        result << MissionFailedError.new(task) if task.failed?
    end
    plan.permanent_tasks.each do |task|
        result << PermanentTaskError.new(task) if task.failed?
    end
    result
end

.instanciate_relation_graphs(graph_observer: nil) ⇒ Object



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/roby/plan.rb', line 137

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
    [task_relation_graphs, event_relation_graphs]
end

Instance Method Details

#[](object, create = true) ⇒ Object

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#[]



1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
# File 'lib/roby/plan.rb', line 1524

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

#add(objects) ⇒ Object

call-seq:

plan.add(task) => plan
plan.add(event) => plan
plan.add([task, event, task2, ...]) => plan
plan.add([t1, t2, ...]) => plan

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).



1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
# File 'lib/roby/plan.rb', line 1107

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

#add_job_action(action) ⇒ Object

Add an action as a job



551
552
553
554
555
# File 'lib/roby/plan.rb', line 551

def add_job_action(action)
    add_mission_task(
        action.as_plan(job_id: Roby::Interface::Job.allocate_job_id)
    )
end

#add_mission(task) ⇒ Object

Deprecated.

use #add_mission_task instead



506
507
508
509
510
511
# File 'lib/roby/plan.rb', line 506

def add_mission(task)
    Roby.warn_deprecated(
        "#add_mission is deprecated, use #add_mission_task instead"
    )
    add_mission_task(task)
end

#add_mission_task(task) ⇒ Object

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.



539
540
541
542
543
544
545
546
547
548
# File 'lib/roby/plan.rb', line 539

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

#add_permanent(object) ⇒ Object

Deprecated.


583
584
585
586
587
588
589
590
591
592
593
594
595
# File 'lib/roby/plan.rb', line 583

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

#add_permanent_event(event) ⇒ Object

Mark an event as permanent, optionally adding to the plan

Permanent events are protected against garbage collection



668
669
670
671
672
673
674
675
676
# File 'lib/roby/plan.rb', line 668

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

#add_permanent_task(task) ⇒ Object

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 Roby::PermanentTaskError. Unlike missions, this exception is non-fatal.



634
635
636
637
638
639
640
641
642
# File 'lib/roby/plan.rb', line 634

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

#add_plan_service(service) ⇒ Object

Register a new plan service on this plan



926
927
928
929
930
931
932
933
934
935
936
# File 'lib/roby/plan.rb', line 926

def add_plan_service(service)
    if service.task.plan != self
        raise ArgumentError,
              "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)
    set << service
    self
end

#add_trigger(query_object) {|task| ... } ⇒ Object

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

Parameters:

  • query_object (#===)

    the object against which tasks are tested. Tasks for which #=== returns true are yield to the block

Yield Parameters:

  • task (Roby::Task)

    the task that matched the query object

Returns:



1195
1196
1197
1198
1199
1200
1201
1202
# File 'lib/roby/plan.rb', line 1195

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

#added_transaction(trsc) ⇒ Object

Hook called when a new transaction has been built on top of this plan



1223
# File 'lib/roby/plan.rb', line 1223

def added_transaction(trsc); end

#apply_replacement_operations(new_relations, removed_relations) ⇒ Object



1049
1050
1051
1052
1053
1054
1055
1056
# File 'lib/roby/plan.rb', line 1049

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

#apply_triggers_matches(matches) ⇒ Object



319
320
321
322
323
324
325
# File 'lib/roby/plan.rb', line 319

def apply_triggers_matches(matches)
    matches.each do |trigger, matched_tasks|
        matched_tasks.each do |t|
            trigger.call(t)
        end
    end
end

#call_structure_check_handler(handler) ⇒ Object



1815
1816
1817
# File 'lib/roby/plan.rb', line 1815

def call_structure_check_handler(handler)
    handler.call(self)
end

#check_structureHash<ExecutionException,Array<Roby::Task>,nil>

Perform the structure checking step by calling the procs registered in #structure_checks and structure_checks

Returns:



1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
# File 'lib/roby/plan.rb', line 1823

def check_structure
    # Do structure checking and gather the raised exceptions
    exceptions = {}
    (Plan.structure_checks + structure_checks).each do |prc|
        new_exceptions = call_structure_check_handler(prc)
        next unless new_exceptions

        format_exception_set(exceptions, new_exceptions)
    end
    exceptions
end

#clearObject

Remove all tasks



1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
# File 'lib/roby/plan.rb', line 1678

def clear
    tasks = @tasks
    @tasks = Set.new
    free_events = @free_events
    @free_events = Set.new

    clear!

    remaining = tasks.find_all do |t|
        if t.running?
            true
        else
            finalize_task(t)
            false
        end
    end

    unless 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

#clear!Object



1668
1669
1670
1671
1672
1673
1674
1675
# File 'lib/roby/plan.rb', line 1668

def clear!
    each_task_relation_graph(&:clear)
    each_event_relation_graph(&:clear)
    @free_events.clear
    @tasks.clear
    @task_index.clear
    @task_events.clear
end

#compute_subplan_replacement(mappings, relation_graphs, child_objects: true) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
# File 'lib/roby/plan.rb', line 999

def compute_subplan_replacement(mappings, relation_graphs, child_objects: true)
    mappings = mappings.dup
    mappings.compare_by_identity
    new_relations = []
    removed_relations = []
    relation_graphs.each do |graph|
        next if graph.strong?

        resolved_mappings = {}
        resolved_mappings.compare_by_identity
        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.key?(parent)

                unless graph.copy_on_replace?
                    removed_relations << [graph, parent, obj]
                end
                unless 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 unless child_objects

            graph.each_out_neighbour(obj) do |child|
                next if mappings.key?(child)

                unless graph.copy_on_replace?
                    removed_relations << [graph, obj, child]
                end
                unless 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
    [new_relations, removed_relations]
end

#compute_useful_free_eventsSet<EventGenerator>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Returns:



1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
# File 'lib/roby/plan.rb', line 1377

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
    until 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 = []
        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 = []
                seeds.each do |vertex|
                    next if visitor.finished_vertex?(vertex)
                    next unless graph.has_vertex?(vertex)

                    graph.depth_first_visit(vertex, visitor) do |v|
                        new_seeds << v
                    end
                end

                unless 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)
        result.merge(component) if visitors.any? { |_, v, _| v.useful? }
    end

    result
end

#compute_useful_tasks(seeds, graphs: default_useful_task_graphs) ⇒ Set

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Compute the subplan that is useful for a given set of tasks

Parameters:

  • seeds (Set<Roby::Task>)

    the root “useful” tasks

  • graphs (Array<Relations::BidirectionalDirectedAdjancencyGraph>) (defaults to: default_useful_task_graphs)

    the graphs through which “usefulness” is propagated

Returns:

  • (Set)

    the set of tasks reachable from ‘seeds’ through the graphs



1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
# File 'lib/roby/plan.rb', line 1247

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_queued_nodes = true
    while has_queued_nodes
        has_queued_nodes = false
        visitors.each do |graph, visitor, queue|
            next if queue.empty?

            new_queue = []
            queue.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_queue << v
                    end
                end
            end
            unless new_queue.empty?
                has_queued_nodes = true
                result.merge(new_queue)
                visitors.each { |g, _, s| s.merge(new_queue) if g != graph }
            end
            queue.clear
        end
    end

    result
end

#copy_relation_graphs_to(copy, mappings) ⇒ Object



428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
# File 'lib/roby/plan.rb', line 428

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

#copy_task_marks(to:, from:) ⇒ Object

Apply to to the marks (permanent, mission) of from

It does not remove any marks from from



1615
1616
1617
1618
1619
# File 'lib/roby/plan.rb', line 1615

def copy_task_marks(to:, from:)
    add_permanent_task(to) if permanent_task?(from)

    add_mission_task(to) if mission_task?(from)
end

#copy_to(copy) ⇒ Object

Deprecated.

use #merge instead



251
252
253
# File 'lib/roby/plan.rb', line 251

def copy_to(copy)
    copy.merge(self)
end

#create_null_relationsObject



111
112
113
114
115
116
117
118
# File 'lib/roby/plan.rb', line 111

def create_null_relations
    @null_task_relation_graphs, @null_event_relation_graphs =
        self.class.instanciate_relation_graphs(graph_observer: graph_observer)
    @null_task_relation_graphs.freeze
    @null_task_relation_graphs.each_value(&:freeze)
    @null_event_relation_graphs.freeze
    @null_event_relation_graphs.each_value(&:freeze)
end

#create_relationsObject



120
121
122
123
124
125
126
127
128
129
130
# File 'lib/roby/plan.rb', line 120

def create_relations
    @task_relation_graphs, @event_relation_graphs =
        self.class.instanciate_relation_graphs(graph_observer: graph_observer)

    @structure_checks = []
    each_relation_graph do |graph|
        if graph.respond_to?(:check_structure)
            structure_checks << graph.method(:check_structure)
        end
    end
end

#dedupe(source) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/roby/plan.rb', line 154

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

#deep_copyObject



370
371
372
373
374
# File 'lib/roby/plan.rb', line 370

def deep_copy
    plan = Roby::Plan.new
    mappings = deep_copy_to(plan)
    [plan, mappings]
end

#deep_copy_to(copy) ⇒ Object

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+


384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/roby/plan.rb', line 384

def deep_copy_to(copy)
    mappings = Hash.new do |_, k|
        if !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

#default_useful_task_graphsObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Default set of graphs that should be discovered by #compute_useful_tasks



1235
1236
1237
# File 'lib/roby/plan.rb', line 1235

def default_useful_task_graphs
    each_task_relation_graph.find_all { |g| g.root_relation? && !g.weak? }
end

#dupObject



231
232
233
234
235
# File 'lib/roby/plan.rb', line 231

def dup
    new_plan = Plan.new
    copy_to(new_plan)
    new_plan
end

#each_event_relation_graph {|graph| ... } ⇒ Object

Enumerate the graph objects that contain this plan’s event relation information

Yield Parameters:



201
202
203
204
205
206
207
# File 'lib/roby/plan.rb', line 201

def each_event_relation_graph
    return enum_for(__method__) unless block_given?

    event_relation_graphs.each do |k, v|
        yield(v) if k == v
    end
end

#each_object_in_transaction_stack(object) {|object| ... } ⇒ Object

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.

Parameters:

Yield Parameters:

  • object (PlanObject)

    the object’s identity at the given level of the stack. Note that the last element is guaranteed to not be a transaction proxy.



2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
# File 'lib/roby/plan.rb', line 2015

def each_object_in_transaction_stack(object)
    return enum_for(__method__, object) unless block_given?

    current_plan = self
    loop do
        yield(current_plan, object)

        return unless object.transaction_proxy?

        current_plan = current_plan.plan
        object = object.__getobj__
    end
    # Never reached
end

#each_relation_graph(&block) ⇒ Object

Enumerate all graphs (event and tasks) that form this plan



190
191
192
193
194
195
# File 'lib/roby/plan.rb', line 190

def each_relation_graph(&block)
    return enum_for(__method__) unless block_given?

    each_task_relation_graph(&block)
    each_event_relation_graph(&block)
end

#each_task {|task| ... } ⇒ Object

Iterates on all tasks

Yield Parameters:



1513
1514
1515
1516
1517
# File 'lib/roby/plan.rb', line 1513

def each_task(&block)
    return enum_for(__method__) unless block_given?

    @tasks.each(&block)
end

#each_task_relation_graph {|graph| ... } ⇒ Object

Enumerate the graph objects that contain this plan’s task relation information

Yield Parameters:



218
219
220
221
222
223
224
# File 'lib/roby/plan.rb', line 218

def each_task_relation_graph
    return enum_for(__method__) unless block_given?

    task_relation_graphs.each do |k, v|
        yield(v) if k == v
    end
end

#editObject



715
716
717
# File 'lib/roby/plan.rb', line 715

def edit
    yield if block_given?
end

#empty?Boolean

Returns true if there is no task in this plan

Returns:

  • (Boolean)


1506
1507
1508
# File 'lib/roby/plan.rb', line 1506

def empty?
    @tasks.empty? && @free_events.empty?
end

#event_relation_graph_for(model) ⇒ Object

Resolves an event graph object from the graph class (i.e. the graph model)



210
211
212
# File 'lib/roby/plan.rb', line 210

def event_relation_graph_for(model)
    event_relation_graphs.fetch(model)
end

#executable?Boolean

Check that this is an executable plan. This is always true for plain Plan objects and false for transcations

Returns:

  • (Boolean)


78
79
80
# File 'lib/roby/plan.rb', line 78

def executable?
    false
end

#executeObject

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



246
247
248
# File 'lib/roby/plan.rb', line 246

def execute
    yield
end

#finalize_event(event, timestamp = nil) ⇒ Object



1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
# File 'lib/roby/plan.rb', line 1601

def finalize_event(event, timestamp = nil)
    verify_plan_object_finalization_sanity(event)

    # 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

#finalize_task(task, timestamp = nil) ⇒ Object



1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
# File 'lib/roby/plan.rb', line 1578

def finalize_task(task, timestamp = nil)
    verify_plan_object_finalization_sanity(task)
    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

    task.bound_events.each_value do |ev|
        finalized_event(ev)
    end
    finalized_task(task)

    task.bound_events.each_value do |ev|
        ev.finalized!(timestamp)
    end
    task.finalized!(timestamp)
end

#finalized_event(event) ⇒ Object

Hook called when event has been removed from this plan



1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
# File 'lib/roby/plan.rb', line 1722

def finalized_event(event)
    log(:finalized_event, droby_id, event)
    return unless event.root_object?

    transactions.each do |trsc|
        next unless trsc.proxying?

        if (proxy = trsc.find_local_object_for_event(event))
            trsc.finalized_plan_event(proxy)
        end
    end
end

#finalized_task(task) ⇒ Object

Hook called when task has been removed from this plan



1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
# File 'lib/roby/plan.rb', line 1710

def finalized_task(task)
    transactions.each do |trsc|
        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_plan_services(task) ⇒ Object

Find all the defined plan services for a given task



961
962
963
# File 'lib/roby/plan.rb', line 961

def find_all_plan_services(task)
    plan_services[task] || []
end

#find_local_tasks(*args, &block) ⇒ Object

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.



1930
1931
1932
1933
1934
# File 'lib/roby/plan.rb', line 1930

def find_local_tasks(*args, &block)
    query = find_tasks(*args, &block)
    query.local_scope
    query
end

#find_plan_difference(other_plan, mappings) ⇒ Object

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



1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
# File 'lib/roby/plan.rb', line 1853

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|
        return [:new_object, obj] unless mappings.key?(obj)

        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

#find_plan_service(task) ⇒ Object

If at least one plan service is defined for task, returns one of them. Otherwise, returns nil.



967
968
969
# File 'lib/roby/plan.rb', line 967

def find_plan_service(task)
    plan_services[task]&.first
end

#find_tasks(model = nil, args = nil) ⇒ Object

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.



1918
1919
1920
1921
1922
# File 'lib/roby/plan.rb', line 1918

def find_tasks(model = nil, args = nil)
    q = Queries::Query.new(self)
    q.which_fullfills(model, args) if model || args
    q
end

#find_triggers_matches(plan) ⇒ Object



313
314
315
316
317
# File 'lib/roby/plan.rb', line 313

def find_triggers_matches(plan)
    triggers.map do |tr|
        [tr, tr.each(plan).to_a]
    end
end

#force_replace(from, to) ⇒ Object



731
732
733
734
735
# File 'lib/roby/plan.rb', line 731

def force_replace(from, to)
    handle_force_replace(from, to) do
        from.replace_subplan_by(to)
    end
end

#force_replace_task(from, to) ⇒ Object



725
726
727
728
729
# File 'lib/roby/plan.rb', line 725

def force_replace_task(from, to)
    handle_force_replace(from, to) do
        from.replace_by(to)
    end
end

#format_exception_set(result, new) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Parameters:

  • result (Hash)
  • new (Array, Hash)


1806
1807
1808
1809
1810
1811
1812
1813
# File 'lib/roby/plan.rb', line 1806

def format_exception_set(result, new)
    [*new].each do |error, tasks|
        roby_exception = error.to_execution_exception
        tasks = [error.parent] if !tasks && error.kind_of?(RelationFailedError)
        result[roby_exception] = tasks
    end
    result
end

#handle_force_replace(from, to) {|from, to| ... } ⇒ Object

Yields:



737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
# File 'lib/roby/plan.rb', line 737

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

#handle_replace(from, to) ⇒ Object

:nodoc:



777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
# File 'lib/roby/plan.rb', line 777

def handle_replace(from, to) # :nodoc:
    handle_force_replace(from, to) do
        # Check that +to+ is valid in all hierarchy relations where
        # +from+ is a child
        unless to.fullfills?(*from.fullfilled_model)
            models = from.fullfilled_model.first
            missing = models.find_all do |m|
                !to.fullfills?(m)
            end
            if missing.empty?
                mismatching_argument =
                    from.fullfilled_model.last.find do |key, expected_value|
                        to.arguments.set?(key) &&
                            (to.arguments[key] != expected_value)
                    end
            end

            if mismatching_argument
                raise InvalidReplace.new(from, to),
                      "argument mismatch for #{mismatching_argument.first}"
            elsif !missing.empty?
                raise InvalidReplace.new(from, to),
                      "missing provided models #{missing.map(&:name).join(', ')}"
            else
                raise InvalidReplace.new(from, to),
                      "#{to} does not fullfill #{from}"
            end
        end

        # Swap the subplans of +from+ and +to+
        yield(from, to)
    end
end

#has_free_event?(generator) ⇒ Boolean

Tests whether a free event is present in this plan

Returns:

  • (Boolean)


1485
1486
1487
# File 'lib/roby/plan.rb', line 1485

def has_free_event?(generator)
    free_events.include?(generator)
end

#has_task?(task) ⇒ Boolean

Tests whether a task is present in this plan

Returns:

  • (Boolean)


1475
1476
1477
# File 'lib/roby/plan.rb', line 1475

def has_task?(task)
    tasks.include?(task)
end

#has_task_event?(generator) ⇒ Boolean

Tests whether a task event is present in this plan

Returns:

  • (Boolean)


1480
1481
1482
# File 'lib/roby/plan.rb', line 1480

def has_task_event?(generator)
    task_events.include?(generator)
end

#in_transactionObject

Creates a new transaction and yields it. Ensures that the transaction is discarded if the block returns without having committed it.



1216
1217
1218
1219
1220
# File 'lib/roby/plan.rb', line 1216

def in_transaction
    yield(trsc = Transaction.new(self))
ensure
    trsc.discard_transaction if trsc && !trsc.finalized?
end

#in_useful_subplan?(reference_task, tested_task) ⇒ Boolean

Tests whether a task is useful from the point of view of a reference 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 #compute_useful_tasks

Parameters:

  • reference_task

    the reference task

  • task

    the task whose usefulness is being tested

Returns:

  • (Boolean)


1999
2000
2001
2002
2003
2004
# File 'lib/roby/plan.rb', line 1999

def in_useful_subplan?(reference_task, tested_task)
    compute_useful_tasks([reference_task]) do |useful_t|
        return true if useful_t == tested_task
    end
    false
end

#include?(object) ⇒ Boolean

Deprecated.

use the more specific #has_task?, #has_free_event? or #has_task_event? instead

Returns:

  • (Boolean)


1491
1492
1493
1494
1495
1496
1497
# File 'lib/roby/plan.rb', line 1491

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

#inspectObject

:nodoc:



237
238
239
240
# File 'lib/roby/plan.rb', line 237

def inspect # :nodoc:
    "#<#{self}: mission_tasks=#{mission_tasks} tasks=#{tasks} "\
    "events=#{free_events} transactions=#{transactions}>"
end

#local_tasksObject



1308
1309
1310
# File 'lib/roby/plan.rb', line 1308

def local_tasks
    task_index.self_owned
end

#locally_useful_roots(with_transactions: true) ⇒ Object



1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
# File 'lib/roby/plan.rb', line 1282

def locally_useful_roots(with_transactions: true)
    # Create the set of tasks which must be kept as-is
    seeds = @task_index.mission_tasks | @task_index.permanent_tasks
    if with_transactions
        transactions.each do |trsc|
            seeds.merge trsc.proxy_tasks.keys.to_set
        end
    end
    seeds
end

#locally_useful_tasksObject



1293
1294
1295
# File 'lib/roby/plan.rb', line 1293

def locally_useful_tasks
    compute_useful_tasks(locally_useful_roots)
end

#make_useless(tasks) ⇒ Object

Ensures that the given tasks will end up being processed without forcefully stopping anything



1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
# File 'lib/roby/plan.rb', line 1322

def make_useless(tasks)
    all_tasks = compute_useful_tasks(
        Array(tasks), graphs: default_useful_task_graphs.map(&:reverse)
    ).to_set
    all_tasks.compare_by_identity
    @task_index.mission_tasks.dup.each do |t|
        unmark_mission_task(t) if all_tasks.include?(t)
    end
    @task_index.permanent_tasks.dup.each do |t|
        unmark_permanent_task(t) if all_tasks.include?(t)
    end
end

#merge(plan) ⇒ Object

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

Parameters:

  • plan (Roby::Plan)

    the plan to merge into self



335
336
337
338
339
340
341
342
343
344
# File 'lib/roby/plan.rb', line 335

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

#merge!(plan) ⇒ Object

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 Roby::PlanObject#plan attribute properly updated, and it cleans plan

Parameters:

  • plan (Roby::Plan)

    the plan to merge into self



354
355
356
357
358
359
360
361
362
# File 'lib/roby/plan.rb', line 354

def merge!(plan)
    return if plan == self

    tasks = plan.tasks.dup
    events = plan.free_events.dup
    tasks.each { |t| t.plan = self }
    events.each { |e| e.plan = self }
    merge(plan)
end

#merge_base(plan) ⇒ Object



255
256
257
258
259
260
261
262
263
# File 'lib/roby/plan.rb', line 255

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

#merge_relation_graphs(plan) ⇒ Object



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/roby/plan.rb', line 265

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

        this_rel = task_relation_graphs.fetch(rel_id, nil)
        next unless this_rel

        this_rel.merge(rel)
    end
    plan.event_relation_graphs.each do |rel_id, rel|
        next if rel_id == rel

        this_rel = event_relation_graphs.fetch(rel_id, nil)
        next unless this_rel

        this_rel.merge(rel)
    end
end

#merge_transaction(transaction, merged_graphs, _added, _removed, _updated) ⇒ Object



296
297
298
299
300
301
# File 'lib/roby/plan.rb', line 296

def merge_transaction(transaction, merged_graphs, _added, _removed, _updated)
    merging_plan(transaction)
    merge_base(transaction)
    replace_relation_graphs(merged_graphs)
    merged_plan(transaction)
end

#merge_transaction!(transaction, merged_graphs, added, removed, updated) ⇒ Object



303
304
305
306
307
308
309
310
311
# File 'lib/roby/plan.rb', line 303

def merge_transaction!(transaction, merged_graphs, added, removed, updated)
    # NOTE: Task#plan= updates its bound events
    tasks = transaction.tasks.dup
    events = 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

#merged_plan(plan) ⇒ Object

Hook called when a #merge has been performed



368
# File 'lib/roby/plan.rb', line 368

def merged_plan(plan); end

#merging_plan(plan) ⇒ Object

Hook called just before performing a #merge



365
# File 'lib/roby/plan.rb', line 365

def merging_plan(plan); end

#mission?(task) ⇒ Boolean

Deprecated.

use #mission_task? instead

Returns:

  • (Boolean)


514
515
516
517
# File 'lib/roby/plan.rb', line 514

def mission?(task)
    Roby.warn_deprecated "#mission? is deprecated, use #mission_task? instead"
    mission_task?(task)
end

#mission_task?(task) ⇒ Boolean

Checks if a task is part of the plan’s missions

Returns:

  • (Boolean)

See Also:



560
561
562
# File 'lib/roby/plan.rb', line 560

def mission_task?(task)
    @task_index.mission_tasks.include?(task.to_task)
end

#mission_tasksObject

The set of the robot’s missions



25
26
27
# File 'lib/roby/plan.rb', line 25

def mission_tasks
    @task_index.mission_tasks
end

#move_plan_service(service, new_task) ⇒ Object

Change the actual task a given plan service is representing



952
953
954
955
956
957
958
# File 'lib/roby/plan.rb', line 952

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

#normalize_add_arguments(objects) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Normalize an validate the arguments to #add into a list of plan objects



467
468
469
470
471
472
473
474
475
476
477
478
479
# File 'lib/roby/plan.rb', line 467

def normalize_add_arguments(objects)
    objects = [objects] unless objects.respond_to?(:each)

    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

#notify_event_status_change(event, status) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Perform notifications related to the status change of an event



711
712
713
# File 'lib/roby/plan.rb', line 711

def notify_event_status_change(event, status)
    log(:event_status_change, event, status)
end

#notify_task_status_change(task, status) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Perform notifications related to the status change of a task



701
702
703
704
705
706
# File 'lib/roby/plan.rb', line 701

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

#num_eventsObject

The number of events, both free and task events



1470
1471
1472
# File 'lib/roby/plan.rb', line 1470

def num_events
    task_events.size + free_events.size
end

#num_free_eventsObject

The number of events that are not task events



1465
1466
1467
# File 'lib/roby/plan.rb', line 1465

def num_free_events
    free_events.size
end

#num_tasksObject

The number of tasks



1460
1461
1462
# File 'lib/roby/plan.rb', line 1460

def num_tasks
    tasks.size
end

#owns?(object) ⇒ Boolean

True if this plan owns the given object, i.e. if all the owners of the object are also owners of the plan.

Returns:

  • (Boolean)


721
722
723
# File 'lib/roby/plan.rb', line 721

def owns?(object)
    (object.owners - owners).empty?
end

#permanent?(object) ⇒ Boolean

Deprecated.

Returns:

  • (Boolean)


614
615
616
617
618
619
620
621
622
623
624
625
626
627
# File 'lib/roby/plan.rb', line 614

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

#permanent_event?(generator) ⇒ Boolean

True if the given event is registered as a permanent event on self

Returns:

  • (Boolean)


679
680
681
# File 'lib/roby/plan.rb', line 679

def permanent_event?(generator)
    @task_index.permanent_events.include?(generator)
end

#permanent_eventsObject

The list of events that are kept outside GC. Do not change that set directly, use #permanent and #auto instead.



40
41
42
# File 'lib/roby/plan.rb', line 40

def permanent_events
    @task_index.permanent_events
end

#permanent_task?(task) ⇒ Boolean

True if the given task is registered as a permanent task on self

Returns:

  • (Boolean)


645
646
647
# File 'lib/roby/plan.rb', line 645

def permanent_task?(task)
    @task_index.permanent_tasks.include?(task)
end

#permanent_tasksObject

The set of tasks that are kept around “just in case”



31
32
33
# File 'lib/roby/plan.rb', line 31

def permanent_tasks
    @task_index.permanent_tasks
end

#query_result_set(matcher) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Internal delegation from the matchers to the plan object to determine the ‘right’ query algorithm



1940
1941
1942
# File 'lib/roby/plan.rb', line 1940

def query_result_set(matcher)
    Queries::PlanQueryResult.from_plan(self, matcher)
end

#real_planObject

If this plan is a toplevel plan, returns self. If it is a transaction, returns the underlying plan



483
484
485
486
487
# File 'lib/roby/plan.rb', line 483

def real_plan
    ret = self
    ret = ret.plan while ret.respond_to?(:plan)
    ret
end

#recreate(task) ⇒ Object

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.



1741
1742
1743
1744
1745
# File 'lib/roby/plan.rb', line 1741

def recreate(task)
    new_task = task.create_fresh_copy
    replace_task(task, new_task)
    new_task
end

#refresh_relationsObject



132
133
134
135
# File 'lib/roby/plan.rb', line 132

def refresh_relations
    create_relations
    create_null_relations
end

#register_event(event) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Registers a task object in this plan

It is for Roby internal usage only, for the creation of template plans. Use #add.



1088
1089
1090
1091
1092
1093
1094
1095
# File 'lib/roby/plan.rb', line 1088

def register_event(event)
    event.plan = self
    if event.root_object?
        free_events << event
    else
        task_events << event
    end
end

#register_task(task) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Registers a task object in this plan

It is for Roby internal usage only, for the creation of template plans. Use #add.



1075
1076
1077
1078
1079
1080
# File 'lib/roby/plan.rb', line 1075

def register_task(task)
    task.plan = self
    tasks << task
    task_index.add(task)
    task_events.merge(task.each_event)
end

#registered_plan_services_for(task) ⇒ Object

Whether there are services registered for the given task



947
948
949
# File 'lib/roby/plan.rb', line 947

def registered_plan_services_for(task)
    @plan_services[task] || Set.new
end

#remote_tasksObject



1312
1313
1314
1315
1316
1317
1318
# File 'lib/roby/plan.rb', line 1312

def remote_tasks
    if (local_tasks = task_index.self_owned)
        tasks - local_tasks
    else
        tasks
    end
end

#remove_fault_response_table(table) ⇒ void #remove_fault_response_table(table_model) ⇒ void

This method returns an undefined value.

Remove a fault response table that has been added with #use_fault_response_table

Overloads:

See Also:



1981
1982
1983
1984
1985
1986
1987
1988
# File 'lib/roby/plan.rb', line 1981

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

#remove_free_event(event, timestamp = Time.now) ⇒ Object



1639
1640
1641
1642
# File 'lib/roby/plan.rb', line 1639

def remove_free_event(event, timestamp = Time.now)
    verify_plan_object_finalization_sanity(event)
    remove_free_event!(event, timestamp)
end

#remove_free_event!(event, timestamp = Time.now) ⇒ Object



1644
1645
1646
1647
1648
1649
# File 'lib/roby/plan.rb', line 1644

def remove_free_event!(event, timestamp = Time.now)
    @free_events.delete(event)
    @task_index.permanent_events.delete(event)
    finalize_event(event, timestamp)
    self
end

#remove_object(object, timestamp = Time.now) ⇒ Object

Deprecated.


1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
# File 'lib/roby/plan.rb', line 1652

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

#remove_plan_service(service) ⇒ Object

Deregisters a plan service from this plan



939
940
941
942
943
944
# File 'lib/roby/plan.rb', line 939

def remove_plan_service(service)
    return unless (set = plan_services[service.task])

    set.delete(service)
    plan_services.delete(service.task) if set.empty?
end

#remove_task(task, timestamp = Time.now) ⇒ Object



1621
1622
1623
1624
# File 'lib/roby/plan.rb', line 1621

def remove_task(task, timestamp = Time.now)
    verify_plan_object_finalization_sanity(task)
    remove_task!(task, timestamp)
end

#remove_task!(task, timestamp = Time.now) ⇒ Object



1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
# File 'lib/roby/plan.rb', line 1626

def remove_task!(task, timestamp = Time.now)
    @tasks.delete(task)
    @task_index.mission_tasks.delete(task)
    @task_index.permanent_tasks.delete(task)
    @task_index.remove(task)

    task.bound_events.each_value do |ev|
        @task_events.delete(ev)
    end
    finalize_task(task, timestamp)
    self
end

#remove_transaction(trsc) ⇒ Object

Removes the transaction trsc from the list of known transactions built on this plan



1227
1228
1229
# File 'lib/roby/plan.rb', line 1227

def remove_transaction(trsc)
    transactions.delete(trsc)
end

#remove_trigger(trigger) ⇒ void

This method returns an undefined value.

Removes a trigger

Parameters:

  • trigger (Object)

    the trigger to be removed. This is the return value of the corresponding #add_trigger call



1209
1210
1211
1212
# File 'lib/roby/plan.rb', line 1209

def remove_trigger(trigger)
    triggers.delete(trigger)
    nil
end

#replace(from, to, filter: ReplacementFilter::Null.new) ⇒ Object

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



919
920
921
922
923
# File 'lib/roby/plan.rb', line 919

def replace(from, to, filter: ReplacementFilter::Null.new)
    handle_replace(from, to) do
        from.replace_subplan_by(to, filter: filter)
    end
end

#replace_relation_graphs(merged_graphs) ⇒ Object



290
291
292
293
294
# File 'lib/roby/plan.rb', line 290

def replace_relation_graphs(merged_graphs)
    merged_graphs.each do |self_g, new_g|
        self_g.replace(new_g)
    end
end

#replace_subplan(task_mappings, event_mappings, task_children: true, event_children: true) ⇒ Object

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]


984
985
986
987
988
989
990
991
992
993
994
995
996
# File 'lib/roby/plan.rb', line 984

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_task(from, to, filter: ReplacementFilter::Null.new) ⇒ Object

Replace the task from by to in all relations from is part of (including events).

See also #replace



906
907
908
909
910
# File 'lib/roby/plan.rb', line 906

def replace_task(from, to, filter: ReplacementFilter::Null.new)
    handle_replace(from, to) do
        from.replace_by(to, filter: filter)
    end
end

#replaced(replaced_task, replacing_task) ⇒ Object

Hook called when replacing_task has replaced replaced_task in this plan



1059
1060
1061
1062
1063
1064
1065
1066
1067
# File 'lib/roby/plan.rb', line 1059

def replaced(replaced_task, replacing_task)
    # Make the PlanService object follow the replacement
    return unless (services = plan_services.delete(replaced_task))

    services.each do |srv|
        srv.task = replacing_task
        (plan_services[replacing_task] ||= Set.new) << srv
    end
end

#replan(task) ⇒ Roby::Task

Creates a new planning pattern replacing the given task and its current planner

Parameters:

  • task (Roby::Task)

    the task that needs to be replanned

Returns:



1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
# File 'lib/roby/plan.rb', line 1752

def replan(task)
    return task.create_fresh_copy unless task.planning_task

    planner = replan(task.planning_task)
    planned = task.create_fresh_copy
    planned.abstract = true
    planned.planned_by planner
    replace(task, planned)
    planned
end

#root_plan?Boolean

True if this plan is root in the plan hierarchy

Returns:

  • (Boolean)


490
491
492
# File 'lib/roby/plan.rb', line 490

def root_plan?
    true
end

#same_plan?(other_plan, mappings) ⇒ Boolean

Compares this plan to other_plan, mappings providing the mapping from task/Events in self to task/events in other_plan

Returns:

  • (Boolean)


1893
1894
1895
# File 'lib/roby/plan.rb', line 1893

def same_plan?(other_plan, mappings)
    !find_plan_difference(other_plan, mappings)
end

#sibling_on?(peer) ⇒ Boolean

If this object is the main plan, checks if we are subscribed to the whole remote plan

Returns:

  • (Boolean)


55
56
57
58
59
60
# File 'lib/roby/plan.rb', line 55

def sibling_on?(peer)
    if Roby.plan == self then peer.remote_plan
    else
        super
    end
end

#sizeObject

Count of tasks in this plan



1500
1501
1502
1503
# File 'lib/roby/plan.rb', line 1500

def size
    Roby.warn_deprecated "Plan#size is deprecated, use #num_tasks instead"
    @tasks.size
end

#static_garbage_collect(protected_roots: Set.new, &block) ⇒ Object

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.



1842
1843
1844
1845
1846
1847
1848
1849
# File 'lib/roby/plan.rb', line 1842

def static_garbage_collect(protected_roots: Set.new, &block)
    unneeded = unneeded_tasks(additional_useful_roots: protected_roots)
    if block
        unneeded.each(&block)
    else
        unneeded.each { |t| remove_task(t) }
    end
end

#task_relation_graph_for(model) ⇒ Object

Resolves a task graph object from the graph class (i.e. the graph model)



227
228
229
# File 'lib/roby/plan.rb', line 227

def task_relation_graph_for(model)
    task_relation_graphs.fetch(model)
end

#template?Boolean

A template plan is meant to be injected in another plan

When a Roby::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

Returns:

  • (Boolean)

See Also:



72
73
74
# File 'lib/roby/plan.rb', line 72

def template?
    false
end

#transaction_stackArray

Returns the set of stacked transaction

Returns:

  • (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)



499
500
501
502
503
# File 'lib/roby/plan.rb', line 499

def transaction_stack
    plan_chain = [self]
    plan_chain << plan_chain.last.plan while plan_chain.last.respond_to?(:plan)
    plan_chain
end

#unmark_mission(task) ⇒ Object

Deprecated.

use #unmark_mission_task instead



520
521
522
523
524
525
# File 'lib/roby/plan.rb', line 520

def unmark_mission(task)
    Roby.warn_deprecated(
        "#unmark_mission is deprecated, use #unmark_mission_task instead"
    )
    unmark_mission_task(task)
end

#unmark_mission_task(task) ⇒ Object

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 Also:



572
573
574
575
576
577
578
579
580
# File 'lib/roby/plan.rb', line 572

def unmark_mission_task(task)
    task = task.to_task
    return unless @task_index.mission_tasks.include?(task)

    @task_index.mission_tasks.delete(task)
    task.mission = false if task.self_owned?
    notify_task_status_change(task, :normal)
    self
end

#unmark_permanent(object) ⇒ Object



598
599
600
601
602
603
604
605
606
607
608
609
610
611
# File 'lib/roby/plan.rb', line 598

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

#unmark_permanent_event(event) ⇒ Object

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 Also:



691
692
693
694
695
696
# File 'lib/roby/plan.rb', line 691

def unmark_permanent_event(event)
    if @task_index.permanent_events.delete?(event.to_event)
        notify_event_status_change(event, :normal)
    end
    nil
end

#unmark_permanent_task(task) ⇒ Object

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 Also:



657
658
659
660
661
662
663
# File 'lib/roby/plan.rb', line 657

def unmark_permanent_task(task)
    if @task_index.permanent_tasks.delete?(task.to_task)

        notify_task_status_change(task, :normal)
    end
    nil
end

#unneeded_eventsObject

The set of events that can be removed from the plan



1449
1450
1451
1452
1453
1454
1455
1456
1457
# File 'lib/roby/plan.rb', line 1449

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

#unneeded_tasks(additional_useful_roots: Set.new) ⇒ Object



1304
1305
1306
# File 'lib/roby/plan.rb', line 1304

def unneeded_tasks(additional_useful_roots: Set.new)
    tasks - useful_tasks(additional_roots: additional_useful_roots)
end

#use_fault_response_table(table_model, arguments = {}) ⇒ Coordination::FaultResponseTable, void

Enables a fault response table on this plan

Parameters:

  • table_model (Model<Coordination::FaultResponseTable>)

    the fault response table model

  • arguments (Hash) (defaults to: {})

    the arguments that should be passed to the created table

Returns:

See Also:



1959
1960
1961
1962
1963
1964
# File 'lib/roby/plan.rb', line 1959

def use_fault_response_table(table_model, arguments = {})
    table = table_model.new(self, arguments)
    table.attach_to(self)
    active_fault_response_tables << table
    table
end

#useful_eventsObject

Computes the set of events that are useful in the plan Events are ‘useful’ when they are chained to a task.



1444
1445
1446
# File 'lib/roby/plan.rb', line 1444

def useful_events
    compute_useful_free_events
end

#useful_task?(task) ⇒ Boolean

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

Returns:

  • (Boolean)


1338
1339
1340
# File 'lib/roby/plan.rb', line 1338

def useful_task?(task)
    tasks.include?(task) && !unneeded_tasks.include?(task)
end

#useful_tasks(additional_roots: Set.new, with_transactions: true) ⇒ Object



1297
1298
1299
1300
1301
1302
# File 'lib/roby/plan.rb', line 1297

def useful_tasks(additional_roots: Set.new, with_transactions: true)
    compute_useful_tasks(
        locally_useful_roots(with_transactions: with_transactions) |
        additional_roots
    )
end

#validate_graphs(graphs) ⇒ Object

Verifies that all graphs that should be acyclic are



449
450
451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/roby/plan.rb', line 449

def validate_graphs(graphs)
    # Make a topological sort of the graphs
    seen = Set.new
    Relations.each_graph_topologically(graphs) do |g|
        next if seen.include?(g)
        next unless g.dag?
        unless g.acyclic?
            raise Relations::CycleFoundError, "#{g.class} has cycles"
        end

        seen << g
        seen.merge(g.recursive_subsets)
    end
end

#verify_plan_object_finalization_sanity(object) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Perform sanity checks on a plan object that will be finalized



1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
# File 'lib/roby/plan.rb', line 1551

def verify_plan_object_finalization_sanity(object)
    unless object.root_object?
        raise ArgumentError, "cannot remove #{object} which is a non-root object"
    end

    return if object.plan == self

    if !object.plan
        if object.removed_at && !object.removed_at.empty?
            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
    elsif object.plan.template?
        raise ArgumentError, "#{object} has never been included in this plan"
    else
        raise ArgumentError,
              "#{object} is not in #{self}: #plan == #{object.plan}"
    end
end