Ravn::BDE::

Bolt class

A coordinated workflow contract for the Battlefield Decision Engine.

Emits: - bolt.initiated.{uuid} - bolt.sent.{uuid} - bolt.received.{uuid} - bolt.responded.{uuid} - bolt.summary.{uuid}

Consumes: - bolt.initiate.{uuid} - bolt.send.{uuid} - bolt.sent.{uuid} - bolt.respond.{uuid} - bolt.responded.{uuid}

Constants

DEFAULT_TIMEOUT

The default timeout if none is configured

PHASE_METHODS

The methods that are triggered by the given events in the execution of the workflow across multiple nodes.

PLACEHOLDER_PATTERN

A pattern for placeholders in workflow strings that get expanded in various places.

VALID_NAME

Pattern for matching valid Bolt name attribute

VALID_TEXT

Pattern for matching valid Bolt text attribute

VALID_TYPE

Pattern for matching valid Bolt type attribute

Attributes

expires_at RW

The Monotonic time at which the bolt will finish via timeout

start_time R

The monotonic time when this bolt was spawned

Public Class Methods

add_components( components )

Add the configured components to the receiving Bolt.

# File lib/ravn/bde/bolt.rb, line 178
def self::add_components( components )
        components.each do |type, opts|
                opts = opts.transform_keys( &:to_sym )
                self.log.debug "Finding and applying the %s component with options: %p" %
                        [ type, opts ]
                component = Ravn::BDE::Bolt::Component.find( type )
                opts = component.validate( opts )
                component.apply( self, **opts )
        rescue LoadError => err
                raise Ravn::BDE::InvalidBoltConfig, "Unknown component type \"%s\" (%s)" % [ type, err.message ]
        rescue Ravn::BDE::ValidationError => err
                raise Ravn::BDE::InvalidBoltConfig, err.message
        end
end
compile_message_handlers()

Gather message types from each workflow and add the callbacks to the

message_handlers.

# File lib/ravn/bde/bolt.rb, line 225
def self::compile_message_handlers
        self.message_handler_map.each do |message_type_template, handler|
                message_type = self.expand_placeholders( message_type_template )
                self.log.debug "Installing a message handler: %s (%s): %p" %
                         [ message_type_template, message_type, handler ]
                self.register_message_handler( message_type, handler )
        end
end
expand_placeholders( string )

Expand placeholders in the given string using config, variables, etc.

# File lib/ravn/bde/bolt.rb, line 236
def self::expand_placeholders( string )
        return string.gsub( PLACEHOLDER_PATTERN ) do |_|
                name = $~[:placeholder]

                case name
                when 'bolt_id'
                        self.id.to_s
                when 'bolt_name'
                        (self.name || self.type).to_s
                when 'bolt_type'
                        self.type.to_s
                when 'bolt_text'
                        self.text.to_s
                when 'device_id'
                        Ravn.device_id
                else
                        raise "No expansion for `%s' in %p" % [ name, string ]
                end
        end
end
inherited( subclass )

Inheritance hook – add some variables to all +subclass+es.

# File lib/ravn/bde/bolt.rb, line 123
def self::inherited( subclass )
        super

        subclass.instance_variable_set( :@message_handler_map, PHASE_METHODS.dup )
end
inspect()

Return a human-readable version of the class suitable for debugging. This is overridden for tailored (anonymous) subclasses.

# File lib/ravn/bde/bolt.rb, line 282
def self::inspect
        string = super

        if self.id
                details = "%s%s Bolt {%s}" % [
                        self.type,
                        @name ? " - #@name" : '',
                        self.id
                ]
                string = string.sub( /Class/, details )
        end

        return string
end
is_draft?()

Returns true if the bolt has a vernacular name in addition to its semantic type.

# File lib/ravn/bde/bolt.rb, line 274
def self::is_draft?
        return @name ? true : false
end
is_relayed?()

Returns true if the bolt has been marked as one that gets relayed by gateways.

# File lib/ravn/bde/bolt.rb, line 267
def self::is_relayed?
        return self.ontological_suffix ? true : false
end
name()

The bolt name when it’s set; falls back to the type.

# File lib/ravn/bde/bolt.rb, line 117
def self::name
        return @name || self.type
end
new()

Create a new bolt instance.

# File lib/ravn/bde/bolt.rb, line 303
def initialize
        self.log.debug "Creating an instance of %p" % [ self ]

        @id         = Ravn.uuid.generate
        @start_time = Ravn.monotonic_time
        @expires_at = nil
        @initiated  = false

        super()
end
path_name()

Return a version of the name of the tailored bolt suitable for use in a path in the actor tree.

# File lib/ravn/bde/bolt.rb, line 260
def self::path_name
        base = self.name || self.type
        return base.downcase.gsub( /\P{alnum}/, '_' )
end
segment_for_config( config )

Given a segment config, return an equivalent Ravn::BDE::Segment.

# File lib/ravn/bde/bolt.rb, line 131
def self::segment_for_config( config )
        case config
        when String, Symbol
                config = 'Everyone' if config == :default
                segment_name = config.to_s
                segment = Ravn::BDE.segments[ segment_name ] or
                        raise Ravn::BDE::ValidationError, "no such segment %s" % [ config ]

                return segment
        when NilClass
                return Ravn::BDE::Segment.default
        else
                raise Ravn::BDE::ValidationError, "no such segment %p" % [ config ]
        end
end
tailor( id, type:, name: nil, text: "${bolt_name}", timeout: DEFAULT_TIMEOUT, to: nil, components: {}, **options ) { |subclass| ... }

Create a new tailored Bolt with the given uuid and name.

# File lib/ravn/bde/bolt.rb, line 149
def self::tailor( id, type:, name: nil, text: "${bolt_name}", timeout: DEFAULT_TIMEOUT,
        to: nil, components: {}, **options )

        self.log.info "Tailoring '%s' bolt: %s (components: %p) with options: %p" %
                [ type, id, components, options ]

        subclass = Class.new( self )
        subclass.instance_variable_set( :@id, id )
        subclass.instance_variable_set( :@type, type )
        subclass.instance_variable_set( :@name, name )
        subclass.instance_variable_set( :@text, text )
        subclass.instance_variable_set( :@components, components )
        subclass.instance_variable_set( :@timeout, timeout )

        to = self.segment_for_config( to )
        subclass.instance_variable_set( :@to, to )

        subclass.instance_variable_set( :@ontological_suffix, options[:ontological_suffix] )

        yield( subclass ) if block_given?

        subclass.add_components( components )
        subclass.compile_message_handlers

        return subclass
end
validate( config )

Validate the specified bolt config, raaising a Ravn::BDE::InvalidBoltConfig if it isn’t valid.

# File lib/ravn/bde/bolt.rb, line 196
def self::validate( config )
        type = config[:type] or raise Ravn::BDE::InvalidBoltConfig, "missing `type' attribute"
        raise Ravn::BDE::InvalidBoltConfig, "invalid `type' attribute" unless
                type.match?( VALID_TYPE )

        name = config[:name]
        raise Ravn::BDE::InvalidBoltConfig, "invalid `name' attribute" if
                name && !name.match?( VALID_NAME )

        text = config[:text]
        raise Ravn::BDE::InvalidBoltConfig, "invalid `text' attribute" if
                text && !text.match?( VALID_TEXT )

        timeout = config[:timeout]
        raise Ravn::BDE::InvalidBoltConfig, "invalid `timeout` attribute" if
                timeout && ( !timeout.is_a?( Numeric ) || timeout < 0 )

        components = config[:components] || {}
        unless components.is_a?( Hash )
                raise Ravn::BDE::InvalidBoltConfig,
                        "invalid `components'; expected a Hash, got %p" % [ components.class ]
        end

        return true
end

Public Instance Methods

components()

The component configuration (as a Hash) that has been applied to the tailored Bolt

# File lib/ravn/bde/bolt.rb, line 101
singleton_attr_reader :components
event_payload_for( phase, originating_event )

Return the ‘data’ section for the bolt message emitted during the given phase.

# File lib/ravn/bde/bolt.rb, line 407
def event_payload_for( phase, originating_event )
        payload = self.fields

        case phase
        when :initiate
                payload[ :to ] = self.to.name
        when :send
                payload[ :to ] = self.to.members.to_a - [Ravn::BDE.callsign]
        when :respond, :receive
                payload[ :instance_id ] = originating_event.data[ :instance_id ] or
                        raise "Originating event %s is missing instance_id" % [ originating_event.id ]
        end

        return payload
end
expects_response_from_me?()

Returns true if the current user is expected to respond

# File lib/ravn/bde/bolt.rb, line 399
def expects_response_from_me?
        # return true or nil, so that the watch can set to false
        return self.class.responses_from.include?( Ravn::BDE.callsign ) || nil
end
fields()

Ravn::Message API – return a Hash of fields for this Bolt when serialized.

# File lib/ravn/bde/bolt.rb, line 466
def fields
        return {
                instance_id: self.id,
                type: self.type,
                name: self.name,
                text: self.text,
                is_relayed: self.class.is_relayed?,
                ontological_suffix: self.class.ontological_suffix,
        }
end
handle_heartbeat( * )

Handle the heartbeat event sent by the runner to expire bolts that have timed out.

# File lib/ravn/bde/bolt.rb, line 460
def handle_heartbeat( * )
        self.tell( timeout: true ) if self.timed_out?
end
handle_timeout( * )

Event handler – called when the Bolt has timed out while executing.

# File lib/ravn/bde/bolt.rb, line 452
def handle_timeout( * )
        self.log.warn "%p timed out." % [ self ]
        self.finish_bolt
end
id()

The unique identifier of a tailored Bolt

# File lib/ravn/bde/bolt.rb, line 85
singleton_attr_reader :id
initiate_phase( event )

Initiate a new run of the Bolt’s workflow

# File lib/ravn/bde/bolt.rb, line 342
def initiate_phase( event )
        self.log.warn "Initiated with: %p" % [ event ]
        if self.initiated?
                self.log.warn "Initiated again before finishing; restarting a new instance"
                return self.finish_bolt( event )
        else
                self.initiated = true
                payload = self.event_payload_for( :initiate, event )
                self.send_correlated_message( "bolt.initiated.${bolt_id}", event, data: payload )
        end
end
initiated?()

True if the bolt has been initiated.

# File lib/ravn/bde/bolt.rb, line 333
attr_predicate_accessor :initiated?
is_target_of?( event )

Returns true if the given event is addressed to the receiving bolt.

# File lib/ravn/bde/bolt.rb, line 432
def is_target_of?( event )
        return case event
        when Ravn::Net::Message
                event.callsign && event.data[:instance_id] == self.id
        when Ravn::HAL::Message
                event.data[:instance_id] == self.id
        else
                false
        end
end
is_timed_out?()
Alias for: timed_out?
message_handler_map()

The map of events to the methods they invoke

# File lib/ravn/bde/bolt.rb, line 113
singleton_attr_reader :message_handler_map
ontological_suffix()

The subtype of the bolt when emitted up and out

# File lib/ravn/bde/bolt.rb, line 109
singleton_attr_reader :ontological_suffix
ready_to_be_finished?()

Returns true if the bolt state is not expected to change further. This should be overridden by components that will add more state in later phases.

# File lib/ravn/bde/bolt.rb, line 426
def ready_to_be_finished?
        return true
end
receive_phase( event )

Start the workflow from an initiating BDE, showing a notification on the display(s).

# File lib/ravn/bde/bolt.rb, line 367
def receive_phase( event )
        self.log.info "Received: %p" % [ event ]
        recipients = event.to || []

        if recipients.empty? || recipients.include?( Ravn::BDE.callsign )
                payload = self.event_payload_for( :receive, event )
                self.send_correlated_message( 'bolt.received.${bolt_id}', event, data: payload )
        end
end
respond_phase( event )

If the message has a response, forward it across the network to the other BDEs.

# File lib/ravn/bde/bolt.rb, line 380
def respond_phase( event )
        self.log.info "Responding with: %p" % [ event ]
        payload = self.event_payload_for( :respond, event )
        self.send_correlated_message( 'bolt.responded.${bolt_id}', event, data: payload )
end
send_phase( event )

Upon confirmation by the watch, start the workflow on the other BDEs

# File lib/ravn/bde/bolt.rb, line 356
def send_phase( event )
        self.log.info "Sent with: %p" % [ event ]
        payload = self.event_payload_for( :send, event )
        self.send_correlated_message( 'bolt.sent.${bolt_id}', event, data: payload )
        self.expires_at = Ravn.monotonic_time + self.class.timeout
        self.finish_bolt if self.ready_to_be_finished?
end
summarize_phase( event )

Gather responses and show a summary of them on the display(s).

# File lib/ravn/bde/bolt.rb, line 388
def summarize_phase( event )
        if self.is_target_of?( event )
                self.log.info "Summary updated with: %p" % [ event ]
                payload = self.event_payload_for( :summarize, event )
                self.send_correlated_message( 'bolt.summary.${bolt_id}', event, data: payload )
                self.finish_bolt if self.ready_to_be_finished?
        end
end
text()

The user-facing content of the bolt

# File lib/ravn/bde/bolt.rb, line 93
singleton_attr_reader :text
timed_out?()

Return true if the bolt has timed out.

# File lib/ravn/bde/bolt.rb, line 445
def timed_out?
        return self.expires_at && Ravn.monotonic_time > self.expires_at
end
Also aliased as: is_timed_out?
timeout()

The user-configured timeout

# File lib/ravn/bde/bolt.rb, line 97
singleton_attr_reader :timeout
to()

The segment that describes the callsigns the message is sent to

# File lib/ravn/bde/bolt.rb, line 105
singleton_attr_reader :to
type()

The semantic type of the tailored Bolt

# File lib/ravn/bde/bolt.rb, line 89
singleton_attr_reader :type

Protected Instance Methods

finish_bolt( forwarded_event=nil )

Complete this bolt’s execution lifetime, saving it to bolt history and resetting state.

# File lib/ravn/bde/bolt.rb, line 505
def finish_bolt( forwarded_event=nil )
        self.log.warn "Finishing bolt %s%s" % [
                self.id,
                forwarded_event ? " with forwarded event: %p" % [ forwarded_event ] : ''
        ]
        self.parent.tell( finish_bolt: [self, forwarded_event] )
end
inspect_details()

Ravn::Inspection API: Return the detail part of the inspect output.

# File lib/ravn/bde/bolt.rb, line 515
def inspect_details
        earlier_details = super if defined?( super )
        return earlier_details ? " #{earlier_details}" : ''
end
send_correlated_message( type, origin_message, data: {} )

Send a message of the specified type, originating with the given origin_message and including the specified data.

# File lib/ravn/bde/bolt.rb, line 484
def send_correlated_message( type, origin_message, data: {} )
        fields = origin_message.fields.merge( data: data )

        return self.send_message( type, fields )
end
send_message( type, fields )

Create a send a message of the given type with the specified fields.

# File lib/ravn/bde/bolt.rb, line 492
def send_message( type, fields )
        type = self.class.expand_placeholders( type )
        message = Ravn::BDE::Message.new( type, fields )

        self.log.info "Sending bolt message: %p" % [ message ]
        self.filter_up( message )

        return message
end