├── examples ├── perf │ ├── requirements.txt │ ├── pub.py │ └── sub.py ├── sub.py └── pub.py ├── setup.py ├── README.md └── pubsub ├── __init__.py ├── common.py ├── server.py └── client.py /examples/perf/requirements.txt: -------------------------------------------------------------------------------- 1 | tqdm 2 | -------------------------------------------------------------------------------- /examples/perf/pub.py: -------------------------------------------------------------------------------- 1 | from pubsub import MessageQueue 2 | 3 | 4 | def main(): 5 | m = MessageQueue() 6 | m.connect() 7 | while True: 8 | m.send_message('random', 'My message.') 9 | 10 | 11 | if __name__ == '__main__': 12 | main() 13 | -------------------------------------------------------------------------------- /examples/perf/sub.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from tqdm import tqdm 4 | 5 | from pubsub import MessageQueue 6 | 7 | m = MessageQueue() 8 | m.connect() 9 | m.subscribe('random') 10 | 11 | for i in tqdm(range(int(1e6)), desc='Benchmarking', file=sys.stdout, unit_scale=True, unit=' msg'): 12 | m.get_message(timeout=None) 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | from setuptools import find_packages 4 | 5 | setup( 6 | name='python-pubsub', 7 | version='1.4.0', 8 | author='Philippe Remy', 9 | packages=find_packages(), 10 | install_requires=[], 11 | entry_points={ 12 | 'console_scripts': [ 13 | 'start_pubsub_broker=pubsub.server:start', 14 | ], 15 | } 16 | ) 17 | -------------------------------------------------------------------------------- /examples/sub.py: -------------------------------------------------------------------------------- 1 | from pubsub import MessageQueue 2 | 3 | # Start the broker with the command: start_pubsub_broker 4 | 5 | m = MessageQueue() 6 | m.connect() 7 | count = 0 8 | m.subscribe('prime') 9 | m.subscribe('random') 10 | while True: 11 | channel, message = m.get_message(timeout=None) 12 | 13 | if channel in ['prime', 'random']: 14 | count += 1 15 | print(f'RECEIVE {count}: channel: {channel}, message: {message["coremq_string"]}') 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python PubSub 2 | A simple python implementation of a message router with many subscribers and many publishers. 3 | 4 | It can be considered as as fork of the project: [CoreMQ](https://github.com/deejross/coremq). 5 | 6 |
7 |
8 |
9 | 10 | This implementation handles: 11 | - 26K messages per second. 12 | - Latency of around 1ms. 13 | - `TCP_NODELAY` flag activated for latency sensitive applications. 14 | - Many publishers. 15 | - Many subscribers. 16 | - Multiple channels. 17 | 18 | ``` 19 | pip install python-pubsub 20 | ``` 21 | 22 | Refer to the [examples](examples). 23 | 24 | -------------------------------------------------------------------------------- /examples/pub.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from pubsub import MessageQueue 4 | from time import sleep 5 | # Start the broker with the command: start_pubsub_broker 6 | 7 | 8 | def prime_numbers(): 9 | for num in range(1, 1001): 10 | for i in range(2, num): 11 | if num % i == 0: 12 | break 13 | else: 14 | yield num 15 | 16 | 17 | def main(): 18 | m = MessageQueue() 19 | m.connect() 20 | count = 0 21 | print(m.welcome_message) 22 | pub_id = random.randint(0, 1000) 23 | print(f'Publisher ID: {pub_id}.') 24 | 25 | def publish(channel, msg): 26 | nonlocal count 27 | count += 1 28 | msg += f' Publisher Id is [{pub_id}].' 29 | print(f'PUBLISH {count}: {msg}.') 30 | sleep(0.0001) 31 | m.send_message(channel, msg) 32 | 33 | for prime in prime_numbers(): 34 | publish('prime', f'Next prime number is [{prime}].') 35 | publish('random', f'Next random number is [{random.randint(0, 10000)}].') 36 | 37 | 38 | if __name__ == '__main__': 39 | main() 40 | -------------------------------------------------------------------------------- /pubsub/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | CoreMQ 3 | ------ 4 | A pure-Python messaging queue. 5 | 6 | License 7 | ------- 8 | The MIT License (MIT) 9 | Copyright (c) 2015 Ross Peoples 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | from .client import MessageQueue 27 | -------------------------------------------------------------------------------- /pubsub/common.py: -------------------------------------------------------------------------------- 1 | """ 2 | CoreMQ 3 | ------ 4 | A pure-Python messaging queue. 5 | 6 | License 7 | ------- 8 | The MIT License (MIT) 9 | Copyright (c) 2015 Ross Peoples 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | import json 28 | import logging 29 | import os 30 | 31 | str_type = str 32 | from configparser import ConfigParser, NoOptionError, NoSectionError 33 | 34 | loggers = dict() 35 | 36 | 37 | class ConnectionClosed(Exception): 38 | pass 39 | 40 | 41 | class ProtocolError(Exception): 42 | pass 43 | 44 | 45 | class CoreConfigParser(ConfigParser, object): 46 | def get(self, section, option, default=None): 47 | try: 48 | return super(CoreConfigParser, self).get(section, option) 49 | except (NoOptionError, NoSectionError): 50 | return default 51 | 52 | 53 | def construct_message(queue, message): 54 | if not isinstance(queue, str_type): 55 | raise ValueError('Queue name must be a string, not %s' % queue) 56 | 57 | if len(queue) < 1: 58 | raise ValueError('Queue name must be at least one character in length') 59 | 60 | if ' ' in queue: 61 | raise ValueError('Queue name must not contain spaces') 62 | 63 | if isinstance(message, str_type): 64 | message = dict(coremq_string=message) 65 | 66 | if isinstance(message, dict): 67 | message = json.dumps(message) 68 | else: 69 | raise ValueError('Messages should be either a dictionary or a string') 70 | 71 | if len(message) > 99999999: # 100 MB max int that can fit in message header (8 characters, plus two controls) 72 | raise ValueError('Message cannot be 100MB or larger') 73 | 74 | if not isinstance(message, bytes): 75 | message = message.encode('utf-8') 76 | 77 | return ('+%s %s ' % (len(message) + len(queue) + 1, queue)).encode('utf-8') + message 78 | 79 | 80 | def send_message(socket, queue, message): 81 | socket.send(construct_message(queue, message)) 82 | 83 | 84 | def get_message(socket, timeout=1): 85 | socket.settimeout(timeout) 86 | data = socket.recv(10).decode('utf-8') 87 | expected_length, data = validate_header(data) 88 | 89 | while len(data) < expected_length: 90 | data += socket.recv(expected_length - len(data)).decode('utf-8') 91 | 92 | if ' ' not in data: 93 | return None, data 94 | 95 | queue, message = data.split(' ', 1) 96 | return queue, json.loads(message) 97 | 98 | 99 | def validate_header(data): 100 | """ 101 | Validates that data is in the form of "+5 Hello", with + beginning messages, followed by the length of the 102 | message as an integer, followed by a space, then the message. 103 | :param data: The raw data from the socket 104 | :return: (int, str) - the expected length of the message, the message 105 | """ 106 | if not data: 107 | raise ConnectionClosed() 108 | 109 | if data[0] != '+': 110 | raise ProtocolError('Missing beginning +') 111 | 112 | if ' ' not in data: 113 | raise ProtocolError('Missing space after length') 114 | 115 | length, data = data.split(' ', 1) 116 | 117 | try: 118 | length = int(length[1:]) 119 | except ValueError: 120 | raise ProtocolError('Length integer must be between + and space') 121 | 122 | return length, data 123 | 124 | 125 | def load_configuration(path=None): 126 | """ 127 | Loads configuration for CoreMQ and CoreWS servers 128 | :param path: Optional path for the config file. Defaults to current directory 129 | :return: CoreConfigParser 130 | """ 131 | if not path: 132 | path = os.path.join(os.getcwd(), 'coremq.conf') 133 | 134 | c = CoreConfigParser() 135 | 136 | try: 137 | c.read(path) 138 | except: 139 | logging.error('Config file could not be loaded') 140 | 141 | return c 142 | 143 | 144 | def get_logger(config, section, logger_name=None): 145 | """ 146 | Configures the Python logging module based on the configuration settings from load_configuration 147 | :param config: The CoreConfigParser instance from load_configuration 148 | :param section: The section where the logging config can be found 149 | :param logger_name: The name of the logger. Uses section by default 150 | :return: Logger 151 | """ 152 | logging.basicConfig() 153 | 154 | if section in loggers: 155 | return loggers[section] 156 | 157 | logger = logging.getLogger(section or logger_name) 158 | logger.propagate = False 159 | 160 | log_file = config.get('CoreMQ', 'log_file', 'stdout') 161 | if log_file == 'stdout': 162 | handler = logging.StreamHandler() 163 | else: 164 | handler = logging.FileHandler(log_file) 165 | 166 | log_level = config.get('CoreMQ', 'log_level', 'DEBUG') 167 | logger.setLevel(logging.getLevelName(log_level)) 168 | 169 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 170 | handler.setFormatter(formatter) 171 | 172 | logger.addHandler(handler) 173 | loggers[section] = logger 174 | return logger 175 | 176 | 177 | def comma_string_to_list(s): 178 | """ 179 | Takes a line of values, seprated by commas and returns the values in a list, removing any extra whitespacing. 180 | :param s: The string with commas 181 | :return: list 182 | """ 183 | if isinstance(s, (list, tuple)): 184 | return s 185 | 186 | result = [] 187 | items = s.split(',') 188 | for i in items: 189 | result.append(i.strip()) 190 | 191 | return result 192 | -------------------------------------------------------------------------------- /pubsub/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | CoreMQ 3 | ------ 4 | A pure-Python messaging queue. 5 | 6 | License 7 | ------- 8 | The MIT License (MIT) 9 | Copyright (c) 2015 Ross Peoples 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | import collections 27 | import signal 28 | import socket 29 | import sys 30 | import time 31 | import traceback 32 | import uuid 33 | from multiprocessing import Process 34 | from socketserver import BaseRequestHandler, ThreadingMixIn, TCPServer 35 | 36 | from pubsub.common import ConnectionClosed, get_message, send_message 37 | 38 | ADDRESS = '127.0.0.1' 39 | PORT = 6747 # spells MSGQ (message queue) 40 | 41 | 42 | class ThreadedTCPServer(ThreadingMixIn, TCPServer): 43 | EXITING = False 44 | PROCESS = None 45 | HISTORY = dict() 46 | 47 | 48 | ThreadedTCPServer.allow_reuse_address = True 49 | 50 | 51 | class TCPRequestHandler(BaseRequestHandler): 52 | connections = dict() 53 | 54 | def handle(self): 55 | conn_id = str(uuid.uuid4()) 56 | TCPRequestHandler.connections[conn_id] = dict(handler=self, subscriptions=[conn_id], options=dict()) 57 | self.respond(conn_id, 'Welcome!') 58 | 59 | print('Clients connected: %s' % len(TCPRequestHandler.connections)) 60 | while True: 61 | try: 62 | queue, message = get_message(self.request) 63 | message['coremq_sender'] = conn_id 64 | message['coremq_sent'] = time.time() 65 | 66 | if 'coremq_subscribe' in message: 67 | self.subscribe(conn_id, message['coremq_subscribe']) 68 | self.respond(conn_id, 'OK: Subscribe successful') 69 | elif 'coremq_unsubscribe' in message: 70 | self.unsubscribe(conn_id, message['coremq_unsubscribe']) 71 | self.respond(conn_id, 'OK: Unsubscribe successful') 72 | elif 'coremq_options' in message: 73 | self.set_options(conn_id, message['coremq_options']) 74 | self.respond(conn_id, 'OK: Options set') 75 | elif 'coremq_gethistory' in message: 76 | self.get_history(conn_id, message['coremq_gethistory']) 77 | else: 78 | self.respond(conn_id, 'OK: Message sent') 79 | self.broadcast(queue, message) 80 | self.store_message(queue, message) 81 | 82 | except socket.timeout: 83 | pass 84 | except (ConnectionClosed, socket.error): 85 | break 86 | except Exception as ex: 87 | self.respond(conn_id, str(ex)) 88 | print(conn_id, ex) 89 | traceback.print_exc() 90 | 91 | try: 92 | self.respond(conn_id, 'BYE') 93 | except socket.error: 94 | pass 95 | 96 | if conn_id in TCPRequestHandler.connections: 97 | del TCPRequestHandler.connections[conn_id] 98 | 99 | print('Clients connected: %s' % len(TCPRequestHandler.connections)) 100 | 101 | def respond(self, conn_id, text): 102 | send_message(self.request, conn_id, dict(response=text)) 103 | 104 | def subscribe(self, conn_id, queues): 105 | if not queues: 106 | return 107 | 108 | if conn_id not in TCPRequestHandler.connections: 109 | return 110 | 111 | if not isinstance(queues, (list, tuple)): 112 | queues = [queues] 113 | 114 | subs = TCPRequestHandler.connections[conn_id]['subscriptions'] 115 | for q in queues: 116 | if q not in subs: 117 | subs.append(q) 118 | 119 | def unsubscribe(self, conn_id, queues): 120 | if not queues: 121 | return 122 | 123 | if conn_id not in TCPRequestHandler.connections: 124 | return 125 | 126 | if not isinstance(queues, (list, tuple)): 127 | queues = [queues] 128 | 129 | subs = TCPRequestHandler.connections[conn_id]['subscriptions'] 130 | for q in queues: 131 | if q in subs: 132 | subs.remove(q) 133 | 134 | def set_options(self, conn_id, options): 135 | opts = TCPRequestHandler.connections[conn_id]['options'] 136 | opts.update(options) 137 | 138 | for key, val in options.items(): 139 | if val is None and key in opts: 140 | del opts[key] 141 | 142 | def broadcast(self, queue, message): 143 | for conn_id, d in TCPRequestHandler.connections.items(): 144 | if conn_id == message['coremq_sender'] and queue != conn_id and d['options'].get('echo', False) is False: 145 | continue 146 | 147 | if queue in d['subscriptions']: 148 | send_message(d['handler'].request, queue, message) 149 | 150 | def store_message(self, queue, message): 151 | if not queue in ThreadedTCPServer.HISTORY: 152 | ThreadedTCPServer.HISTORY[queue] = collections.deque(maxlen=10) 153 | 154 | ThreadedTCPServer.HISTORY[queue].append(message) 155 | 156 | def get_history(self, conn_id, queues): 157 | result = dict() 158 | for q in queues: 159 | if q in ThreadedTCPServer.HISTORY: 160 | result[q] = list(ThreadedTCPServer.HISTORY[q]) 161 | 162 | send_message(self.request, conn_id, dict(response=result)) 163 | 164 | 165 | def signal_handler(signal, frame): 166 | print('Shutting down message queue') 167 | ThreadedTCPServer.EXITING = True 168 | 169 | 170 | def message_queue_process(): 171 | print('Starting message queue') 172 | server = ThreadedTCPServer((ADDRESS, PORT), TCPRequestHandler) 173 | server.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 174 | server.timeout = 1 175 | while not ThreadedTCPServer.EXITING: 176 | server.handle_request() 177 | 178 | print('Message queue stopped') 179 | sys.exit(0) 180 | 181 | 182 | def start(): 183 | signal.signal(signal.SIGINT, signal_handler) 184 | signal.signal(signal.SIGTERM, signal_handler) 185 | ThreadedTCPServer.PROCESS = Process(target=message_queue_process) 186 | ThreadedTCPServer.PROCESS.start() 187 | 188 | 189 | if __name__ == '__main__': 190 | start() 191 | -------------------------------------------------------------------------------- /pubsub/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | CoreMQ 3 | ------ 4 | A pure-Python messaging queue. 5 | 6 | License 7 | ------- 8 | The MIT License (MIT) 9 | Copyright (c) 2015 Ross Peoples 10 | Permission is hereby granted, free of charge, to any person obtaining a copy 11 | of this software and associated documentation files (the "Software"), to deal 12 | in the Software without restriction, including without limitation the rights 13 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 14 | copies of the Software, and to permit persons to whom the Software is 15 | furnished to do so, subject to the following conditions: 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | """ 26 | 27 | import socket 28 | from json import JSONDecodeError 29 | 30 | from pubsub.common import get_message, send_message, ProtocolError 31 | 32 | 33 | class MessageQueue(object): 34 | def __init__(self, server='127.0.0.1', port=6747): 35 | self.server = server 36 | self.port = port 37 | self.socket = None 38 | self.connection_id = None 39 | self.welcome_message = None 40 | self.subscriptions = [] 41 | self.options = dict() 42 | self.last_message_time = 0 43 | 44 | def connect(self): 45 | if self.socket: 46 | return 47 | 48 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 49 | self.socket.settimeout(30) 50 | self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 51 | 52 | self.socket.connect((self.server, self.port)) 53 | self.connection_id, self.welcome_message = get_message(self.socket) 54 | 55 | if self.subscriptions: 56 | self.subscribe(*self.subscriptions) 57 | 58 | if self.options: 59 | self.set_options(**self.options) 60 | 61 | def close(self): 62 | if self.socket: 63 | self.socket.close() 64 | self.socket = None 65 | 66 | def send_message(self, queue, message): 67 | if not self.socket: 68 | self.connect() 69 | 70 | try: 71 | send_message(self.socket, queue, message) 72 | except socket.error: 73 | # attempt to reconnect if there was a connection error 74 | self.close() 75 | self.connect() 76 | send_message(self.socket, queue, message) 77 | 78 | # try: 79 | # return self.get_message() 80 | # except socket.error: 81 | # return None, None 82 | 83 | def get_message(self, timeout=1): 84 | if not self.socket: 85 | self.connect() 86 | 87 | try: 88 | queue, message = get_message(self.socket, timeout=timeout) 89 | except socket.timeout: 90 | return None, None 91 | except socket.error: 92 | # attempt to reconnect if there was a connection error 93 | self.close() 94 | self.connect() 95 | try: 96 | queue, message = get_message(self.socket, timeout=timeout) 97 | except socket.timeout: 98 | return None, None 99 | 100 | if 'response' in message and message['response'] == 'BYE': 101 | self.close() 102 | 103 | return queue, message 104 | 105 | def listen(self, seconds=30): 106 | for i in range(seconds): 107 | m = self.get_message() 108 | if m[0]: 109 | print(m[0], m[1]) 110 | 111 | def stress(self, queue, count=10000, wait=0, silent=False): 112 | import time 113 | 114 | start_time = time.time() 115 | for i in range(count): 116 | self.send_message(queue, dict(iteration=i)) 117 | if wait: 118 | time.sleep(wait) 119 | end_time = time.time() 120 | 121 | if not silent: 122 | print('Stress results:') 123 | print('Iterations: %s' % count) 124 | print('Forced wait time: %s' % wait) 125 | print('Time taken: %s seconds' % (end_time - start_time)) 126 | print('Messages per second: %s' % (float(count) / (end_time - start_time))) 127 | 128 | def get_history(self, *queues): 129 | if not queues: 130 | queues = self.subscriptions 131 | 132 | if not queues: 133 | raise ValueError('Must pass at least one queue name') 134 | 135 | if not isinstance(queues, (list, tuple)): 136 | queues = [queues] 137 | 138 | return self.send_message(self.connection_id, dict(coremq_gethistory=queues)) 139 | 140 | def subscribe(self, *queues): 141 | if not queues: 142 | raise ValueError('Must pass at least one queue name') 143 | 144 | if not isinstance(queues, (list, tuple)): 145 | queues = [queues] 146 | 147 | for q in queues: 148 | if q not in self.subscriptions: 149 | self.subscriptions.append(q) 150 | 151 | return self.send_message(self.connection_id, dict(coremq_subscribe=queues)) 152 | 153 | def unsubscribe(self, *queues): 154 | if not queues: 155 | raise ValueError('Must pass at least one queue name') 156 | 157 | if not isinstance(queues, (list, tuple)): 158 | queues = [queues] 159 | 160 | for q in queues: 161 | if q in self.subscriptions: 162 | self.subscriptions.remove(q) 163 | 164 | return self.send_message(self.connection_id, dict(coremq_unsubscribe=queues)) 165 | 166 | def set_options(self, **options): 167 | self.options.update(options) 168 | 169 | for key, val in options.items(): 170 | if val is None and key in self.options: 171 | del self.options[key] 172 | 173 | return self.send_message(self.connection_id, dict(coremq_options=options)) 174 | 175 | 176 | def stress_worker(args): 177 | import random 178 | import time 179 | server, port, queue, count, wait, listen_only = args 180 | 181 | time.sleep(random.uniform(0, 1)) 182 | m = MessageQueue(server, port) 183 | try: 184 | m.connect() 185 | except: 186 | time.sleep(1) 187 | m.connect() 188 | 189 | if listen_only: 190 | try: 191 | m.subscribe(queue) 192 | messages = 0 193 | null_msg = 0 194 | errors = 0 195 | for i in range(count): 196 | try: 197 | msg = m.get_message(timeout=10) 198 | if msg[0] is not None: 199 | messages += 1 200 | else: 201 | null_msg += 1 202 | if null_msg >= 2: 203 | break 204 | except: 205 | errors += 1 206 | except: 207 | return count, 1 208 | 209 | m.close() 210 | 211 | return count - messages, errors 212 | else: 213 | time.sleep(random.uniform(0, 1)) 214 | errors = 0 215 | start_time = time.time() 216 | for i in range(count): 217 | try: 218 | m.send_message(queue, dict(iteration=i + 1)) 219 | if wait: 220 | time.sleep(wait) 221 | except: 222 | errors += 1 223 | 224 | end_time = time.time() 225 | 226 | m.close() 227 | return end_time - start_time, float(count) / (end_time - start_time), errors 228 | 229 | 230 | def parallel_stress(server, port, queue, count=1000, wait=0.01, workers=10, listeners=90): 231 | import multiprocessing 232 | 233 | print('Beginning stress test with these settings:') 234 | print('Server: %s' % server) 235 | print('Queue: %s' % queue) 236 | print('Count per worker: %s' % count) 237 | print('Delay between messages: %s' % wait) 238 | print('Number of workers: %s' % workers) 239 | print('Number of listeners: %s' % listeners) 240 | print('') 241 | 242 | pool1 = multiprocessing.Pool(listeners) 243 | pool2 = multiprocessing.Pool(workers) 244 | tasks1 = [(server, port, queue, count, wait, True) for i in range(listeners)] 245 | tasks2 = [(server, port, queue, count, wait, False) for i in range(workers)] 246 | 247 | job1 = pool1.map_async(stress_worker, tasks1) 248 | job2 = pool2.map_async(stress_worker, tasks2) 249 | job1.wait() 250 | job2.wait() 251 | listen_results = job1.get() 252 | worker_results = job2.get() 253 | slowest = 0 254 | fastest = 9999 255 | average_mps = 0 256 | errors = 0 257 | missed = 0 258 | 259 | for t, mps, errs in worker_results: 260 | if t > slowest: 261 | slowest = t 262 | 263 | if t < fastest: 264 | fastest = t 265 | 266 | average_mps += mps 267 | errors += errs 268 | 269 | average_mps /= float(workers) 270 | 271 | for m, errs in listen_results: 272 | missed += m 273 | 274 | print('Test results:') 275 | print('Total count: %s' % (count * workers)) 276 | print('Fastest worker: %s seconds' % fastest) 277 | print('Slowest worker: %s seconds' % slowest) 278 | print('Number of errors: %s' % errors) 279 | print('Number of missing messages: %s' % missed) 280 | print('Average messages per second: %s' % average_mps) 281 | --------------------------------------------------------------------------------