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()
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()
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()
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()
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()
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()
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()
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()
class SchoolbusWikipedia(object): ''' {"summary" : "2", "topic" : "Germany"} ''' TOPIC = 'wikipedia' def __init__(self): ''' Constructor ''' self.bus = BusAdapter() self.bus.subscribeToTopic(SchoolbusWikipedia.TOPIC, functools.partial(self.get_info_handler)) # Hang till keyboard_interrupt: try: self.exit_event = threading.Event().wait() except KeyboardInterrupt: print('Exiting wikipedia module.') def summary(self, term, num_sentences=1): return wikipedia.summary(term, sentences=num_sentences) def page(self, term): return wikipedia.search(term, results=1) def geosearch(self, lat, longitude, num_results=1, radius=1000): return wikipedia.geosearch(lat, longitude, results=num_results, radius=radius) def get_info_handler(self, bus_message): ''' {'topic' : <keyword>, 'summary' : <numSentences>, 'geosearch' : {'lat' : <float>, 'long' : <float> 'radius' : <int> }, 'coordinates' : 'True', 'references' : 'True' } :param bus_message: :type bus_message: ''' #print(bus_message.content) try: req_dict = json.loads(bus_message.content) except ValueError: err_resp = { 'error': 'Bad json in wikipedia request: %s' % str(bus_message.content) } resp = self.bus.makeResponseMsg(bus_message, json.dumps(err_resp)) self.bus.publish(resp) return try: self.check_req_correctness(req_dict) except ValueError as e: err_resp = {'error': '%s' % ` e `} resp = self.bus.makeResponseMsg(bus_message, json.dumps(err_resp)) self.bus.publish(resp) res_dict = {} if req_dict.get('summary', None) is not None: summary = wikipedia.summary(req_dict['topic'], sentences=req_dict['summary']) res_dict['summary'] = summary.encode('UTF-8', 'replace') wants_page = False else: # Wants whole page content: wants_page = True if req_dict.get('geosearch', None) is not None: geo_parms = req_dict['geosearch'] lat = geo_parms['lat'] longitude = geo_parms['long'] radius = geo_parms['radius'] res_dict['geosearch'] = wikipedia.geosearch( lat, longitude, req_dict['topic'], radius) # Remaining request possibilities require the page to be obtained, # even if summary was requested: page = None if req_dict.get(u'coordinates', None) is not None and req_dict['coordinates']: if page is None: page = wikipedia.page(req_dict['topic']) try: (lat, longitude) = page.coordinates res_dict['coordinates'] = '{"lat" : "%s", "long" : "%s"}' %\ (str(lat), str(longitude)) except KeyError: # Wikipedia entry has not coordinates associated with it: res_dict['coordinates'] = '"None"' if req_dict.get('references', None) is not None and req_dict['references']: if page is None: page = wikipedia.page(req_dict['topic']) res_dict['references'] = page.coordinates if wants_page: if page is None: page = wikipedia.page(req_dict['topic']) res_dict['content'] = page.content resp_msg = self.bus.makeResponseMsg(bus_message, json.dumps(res_dict)) self.bus.publish(resp_msg) def check_req_correctness(self, req_dict): if req_dict.get('topic', None) is None: raise ValueError('No topic supplied in wikipedia request.') if req_dict.get('summary', None) is not None: # Must have numSentences as int: num_sentences = req_dict['summary'] try: num_sentences = int(num_sentences) except ValueError: raise ValueError('Summary request must have a positive integer indicating number of sentences requested; was %s' %\ str(num_sentences)) if num_sentences < 1: raise ValueError('Summary request must have a positive integer indicating number of sentences requested; was %s' %\ str(num_sentences)) if req_dict.get('geosearch', None) is not None: geo_parms = req_dict['geosearch'] try: if type(geo_parms['lat']) != float or\ type(geo_parms['long']) != float or\ type(geo_parms['radius']) != int or\ geo_parms['radius'] < 1: raise ValueError('Bad parameters to wikipedia geo search (lat/long must be floats; radius must be positive int):' %\ str(geo_parms)) except KeyError: raise ValueError( 'Missing parameter to wikipedia geo search; must have lat/long/radius' ) if req_dict.get('coordinates', None) is not None: coords_wanted = str(req_dict['coordinates']).lower() if coords_wanted != 'true' and coords_wanted != 'false': raise ValueError("Request for wikipedia topic coordinates must be 'true', or 'false', not %s" %\ str(coords_wanted)) # Normalize value to bool: req_dict[ 'coordinates'] = True if coords_wanted == 'true' else False if req_dict.get('references', None) is not None: refs_wanted = str(req_dict['references']).lower() if refs_wanted != 'true' and refs_wanted != 'false': raise ValueError("Request for wikipedia topic reference links must be 'true', or 'false', not %s" %\ str(coords_wanted)) # Normalize to bool: req_dict['references'] = True if refs_wanted == 'true' else False
class SchoolbusWikipedia(object): ''' {"summary" : "2", "topic" : "Germany"} ''' TOPIC = 'wikipedia' def __init__(self): ''' Constructor ''' self.bus = BusAdapter() self.bus.subscribeToTopic(SchoolbusWikipedia.TOPIC, functools.partial(self.get_info_handler)) # Hang till keyboard_interrupt: try: self.exit_event = threading.Event().wait() except KeyboardInterrupt: print('Exiting wikipedia module.') def summary(self, term, num_sentences=1): return wikipedia.summary(term, sentences=num_sentences) def page(self, term): return wikipedia.search(term, results=1) def geosearch(self, lat, longitude, num_results=1, radius=1000): return wikipedia.geosearch(lat, longitude, results=num_results, radius=radius) def get_info_handler(self, bus_message): ''' {'topic' : <keyword>, 'summary' : <numSentences>, 'geosearch' : {'lat' : <float>, 'long' : <float> 'radius' : <int> }, 'coordinates' : 'True', 'references' : 'True' } :param bus_message: :type bus_message: ''' #print(bus_message.content) try: req_dict = json.loads(bus_message.content) except ValueError: err_resp = {'error' : 'Bad json in wikipedia request: %s' % str(bus_message.content)} resp = self.bus.makeResponseMsg(bus_message, json.dumps(err_resp)) self.bus.publish(resp) return try: self.check_req_correctness(req_dict) except ValueError as e: err_resp = {'error' : '%s' % `e`} resp = self.bus.makeResponseMsg(bus_message, json.dumps(err_resp)) self.bus.publish(resp) res_dict = {} if req_dict.get('summary', None) is not None: summary = wikipedia.summary(req_dict['topic'], sentences=req_dict['summary']) res_dict['summary'] = summary.encode('UTF-8', 'replace') wants_page = False else: # Wants whole page content: wants_page = True if req_dict.get('geosearch', None) is not None: geo_parms = req_dict['geosearch'] lat = geo_parms['lat'] longitude = geo_parms['long'] radius = geo_parms['radius'] res_dict['geosearch'] = wikipedia.geosearch(lat, longitude, req_dict['topic'], radius) # Remaining request possibilities require the page to be obtained, # even if summary was requested: page = None if req_dict.get(u'coordinates', None) is not None and req_dict['coordinates']: if page is None: page = wikipedia.page(req_dict['topic']) try: (lat, longitude) = page.coordinates res_dict['coordinates'] = '{"lat" : "%s", "long" : "%s"}' %\ (str(lat), str(longitude)) except KeyError: # Wikipedia entry has not coordinates associated with it: res_dict['coordinates'] = '"None"' if req_dict.get('references', None) is not None and req_dict['references']: if page is None: page = wikipedia.page(req_dict['topic']) res_dict['references'] = page.coordinates if wants_page: if page is None: page = wikipedia.page(req_dict['topic']) res_dict['content'] = page.content resp_msg = self.bus.makeResponseMsg(bus_message, json.dumps(res_dict)) self.bus.publish(resp_msg) def check_req_correctness(self, req_dict): if req_dict.get('topic', None) is None: raise ValueError('No topic supplied in wikipedia request.') if req_dict.get('summary', None) is not None: # Must have numSentences as int: num_sentences = req_dict['summary'] try: num_sentences = int(num_sentences) except ValueError: raise ValueError('Summary request must have a positive integer indicating number of sentences requested; was %s' %\ str(num_sentences)) if num_sentences < 1: raise ValueError('Summary request must have a positive integer indicating number of sentences requested; was %s' %\ str(num_sentences)) if req_dict.get('geosearch', None) is not None: geo_parms = req_dict['geosearch'] try: if type(geo_parms['lat']) != float or\ type(geo_parms['long']) != float or\ type(geo_parms['radius']) != int or\ geo_parms['radius'] < 1: raise ValueError('Bad parameters to wikipedia geo search (lat/long must be floats; radius must be positive int):' %\ str(geo_parms)) except KeyError: raise ValueError('Missing parameter to wikipedia geo search; must have lat/long/radius') if req_dict.get('coordinates', None) is not None: coords_wanted = str(req_dict['coordinates']).lower() if coords_wanted != 'true' and coords_wanted != 'false': raise ValueError("Request for wikipedia topic coordinates must be 'true', or 'false', not %s" %\ str(coords_wanted)) # Normalize value to bool: req_dict['coordinates'] = True if coords_wanted == 'true' else False if req_dict.get('references', None) is not None: refs_wanted = str(req_dict['references']).lower() if refs_wanted != 'true' and refs_wanted != 'false': raise ValueError("Request for wikipedia topic reference links must be 'true', or 'false', not %s" %\ str(coords_wanted)) # Normalize to bool: req_dict['references'] = True if refs_wanted == 'true' else False