Example #1
0
def test_basic_sensorgraph():
    """Make sure we can parse, load and run a basic sensor graph."""

    model = DeviceModel()
    log = SensorLog(model=model)
    sg = SensorGraph(log, model=model)

    sg.add_node('(input 1 always && input 2 when count >= 1) => unbuffered 1 using copy_all_a')
    sg.process_input(DataStream.FromString('input 1'), IOTileReading(0, 1, 1), rpc_executor=None)
    sg.process_input(DataStream.FromString('input 2'), IOTileReading(0, 1, 1), rpc_executor=None)

    assert sg.sensor_log.inspect_last(DataStream.FromString('unbuffered 1')).value == 1
Example #2
0
def test_triggering_streamers():
    model = DeviceModel()
    log = SensorLog(model=model)
    sg = SensorGraph(log, model=model)

    sg.add_node('(input 1 always) => output 1 using copy_all_a')
    sg.add_node('(input 1 always) => output 2 using copy_all_a')

    sg.add_streamer(parse_string_descriptor('streamer on output 1'))
    sg.add_streamer(
        parse_string_descriptor('manual streamer on output 2 with streamer 0'))

    triggered = sg.check_streamers()
    assert len(triggered) == 0

    sg.process_input(DataStream.FromString('input 1'),
                     IOTileReading(0, 1, 1),
                     rpc_executor=None)
    triggered = sg.check_streamers()
    assert len(triggered) == 2
Example #3
0
class SensorGraphSubsystem(object):
    """Container for sensor graph state.

    There is a distinction between which sensor-graph is saved into persisted
    storage vs currently loaded and running.  This subsystem needs to be
    created with a shared mutex with the sensor_log subsystem to make sure
    all accesses are properly synchronized.
    """

    def __init__(self, sensor_log_system, stream_manager, model):
        self._logger = logging.getLogger(__name__)

        self._model = model

        self._sensor_log = sensor_log_system.storage
        self._allocate_id = sensor_log_system.allocate_id

        self._stream_manager = stream_manager
        self._rsl = sensor_log_system

        self.graph = SensorGraph(self._sensor_log, model=model, enforce_limits=True)

        self.persisted_exists = False
        self.persisted_nodes = []
        self.persisted_streamers = []
        self.persisted_constants = []

        self.streamer_acks = {}
        self.streamer_status = {}

        self.enabled = False

        # Clock manager linkage
        self.get_timestamp = lambda: 0

    def clear_to_reset(self, _config_vars):
        """Clear all volatile information across a reset.

        The reset behavior is that:
        - any persisted sensor_graph is loaded
        - if there is a persisted graph found, enabled is set to True
        - if there is a persisted graph found, reset readings are pushed
          into it.
        """

        self.graph.clear()

        if not self.persisted_exists:
            return

        for node in self.persisted_nodes:
            self.graph.add_node(node)

        for streamer_desc in self.persisted_streamers:
            streamer = streamer_descriptor.parse_string_descriptor(streamer_desc)
            self.graph.add_streamer(streamer)

        # Load in the constants
        for stream, reading in self.persisted_constants:
            self._sensor_log.push(stream, reading)

        self.enabled = True

        # Set up all streamers
        for index, value in viewitems(self.streamer_acks):
            self._seek_streamer(index, value)

        #FIXME: queue sending reset readings

    def process_input(self, encoded_stream, value):
        """Process or drop a graph input.

        This must not be called directly from an RPC but always via a deferred
        task.
        """

        if not self.enabled:
            return

        stream = DataStream.FromEncoded(encoded_stream)
        reading = IOTileReading(self.get_timestamp(), encoded_stream, value)

        self.graph.process_input(stream, reading, None)  #FIXME: add in an rpc executor for this device.

        self.process_streamers()

    def _seek_streamer(self, index, value):
        """Complex logic for actually seeking a streamer to a reading_id.

        This routine hides all of the gnarly logic of the various edge cases.
        In particular, the behavior depends on whether the reading id is found,
        and if it is found, whether it belongs to the indicated streamer or not.

        If not, the behavior depends on whether the sought reading it too high
        or too low.
        """

        highest_id = self._rsl.highest_stored_id()

        streamer = self.graph.streamers[index]
        if not streamer.walker.buffered:
            return _pack_sgerror(SensorLogError.CANNOT_USE_UNBUFFERED_STREAM)

        find_type = None
        try:
            exact = streamer.walker.seek(value, target='id')
            if exact:
                find_type = 'exact'
            else:
                find_type = 'other_stream'

        except UnresolvedIdentifierError:
            if value > highest_id:
                find_type = 'too_high'
            else:
                find_type = 'too_low'

        # If we found an exact match, move one beyond it

        if find_type == 'exact':
            try:
                streamer.walker.pop()
            except StreamEmptyError:
                pass

            error = Error.NO_ERROR
        elif find_type == 'too_high':
            streamer.walker.skip_all()
            error = _pack_sgerror(SensorLogError.NO_MORE_READINGS)
        elif find_type == 'too_low':
            streamer.walker.seek(0, target='offset')
            error = _pack_sgerror(SensorLogError.NO_MORE_READINGS)
        else:
            error = _pack_sgerror(SensorLogError.ID_FOUND_FOR_ANOTHER_STREAM)

        return error

    def acknowledge_streamer(self, index, ack, force):
        """Acknowledge a streamer value as received from the remote side."""

        if index >= len(self.graph.streamers):
            return _pack_sgerror(SensorGraphError.STREAMER_NOT_ALLOCATED)

        old_ack = self.streamer_acks.get(index, 0)

        if ack != 0:
            if ack <= old_ack and not force:
                return _pack_sgerror(SensorGraphError.OLD_ACKNOWLEDGE_UPDATE)

            self.streamer_acks[index] = ack

        current_ack = self.streamer_acks.get(index, 0)
        return self._seek_streamer(index, current_ack)

    def _handle_streamer_finished(self, index, succeeded, highest_ack):
        """Callback when a streamer finishes processing."""

        self._logger.debug("Rolling back streamer %d after streaming, highest ack from streaming subsystem was %d", index, highest_ack)
        self.acknowledge_streamer(index, highest_ack, False)

    def process_streamers(self):
        """Check if any streamers should be handed to the stream manager."""

        # Check for any triggered streamers and pass them to stream manager
        in_progress = self._stream_manager.in_progress()
        triggered = self.graph.check_streamers(blacklist=in_progress)

        for streamer in triggered:
            self._stream_manager.process_streamer(streamer, callback=self._handle_streamer_finished)

    def trigger_streamer(self, index):
        """Pass a streamer to the stream manager if it has data."""

        self._logger.debug("trigger_streamer RPC called on streamer %d", index)

        if index >= len(self.graph.streamers):
            return _pack_sgerror(SensorGraphError.STREAMER_NOT_ALLOCATED)

        if index in self._stream_manager.in_progress():
            return _pack_sgerror(SensorGraphError.STREAM_ALREADY_IN_PROGRESS)

        streamer = self.graph.streamers[index]
        if not streamer.triggered(manual=True):
            return _pack_sgerror(SensorGraphError.STREAMER_HAS_NO_NEW_DATA)

        self._logger.debug("calling mark_streamer on streamer %d from trigger_streamer RPC", index)
        self.graph.mark_streamer(index)

        self.process_streamers()

        return Error.NO_ERROR

    def count_nodes(self):
        """Count the number of nodes."""

        return len(self.graph.nodes)

    def persist(self):
        """Trigger saving the current sensorgraph to persistent storage."""

        self.persisted_nodes = self.graph.dump_nodes()
        self.persisted_streamers = self.graph.dump_streamers()
        self.persisted_exists = True
        self.persisted_constants = self._sensor_log.dump_constants()

    def reset(self):
        """Clear the sensorgraph from RAM and flash."""

        self.persisted_exists = False
        self.persisted_nodes = []
        self.persisted_streamers = []
        self.persisted_constants = []
        self.graph.clear()

        self.streamer_status = {}

    def add_node(self, binary_descriptor):
        """Add a node to the sensor_graph using a binary node descriptor.

        Args:
            binary_descriptor (bytes): An encoded binary node descriptor.

        Returns:
            int: A packed error code.
        """

        try:
            node_string = parse_binary_descriptor(binary_descriptor)
        except:
            self._logger.exception("Error parsing binary node descriptor: %s", binary_descriptor)
            return _pack_sgerror(SensorGraphError.INVALID_NODE_STREAM)  # FIXME: Actually provide the correct error codes here

        try:
            self.graph.add_node(node_string)
        except NodeConnectionError:
            return _pack_sgerror(SensorGraphError.STREAM_NOT_IN_USE)
        except ProcessingFunctionError:
            return _pack_sgerror(SensorGraphError.INVALID_PROCESSING_FUNCTION)
        except ResourceUsageError:
            return _pack_sgerror(SensorGraphError.NO_NODE_SPACE_AVAILABLE)

        return Error.NO_ERROR

    def add_streamer(self, binary_descriptor):
        """Add a streamer to the sensor_graph using a binary streamer descriptor.

        Args:
            binary_descriptor (bytes): An encoded binary streamer descriptor.

        Returns:
            int: A packed error code
        """

        streamer = streamer_descriptor.parse_binary_descriptor(binary_descriptor)

        try:
            self.graph.add_streamer(streamer)
            self.streamer_status[len(self.graph.streamers) - 1] = StreamerStatus()

            return Error.NO_ERROR
        except ResourceUsageError:
            return _pack_sgerror(SensorGraphError.NO_MORE_STREAMER_RESOURCES)

    def inspect_streamer(self, index):
        """Inspect the streamer at the given index."""

        if index >= len(self.graph.streamers):
            return [_pack_sgerror(SensorGraphError.STREAMER_NOT_ALLOCATED), b'\0'*14]

        return [Error.NO_ERROR, streamer_descriptor.create_binary_descriptor(self.graph.streamers[index])]

    def inspect_node(self, index):
        """Inspect the graph node at the given index."""

        if index >= len(self.graph.nodes):
            raise RPCErrorCode(6)  #FIXME: use actual error code here for UNKNOWN_ERROR status

        return create_binary_descriptor(str(self.graph.nodes[index]))

    def query_streamer(self, index):
        """Query the status of the streamer at the given index."""

        if index >= len(self.graph.streamers):
            return None

        info = self.streamer_status[index]
        highest_ack = self.streamer_acks.get(index, 0)

        return [info.last_attempt_time, info.last_success_time, info.last_error, highest_ack, info.last_status, info.attempt_number, info.comm_status]