Class: Roby::PlanObject

Inherits:
DistributedObject show all
Extended by:
Models::PlanObject
Includes:
DRoby::Identifiable, GUI::GraphvizPlanObject, GUI::RelationsCanvasPlanObject, Relations::DirectedRelationSupport
Defined in:
lib/roby/plan_object.rb,
lib/roby.rb,
lib/roby/droby/enable.rb

Overview

Base class for all objects which are included in a plan.

Direct Known Subclasses

EventGenerator, Task

Defined Under Namespace

Classes: InstanceHandler

Instance Attribute Summary collapse

Attributes included from Transaction::Proxying::Cache

#transaction_forwarder_module, #transaction_proxy_module

Attributes included from Relations::DirectedRelationSupport

#relation_graphs

Attributes inherited from DistributedObject

#local_owner_id, #owners

Instance Method Summary collapse

Methods included from Models::PlanObject

child_plan_object, finalization_handler, match

Methods included from DRoby::Identifiable

#droby_id

Methods included from GUI::RelationsCanvasPlanObject

#display, #display_create, #display_events, #display_name, #display_parent

Methods included from GUI::GraphvizPlanObject

#apply_layout, #dot_label, #to_dot

Methods included from Relations::DirectedRelationSupport

#[], #[]=, #add_parent_object, #child_object?, #child_objects, #clear_vertex, #each_child_object, #each_in_neighbour, #each_out_neighbour, #each_parent_object, #each_relation, #each_relation_graph, #each_relation_sorted, #each_root_relation_graph, #enum_child_objects, #enum_parent_objects, #enum_relations, #leaf?, #parent_object?, #parent_objects, #related_object?, #related_objects, #relation_graph_for, #relations, #remove_child_object, #remove_children, #remove_parent_object, #remove_parents, #remove_relations, #root?, #sorted_relations

Methods inherited from DistributedObject

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

Constructor Details

#initialize(plan: TemplatePlan.new) ⇒ PlanObject

Returns a new instance of PlanObject.



123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/roby/plan_object.rb', line 123

def initialize(plan: TemplatePlan.new)
    super()

    @plan = plan
    self.plan = @plan

    @removed_at = nil
    @executable = nil
    @garbage = false
    @finalization_handlers = []
    @model = self.class
end

Instance Attribute Details

#addition_timeObject

The time at which this plan object has been added into its first plan



630
631
632
# File 'lib/roby/plan_object.rb', line 630

def addition_time
  @addition_time
end

#executable=(value) ⇒ Object (writeonly)

A three-state flag with the following values:

nil

the object is executable if its plan is

true

the object is executable

false

the object is not executable



355
356
357
# File 'lib/roby/plan_object.rb', line 355

def executable=(value)
  @executable = value
end

#execution_engineObject (readonly)

The underlying execution engine if #plan is executable



23
24
25
# File 'lib/roby/plan_object.rb', line 23

def execution_engine
  @execution_engine
end

#finalization_handlersArray<InstanceHandler> (readonly)

Returns set of finalization handlers defined on this task instance.

Returns:

  • (Array<InstanceHandler>)

    set of finalization handlers defined on this task instance

See Also:



563
564
565
# File 'lib/roby/plan_object.rb', line 563

def finalization_handlers
  @finalization_handlers
end

#finalization_timeObject

The time at which this plan object has been finalized (i.e. removed from plan), or nil if it has not been (yet)



634
635
636
# File 'lib/roby/plan_object.rb', line 634

def finalization_time
  @finalization_time
end

#modelObject (readonly)

This object’s model

This is usually self.class, unless #specialize has been called in which case it is this object’s singleton class



13
14
15
# File 'lib/roby/plan_object.rb', line 13

def model
  @model
end

#planObject

The plan this object belongs to



146
147
148
# File 'lib/roby/plan_object.rb', line 146

def plan
  @plan
end

#promise_executorObject (readonly)

A thread pool that ensures that any work queued using #promise is serialized



27
28
29
# File 'lib/roby/plan_object.rb', line 27

def promise_executor
  @promise_executor
end

#removed_atObject

The place where this object has been removed from its plan. Once an object is removed from its plan, it cannot be added back again.



161
162
163
# File 'lib/roby/plan_object.rb', line 161

def removed_at
  @removed_at
end

Instance Method Details

#add_child_object(child, type, info = nil) ⇒ Object

Synchronizes the plan of this object from the one of its peer



460
461
462
463
464
465
466
467
468
469
470
471
# File 'lib/roby/plan_object.rb', line 460

def add_child_object(child, type, info = nil) # :nodoc:
    if child.plan != plan
        root_object.synchronize_plan(child.root_object)
        unless type.kind_of?(Class)
            # If given a graph, we need to re-resolve it as #plan might
            # have changed
            type = relation_graphs.fetch(type.class)
        end
    end

    super
end

#apply_relation_changes(object, changes) ⇒ Object

Transfers a set of relations from this plan object to object. changes is formatted as a sequence of relation, parents, children slices, where parents and children are sets of objects.

For each of these slices, the method removes the parent->self and self->child edges in the given relation, and then adds the corresponding parent->object and object->child edges.



497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
# File 'lib/roby/plan_object.rb', line 497

def apply_relation_changes(object, changes)
    # The operation is done in two parts to avoid problems with
    # creating cycles in the graph: first we remove the old edges, then
    # we add the new ones.
    changes.each_slice(3) do |rel, parents, children|
        next if rel.copy_on_replace?

        parents.each_slice(2) do |parent, info|
            parent.remove_child_object(self, rel)
        end
        children.each_slice(2) do |child, info|
            remove_child_object(child, rel)
        end
    end

    # See comment above about keeping the two loops separate
    changes.each_slice(3) do |rel, parents, children| # rubocop:disable Style/CombinableLoops
        parents.each_slice(2) do |parent, info|
            parent.add_child_object(object, rel, info)
        end
        children.each_slice(2) do |child, info|
            object.add_child_object(child, rel, info)
        end
    end
end

#as_planObject

Used in plan management as a way to extract a plan object from any object



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

def as_plan
    self
end

#can_finalize?Boolean

Whether this task can be finalized

Unlike plan-level structure, a task that is marked as keepalive will be processed by garbage collection (e.g. stopping it). However, it will not be finalized (removed from the plan). The garbage collection will clear its relations instead of finalizing it

The default returns true

Returns:

  • (Boolean)


41
42
43
# File 'lib/roby/plan_object.rb', line 41

def can_finalize?
    true
end

#commit_transactionObject

Method called to apply modifications needed to commit this object into the underlying plan

For instance, Task will make sure that argument objects that are transaction proxies are unwrapped

Note that the method should only deal with modifications that are internal to the task itself. Modifications of the relation graphs are handled by Transaction itself in Transaction#apply_modifications_to_plan

The default implementation does nothing



405
# File 'lib/roby/plan_object.rb', line 405

def commit_transaction; end

#concrete_modelObject

The non-specialized model for self

It is always self.class



18
19
20
# File 'lib/roby/plan_object.rb', line 18

def concrete_model
    self.class
end

#connection_spaceObject



29
30
31
# File 'lib/roby/plan_object.rb', line 29

def connection_space
    plan&.connection_space
end

#each_finalization_handler {|block| ... } ⇒ void

This method returns an undefined value.

Enumerates the finalization handlers that should be applied in finalized!

Yield Parameters:

  • block (#call)

    the handler’s block



570
571
572
573
574
575
576
577
# File 'lib/roby/plan_object.rb', line 570

def each_finalization_handler(&block)
    finalization_handlers.each do |handler|
        yield(handler.block)
    end
    self.class.each_finalization_handler do |model_handler|
        model_handler.bind(self).call(&block)
    end
end

#each_in_neighbour_merged(relation, intrusive: nil, &block) ⇒ Object



237
238
239
240
241
242
243
244
245
# File 'lib/roby/plan_object.rb', line 237

def each_in_neighbour_merged(relation, intrusive: nil, &block)
    if intrusive.nil?
        raise ArgumentError, "you must give a true or false to the intrusive flag"
    end

    merged_relations(
        proc { |o, &b| o.each_in_neighbour(relation, &b) },
        intrusive, &block)
end

#each_out_neighbour_merged(relation, intrusive: nil, &block) ⇒ Object



247
248
249
250
251
252
253
254
255
# File 'lib/roby/plan_object.rb', line 247

def each_out_neighbour_merged(relation, intrusive: nil, &block)
    if intrusive.nil?
        raise ArgumentError, "you must give a true or false to the intrusive flag"
    end

    merged_relations(
        proc { |o, &b| o.each_out_neighbour(relation, &b) },
        intrusive, &block)
end

#each_plan_childObject

Iterates on all the children of this root object



484
485
486
# File 'lib/roby/plan_object.rb', line 484

def each_plan_child
    self
end

#engineObject

Deprecated.

use #execution_engine instead



149
150
151
152
# File 'lib/roby/plan_object.rb', line 149

def engine
    Roby.warn_deprecated "PlanObject#engine is deprecated, use #execution_engine instead"
    execution_engine
end

#executable?Boolean

If this object is executable

Returns:

  • (Boolean)


358
359
360
# File 'lib/roby/plan_object.rb', line 358

def executable?
    @executable || (@executable.nil? && !garbage? && plan && plan.executable?)
end

#finalized!(timestamp = nil) ⇒ void

This method returns an undefined value.

Called when a particular object has been removed from its plan

If PlanObject.debug_finalization_place? is true (set with PlanObject.debug_finalization_place=, the backtrace in this call is stored in #removed_at. It is false by default, as it is pretty expensive.

Parameters:

  • timestamp (Time, nil) (defaults to: nil)

    the time at which it got finalized. It is stored in #finalization_time



589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
# File 'lib/roby/plan_object.rb', line 589

def finalized!(timestamp = nil)
    if self.plan.executable?
        # call finalization handlers
        each_finalization_handler do |handler|
            handler.call(self)
        end
    end

    if root_object?
        self.plan = nil
        if PlanObject.debug_finalization_place?
            self.removed_at = caller
        else
            self.removed_at = []
        end
        self.finalization_time = timestamp || Time.now
        self.finalized = true
    end
end

#finalized?Boolean

True if this object has been included in a plan, but has been removed from it since

Returns:

  • (Boolean)


165
166
167
# File 'lib/roby/plan_object.rb', line 165

def finalized?
    !!removed_at
end

#forget_peer(peer) ⇒ Object

Called when all links to peer should be removed.



448
449
450
451
452
453
454
455
456
457
# File 'lib/roby/plan_object.rb', line 448

def forget_peer(peer)
    unless root_object?
        raise ArgumentError, "#{self} is not root"
    end

    each_plan_child do |child|
        child.forget_peer(peer)
    end
    super
end

#fullfills?(models) ⇒ Boolean

Returns true if this object provides all the given models.

Returns:

  • (Boolean)

    true if this object provides all the given models



637
638
639
640
641
# File 'lib/roby/plan_object.rb', line 637

def fullfills?(models)
    Array(models).all? do |m|
        self.model <= m
    end
end

#garbage!Object

Mark this task as garbage

Garbage tasks cannot be involved in new relations, and cannot be executed

Once set, this flag cannot be unset

See Also:



378
379
380
# File 'lib/roby/plan_object.rb', line 378

def garbage!
    @garbage = true
end

#garbage?Object

Whether this task has been marked as garbage by the garbage collection process

See Also:



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

attr_predicate :garbage?

#initialize_copy(other) ⇒ Object



136
137
138
139
140
141
142
143
# File 'lib/roby/plan_object.rb', line 136

def initialize_copy(other)
    # Consider whether subclasses initialized the plan before calling us
    # ... in which case we handle it specially and propagate it
    super
    @plan = nil
    @relation_graphs = nil
    @finalization_handlers = other.finalization_handlers.dup
end

#initialize_replacement(object) ⇒ Object

Called by #replace_by and #replace_subplan_by to do object-specific initialization of object when object is used to replace self in a plan

The default implementation does nothing



542
543
544
545
546
547
548
549
# File 'lib/roby/plan_object.rb', line 542

def initialize_replacement(object)
    finalization_handlers.each do |handler|
        if handler.copy_on_replace?
            object ||= yield
            object.when_finalized(handler.as_options, &handler.block)
        end
    end
end

#merged_relations(enumerator, intrusive, &block) ⇒ Object

call-seq:

merged_relation(enumeration_method, false[, arg1, arg2]) do |self_t, related_t|
end
merged_relation(enumeration_method, true[, arg1, arg2]) do |related_t|
end

It is assumed that enumeration_method is the name of a method on self that will yield an object related to self.

This method computes the same set of related objects, but does so while merging all the changes that underlying transactions may have applied. I.e. it is equivalent to calling enumeration_method on the plan that would be the result of the application of the whole transaction stack

If instrusive is false, the edges are yielded at the level they appear. I.e. both self and the related object are given, and

self_t, related_t

may be part of a parent plan of self.plan. I.e.

self_t is either self itself, or the task that self represents in a parent plan / transaction.

If instrusive is true, the related objects are recursively added to all transactions in the transaction stack, and are given at the end. I.e. only the related object is yield, and it is guaranteed to be included in self.plan.

For instance,

merged_relations(:each_child, false) do |parent, child|
   ...
end

yields the children of self according to the modifications that the transactions apply, but may do so in the transaction’s parent plans.

merged_relations(:each_child, true) do |child|
   ...
end

Will yield the same set of tasks, but included in self.plan.



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
# File 'lib/roby/plan_object.rb', line 297

def merged_relations(enumerator, intrusive, &block)
    return enum_for(__method__, enumerator, intrusive) unless block_given?

    plan_chain = self.transaction_stack
    object     = self.real_object
    enumerator = enumerator.to_proc

    pending = []
    while plan_chain.size > 1
        plan      = plan_chain.pop
        next_plan = plan_chain.last

        # Objects that are in +plan+ but not in +next_plan+ are
        # automatically added, as +next_plan+ is not able to change
        # them. Those that are included in +next_plan+ are handled
        # later.
        new_objects = []
        enumerator.call(object) do |related_object|
            next if next_plan[related_object, create: false]

            if !intrusive
                yield(object, related_object)
            else
                new_objects << related_object
            end
        end

        # Here, pending contains objects from the previous plan (i.e. in
        # plan.plan). Proxy them in +plan+.
        #
        # It is important to do that *after* we enumerated the relations
        # that exist in +plan+ (above), as it reduces the number of
        # relations at each level.
        pending.map! { |t| plan[t] }
        # And add the new objects that we just discovered
        pending.concat(new_objects)

        if next_plan
            object = next_plan[object]
        end
    end

    if intrusive
        enumerator.call(self, &block)
        for related_object in pending
            yield(self.plan[related_object])
        end
    else
        enumerator.call(self) do |related_object|
            yield(self, related_object)
        end
    end
end

#promise(description: "#{self}.promise", executor: promise_executor, &block) ⇒ Promise

Create a promise that is serialized with all promises created for this object

Parameters:

  • description (String) (defaults to: "#{self}.promise")

    a textual description of the promise’s role (used for debugging and timing)

Returns:



204
205
206
# File 'lib/roby/plan_object.rb', line 204

def promise(description: "#{self}.promise", executor: promise_executor, &block)
    execution_engine.promise(description: description, executor: executor, &block)
end

#read_write?Boolean

True if this object can be modified by the local plan manager

Returns:

  • (Boolean)


552
553
554
555
556
557
558
# File 'lib/roby/plan_object.rb', line 552

def read_write?
    if self_owned?
        true
    elsif plan.self_owned?
        owners.all? { |p| plan.owned_by?(p) }
    end
end

#real_objectObject

If self is a transaction proxy, returns the underlying plan object, regardless of how many transactions there is on the stack. Otherwise, return self.



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

def real_object
    result = self
    while result.respond_to?(:__getobj__)
        result = result.__getobj__
    end
    result
end

#remotely_useful?Boolean

True if this object is useful for one of our peers

Returns:

  • (Boolean)


420
421
422
# File 'lib/roby/plan_object.rb', line 420

def remotely_useful?
    plan&.remotely_useful? || super
end

#replace_by(object) ⇒ Object

Replaces self by object in all graphs self is part of.

Raises:

  • (NotImplementedError)


533
534
535
# File 'lib/roby/plan_object.rb', line 533

def replace_by(object)
    raise NotImplementedError, "#{self.class} did not reimplement #replace_by"
end

#replace_subplan_by(object) ⇒ Object

Replaces, in the plan, the subplan generated by this plan object by the one generated by object. In practice, it means that we transfer all parent edges whose target is self from the receiver to object. It calls the various add/remove hooks defined in Relations::DirectedRelationSupport.

Raises:

  • (NotImplementedError)


528
529
530
# File 'lib/roby/plan_object.rb', line 528

def replace_subplan_by(object)
    raise NotImplementedError, "#{self.class} did not reimplement #replace_subplan_by"
end

#root_objectObject

Return the root plan object for this object.



474
475
476
# File 'lib/roby/plan_object.rb', line 474

def root_object
    self
end

#root_object?Boolean

True if this object is a root object in the plan.

Returns:

  • (Boolean)


479
480
481
# File 'lib/roby/plan_object.rb', line 479

def root_object?
    root_object == self
end

#subscribed?Boolean

True if we are explicitely subscribed to this object

Returns:

  • (Boolean)


383
384
385
386
387
388
389
390
391
# File 'lib/roby/plan_object.rb', line 383

def subscribed?
    if root_object?
        plan&.subscribed? ||
            (!self_owned? && owners.any?(&:subscribed_plan?)) ||
            super
    else
        root_object.subscribed?
    end
end

#transaction_proxy?Boolean

True if this object is a transaction proxy, false otherwise

Returns:

  • (Boolean)


155
156
157
# File 'lib/roby/plan_object.rb', line 155

def transaction_proxy?
    false
end

#transaction_stackObject

Returns the stack of transactions/plans this object is part of, starting with self.plan.



227
228
229
230
231
232
233
234
235
# File 'lib/roby/plan_object.rb', line 227

def transaction_stack
    result = [plan]
    obj    = self
    while obj.respond_to?(:__getobj__)
        obj = obj.__getobj__
        result << obj.plan
    end
    result
end

#update_on?(peer) ⇒ Boolean

True if we should send updates about this object to peer

Returns:

  • (Boolean)


410
411
412
# File 'lib/roby/plan_object.rb', line 410

def update_on?(peer)
    plan&.update_on?(peer) || super
end

#updated_by?(peer) ⇒ Boolean

True if we receive updates for this object from peer

Returns:

  • (Boolean)


415
416
417
# File 'lib/roby/plan_object.rb', line 415

def updated_by?(peer)
    plan&.updated_by?(peer) || super
end

#when_finalized(options = {}) {|task| ... } ⇒ void

This method returns an undefined value.

Called when the task gets finalized, i.e. removed from the main plan

Parameters:

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

    a customizable set of options

Options Hash (options):

  • :on_replace (:copy, :drop) — default: :drop

    behaviour to be followed when the task gets replaced. If :drop, the handler is not passed, if :copy it is installed on the new task as well as kept on the old one

Yield Parameters:

  • task (Roby::Task)

    the task that got finalized. It might be different than the one on which the handler got installed because of replacements



619
620
621
622
623
# File 'lib/roby/plan_object.rb', line 619

def when_finalized(options = {}, &block)
    options = InstanceHandler.validate_options options
    check_arity(block, 1)
    finalization_handlers << InstanceHandler.new(block, (options[:on_replace] == :copy))
end