Пример #1
0
class MsgFileWriter(object):
    '''
    classdocs
    '''
    def __init__(self, topic_to_follow):
        '''
        Constructor
        '''
        target_file = '/tmp/msgFile.txt'
        self.bus = BusAdapter()

        # Make sure to start with a new file
        try:
            os.remove(target_file)
        except:
            pass

        self.fd = open(target_file, 'a')
        self.bus.subscribeToTopic(topic_to_follow,
                                  functools.partial(self.handle_bus_msg))

    def handle_bus_msg(self, bus_msg):
        self.fd.write(bus_msg.content + '\n')

    def shutdown(self):
        self.bus.close()
Пример #2
0
class ReceptionTester(threading.Thread):
    '''
    Thread that receives messages, and asserts
    that the received values are what was passed
    into the thread's init method. Keeps listening
    till stop() is called.
    '''
    def __init__(self,
                 msgMd5=None,
                 beSynchronous=False,
                 topic_to_wait_on=RedisPerformanceTester.PUBLISH_TOPIC):
        threading.Thread.__init__(self, name='PerfTestReceptor')
        self.daemon = True

        self.rxed_count = 0
        self.beSynchronous = beSynchronous

        self.testBus = BusAdapter()

        # Subscribe, and ensure that context is delivered
        # with each message:
        self.testBus.subscribeToTopic(topic_to_wait_on,
                                      functools.partial(self.messageReceiver),
                                      context=msgMd5)
        self.interruptEvent = threading.Event()
        self.done = False

    def messageReceiver(self, busMsg, context=None):
        '''
        Method that is called with each received message.
        
        :param busMsg: bus message object
        :type busMsg: BusMessage
        :param context: context Python structure, if subscribeToTopic() was
            called with one.
        :type context: <any>
        '''
        if context is not None:
            inMd5 = hashlib.md5(str(busMsg.content)).hexdigest()
            # Check that the context was delivered:
            if inMd5 != context:
                raise ValueError("md5 in msg should be %s, but was %s" %
                                 (inMd5, context))

        self.rxed_count += 1
        if self.rxed_count % 1000 == 0:
            print(self.rxed_count)

        if self.beSynchronous:
            # Publish a response:
            self.testBus.publish(self.testBus.makeResponseMsg(busMsg),
                                 busMsg.content)

    def stop(self):
        self.interruptEvent.set()

    def run(self):
        self.interruptEvent.wait()
        self.testBus.unsubscribeFromTopic(RedisPerformanceTester.PUBLISH_TOPIC)
        self.testBus.close()
Пример #3
0
class ReceptionTester(threading.Thread):
    '''
    Thread that receives messages, and asserts
    that the received values are what was passed
    into the thread's init method. Keeps listening
    till stop() is called.
    '''
    def __init__(self,
                 msgMd5=None,
                 beSynchronous=False,
                 topic_to_wait_on='test'):
        threading.Thread.__init__(self, name='PerfTestReceptor')
        self.setDaemon(True)

        self.beSynchronous = beSynchronous
        self.topic_to_wait_on = topic_to_wait_on

        self.testBus = BusAdapter()

        # Subscribe, and ensure that context is delivered
        # with each message:
        self.testBus.subscribeToTopic(topic_to_wait_on,
                                      functools.partial(self.messageReceiver),
                                      context=msgMd5)
        self.interruptEvent = threading.Event()
        self.done = False

    def messageReceiver(self, busMsg, context=None):
        '''
        Method that is called with each received message.
        
        :param busMsg: bus message object
        :type busMsg: BusMessage
        :param context: context Python structure, if subscribeToTopic() was
            called with one.
        :type context: <any>
        '''

        #         inMd5 = hashlib.md5(str(busMsg.content)).hexdigest()
        #         # Check that the context was delivered:
        #         if inMd5 != context:
        #             raise ValueError("md5 in msg should be %s, but was %s" % (inMd5, context))

        if self.beSynchronous:
            # Publish a response:
            self.testBus.publish(
                self.testBus.makeResponseMsg(busMsg, busMsg.content))

    def stop(self, signum=None, frame=None):
        #**********
        print('Cntr-C called')
        #**********
        self.interruptEvent.set()

    def run(self):
        print("Sync-call test server started; listening on %s; Cnt-C to quit" %
              self.topic_to_wait_on)
        self.interruptEvent.wait()
        self.testBus.unsubscribeFromTopic('test')
        self.testBus.close()
Пример #4
0
class MsgFileWriter(object):
    '''
    classdocs
    '''


    def __init__(self, topic_to_follow):
        '''
        Constructor
        '''
        target_file = '/tmp/msgFile.txt'
        self.bus = BusAdapter()
        
        # Make sure to start with a new file
        try:
            os.remove(target_file)
        except:
            pass
        
        self.fd  = open(target_file, 'a') 
        self.bus.subscribeToTopic(topic_to_follow, functools.partial(self.handle_bus_msg))
        
    def handle_bus_msg(self, bus_msg):
        self.fd.write(bus_msg.content + '\n')
        
    def shutdown(self):
        self.bus.close()
Пример #5
0
class ReceptionTester(threading.Thread):
    '''
    Thread that receives messages, and asserts
    that the received values are what was passed
    into the thread's init method. Keeps listening
    till stop() is called.
    '''
    
    def __init__(self, msgMd5=None, beSynchronous=False, topic_to_wait_on='test'):
        threading.Thread.__init__(self, name='PerfTestReceptor')
        self.setDaemon(True)
        
        self.beSynchronous = beSynchronous
        self.topic_to_wait_on = topic_to_wait_on
        
        self.testBus = BusAdapter()
        
        # Subscribe, and ensure that context is delivered
        # with each message:
        self.testBus.subscribeToTopic(topic_to_wait_on, 
                                      functools.partial(self.messageReceiver), 
                                      context=msgMd5)
        self.interruptEvent = threading.Event()
        self.done = False
                
    def messageReceiver(self, busMsg, context=None):
        '''
        Method that is called with each received message.
        
        :param busMsg: bus message object
        :type busMsg: BusMessage
        :param context: context Python structure, if subscribeToTopic() was
            called with one.
        :type context: <any>
        '''
        
#         inMd5 = hashlib.md5(str(busMsg.content)).hexdigest()
#         # Check that the context was delivered:
#         if inMd5 != context:
#             raise ValueError("md5 in msg should be %s, but was %s" % (inMd5, context))
                
        if self.beSynchronous:
            # Publish a response:
            self.testBus.publish(self.testBus.makeResponseMsg(busMsg, busMsg.content))

    
    def stop(self, signum=None, frame=None):
        #**********
        print('Cntr-C called')
        #**********
        self.interruptEvent.set()
            
    def run(self):
        print("Sync-call test server started; listening on %s; Cnt-C to quit" % self.topic_to_wait_on)
        self.interruptEvent.wait()
        self.testBus.unsubscribeFromTopic('test')
        self.testBus.close()
Пример #6
0
class ReceptionTester(threading.Thread):
    '''
    Thread that receives messages, and asserts
    that the received values are what was passed
    into the thread's init method. Keeps listening
    till stop() is called.
    '''
    
    def __init__(self, msgMd5=None, beSynchronous=False, topic_to_wait_on=RedisPerformanceTester.PUBLISH_TOPIC):
        threading.Thread.__init__(self, name='PerfTestReceptor')
        self.daemon = True
       
        self.rxed_count = 0
        self.beSynchronous = beSynchronous
        
        self.testBus = BusAdapter()
        
        # Subscribe, and ensure that context is delivered
        # with each message:
        self.testBus.subscribeToTopic(topic_to_wait_on, 
                                      functools.partial(self.messageReceiver), 
                                      context=msgMd5)
        self.interruptEvent = threading.Event()
        self.done = False
                
    def messageReceiver(self, busMsg, context=None):
        '''
        Method that is called with each received message.
        
        :param busMsg: bus message object
        :type busMsg: BusMessage
        :param context: context Python structure, if subscribeToTopic() was
            called with one.
        :type context: <any>
        '''
        if context is not None:
            inMd5 = hashlib.md5(str(busMsg.content)).hexdigest()
            # Check that the context was delivered:
            if inMd5 != context:
                raise ValueError("md5 in msg should be %s, but was %s" % (inMd5, context))

        self.rxed_count += 1
        if self.rxed_count % 1000 == 0:
            print(self.rxed_count)
        
        if self.beSynchronous:
            # Publish a response:
            self.testBus.publish(self.testBus.makeResponseMsg(busMsg), busMsg.content)

    
    def stop(self):
        self.interruptEvent.set()
            
    def run(self):
        self.interruptEvent.wait()
        self.testBus.unsubscribeFromTopic(RedisPerformanceTester.PUBLISH_TOPIC)
        self.testBus.close()
Пример #7
0
class ReceptionTester(threading.Thread):
    '''
    Thread that receives messages, and asserts
    that the received values are what was passed
    into the thread's init method. Keeps listening
    till stop() is called.
    '''
    def __init__(self, correctValue=None, beSynchronous=False):
        threading.Thread.__init__(self, name='ReceptionTesterThread')
        self.correctValue = correctValue
        self.beSynchronous = beSynchronous

        self.testBus = BusAdapter()

        # Subscribe, and ensure that context is delivered
        # with each message:
        self.testBus.subscribeToTopic('myTopic',
                                      deliveryCallback=functools.partial(
                                          self.messageReceiver),
                                      context={
                                          'foo': 10,
                                          'bar': 'my string'
                                      })
        self.interruptEvent = threading.Event()
        self.done = False

    def messageReceiver(self, busMsg, context=None):
        '''
        Method that is called with each received message.
        
        :param busMsg: bus message object
        :type busMsg: BusMessage
        :param context: context Python structure, if subscribeToTopic() was
            called with one.
        :type context: <any>
        '''

        # Check that the context was delivered:
        assert (context['foo'] == 10)
        assert (context['bar'] == 'my string')

        if self.beSynchronous:
            # Publish a response: doubling the int-ified content
            # of the incoming msg:
            response = int(busMsg.content) * 2
            self.testBus.publish(self.testBus.makeResponseMsg(
                busMsg, response))
        else:
            assert (busMsg.content == self.correctValue)

    def stop(self):
        self.interruptEvent.set()

    def run(self):
        self.interruptEvent.wait()
        self.testBus.close()
Пример #8
0
class ReceptionTester(threading.Thread):
    '''
    Thread that receives messages, and asserts
    that the received values are what was passed
    into the thread's init method. Keeps listening
    till stop() is called.
    '''
    
    def __init__(self, correctValue=None, beSynchronous=False):
        threading.Thread.__init__(self, name='ReceptionTesterThread')
        self.correctValue = correctValue
        self.beSynchronous = beSynchronous
        
        self.testBus = BusAdapter()
        
        # Subscribe, and ensure that context is delivered
        # with each message:
        self.testBus.subscribeToTopic('myTopic', 
                                      deliveryCallback=functools.partial(self.messageReceiver), 
                                      context={'foo' : 10, 'bar' : 'my string'})
        self.interruptEvent = threading.Event()
        self.done = False
        
    def messageReceiver(self, busMsg, context=None):
        '''
        Method that is called with each received message.
        
        :param busMsg: bus message object
        :type busMsg: BusMessage
        :param context: context Python structure, if subscribeToTopic() was
            called with one.
        :type context: <any>
        '''
        
        # Check that the context was delivered:
        assert(context['foo'] == 10)
        assert(context['bar'] == 'my string')
        
        if self.beSynchronous:
            # Publish a response: doubling the int-ified content
            # of the incoming msg:
            response = int(busMsg.content) * 2
            self.testBus.publish(self.testBus.makeResponseMsg(busMsg, response))
        else:
            assert(busMsg.content == self.correctValue)

    
    def stop(self):
        self.interruptEvent.set()
            
    def run(self):
        self.interruptEvent.wait()
        self.testBus.close()
Пример #9
0
class MsgDeliveryTest(unittest.TestCase):
    def setUp(self):
        self.msg_server = OnDemandPublisher()
        self.msg_server.start()
        self.deliveryDest = functools.partial(self._deliveryDest)
        self.bus = BusAdapter()
        self.delivery_event = threading.Event()

        # Content and topic for outgoing msgs sent
        # via the OnDemandPublisher instance:

        self.reference_msg_content = 'rTmzntNSQLmXokesBpLmAbPYeysftXnuntfdPKrxMVNUuqVFHFzfcrrSaRssdHuMRhPYjXKYrJjwKcyeYycEzQSkJubTabeSFLRS'
        self.reference_named_topic = 'MyTopic'
        self.reference_pattern_topic = r'MyTopic*'
        self.reference_pattern_obj = re.compile(self.reference_pattern_topic)
        self.reference_pattern_matching_topic = 'MyTopicHurray'
        self.reference_context = 'myContext'

        # Msg with a non-pattern, i.e. fixed-name topic:
        self.reference_bus_msg = BusMessage(
            topicName=self.reference_named_topic,
            content=self.reference_msg_content,
            context=self.reference_context)

        # Msg with a topic that will match self.reference_pattern_topic:
        self.reference_bus_wildcard_msg = BusMessage(
            topicName=self.reference_pattern_matching_topic,
            content=self.reference_msg_content,
            context=self.reference_context)

    def tearDown(self):
        self.msg_server.stop()
        self.msg_server.join()
        self.bus.close()
        self.delivery_event.clear()

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testDeliveryNameNotThreaded(self):
        self.bus.subscribeToTopic(self.reference_named_topic,
                                  self.deliveryDest,
                                  threaded=False,
                                  context=self.reference_context)

        self.delivery_event.clear()
        self.msg_server.sendMessage(self.reference_bus_msg)

        # Wait for message to arrive:
        self.delivery_event.wait()
        # Compare our expected incoming bus message with
        # what we actually got:
        self.assertBusMsgsEqual(self.reference_bus_msg, self.received_bus_msg)

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testDeliveryNameThreaded(self):
        self.bus.subscribeToTopic(self.reference_named_topic,
                                  self.deliveryDest,
                                  threaded=True,
                                  context=self.reference_context)
        self.delivery_event.clear()
        self.msg_server.sendMessage(self.reference_bus_msg)

        # Wait for message to arrive:
        self.delivery_event.wait()

        # Compare our expected incoming bus message with
        # what we actually got:
        self.assertBusMsgsEqual(self.reference_bus_msg, self.received_bus_msg)

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testDeliveryPatternNotThreaded(self):
        # Subscribe to a pattern topic, using an re.Pattern instance:
        self.bus.subscribeToTopic(self.reference_pattern_obj,
                                  self.deliveryDest,
                                  threaded=False,
                                  context=self.reference_context)

        self.delivery_event.clear()
        # Send to a topic that fits the reference pattern ('MyTopic*'):
        self.msg_server.sendMessage(self.reference_bus_wildcard_msg)

        self.delivery_event.wait()

        self.assertBusMsgsEqual(self.reference_bus_wildcard_msg,
                                self.received_bus_msg)

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testDeliveryPatternThreaded(self):
        # Subscribe to a pattern topic, using an re.Pattern instance:
        self.bus.subscribeToTopic(self.reference_pattern_obj,
                                  self.deliveryDest,
                                  threaded=True,
                                  context=self.reference_context)

        self.delivery_event.clear()
        # Send to a topic that fits the reference pattern ('MyTopic*'):
        self.msg_server.sendMessage(self.reference_bus_wildcard_msg)

        self.delivery_event.wait()

        self.assertBusMsgsEqual(self.reference_bus_wildcard_msg,
                                self.received_bus_msg)

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testSyncPublish(self):

        echo_msg = self.reference_bus_msg

        # Change target topic to what the test harness echo server
        # listens for:
        echo_msg.topicName = self.msg_server.ECHO_CHANNEL

        result = self.bus.publish(echo_msg, sync=True)
        self.assertEqual(self.reference_msg_content, result)

    # -------------------------  Service Methods --------------

    def assertBusMsgsEqual(self, expected_msg, actual_msg):
        self.assertEqual(expected_msg.topicName, actual_msg.topicName)
        self.assertEqual(expected_msg.context, actual_msg.context)
        self.assertEqual(expected_msg.content, actual_msg.content)

    def _deliveryDest(self, bus_msg):
        '''
        Receives incoming messages, and places them into
        instance variable received_bus_msg.
        
        :param bus_msg:
        :type bus_msg:
        '''
        #print('BusMessage: context is %s' % bus_msg.context)
        self.received_bus_msg = bus_msg
        self.delivery_event.set()
class MessageOutStreamer(threading.Thread):
    '''
    Thread that keeps sending the same message over and over,
    except for changing the time field.
    '''
    
    def __init__(self, busMsg=None):
        super(MessageOutStreamer, self).__init__()
        
        self.daemon = True
        self.streamBus = BusAdapter()
        self.busMsg = BusMessage() if busMsg is None else busMsg
    
        self.done = False
        self._paused = False
        self._stream_interval = STREAM_INTERVAL
        
    def pause(self, do_pause):
        if do_pause:
            self._paused = True
        else:
            self._paused = False

    @property
    def paused(self):
        return self._paused
    
    @property
    def stream_interval(self):
        return self._stream_interval
    
    @stream_interval.setter
    def stream_interval(self, new_val):
        '''
        Note: we trust that caller ensures new_val to 
        be a non-negative float. 
        
        :param new_val: number of (fractional) seconds to wait between
            stream messages. Zero means no wait.
        :type new_val:float
        '''
        self._stream_interval = new_val
    
    def change_stream_topic(self, newTopic):
        self.busMsg.topicName = newTopic
    
    def change_stream_content(self, newContent):
        self.busMsg.content = newContent

    def run(self):
        '''
        Keep publishing one message over and over,
        
        :param msgLen: length of payload
        :type msgLen: int
        '''
        while not self.done:
            self.busMsg.time = time.time()
            self.streamBus.publish(self.busMsg)
            while self._paused and not self.done:
                time.sleep(1)
            if self._stream_interval > 0:
                time.sleep(self._stream_interval)
            
    def stop(self, signum, frame):
        self.done = True
        self.streamBus.close()
class OnDemandPublisher(threading.Thread):
    '''
    Server for testing Redis bus modules. Started from the
    command line, or imported into applications. Serves multiple
    functions, individually or together:
    
        1. echoes messages it receives on topic ECHO_TOPIC.
        2. listens to messages without doing anything
        3. sends a continuous stream of messages
        4. check the syntax of an incoming msg, returning the result
        5. send one message on demand.

	USAGE: test_harness_server.py [-h] [-e] [-s [topic [content ...]]] [-c]
	                              [-l topic [topic ...]]
	                              [-o [topic [content ...]]]
	
	optional arguments:
	  -h, --help            show this help message and exit
	  -e, --echo            Echo messages arriving on topic 'echo' as sychronous replies.
	  -s [topic [content ...]], --streamMsgs [topic [content ...]]
	                        Send the same bus message over and over. Topic, or both,
	                        topic and content may be provided. If content is omitted,
	                        a random string of 100 characters will be used.
	                        If topic is omitted as well, messages will be streamed to 'test'
	  -c, --checkSyntax     Check syntax of messages arriving on topic 'bus_syntax'; 
	                        synchronously return result report.
	  -l topic [topic ...], --listenOn topic [topic ...]
	                        Subscribe to given topic(s), and throw the messages away
	  -o [topic [content ...]], --oneshot [topic [content ...]]
	                        Given a topic and a content string, send a bus message. Topic, or both 
	                        topic and content may be provided. If content is omitted,
	                        a random string of 100 characters will be used.
	                        If topic is omitted as well, messages will be streamed to 'test'
	    

    The thread always listens on the ECHO_TOPIC. But
    received messages are only **echoed** if the service was
    started with the 'echo' option
    
    The echo function acts like a synch-call service
    should: echoing on the response topic, which it
    derives from the msg ID. The content will be the
    content of the incoming message, the timestamp
    will be the receipt time.
    
    When echoing, the service keeps track of how many messages 
    it has echoed. But after not receiving any messages for 
    OnDemandPublisher.MAX_IDLE_TIME seconds, the
    current count is printed, and the counter is 
    reset to zero. After every 1000 messages, the total
    number echoed is printed to the console. Every 10,000
    messages, the message/sec rate is printed as well.
    
    When asked to **listen** to some topic, messages on that
    topic are received and counted. Then the message is
    placed into a queue where it can be picked up (self.bus_msg_queue). 
    If nobody attends to the queue, it fills up, and then sits there;
    i.e. no big harm. Statistics printing is as for echoing.
    Stats are placed on the bus_stats_queue for consumption by anyone
    interested (or not).
    
    Messages sent in a **stream** by sendMessageStream()
    contain their content's MD5 in the context field. This constant
    message sending is initiated only if the service is
    started with the 'send_stream' option.

    When asked to check message **syntax**, the service listens
    on SYNTAX_TOPIC, and synchronously returns a string 
    reporting on violations of SchoolBus specifications in 
    the incoming message.
    
    A **oneshot message** is sent if the server is started
    with the --oneshot option. If topic and/or content are provided,
    the message is loaded with the given content and published to
    the given topic. Defaults are to fill the content with a random
    string, and to publish to STREAM_TOPIC.
    
    '''
    
    # ----------------------------  Constants ----------------

    # Maximum time for no message to arrive before
    # starting over counting messages:
    MAX_IDLE_TIME = 5
    
    FIND_COMMA_PATTERN = re.compile(r'[,]+')
    FIND_SPACE_PATTERN = re.compile(r'[ ]+')
    
    LOG_LEVEL_NONE  = 0
    LOG_LEVEL_ERR   = 1
    LOG_LEVEL_INFO  = 2
    LOG_LEVEL_DEBUG = 3
    
    def __init__(self, serveEchos=True, 
                 listenOn=None, 
                 streamMsgs=False, 
                 checkSyntax=True,
                 oneShotMsg=None):
        '''
        Initialize the test harness server. 
        
        :param serveEchos: if True receives messages on ECHO_TOPIC, and returns  
            message with the same content, but different timestamp as a synchronous call. 
        :type serveEchos: bool
        :param listenOn: if this parameter provides an array of topic(s), messages on these
            topics are received and counted, then placed on self.bus_msg_queue. Intermittent reception counts
            and reception rates are printed to the console, and place on self.bus_stats_queue.
        :type listenOn: {None | [string]}
        :param streamMsgs: if True, a continuous stream of messages are sent to
            STREAM_TOPIC. The timestamp is changed each time.
        :type streamMsgs: bool
        :param checkSyntax: if true, listens on SYNTAX_TOPIC, checks incoming messages
            for conformance to SchoolBus message format, and synchronously returns a result.
        :type checkSyntax: bool
        :param oneShotMsg: if non-none this parameter is expected to be a two-tuple
            containing a topic and a content. Either may be None. If topic is None,
            the message is sent to STREAM_TOPIC. If content is None, content will be
            a random string of length STANDARD_MSG_LENGTH.
        :type oneShotMsg: { None | ({string | None}, {string | None})}
        '''
        threading.Thread.__init__(self)
        
        self.loglevel = OnDemandPublisher.LOG_LEVEL_DEBUG
        #self.loglevel = OnDemandPublisher.LOG_LEVEL_INFO
        #self.loglevel = OnDemandPublisher.LOG_LEVEL_NONE
        
        #***********
#         print ('serveEchos %s' % serveEchos)
#         print ('listenOn %s' % listenOn)
#         print ('streamMsgs %s' % streamMsgs)
#         print('checkSyntax %s' % checkSyntax)
#         print('oneShotMsg %s' % str(oneShotMsg))
#         sys.exit()
        #***********
        
        self._serveEchos = serveEchos
        
        self.daemon = True
        self.testBus = BusAdapter()
        self.done = False
        # No topics yet that we are to listen to, but drop msgs for:
        self.topics_to_rx = []
        
        self.echo_topic = ECHO_TOPIC
        self.syntax_check_topic = SYNTAX_TOPIC
        self.standard_msg_len = STANDARD_MSG_LENGTH
        self._stream_interval = STREAM_INTERVAL
        
        self._stream_topic = STREAM_TOPIC
        self.standard_bus_msg = self.createMessage(STREAM_TOPIC, STANDARD_MSG_LENGTH, content=None)
        
        self.one_shot_topic = STREAM_TOPIC
        self.one_shot_content = self.standard_bus_msg
        
        # Queue into which incoming messages are fed
        # for consumers to communicated to clients, such
        # as a Web UI:
        self.bus_msg_queue   = Queue.Queue(MAX_SAVED_MSGS)
        
        # Queue to which aggregate stats will be written:  
        self.bus_stats_queue = Queue.Queue(MAX_SAVED_MSGS) 

        # Create a 'standard' message to send out
        # when asked to via SIGUSR1 or the Web:

        if oneShotMsg is not None:
            # The oneShotMsg arg may have one of the following values: 
            #   - None if oneshot was not requested. 
            #   - An empty array if -o (or --oneshot) 
            #        was listed in the command line, 
            #        but neither topic nor message content
            #        were specified.
            #   - A one-element array if the topic was
            #        specified
            #   - A two-element array if both topic and
            #        content were specified.

            (one_shot_topic, self.one_shot_content) = oneShotMsg
            if one_shot_topic is not None:
                self.one_shot_topic = one_shot_topic
            
            # Create a message that one can have sent on demand
            # by sending a SIGUSR1 to the server, or using
            # the Web UI:

            self.standard_oneshot_msg = self.createMessage(self.one_shot_topic, content=self.one_shot_content)
            # Send the standard msg:
            self.sendMessage(self.standard_oneshot_msg)
        else:
            # Create a standard message with default topic/content:
            self.standard_oneshot_msg = self.createMessage()
       
        # Start time of a 10,000 messages batch:
        self.batch_start_time = time.time()        
        
        # Subscribe, and ensure that context is delivered
        # with each message:

        if serveEchos:
            self.serve_echo = True
        
        if listenOn is not None:
            # Array of topics to listen to 
            # If we are to listen to some (additional) topic(s),
            # on which no action is taken, subscribe to it/them now:
            self['topicsToRx'] = listenOn

        if checkSyntax:
            self.check_syntax = True
            
        self.interruptEvent = threading.Event()
        
        # Number of messages received and echoed:
        self.numEchoed = 0
        # Alternative to above when not echoing: Number of msgs just received:
        self.numReceived = 0
        self.mostRecentRxTime = None
        self.printedResetting = False
        
        # Not asked to send a message yet.
        self.sendMsg = False
        
        if streamMsgs is not None:
            # The streamMsgs arg may have one of the following values: 
            #   - None if streaming messages was not requested. 
            #   - An empty array if -s (or --streamMsgs) 
            #        was listed in the command line, 
            #        but neither topic nor message content
            #        were specified.
            #   - A one-element array if the topic was
            #        specified
            #   - A two-element array if both topic and
            #        content were specified.

            self.msg_streamer = MessageOutStreamer(self.standard_bus_msg)
            
            # The caller passed in a tuple with
            # topic and content for streaming. If they
            # are non-None, update the streamer:
            
            (stream_topic, stream_content) = streamMsgs
            if stream_topic is not None:
                self.stream_topic = stream_topic
            if stream_content is not None:
                self.stream_content = stream_content

            # Start streaming:
            
            self.msg_streamer.pause(True)
            self.msg_streamer.start()
            
        else:
            # Create a standard message with default topic/content:
            self.standard_stream_msg = self.createMessage()
            self.msg_streamer = None

    @property
    def running(self):
        return not self.done

    @property
    def echo_topic(self):
        return self._echo_topic
    
    @echo_topic.setter
    def echo_topic(self, new_echo_topic):
        if new_echo_topic is None or len(new_echo_topic) == 0:
            # Set to default echo topic:
            new_echo_topic = ECHO_TOPIC 
        self._echo_topic = new_echo_topic

    @property
    def serve_echo(self):
        return self._serveEchos

    @serve_echo.setter
    def serve_echo(self, do_serve):
        if do_serve:
            # Have incoming messages delivered to messageReceiver() not
            # via a queue, but by direct call from the library:
            self.testBus.subscribeToTopic(self.echo_topic, functools.partial(self.messageReceiver), threaded=False)
            self._serveEchos = True
        else:
            self.testBus.unsubscribeFromTopic(self.echo_topic)
            self._serveEchos = False

    @property
    def stream_topic(self):
        return self.msg_streamer.busMsg.topicName
    
    @stream_topic.setter
    def stream_topic(self, new_stream_topic):
        if new_stream_topic is None or len(new_stream_topic) == 0:
            # Set to default stream topic:
            new_stream_topic = STREAM_TOPIC 
        self.msg_streamer.change_stream_topic(new_stream_topic)

    @property
    def stream_content(self):
        return self.msg_streamer.busMsg.content
    
    @stream_content.setter
    def stream_content(self, new_stream_content):
        if new_stream_content is None or len(new_stream_content) == 0:
            self.msg_streamer.change_stream_content(self.createRandomStr(self.standard_msg_len))
        else:
            self.msg_streamer.change_stream_content(new_stream_content)

    @property
    def streaming(self):
        return not self.msg_streamer.paused
    
    @streaming.setter
    def streaming(self, should_stream):
        should_pause = not should_stream
        self.msg_streamer.pause(should_pause)

    @property
    def stream_interval(self):
        return self._stream_interval
    
    @stream_interval.setter
    def stream_interval(self, new_interval):
        # If the new value is not a number or empty str, set to default:
        if type(new_interval) == str  and len(new_interval) == 0:
            new_interval = STREAM_INTERVAL
        else:
            # Non-empty string or number; 
            # Ensure float, and replace negative
            # values with 0:
            try:
                if float(new_interval) < 0:
                    new_interval = 0.0
            except ValueError:
                raise ValueError("Attempt to set streaming interval to a non-numeric quantity: '%s'" % str(new_interval))
        
        self.msg_streamer.stream_interval = new_interval
        self._stream_interval = new_interval


    @property
    def one_shot_topic(self):
        return self._one_shot_topic
    
    @one_shot_topic.setter
    def one_shot_topic(self, new_one_shot_topic):
        if new_one_shot_topic is None or len(new_one_shot_topic) == 0:
            # Set to default oneshot topic:
            new_one_shot_topic = STREAM_TOPIC
        else:
            self._one_shot_topic = new_one_shot_topic
        
    @property
    def one_shot_content(self):
        return self._one_shot_content
    
    @one_shot_content.setter
    def one_shot_content(self, new_one_shot_content):
        if new_one_shot_content is None or len(new_one_shot_content) == 0:
            self._one_shot_content = self.createRandomStr(self.standard_msg_len)
        else:
            self._one_shot_content = new_one_shot_content


    @property
    def syntax_check_topic(self):
        return self._syntax_check_topic
    
    @syntax_check_topic.setter
    def syntax_check_topic(self, new_syntax_check_topic):
        if new_syntax_check_topic is None or len(new_syntax_check_topic) == 0:
            # Set to default syntax check topic:
            new_syntax_check_topic = SYNTAX_TOPIC
        self._syntax_check_topic = new_syntax_check_topic

    @property
    def check_syntax(self):
        return self._checkSyntax

    @check_syntax.setter
    def check_syntax(self, do_check):
        if do_check:
            self.testBus.subscribeToTopic(self.syntax_check_topic, functools.partial(self.syntaxCheckReceiver))
            self._checkSyntax = True
        else:
            self.testBus.unsubscribeFromTopic(self.syntax_check_topic)
            self._checkSyntax = False

    @property
    def standard_msg_len(self):
        return self._standard_msg_len
    
    @standard_msg_len.setter
    def standard_msg_len(self, new_standard_msg_len):
        if new_standard_msg_len == 0 or \
           new_standard_msg_len is None:
            new_standard_msg_len = STANDARD_MSG_LENGTH
        try:
            new_standard_msg_len = int(new_standard_msg_len)
        except:
            new_standard_msg_len = STANDARD_MSG_LENGTH
            
        self._standard_msg_len = new_standard_msg_len
    
    def __getitem__(self, item):
        if item == 'echoTopic':
            return self.echo_topic

        elif item == 'echo':
            return self.testBus.subscribedTo(self['echoTopic'])
        
        elif item == 'streamTopic':
            return self.stream_topic
        elif item == 'streamContent':
            return self.stream_content
        elif item == 'streaming':
            return self.streaming
        elif item == 'streamInterval':
            return self.stream_interval
        
        
        elif item == 'oneShotTopic':
            return self.one_shot_topic
        elif item == 'oneShotContent':
            if isinstance(self.one_shot_content, BusMessage):
                return self.one_shot_content.content
            else:
                return self.one_shot_content
        
        elif item == 'strLen':
            return self.standard_msg_len
        
        elif item == 'syntaxTopic':
            return self.syntax_check_topic
        
        elif item == 'chkSyntax':
            return self.testBus.subscribedTo(self['syntaxTopic'])

        
        elif item == 'topicsToRx':
            return self.topics_to_rx
        
        else:
            raise KeyError('Key %s is not in schoolbus tester' % item)
    
    def __setitem__(self, item, new_val):
                
        if item == 'echoTopic':
            self.echo_topic = new_val
            
        elif item == 'echo':
            # Deal with both string and bool:
            new_val = (new_val == 'True' or new_val == True)
            self.serve_echo = new_val

        elif item == 'streamTopic':
            self.stream_topic = new_val
        elif item == 'streamContent':
            self.stream_content = new_val
        elif item == 'streaming':
            # Deal with both string and bool:
            new_val = (new_val == 'True' or new_val == True)
            self.streaming = new_val
        elif item == 'streamInterval':
            self.stream_interval = new_val
            
            
        elif item == 'oneShotContent':
            self.one_shot_content = new_val
        
        elif item == 'oneShotTopic':
            self.one_shot_topic = new_val
        elif item == 'oneShotContent':
            self.one_shot_content = new_val
            
        elif item == 'strLen':
            self.standard_msg_len = new_val
            
        elif item == 'syntaxTopic':
            self.syntax_check_topic = new_val
            
        elif item == 'chkSyntax':
            # Deal with both string and bool:
            new_val = (new_val == 'True' or new_val == True)
            self.check_syntax = new_val
            
        elif item == 'topicsToRx':
            # Topics to which we should listen, but
            # whose msgs we are to receive. Check
            # whether empty str or empty array, which
            # means unsubscribe from all topics-to-rx
            # (though not from syntax checker and echo):
            if ((type(new_val) == str and len(new_val) == 0)) or\
                (type(new_val) == list and len(new_val) == 1 and len(new_val[0]) == 0):
                for (indx, topic) in enumerate(self.topics_to_rx):
                    self.unsubscribeFromTopicOrPattern(topic)
                    del self.topics_to_rx[indx]
                return
            # We are given one or more topics. Tolerate
            # comma-separated, space-separated strings, 
            # and also arrays:
            if type(new_val) != list:
                # comma-separated string of topics?
                if OnDemandPublisher.FIND_COMMA_PATTERN.search(new_val) is not None:
                    # Yes, commas found: remove all spaces that might be around the commas:
                    new_val = OnDemandPublisher.FIND_SPACE_PATTERN.sub('', new_val)
                    new_val = new_val.split(',')
                # Else must be space-separated string of topics or just a single word:
                else:
                    # Replace all multi-space areas with single spaces; the strip()
                    # eliminates spaces at start and end:
                    new_val = OnDemandPublisher.FIND_SPACE_PATTERN.sub(' ', new_val).strip()
                    # Get an array of topic strings (works even with singleton topic):
                    new_val = new_val.split(' ')

            # Unsubscribe from all topics that are *not*
            # in the new list of topics:
            for (topic_pos, curr_topic) in enumerate(self.topics_to_rx):
                if curr_topic not in new_val:
                    self.topics_to_rx.pop(topic_pos)
                    self.unsubscribeFromTopicOrPattern(curr_topic)
            
            # Subscribe to any topics in the new-list that
            # we are not already subscribed to:  
            for topic in new_val:
                # Disallow use of reserved topics self.echo_topic
                # and self.syntax_check_topic:
                if topic in (self.echo_topic, self.syntax_check_topic):
                    raise ValueError("Warning: '%s' and '%s' are reserved topic names." % (self.echo_topic, self.syntax_check_topic))
                
                try:
                    # Already subscribed to it?    
                    self.topics_to_rx.index(topic)
                    # At least by our bookkeeping, yes 
                    # (else we would have had the exception.
                    # Ensure we really are:
                    if not self.testBus.subscribedTo(topic):
                        self.subscribeTopicOrPattern(topic)
                    continue
                except ValueError:
                    pass
                # New topic:
                self.subscribeTopicOrPattern(topic)
                self.topics_to_rx.append(topic)
                
        else:
            raise KeyError('Key %s is not in schoolbus tester' % item)
        return new_val

    def subscribeTopicOrPattern(self, topic_or_pattern):
        # If the topic contains an asterisk, then
        # build a regex pattern, rather than passing
        # the topic itself:
        try:
            topic_or_pattern.index('*')
            # Topic is a pattern of topics:
            topic = re.compile(topic_or_pattern)
        except ValueError:
            # Not a pattern, just a regular topic name:
            topic = topic_or_pattern
        self.testBus.subscribeToTopic(topic, functools.partial(self.messageReceiver))

    def unsubscribeFromTopicOrPattern(self, topic_or_pattern):
        # If the topic contains an asterisk, then
        # build a regex pattern, rather than passing
        # the topic itself:
        try:
            topic_or_pattern.index('*')
            # Topic is a pattern of topics:
            topic = re.compile(topic_or_pattern)
        except ValueError:
            # Not a pattern, just a regular topic name:
            topic = topic_or_pattern
        self.testBus.unsubscribeFromTopic(topic)

    
    def createMessage(self, topic=None, msgLen=None, content=None):
        '''
        Returns a BusMessage whose content is the given length,
        and whose topicName is the given topic.
        
        :param topic: topic for which this message is destined.
        :type topic: string
        :param msgLen: desired str length
        :type msgLen: int
        :param content: content of the message. If None, a random content
            of length msgLen will be created
        :return: BusMessage object ready to publish. The context field of the
            message instance will be the MD5 of the content.
        :rtype: BusMessage
        '''
    
        if topic is None:
            topic = self._stream_topic
            
        if msgLen is None:
            msgLen = self.standard_msg_len 
    
        if content is None:    
            content = self.createRandomStr(msgLen)
        
        msg = BusMessage(content=content, topicName=topic, context=hashlib.md5(str(content)).hexdigest())

        return msg
        
    def createRandomStr(self, the_len):
        the_len = int(the_len)
        content = bytearray()
        for _ in range(the_len):
            content.append(random.choice(string.letters))
        return str(content)
                
        
    def sendMessage(self, bus_msg=None):
        '''
        Publish the given BusMessage, or
        the standard message:
        '''
        if bus_msg is None:
            bus_msg = self.standard_oneshot_msg
        self.testBus.publish(bus_msg)
    
    def logInfo(self, msg):
        if self.loglevel >= OnDemandPublisher.LOG_LEVEL_INFO:
            print(str(datetime.datetime.now()) + ' info: ' + msg)

    def logErr(self, msg):
        if self.loglevel >= OnDemandPublisher.LOG_LEVEL_ERR:
            print(str(datetime.datetime.now()) + ' error: ' + msg)

    def logDebug(self, msg):
        if self.loglevel >= OnDemandPublisher.LOG_LEVEL_DEBUG:
            print(str(datetime.datetime.now()) + ' debug: ' + msg)
        
        
    def syntaxCheckReceiver(self, busMsg, context=None):
        '''
        Given a BusMessage instance, determine whether it contains 
        all necessary attributes: ID, and time. This method 
        is a callback for topic specified in module variable
        self.syntaxTopic. Constructs a string that lists any
        errors or warnings, and returns that string as a
        synchronous response.
        
        :param busMsg: the message to be evaluated
        :type busMsg: BusMessage
        :param context: not used
        :type context: <any>
        '''
        errors = []
        
        if busMsg.id is None:
            errors.append('Error: No ID field')
        if type(busMsg.time) != int and type(busMsg.time) != float:
            errors.append("Error: Time must be an int, was '%s'" % busMsg.time)
            try:
                busMsg.isoTime
            except (ValueError, TypeError):
                errors.append("Error: Time value not transformable to ISO time '%s'" % busMsg.time)
        try:
            json.loads(busMsg.content)
        except AttributeError:
            errors.append("Warning: no 'content' field.")
        except ValueError:
            errors.append("Warning: 'content' field present, but not valid JSON.")

        if len(errors) == 0:
            response = 'Message OK'
        else:
            response = self.testBus.makeResponseMsg(busMsg, '; '.join(errors))
        
        self.testBus.publish(response)
        
    def messageReceiver(self, busMsg, context=None):
        '''
        Method that is called with each received message.
        
        :param busMsg: bus message object
        :type busMsg: BusMessage
        :param context: context Python structure, if subscribeToTopic() was
            called with one.uu
        :type context: <any>
        '''
        
        self.mostRecentRxTime = time.time()
        self.printedResetting = False
        
        if self.serve_echo and busMsg.topicName == 'echo':
            # Create a message with the same content as the incoming
            # message, timestamp the new message, and publish it
            # on the response topic (i.e. tmp.<msgId>):
            respMsg = self.testBus.makeResponseMsg(busMsg, busMsg.content)

            # Publish a response:
            self.testBus.publish(respMsg)
            self.numEchoed += 1

            if self.numEchoed % 1000 == 0:
                print('Echoed %d' % self.numEchoed)
                
            if self.numEchoed % 10000 == 0:
                # Messages per second:
                msgs_per_sec = 10000.0 / (time.time() - self.batch_start_time)
                print('Echoing: %f Msgs/sec' % msgs_per_sec)
                self.batch_start_time = time.time()
              

        self.numReceived += 1
        if self.numReceived % 1000 == 0:
            stats_msg = 'Rxed %d' % self.numReceived
            self.logInfo(stats_msg)
            self.bus_stats_queue.put_nowait(stats_msg)
            
        if self.numReceived % 10000 == 0:
            # Messages per second:
            msgs_per_sec = 10000.0 / (time.time() - self.batch_start_time)
            stats_msg1 = 'Rx (no echo): %f Msgs/sec' % msgs_per_sec
            self.logInfo(stats_msg1)
            if not self.bus_stats_queue.full():
                self.bus_stats_queue.put_nowait(stats_msg1)
            self.batch_start_time = time.time()
                        
        topic = busMsg.topicName
        matching_subscribed_topic = self.findTopic(topic, self.topics_to_rx)
        if matching_subscribed_topic is not None:
            if not self.bus_msg_queue.full():
                self.bus_msg_queue.put_nowait(str(datetime.datetime.now()) +\
                                          "--%s: " % matching_subscribed_topic +\
                                          busMsg.content)
    
    def findTopic(self, topicIdentifier, topicStrArr):
        '''
        Given either a string or a pattern, and an
        array of strings that correspond to topic names
        return return the topic name that matches the
        topicIdentifier, or None. The strings in topicStrArr
        may be regular expressions as used in pattern subscriptions.
        In that case a regex match is done against the topicIdentifier.
        
        :param topicIdentifier: a string that names a topic. This is
            a straight name, not a regex
        :type topicIdentifier: string
        :param topicsStrArr: array of strings, some of which may contain
            the wildcard char '*', indicating that the string is a 
            regex pattern. In such a case the topicIdentifier is
            checked against that regex pattern.
        :return: the string in topicsStrArr that matched, or None. 
            The returned string may, of course be a regex string if
            that's what caused a match.
        :rType: {string | None}
        '''
         
        for topic_in_list in topicStrArr:
            # Is it a pattern?
            asterisk_pos = string.find(topic_in_list, '*')
            if asterisk_pos == -1:
                # Simple topic name, check exact match:
                if topicIdentifier == topic_in_list:
                    return topic_in_list
            else:
                # Topic 'name' in the list is actually a pattern:
                if re.match(topic_in_list, topicIdentifier) is not None:
                    return topic_in_list
                
        # No match:
        return None

                       
    def resetEchoedCounter(self):
        currTime = time.time()
        
        if self.mostRecentRxTime is None:
            # Nothing received yet:
            self.startTime = time.time()
            self.batch_start_time = time.time()
            self.startIdleTimer()
            return
        
        if currTime - self.mostRecentRxTime <= OnDemandPublisher.MAX_IDLE_TIME:
            # Received msgs during more recently than idle time:
            self.startIdleTimer() 
            return
        
        # Did not receive msgs within idle time:
        self.printTiming()
        if not self.printedResetting:
            resetMsg = 'Resetting (echoed %d)' % self.numEchoed
            self.logInfo(resetMsg)
            self.bus_stats_queue.put_nowait(resetMsg)
            self.printedResetting = True
        self.numEchoed = 0
        self.startTime = time.time()
        self.batch_start_time = time.time()
        self.timer = self.startIdleTimer()

    def startIdleTimer(self):
        threading.Timer(OnDemandPublisher.MAX_IDLE_TIME, functools.partial(self.resetEchoedCounter)).start()

    def stop(self, signum=None, frame=None):
        try:
            self.logDebug('Unsubscribing from echo...')
            self.serve_echo = False
            self.logDebug('Unsubscribing from check_syntax...')
            self.check_syntax = False
            
            self.logInfo('Shutting down streamer thread...')
            self.msg_streamer.stop(signum=None, frame=None)
            self.msg_streamer.join(JOIN_WAIT_TIME)
            if self.msg_streamer.is_alive():
                raise TimeoutError("Unable to stop message streamer thread '%s'." % self.msg_streamer.name)
            
        except Exception as e:
            print("Error while shutting down message streamer thread: '%s'" % `e`)

        self.done = True
        self.interruptEvent.set()
        
        
    def printTiming(self, startTime=None):
        currTime = time.time()
        if startTime is None:
            startTime = self.startTime
            return
        echoMsg = 'Echoed %d messages' % self.numEchoed
        self.logInfo(echoMsg)
        if self.numEchoed > 0:
            timeElapsed = float(currTime) - float(self.startTime)
            rateMsg = 'Msgs per second: %d' % (timeElapsed / self.numEchoed)
            self.logInfo(rateMsg)
            self.bus_stats_queue.put_nowait(rateMsg)
            
    def run(self):
        while not self.done:
            self.startTime = time.time()
            self.startIdleTimer()
            self.interruptEvent.wait()
            if not self.done and self.sendMsg:
                self.testBus.publish(self.outMsg)
                self.interruptEvent.clear()
                self.sendMsg = False
            continue
        
        self.testBus.unsubscribeFromTopic(self._echo_topic)
        self.testBus.close()
Пример #12
0
class MessageOutStreamer(threading.Thread):
    '''
    Thread that keeps sending the same message over and over,
    except for changing the time field.
    '''
    def __init__(self, busMsg=None):
        super(MessageOutStreamer, self).__init__()

        self.daemon = True
        self.streamBus = BusAdapter()
        self.busMsg = BusMessage() if busMsg is None else busMsg

        self.done = False
        self._paused = False
        self._stream_interval = STREAM_INTERVAL

    def pause(self, do_pause):
        if do_pause:
            self._paused = True
        else:
            self._paused = False

    @property
    def paused(self):
        return self._paused

    @property
    def stream_interval(self):
        return self._stream_interval

    @stream_interval.setter
    def stream_interval(self, new_val):
        '''
        Note: we trust that caller ensures new_val to 
        be a non-negative float. 
        
        :param new_val: number of (fractional) seconds to wait between
            stream messages. Zero means no wait.
        :type new_val:float
        '''
        self._stream_interval = new_val

    def change_stream_topic(self, newTopic):
        self.busMsg.topicName = newTopic

    def change_stream_content(self, newContent):
        self.busMsg.content = newContent

    def run(self):
        '''
        Keep publishing one message over and over,
        
        :param msgLen: length of payload
        :type msgLen: int
        '''
        while not self.done:
            self.busMsg.time = time.time()
            self.streamBus.publish(self.busMsg)
            while self._paused and not self.done:
                time.sleep(1)
            if self._stream_interval > 0:
                time.sleep(self._stream_interval)

    def stop(self, signum, frame):
        self.done = True
        self.streamBus.close()
Пример #13
0
class OnDemandPublisher(threading.Thread):
    '''
    Server for testing Redis bus modules. Started from the
    command line, or imported into applications. Serves multiple
    functions, individually or together:
    
        1. echoes messages it receives on topic ECHO_TOPIC.
        2. listens to messages without doing anything
        3. sends a continuous stream of messages
        4. check the syntax of an incoming msg, returning the result
        5. send one message on demand.

	USAGE: test_harness_server.py [-h] [-e] [-s [topic [content ...]]] [-c]
	                              [-l topic [topic ...]]
	                              [-o [topic [content ...]]]
	
	optional arguments:
	  -h, --help            show this help message and exit
	  -e, --echo            Echo messages arriving on topic 'echo' as sychronous replies.
	  -s [topic [content ...]], --streamMsgs [topic [content ...]]
	                        Send the same bus message over and over. Topic, or both,
	                        topic and content may be provided. If content is omitted,
	                        a random string of 100 characters will be used.
	                        If topic is omitted as well, messages will be streamed to 'test'
	  -c, --checkSyntax     Check syntax of messages arriving on topic 'bus_syntax'; 
	                        synchronously return result report.
	  -l topic [topic ...], --listenOn topic [topic ...]
	                        Subscribe to given topic(s), and throw the messages away
	  -o [topic [content ...]], --oneshot [topic [content ...]]
	                        Given a topic and a content string, send a bus message. Topic, or both 
	                        topic and content may be provided. If content is omitted,
	                        a random string of 100 characters will be used.
	                        If topic is omitted as well, messages will be streamed to 'test'
	    

    The thread always listens on the ECHO_TOPIC. But
    received messages are only **echoed** if the service was
    started with the 'echo' option
    
    The echo function acts like a synch-call service
    should: echoing on the response topic, which it
    derives from the msg ID. The content will be the
    content of the incoming message, the timestamp
    will be the receipt time.
    
    When echoing, the service keeps track of how many messages 
    it has echoed. But after not receiving any messages for 
    OnDemandPublisher.MAX_IDLE_TIME seconds, the
    current count is printed, and the counter is 
    reset to zero. After every 1000 messages, the total
    number echoed is printed to the console. Every 10,000
    messages, the message/sec rate is printed as well.
    
    When asked to **listen** to some topic, messages on that
    topic are received and counted. Then the message is
    placed into a queue where it can be picked up (self.bus_msg_queue). 
    If nobody attends to the queue, it fills up, and then sits there;
    i.e. no big harm. Statistics printing is as for echoing.
    Stats are placed on the bus_stats_queue for consumption by anyone
    interested (or not).
    
    Messages sent in a **stream** by sendMessageStream()
    contain their content's MD5 in the context field. This constant
    message sending is initiated only if the service is
    started with the 'send_stream' option.

    When asked to check message **syntax**, the service listens
    on SYNTAX_TOPIC, and synchronously returns a string 
    reporting on violations of SchoolBus specifications in 
    the incoming message.
    
    A **oneshot message** is sent if the server is started
    with the --oneshot option. If topic and/or content are provided,
    the message is loaded with the given content and published to
    the given topic. Defaults are to fill the content with a random
    string, and to publish to STREAM_TOPIC.
    
    '''

    # ----------------------------  Constants ----------------

    # Maximum time for no message to arrive before
    # starting over counting messages:
    MAX_IDLE_TIME = 5

    FIND_COMMA_PATTERN = re.compile(r'[,]+')
    FIND_SPACE_PATTERN = re.compile(r'[ ]+')

    LOG_LEVEL_NONE = 0
    LOG_LEVEL_ERR = 1
    LOG_LEVEL_INFO = 2
    LOG_LEVEL_DEBUG = 3

    def __init__(self,
                 serveEchos=True,
                 listenOn=None,
                 streamMsgs=False,
                 checkSyntax=True,
                 oneShotMsg=None):
        '''
        Initialize the test harness server. 
        
        :param serveEchos: if True receives messages on ECHO_TOPIC, and returns  
            message with the same content, but different timestamp as a synchronous call. 
        :type serveEchos: bool
        :param listenOn: if this parameter provides an array of topic(s), messages on these
            topics are received and counted, then placed on self.bus_msg_queue. Intermittent reception counts
            and reception rates are printed to the console, and place on self.bus_stats_queue.
        :type listenOn: {None | [string]}
        :param streamMsgs: if True, a continuous stream of messages are sent to
            STREAM_TOPIC. The timestamp is changed each time.
        :type streamMsgs: bool
        :param checkSyntax: if true, listens on SYNTAX_TOPIC, checks incoming messages
            for conformance to SchoolBus message format, and synchronously returns a result.
        :type checkSyntax: bool
        :param oneShotMsg: if non-none this parameter is expected to be a two-tuple
            containing a topic and a content. Either may be None. If topic is None,
            the message is sent to STREAM_TOPIC. If content is None, content will be
            a random string of length STANDARD_MSG_LENGTH.
        :type oneShotMsg: { None | ({string | None}, {string | None})}
        '''
        threading.Thread.__init__(self)

        self.loglevel = OnDemandPublisher.LOG_LEVEL_DEBUG
        #self.loglevel = OnDemandPublisher.LOG_LEVEL_INFO
        #self.loglevel = OnDemandPublisher.LOG_LEVEL_NONE

        #***********
        #         print ('serveEchos %s' % serveEchos)
        #         print ('listenOn %s' % listenOn)
        #         print ('streamMsgs %s' % streamMsgs)
        #         print('checkSyntax %s' % checkSyntax)
        #         print('oneShotMsg %s' % str(oneShotMsg))
        #         sys.exit()
        #***********

        self._serveEchos = serveEchos

        self.daemon = True
        self.testBus = BusAdapter()
        self.done = False
        # No topics yet that we are to listen to, but drop msgs for:
        self.topics_to_rx = []

        self.echo_topic = ECHO_TOPIC
        self.syntax_check_topic = SYNTAX_TOPIC
        self.standard_msg_len = STANDARD_MSG_LENGTH
        self._stream_interval = STREAM_INTERVAL

        self._stream_topic = STREAM_TOPIC
        self.standard_bus_msg = self.createMessage(STREAM_TOPIC,
                                                   STANDARD_MSG_LENGTH,
                                                   content=None)

        self.one_shot_topic = STREAM_TOPIC
        self.one_shot_content = self.standard_bus_msg

        # Queue into which incoming messages are fed
        # for consumers to communicated to clients, such
        # as a Web UI:
        self.bus_msg_queue = Queue.Queue(MAX_SAVED_MSGS)

        # Queue to which aggregate stats will be written:
        self.bus_stats_queue = Queue.Queue(MAX_SAVED_MSGS)

        # Create a 'standard' message to send out
        # when asked to via SIGUSR1 or the Web:

        if oneShotMsg is not None:
            # The oneShotMsg arg may have one of the following values:
            #   - None if oneshot was not requested.
            #   - An empty array if -o (or --oneshot)
            #        was listed in the command line,
            #        but neither topic nor message content
            #        were specified.
            #   - A one-element array if the topic was
            #        specified
            #   - A two-element array if both topic and
            #        content were specified.

            (one_shot_topic, self.one_shot_content) = oneShotMsg
            if one_shot_topic is not None:
                self.one_shot_topic = one_shot_topic

            # Create a message that one can have sent on demand
            # by sending a SIGUSR1 to the server, or using
            # the Web UI:

            self.standard_oneshot_msg = self.createMessage(
                self.one_shot_topic, content=self.one_shot_content)
            # Send the standard msg:
            self.sendMessage(self.standard_oneshot_msg)
        else:
            # Create a standard message with default topic/content:
            self.standard_oneshot_msg = self.createMessage()

        # Start time of a 10,000 messages batch:
        self.batch_start_time = time.time()

        # Subscribe, and ensure that context is delivered
        # with each message:

        if serveEchos:
            self.serve_echo = True

        if listenOn is not None:
            # Array of topics to listen to
            # If we are to listen to some (additional) topic(s),
            # on which no action is taken, subscribe to it/them now:
            self['topicsToRx'] = listenOn

        if checkSyntax:
            self.check_syntax = True

        self.interruptEvent = threading.Event()

        # Number of messages received and echoed:
        self.numEchoed = 0
        # Alternative to above when not echoing: Number of msgs just received:
        self.numReceived = 0
        self.mostRecentRxTime = None
        self.printedResetting = False

        # Not asked to send a message yet.
        self.sendMsg = False

        if streamMsgs is not None:
            # The streamMsgs arg may have one of the following values:
            #   - None if streaming messages was not requested.
            #   - An empty array if -s (or --streamMsgs)
            #        was listed in the command line,
            #        but neither topic nor message content
            #        were specified.
            #   - A one-element array if the topic was
            #        specified
            #   - A two-element array if both topic and
            #        content were specified.

            self.msg_streamer = MessageOutStreamer(self.standard_bus_msg)

            # The caller passed in a tuple with
            # topic and content for streaming. If they
            # are non-None, update the streamer:

            (stream_topic, stream_content) = streamMsgs
            if stream_topic is not None:
                self.stream_topic = stream_topic
            if stream_content is not None:
                self.stream_content = stream_content

            # Start streaming:

            self.msg_streamer.pause(True)
            self.msg_streamer.start()

        else:
            # Create a standard message with default topic/content:
            self.standard_stream_msg = self.createMessage()
            self.msg_streamer = None

    @property
    def running(self):
        return not self.done

    @property
    def echo_topic(self):
        return self._echo_topic

    @echo_topic.setter
    def echo_topic(self, new_echo_topic):
        if new_echo_topic is None or len(new_echo_topic) == 0:
            # Set to default echo topic:
            new_echo_topic = ECHO_TOPIC
        self._echo_topic = new_echo_topic

    @property
    def serve_echo(self):
        return self._serveEchos

    @serve_echo.setter
    def serve_echo(self, do_serve):
        if do_serve:
            # Have incoming messages delivered to messageReceiver() not
            # via a queue, but by direct call from the library:
            self.testBus.subscribeToTopic(self.echo_topic,
                                          functools.partial(
                                              self.messageReceiver),
                                          threaded=False)
            self._serveEchos = True
        else:
            self.testBus.unsubscribeFromTopic(self.echo_topic)
            self._serveEchos = False

    @property
    def stream_topic(self):
        return self.msg_streamer.busMsg.topicName

    @stream_topic.setter
    def stream_topic(self, new_stream_topic):
        if new_stream_topic is None or len(new_stream_topic) == 0:
            # Set to default stream topic:
            new_stream_topic = STREAM_TOPIC
        self.msg_streamer.change_stream_topic(new_stream_topic)

    @property
    def stream_content(self):
        return self.msg_streamer.busMsg.content

    @stream_content.setter
    def stream_content(self, new_stream_content):
        if new_stream_content is None or len(new_stream_content) == 0:
            self.msg_streamer.change_stream_content(
                self.createRandomStr(self.standard_msg_len))
        else:
            self.msg_streamer.change_stream_content(new_stream_content)

    @property
    def streaming(self):
        return not self.msg_streamer.paused

    @streaming.setter
    def streaming(self, should_stream):
        should_pause = not should_stream
        self.msg_streamer.pause(should_pause)

    @property
    def stream_interval(self):
        return self._stream_interval

    @stream_interval.setter
    def stream_interval(self, new_interval):
        # If the new value is not a number or empty str, set to default:
        if type(new_interval) == str and len(new_interval) == 0:
            new_interval = STREAM_INTERVAL
        else:
            # Non-empty string or number;
            # Ensure float, and replace negative
            # values with 0:
            try:
                if float(new_interval) < 0:
                    new_interval = 0.0
            except ValueError:
                raise ValueError(
                    "Attempt to set streaming interval to a non-numeric quantity: '%s'"
                    % str(new_interval))

        self.msg_streamer.stream_interval = new_interval
        self._stream_interval = new_interval

    @property
    def one_shot_topic(self):
        return self._one_shot_topic

    @one_shot_topic.setter
    def one_shot_topic(self, new_one_shot_topic):
        if new_one_shot_topic is None or len(new_one_shot_topic) == 0:
            # Set to default oneshot topic:
            new_one_shot_topic = STREAM_TOPIC
        else:
            self._one_shot_topic = new_one_shot_topic

    @property
    def one_shot_content(self):
        return self._one_shot_content

    @one_shot_content.setter
    def one_shot_content(self, new_one_shot_content):
        if new_one_shot_content is None or len(new_one_shot_content) == 0:
            self._one_shot_content = self.createRandomStr(
                self.standard_msg_len)
        else:
            self._one_shot_content = new_one_shot_content

    @property
    def syntax_check_topic(self):
        return self._syntax_check_topic

    @syntax_check_topic.setter
    def syntax_check_topic(self, new_syntax_check_topic):
        if new_syntax_check_topic is None or len(new_syntax_check_topic) == 0:
            # Set to default syntax check topic:
            new_syntax_check_topic = SYNTAX_TOPIC
        self._syntax_check_topic = new_syntax_check_topic

    @property
    def check_syntax(self):
        return self._checkSyntax

    @check_syntax.setter
    def check_syntax(self, do_check):
        if do_check:
            self.testBus.subscribeToTopic(
                self.syntax_check_topic,
                functools.partial(self.syntaxCheckReceiver))
            self._checkSyntax = True
        else:
            self.testBus.unsubscribeFromTopic(self.syntax_check_topic)
            self._checkSyntax = False

    @property
    def standard_msg_len(self):
        return self._standard_msg_len

    @standard_msg_len.setter
    def standard_msg_len(self, new_standard_msg_len):
        if new_standard_msg_len == 0 or \
           new_standard_msg_len is None:
            new_standard_msg_len = STANDARD_MSG_LENGTH
        try:
            new_standard_msg_len = int(new_standard_msg_len)
        except:
            new_standard_msg_len = STANDARD_MSG_LENGTH

        self._standard_msg_len = new_standard_msg_len

    def __getitem__(self, item):
        if item == 'echoTopic':
            return self.echo_topic

        elif item == 'echo':
            return self.testBus.subscribedTo(self['echoTopic'])

        elif item == 'streamTopic':
            return self.stream_topic
        elif item == 'streamContent':
            return self.stream_content
        elif item == 'streaming':
            return self.streaming
        elif item == 'streamInterval':
            return self.stream_interval

        elif item == 'oneShotTopic':
            return self.one_shot_topic
        elif item == 'oneShotContent':
            if isinstance(self.one_shot_content, BusMessage):
                return self.one_shot_content.content
            else:
                return self.one_shot_content

        elif item == 'strLen':
            return self.standard_msg_len

        elif item == 'syntaxTopic':
            return self.syntax_check_topic

        elif item == 'chkSyntax':
            return self.testBus.subscribedTo(self['syntaxTopic'])

        elif item == 'topicsToRx':
            return self.topics_to_rx

        else:
            raise KeyError('Key %s is not in schoolbus tester' % item)

    def __setitem__(self, item, new_val):

        if item == 'echoTopic':
            self.echo_topic = new_val

        elif item == 'echo':
            # Deal with both string and bool:
            new_val = (new_val == 'True' or new_val == True)
            self.serve_echo = new_val

        elif item == 'streamTopic':
            self.stream_topic = new_val
        elif item == 'streamContent':
            self.stream_content = new_val
        elif item == 'streaming':
            # Deal with both string and bool:
            new_val = (new_val == 'True' or new_val == True)
            self.streaming = new_val
        elif item == 'streamInterval':
            self.stream_interval = new_val

        elif item == 'oneShotContent':
            self.one_shot_content = new_val

        elif item == 'oneShotTopic':
            self.one_shot_topic = new_val
        elif item == 'oneShotContent':
            self.one_shot_content = new_val

        elif item == 'strLen':
            self.standard_msg_len = new_val

        elif item == 'syntaxTopic':
            self.syntax_check_topic = new_val

        elif item == 'chkSyntax':
            # Deal with both string and bool:
            new_val = (new_val == 'True' or new_val == True)
            self.check_syntax = new_val

        elif item == 'topicsToRx':
            # Topics to which we should listen, but
            # whose msgs we are to receive. Check
            # whether empty str or empty array, which
            # means unsubscribe from all topics-to-rx
            # (though not from syntax checker and echo):
            if ((type(new_val) == str and len(new_val) == 0)) or\
                (type(new_val) == list and len(new_val) == 1 and len(new_val[0]) == 0):
                for (indx, topic) in enumerate(self.topics_to_rx):
                    self.unsubscribeFromTopicOrPattern(topic)
                    del self.topics_to_rx[indx]
                return
            # We are given one or more topics. Tolerate
            # comma-separated, space-separated strings,
            # and also arrays:
            if type(new_val) != list:
                # comma-separated string of topics?
                if OnDemandPublisher.FIND_COMMA_PATTERN.search(
                        new_val) is not None:
                    # Yes, commas found: remove all spaces that might be around the commas:
                    new_val = OnDemandPublisher.FIND_SPACE_PATTERN.sub(
                        '', new_val)
                    new_val = new_val.split(',')
                # Else must be space-separated string of topics or just a single word:
                else:
                    # Replace all multi-space areas with single spaces; the strip()
                    # eliminates spaces at start and end:
                    new_val = OnDemandPublisher.FIND_SPACE_PATTERN.sub(
                        ' ', new_val).strip()
                    # Get an array of topic strings (works even with singleton topic):
                    new_val = new_val.split(' ')

            # Unsubscribe from all topics that are *not*
            # in the new list of topics:
            for (topic_pos, curr_topic) in enumerate(self.topics_to_rx):
                if curr_topic not in new_val:
                    self.topics_to_rx.pop(topic_pos)
                    self.unsubscribeFromTopicOrPattern(curr_topic)

            # Subscribe to any topics in the new-list that
            # we are not already subscribed to:
            for topic in new_val:
                # Disallow use of reserved topics self.echo_topic
                # and self.syntax_check_topic:
                if topic in (self.echo_topic, self.syntax_check_topic):
                    raise ValueError(
                        "Warning: '%s' and '%s' are reserved topic names." %
                        (self.echo_topic, self.syntax_check_topic))

                try:
                    # Already subscribed to it?
                    self.topics_to_rx.index(topic)
                    # At least by our bookkeeping, yes
                    # (else we would have had the exception.
                    # Ensure we really are:
                    if not self.testBus.subscribedTo(topic):
                        self.subscribeTopicOrPattern(topic)
                    continue
                except ValueError:
                    pass
                # New topic:
                self.subscribeTopicOrPattern(topic)
                self.topics_to_rx.append(topic)

        else:
            raise KeyError('Key %s is not in schoolbus tester' % item)
        return new_val

    def subscribeTopicOrPattern(self, topic_or_pattern):
        # If the topic contains an asterisk, then
        # build a regex pattern, rather than passing
        # the topic itself:
        try:
            topic_or_pattern.index('*')
            # Topic is a pattern of topics:
            topic = re.compile(topic_or_pattern)
        except ValueError:
            # Not a pattern, just a regular topic name:
            topic = topic_or_pattern
        self.testBus.subscribeToTopic(topic,
                                      functools.partial(self.messageReceiver))

    def unsubscribeFromTopicOrPattern(self, topic_or_pattern):
        # If the topic contains an asterisk, then
        # build a regex pattern, rather than passing
        # the topic itself:
        try:
            topic_or_pattern.index('*')
            # Topic is a pattern of topics:
            topic = re.compile(topic_or_pattern)
        except ValueError:
            # Not a pattern, just a regular topic name:
            topic = topic_or_pattern
        self.testBus.unsubscribeFromTopic(topic)

    def createMessage(self, topic=None, msgLen=None, content=None):
        '''
        Returns a BusMessage whose content is the given length,
        and whose topicName is the given topic.
        
        :param topic: topic for which this message is destined.
        :type topic: string
        :param msgLen: desired str length
        :type msgLen: int
        :param content: content of the message. If None, a random content
            of length msgLen will be created
        :return: BusMessage object ready to publish. The context field of the
            message instance will be the MD5 of the content.
        :rtype: BusMessage
        '''

        if topic is None:
            topic = self._stream_topic

        if msgLen is None:
            msgLen = self.standard_msg_len

        if content is None:
            content = self.createRandomStr(msgLen)

        msg = BusMessage(content=content,
                         topicName=topic,
                         context=hashlib.md5(str(content)).hexdigest())

        return msg

    def createRandomStr(self, the_len):
        the_len = int(the_len)
        content = bytearray()
        for _ in range(the_len):
            content.append(random.choice(string.letters))
        return str(content)

    def sendMessage(self, bus_msg=None):
        '''
        Publish the given BusMessage, or
        the standard message:
        '''
        if bus_msg is None:
            bus_msg = self.standard_oneshot_msg
        self.testBus.publish(bus_msg)

    def logInfo(self, msg):
        if self.loglevel >= OnDemandPublisher.LOG_LEVEL_INFO:
            print(str(datetime.datetime.now()) + ' info: ' + msg)

    def logErr(self, msg):
        if self.loglevel >= OnDemandPublisher.LOG_LEVEL_ERR:
            print(str(datetime.datetime.now()) + ' error: ' + msg)

    def logDebug(self, msg):
        if self.loglevel >= OnDemandPublisher.LOG_LEVEL_DEBUG:
            print(str(datetime.datetime.now()) + ' debug: ' + msg)

    def syntaxCheckReceiver(self, busMsg, context=None):
        '''
        Given a BusMessage instance, determine whether it contains 
        all necessary attributes: ID, and time. This method 
        is a callback for topic specified in module variable
        self.syntaxTopic. Constructs a string that lists any
        errors or warnings, and returns that string as a
        synchronous response.
        
        :param busMsg: the message to be evaluated
        :type busMsg: BusMessage
        :param context: not used
        :type context: <any>
        '''
        errors = []

        if busMsg.id is None:
            errors.append('Error: No ID field')
        if type(busMsg.time) != int and type(busMsg.time) != float:
            errors.append("Error: Time must be an int, was '%s'" % busMsg.time)
            try:
                busMsg.isoTime
            except (ValueError, TypeError):
                errors.append(
                    "Error: Time value not transformable to ISO time '%s'" %
                    busMsg.time)
        try:
            json.loads(busMsg.content)
        except AttributeError:
            errors.append("Warning: no 'content' field.")
        except ValueError:
            errors.append(
                "Warning: 'content' field present, but not valid JSON.")

        if len(errors) == 0:
            response = 'Message OK'
        else:
            response = self.testBus.makeResponseMsg(busMsg, '; '.join(errors))

        self.testBus.publish(response)

    def messageReceiver(self, busMsg, context=None):
        '''
        Method that is called with each received message.
        
        :param busMsg: bus message object
        :type busMsg: BusMessage
        :param context: context Python structure, if subscribeToTopic() was
            called with one.uu
        :type context: <any>
        '''

        self.mostRecentRxTime = time.time()
        self.printedResetting = False

        if self.serve_echo and busMsg.topicName == 'echo':
            # Create a message with the same content as the incoming
            # message, timestamp the new message, and publish it
            # on the response topic (i.e. tmp.<msgId>):
            respMsg = self.testBus.makeResponseMsg(busMsg, busMsg.content)

            # Publish a response:
            self.testBus.publish(respMsg)
            self.numEchoed += 1

            if self.numEchoed % 1000 == 0:
                print('Echoed %d' % self.numEchoed)

            if self.numEchoed % 10000 == 0:
                # Messages per second:
                msgs_per_sec = 10000.0 / (time.time() - self.batch_start_time)
                print('Echoing: %f Msgs/sec' % msgs_per_sec)
                self.batch_start_time = time.time()

        self.numReceived += 1
        if self.numReceived % 1000 == 0:
            stats_msg = 'Rxed %d' % self.numReceived
            self.logInfo(stats_msg)
            self.bus_stats_queue.put_nowait(stats_msg)

        if self.numReceived % 10000 == 0:
            # Messages per second:
            msgs_per_sec = 10000.0 / (time.time() - self.batch_start_time)
            stats_msg1 = 'Rx (no echo): %f Msgs/sec' % msgs_per_sec
            self.logInfo(stats_msg1)
            if not self.bus_stats_queue.full():
                self.bus_stats_queue.put_nowait(stats_msg1)
            self.batch_start_time = time.time()

        topic = busMsg.topicName
        matching_subscribed_topic = self.findTopic(topic, self.topics_to_rx)
        if matching_subscribed_topic is not None:
            if not self.bus_msg_queue.full():
                self.bus_msg_queue.put_nowait(str(datetime.datetime.now()) +\
                                          "--%s: " % matching_subscribed_topic +\
                                          busMsg.content)

    def findTopic(self, topicIdentifier, topicStrArr):
        '''
        Given either a string or a pattern, and an
        array of strings that correspond to topic names
        return return the topic name that matches the
        topicIdentifier, or None. The strings in topicStrArr
        may be regular expressions as used in pattern subscriptions.
        In that case a regex match is done against the topicIdentifier.
        
        :param topicIdentifier: a string that names a topic. This is
            a straight name, not a regex
        :type topicIdentifier: string
        :param topicsStrArr: array of strings, some of which may contain
            the wildcard char '*', indicating that the string is a 
            regex pattern. In such a case the topicIdentifier is
            checked against that regex pattern.
        :return: the string in topicsStrArr that matched, or None. 
            The returned string may, of course be a regex string if
            that's what caused a match.
        :rType: {string | None}
        '''

        for topic_in_list in topicStrArr:
            # Is it a pattern?
            asterisk_pos = string.find(topic_in_list, '*')
            if asterisk_pos == -1:
                # Simple topic name, check exact match:
                if topicIdentifier == topic_in_list:
                    return topic_in_list
            else:
                # Topic 'name' in the list is actually a pattern:
                if re.match(topic_in_list, topicIdentifier) is not None:
                    return topic_in_list

        # No match:
        return None

    def resetEchoedCounter(self):
        currTime = time.time()

        if self.mostRecentRxTime is None:
            # Nothing received yet:
            self.startTime = time.time()
            self.batch_start_time = time.time()
            self.startIdleTimer()
            return

        if currTime - self.mostRecentRxTime <= OnDemandPublisher.MAX_IDLE_TIME:
            # Received msgs during more recently than idle time:
            self.startIdleTimer()
            return

        # Did not receive msgs within idle time:
        self.printTiming()
        if not self.printedResetting:
            resetMsg = 'Resetting (echoed %d)' % self.numEchoed
            self.logInfo(resetMsg)
            self.bus_stats_queue.put_nowait(resetMsg)
            self.printedResetting = True
        self.numEchoed = 0
        self.startTime = time.time()
        self.batch_start_time = time.time()
        self.timer = self.startIdleTimer()

    def startIdleTimer(self):
        threading.Timer(OnDemandPublisher.MAX_IDLE_TIME,
                        functools.partial(self.resetEchoedCounter)).start()

    def stop(self, signum=None, frame=None):
        try:
            self.logDebug('Unsubscribing from echo...')
            self.serve_echo = False
            self.logDebug('Unsubscribing from check_syntax...')
            self.check_syntax = False

            self.logInfo('Shutting down streamer thread...')
            self.msg_streamer.stop(signum=None, frame=None)
            self.msg_streamer.join(JOIN_WAIT_TIME)
            if self.msg_streamer.is_alive():
                raise TimeoutError(
                    "Unable to stop message streamer thread '%s'." %
                    self.msg_streamer.name)

        except Exception as e:
            print("Error while shutting down message streamer thread: '%s'" %
                  ` e `)

        self.done = True
        self.interruptEvent.set()

    def printTiming(self, startTime=None):
        currTime = time.time()
        if startTime is None:
            startTime = self.startTime
            return
        echoMsg = 'Echoed %d messages' % self.numEchoed
        self.logInfo(echoMsg)
        if self.numEchoed > 0:
            timeElapsed = float(currTime) - float(self.startTime)
            rateMsg = 'Msgs per second: %d' % (timeElapsed / self.numEchoed)
            self.logInfo(rateMsg)
            self.bus_stats_queue.put_nowait(rateMsg)

    def run(self):
        while not self.done:
            self.startTime = time.time()
            self.startIdleTimer()
            self.interruptEvent.wait()
            if not self.done and self.sendMsg:
                self.testBus.publish(self.outMsg)
                self.interruptEvent.clear()
                self.sendMsg = False
            continue

        self.testBus.unsubscribeFromTopic(self._echo_topic)
        self.testBus.close()
Пример #14
0
class RedisPerformanceTester(object):
    '''
    classdocs
    '''

    PUBLISH_TOPIC = 'test'

    def __init__(self):
        '''
        Constructor
        '''
        self.host = 'localhost'
        self.port = 6379

        self.letterChoice = string.letters
        self.bus = BusAdapter()

    def close(self):
        self.bus.close()

    def publishToUnsubscribedTopic(self, numMsgs, msgLen, block=True):
        '''
        Publish given number of messages to a topic that
        nobody is subscribed to. Each msg is of length msgLen.
        
        :param numMsgs:
        :type numMsgs:
        :param msgLen:
        :type msgLen:
        '''
        (msg, md5) = self.createMessage(msgLen)  #@UnusedVariable
        busMsg = BusMessage(content=msg,
                            topicName=RedisPerformanceTester.PUBLISH_TOPIC)
        startTime = time.time()
        for _ in range(numMsgs):
            self.bus.publish(busMsg, block=block)
        endTime = time.time()
        self.printResult(
            'Publishing %s msgs to empty space (block==%s): ' %
            (str(numMsgs), str(block)), startTime, endTime, numMsgs)

    def publishToSubscribedTopic(self,
                                 numMsgs,
                                 msgLen,
                                 block=True,
                                 sameProcessListener=True):
        '''
        Publish given number of messages to a topic that
        a thread in this same process is subscribed to.
        These are asynchronous publish() calls. The receiving
        thread computes MD5 of the received msg to verify.
        
        :param numMsgs:
        :type numMsgs:
        :param msgLen:
        :type msgLen:
        :param block: if True, publish() will wait for server confirmation.
        :type block: bool
        :param sameProcessListener: if True, the listener to messages will be a thread in this
            Python process (see below). Else an outside process is expected to be subscribed
            to RedisPerformanceTester.PUBLISH_TOPIC.
        :type sameProcessListener: bool
        '''

        (msg, md5) = self.createMessage(msgLen)
        busMsg = BusMessage(content=msg,
                            topicName=RedisPerformanceTester.PUBLISH_TOPIC)
        try:
            listenerThread = ReceptionTester(msgMd5=md5, beSynchronous=False)
            listenerThread.daemon = True
            listenerThread.start()

            startTime = time.time()
            for _ in range(numMsgs):
                self.bus.publish(busMsg, block=block)
            endTime = time.time()
            self.printResult(
                'Publishing %s msgs to a subscribed topic (block==%s): ' %
                (str(numMsgs), str(block)), startTime, endTime, numMsgs)
        except Exception:
            raise
        finally:
            listenerThread.stop()
            listenerThread.join(3)

    def syncPublishing(self, numMsgs, msgLen, block=True):

        #sys.stdout.write('Run python src/redis_bus_python/test/test_harness_server.py echo and hit ENTER...')
        #sys.stdin.readline()

        (msg, md5) = self.createMessage(msgLen)  #@UnusedVariable

        busMsg = BusMessage(content=msg, topicName=ECHO_TOPIC)

        startTime = time.time()
        try:
            for serialNum in range(numMsgs):
                try:
                    busMsg.id = serialNum
                    res = self.bus.publish(busMsg,
                                           sync=True,
                                           timeout=5,
                                           block=block)  #@UnusedVariable
                except SyncCallTimedOut:
                    #printThreadTraces()
                    raise

            endTime = time.time()
            self.printResult(
                'Publishing %s synch msgs (block==%s): ' %
                (str(numMsgs), str(block)), startTime, endTime, numMsgs)
        finally:
            pass

    def rawIronPublish(self, numMsgs, msgLen, block=True):

        sock = self.bus.topicWaiterThread.pubsub.connection._sock
        sock.setblocking(1)
        sock.settimeout(2)

        (msg, md5) = self.createMessage(msgLen)  #@UnusedVariable
        wireMsg = '*3\r\n$7\r\nPUBLISH\r\n$4\r\ntest\r\n$190\r\n{"content": \r\n "dRkLSUQxFVSHuVnEekLtfPsXULtWEESQwaRYZtxpFGYRGphNTkRQAMPJfDxoGKOKPCMmptZBriVVfV\r\n LvYisehirsYSHdDrhXRgGl", "id": "a62b8cde-6bf6-4f75-a1f3-e768bec4d5e1", "time": 1437516321946}\r\n'
        num_sent = 0
        start_time = time.time()
        try:
            for _ in range(numMsgs):
                sock.sendall(wireMsg)
                num_sent += 1
                #             if num_sent % 1000 == 0:
                #                 print('Sent %d' % num_sent)
                if block:
                    #time.sleep(0.01)
                    numListeners = sock.recv(1024)  #@UnusedVariable
        except socket.timeout:
            end_time = time.time()
            self.printResult(
                'Sending on raw socket; result timeout after %d msgs.' %
                num_sent, start_time, end_time, numMsgs)
            sys.exit()
        except Exception:
            end_time = time.time()
            self.printResult(
                'Sending on raw socket; error %d msgs.' % num_sent, start_time,
                end_time, numMsgs)
            raise

        end_time = time.time()
        self.printResult(
            'Sent %d msgs on raw socket; block==%s' % (numMsgs, block),
            start_time, end_time, numMsgs)

    def justListenAndCount(self):
        listenerThread = ReceptionTester(beSynchronous=False)
        listenerThread.daemon = True
        listenerThread.start()
        signal.pause()
        listenerThread.stop()
        listenerThread.join()

    def createMessage(self, msgLen):
        '''
        Returns a string of a given length, and its checksum
        
        :param msgLen: desired str length
        :type msgLen: int
        '''

        msg = bytearray()
        for _ in range(msgLen):
            msg.append(random.choice(self.letterChoice))
        return (str(msg), hashlib.md5(str(msg)).hexdigest())

    def _connect(self):

        err = None
        # Get addr options for Redis host/port, with arbitrary
        # socket family (the 0), and stream type:
        for res in socket.getaddrinfo(self.host, self.port, 0,
                                      socket.SOCK_STREAM):
            family, socktype, proto, canonname, socket_address = res  #@UnusedVariable
            sock = None
            try:
                sock = socket.socket(family, socktype, proto)
                # TCP_NODELAY
                sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

                sock.connect((self.host, self.port))
                return sock

            except socket.error as _:
                err = _
                if sock is not None:
                    sock.close()

        if err is not None:
            raise err
        raise socket.error("socket.getaddrinfo returned an empty list")

    def printResult(self, headerMsg, startTime, endTime, numMsgs):
        totalTime = endTime - startTime
        timePerMsg = totalTime / numMsgs
        msgPerSec = numMsgs / totalTime
        print(headerMsg)
        print('msgsPerSec: %s' % str(msgPerSec))
        print('timePerMsg: %s' % str(timePerMsg))
Пример #15
0
class RedisPerformanceTester(object):
    '''
    classdocs
    '''

    PUBLISH_TOPIC = 'test'

    def __init__(self):
        '''
        Constructor
        '''
        self.host = 'localhost'
        self.port = 6379
        
        self.letterChoice = string.letters
        self.bus = BusAdapter()
        
    def close(self):
        self.bus.close()
    
    def publishToUnsubscribedTopic(self, numMsgs, msgLen, block=True):
        '''
        Publish given number of messages to a topic that
        nobody is subscribed to. Each msg is of length msgLen.
        
        :param numMsgs:
        :type numMsgs:
        :param msgLen:
        :type msgLen:
        '''
        (msg, md5) = self.createMessage(msgLen) #@UnusedVariable
        busMsg = BusMessage(content=msg, topicName=RedisPerformanceTester.PUBLISH_TOPIC)
        startTime = time.time()
        for _ in range(numMsgs):
            self.bus.publish(busMsg, block=block)
        endTime = time.time()
        self.printResult('Publishing %s msgs to empty space (block==%s): ' % (str(numMsgs),str(block)), startTime, endTime, numMsgs)
    
    def publishToSubscribedTopic(self, numMsgs, msgLen, block=True, sameProcessListener=True):
        '''
        Publish given number of messages to a topic that
        a thread in this same process is subscribed to.
        These are asynchronous publish() calls. The receiving
        thread computes MD5 of the received msg to verify.
        
        :param numMsgs:
        :type numMsgs:
        :param msgLen:
        :type msgLen:
        :param block: if True, publish() will wait for server confirmation.
        :type block: bool
        :param sameProcessListener: if True, the listener to messages will be a thread in this
            Python process (see below). Else an outside process is expected to be subscribed
            to RedisPerformanceTester.PUBLISH_TOPIC.
        :type sameProcessListener: bool
        '''

        (msg, md5) = self.createMessage(msgLen)
        busMsg = BusMessage(content=msg, topicName=RedisPerformanceTester.PUBLISH_TOPIC)
        try:
            listenerThread = ReceptionTester(msgMd5=md5, beSynchronous=False)
            listenerThread.daemon = True
            listenerThread.start()
            
            startTime = time.time()
            for _ in range(numMsgs):
                self.bus.publish(busMsg, block=block)
            endTime = time.time()
            self.printResult('Publishing %s msgs to a subscribed topic (block==%s): ' % (str(numMsgs), str(block)), startTime, endTime, numMsgs)
        except Exception:
            raise
        finally:
            listenerThread.stop()
            listenerThread.join(3)
            
    def syncPublishing(self, numMsgs, msgLen, block=True):

        #sys.stdout.write('Run python src/redis_bus_python/test/test_harness_server.py echo and hit ENTER...')
        #sys.stdin.readline()
        
        (msg, md5) = self.createMessage(msgLen) #@UnusedVariable

        busMsg = BusMessage(content=msg, topicName=ECHO_TOPIC)


        startTime = time.time()
        try:
            for serialNum in range(numMsgs):
                try:
                    busMsg.id = serialNum
                    res = self.bus.publish(busMsg, sync=True, timeout=5, block=block) #@UnusedVariable
                except SyncCallTimedOut:
                    #printThreadTraces()
                    raise
                    
            endTime = time.time()
            self.printResult('Publishing %s synch msgs (block==%s): ' % (str(numMsgs), str(block)), startTime, endTime, numMsgs)
        finally:
            pass
        
        
    def rawIronPublish(self, numMsgs, msgLen, block=True):
        
        sock = self.bus.topicWaiterThread.pubsub.connection._sock
        sock.setblocking(1)
        sock.settimeout(2)
        
        (msg, md5) = self.createMessage(msgLen) #@UnusedVariable
        wireMsg = '*3\r\n$7\r\nPUBLISH\r\n$4\r\ntest\r\n$190\r\n{"content": \r\n "dRkLSUQxFVSHuVnEekLtfPsXULtWEESQwaRYZtxpFGYRGphNTkRQAMPJfDxoGKOKPCMmptZBriVVfV\r\n LvYisehirsYSHdDrhXRgGl", "id": "a62b8cde-6bf6-4f75-a1f3-e768bec4d5e1", "time": 1437516321946}\r\n'
        num_sent = 0
        start_time = time.time()
        try:
            for _ in range(numMsgs):
                sock.sendall(wireMsg)
                num_sent += 1
    #             if num_sent % 1000 == 0:
    #                 print('Sent %d' % num_sent)
                if block:
                    #time.sleep(0.01)
                    numListeners = sock.recv(1024) #@UnusedVariable
        except socket.timeout:
            end_time = time.time()
            self.printResult('Sending on raw socket; result timeout after %d msgs.' % num_sent, start_time, end_time, numMsgs)
            sys.exit()  
        except Exception:
            end_time = time.time()
            self.printResult('Sending on raw socket; error %d msgs.' % num_sent, start_time, end_time, numMsgs)
            raise
            
        end_time = time.time()
        self.printResult('Sent %d msgs on raw socket; block==%s' % (numMsgs, block), start_time, end_time, numMsgs)
    
    def justListenAndCount(self):
        listenerThread = ReceptionTester(beSynchronous=False)
        listenerThread.daemon = True
        listenerThread.start()
        signal.pause()
        listenerThread.stop()
        listenerThread.join()
    
    def createMessage(self, msgLen):
        '''
        Returns a string of a given length, and its checksum
        
        :param msgLen: desired str length
        :type msgLen: int
        '''
        
        msg = bytearray()
        for _ in range(msgLen):
            msg.append(random.choice(self.letterChoice))
        return (str(msg), hashlib.md5(str(msg)).hexdigest())

    def _connect(self):

        err = None
        # Get addr options for Redis host/port, with arbitrary
        # socket family (the 0), and stream type:
        for res in socket.getaddrinfo(self.host, self.port, 0,
                                      socket.SOCK_STREAM):
            family, socktype, proto, canonname, socket_address = res  #@UnusedVariable
            sock = None
            try:
                sock = socket.socket(family, socktype, proto)
                # TCP_NODELAY
                sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                
                sock.connect((self.host, self.port))                
                return sock

            except socket.error as _:
                err = _
                if sock is not None:
                    sock.close()

        if err is not None:
            raise err
        raise socket.error("socket.getaddrinfo returned an empty list")

    
    def printResult(self, headerMsg, startTime, endTime, numMsgs):
        totalTime  = endTime - startTime
        timePerMsg = totalTime/numMsgs
        msgPerSec  = numMsgs/totalTime
        print(headerMsg)
        print('msgsPerSec: %s' % str(msgPerSec))
        print('timePerMsg: %s' % str(timePerMsg))
class MsgDeliveryTest(unittest.TestCase):

    def setUp(self):
        self.msg_server = OnDemandPublisher()
        self.msg_server.start()
        self.deliveryDest = functools.partial(self._deliveryDest)
        self.bus = BusAdapter()
        self.delivery_event = threading.Event()
        
        # Content and topic for outgoing msgs sent
        # via the OnDemandPublisher instance:
        
        self.reference_msg_content = 'rTmzntNSQLmXokesBpLmAbPYeysftXnuntfdPKrxMVNUuqVFHFzfcrrSaRssdHuMRhPYjXKYrJjwKcyeYycEzQSkJubTabeSFLRS'
        self.reference_named_topic = 'MyTopic'
        self.reference_pattern_topic = r'MyTopic*'
        self.reference_pattern_obj = re.compile(self.reference_pattern_topic)
        self.reference_pattern_matching_topic = 'MyTopicHurray'
        self.reference_context = 'myContext'
        
        # Msg with a non-pattern, i.e. fixed-name topic:
        self.reference_bus_msg = BusMessage(topicName=self.reference_named_topic, 
                                            content=self.reference_msg_content, 
                                            context=self.reference_context)
        
        # Msg with a topic that will match self.reference_pattern_topic: 
        self.reference_bus_wildcard_msg = BusMessage(topicName=self.reference_pattern_matching_topic, 
                                                     content=self.reference_msg_content,
                                                     context=self.reference_context) 
        
    def tearDown(self):
        self.msg_server.stop()
        self.msg_server.join()
        self.bus.close()
        self.delivery_event.clear()

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testDeliveryNameNotThreaded(self):
        self.bus.subscribeToTopic(self.reference_named_topic, self.deliveryDest, threaded=False, context=self.reference_context)
        
        self.delivery_event.clear()
        self.msg_server.sendMessage(self.reference_bus_msg)
        
        # Wait for message to arrive:
        self.delivery_event.wait()
        # Compare our expected incoming bus message with
        # what we actually got:
        self.assertBusMsgsEqual(self.reference_bus_msg, self.received_bus_msg)

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testDeliveryNameThreaded(self):
        self.bus.subscribeToTopic(self.reference_named_topic, self.deliveryDest, threaded=True, context=self.reference_context)
        self.delivery_event.clear()
        self.msg_server.sendMessage(self.reference_bus_msg)
        
        # Wait for message to arrive:
        self.delivery_event.wait()
        
        # Compare our expected incoming bus message with
        # what we actually got:
        self.assertBusMsgsEqual(self.reference_bus_msg, self.received_bus_msg)

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testDeliveryPatternNotThreaded(self):
        # Subscribe to a pattern topic, using an re.Pattern instance:
        self.bus.subscribeToTopic(self.reference_pattern_obj, self.deliveryDest, threaded=False, context=self.reference_context)        
        
        self.delivery_event.clear()
        # Send to a topic that fits the reference pattern ('MyTopic*'):
        self.msg_server.sendMessage(self.reference_bus_wildcard_msg)

        self.delivery_event.wait()

        self.assertBusMsgsEqual(self.reference_bus_wildcard_msg, self.received_bus_msg)

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testDeliveryPatternThreaded(self):
        # Subscribe to a pattern topic, using an re.Pattern instance:
        self.bus.subscribeToTopic(self.reference_pattern_obj, self.deliveryDest, threaded=True, context=self.reference_context)        
        
        self.delivery_event.clear()
        # Send to a topic that fits the reference pattern ('MyTopic*'):
        self.msg_server.sendMessage(self.reference_bus_wildcard_msg)

        self.delivery_event.wait()

        self.assertBusMsgsEqual(self.reference_bus_wildcard_msg, self.received_bus_msg)

    @unittest.skipIf(not TEST_ALL, 'Temporarily disabled')
    def testSyncPublish(self):
        
        echo_msg = self.reference_bus_msg
        
        # Change target topic to what the test harness echo server
        # listens for:
        echo_msg.topicName = self.msg_server.ECHO_CHANNEL
        
        result = self.bus.publish(echo_msg, sync=True)
        self.assertEqual(self.reference_msg_content, result)

    # -------------------------  Service Methods --------------

    def assertBusMsgsEqual(self, expected_msg, actual_msg):
        self.assertEqual(expected_msg.topicName, actual_msg.topicName)
        self.assertEqual(expected_msg.context, actual_msg.context)
        self.assertEqual(expected_msg.content, actual_msg.content)

    def _deliveryDest(self, bus_msg):
        '''
        Receives incoming messages, and places them into
        instance variable received_bus_msg.
        
        :param bus_msg:
        :type bus_msg:
        '''
        #print('BusMessage: context is %s' % bus_msg.context)
        self.received_bus_msg = bus_msg
        self.delivery_event.set()