Exemple #1
0
    def test_distributed_integration_run(self):
        """
        Full integration test that starts both a MasterLocustRunner and three WorkerLocustRunner instances 
        and makes sure that their stats is sent to the Master.
        """
        class TestUser(Locust):
            wait_time = constant(0.1)

            @task
            def incr_stats(l):
                l.environment.events.request_success.fire(
                    request_type="GET",
                    name="/",
                    response_time=1337,
                    response_length=666,
                )

        with mock.patch("locust.runners.WORKER_REPORT_INTERVAL", new=0.3):
            # start a Master runner
            master_env = Environment()
            master = MasterLocustRunner(master_env, [TestUser],
                                        master_bind_host="*",
                                        master_bind_port=0)
            sleep(0)
            # start 3 Worker runners
            workers = []
            for i in range(3):
                worker_env = Environment()
                worker = WorkerLocustRunner(worker_env, [TestUser],
                                            master_host="127.0.0.1",
                                            master_port=master.server.port)
                workers.append(worker)

            # give workers time to connect
            sleep(0.1)
            # issue start command that should trigger TestUsers to be spawned in the Workers
            master.start(6, hatch_rate=1000)
            sleep(0.1)
            # check that slave nodes have started locusts
            for worker in workers:
                self.assertEqual(2, worker.user_count)
            # give time for users to generate stats, and stats to be sent to master
            sleep(1)
            master.quit()
            # make sure users are killed
            for worker in workers:
                self.assertEqual(0, worker.user_count)

        # check that stats are present in master
        self.assertGreater(
            master_env.runner.stats.total.num_requests,
            20,
            "For some reason the master node's stats has not come in",
        )
Exemple #2
0
class Plugin(AbstractPlugin, GeneratorPlugin):

    """ Locust tank plugin """
    SECTION = 'locust'


    def __init__(self, core, cfg, cfg_updater):
        AbstractPlugin.__init__(self, core, cfg, cfg_updater)
        self.core = core
        self._locustrunner = None
        self._locustclasses = None
        self._options = None
        self._user_count = 0
        self._state = ''
        self._locuststats = ''
        self._locustslaves = None
        self.stats_reader = None
        self.reader = None
        self.host = None
        self.web_host = ''
        self.port = 8089
        self.locustfile = 'locustfile'
        self.master = False
        self.slave = False
        self.master_host = "127.0.0.1"
        self.master_port = 5557
        self.master_bind_host = "*"
        self.master_bind_port = 5557
        self.expect_slaves = 0
        self.no_web = True
        self.num_clients = int(1)
        self.hatch_rate = float(1)
        self.num_requests = None
        self.run_time = None
        self.loglevel = 'INFO'
        self.logfile = None
        self.csvfilebase = None
        self.csvappend = True
        self.print_stats = True
        self.only_summary = True
        self.list_commands = False
        self.show_task_ratio = False
        self.show_task_ratio_json = False
        self.show_version = True
        self.locustlog_level = 'INFO'
        self.cfg = cfg

        # setup logging
        ll.setup_logging(self.loglevel, self.logfile)

    @property
    def locustlog_file(self):
        logger.debug("######## DEBUG: self.core.artifacts_dir = {}".format(self.core.artifacts_dir))
        return "{}/locust.log".format(self.core.artifacts_dir)


    def get_available_options(self):
        return [
            "host", "port", "locustfile",
            "num_clients", "hatch_rate", "run_time", #"num_requests",
            "logfile", "loglevel", "csvfilebase",
            "master", "master_bind_host", "master_bind_port", "expect_slaves",
            "master_host", "master_port"
        ]


    def _get_variables(self):
        res = {}
        for option in self.core.config.get_options(self.SECTION):
            if option[0] not in self.get_available_options():
                res[option[0]] = option[1]
        logger.debug("Variables: %s", res)
        return res


    def get_reader(self):
        if self.reader is None:
            self.reader = LocustReader(self, self.locustlog_file)
        return self.reader

    def get_stats_reader(self):
        if self.stats_reader is None:
            self.stats_reader = self.reader.stats_reader
            logger.debug("######## DEBUG: plugin.reader.stats_reader.source = %s" % self.stats_reader.source)
            return self.stats_reader

    def configure(self):
        self.host = self.get_option("host")
        self.port = self.get_option("port")
        self.locustfile = self.get_option("locustfile")
        self.num_clients = int(self.get_option ("num_clients"))
        self.hatch_rate = float(self.get_option("hatch_rate"))
        self.run_time = self.get_option("run_time")
        self.logfile = self.get_option("logfile")
        self.loglevel = self.get_option("loglevel")
        self.csvfilebase = self.get_option("csvfilebase")
        self.locustlog_level = self.get_option("locustlog_level")
        self.show_version = True
        self.master = self.get_option("master")
        self.master_bind_host = self.get_option("master_bind_host")
        self.master_bind_port = self.get_option("master_bind_port")
        self.expect_slaves = self.get_option("expect_slaves")
        self.master_host = self.get_option("master_host")
        self.master_port = self.get_option("master_port")


        if self.locustlog_file:
            logger.debug("######## DEBUG: configuring Locust resplog")
            ll.setup_resplogging(self.locustlog_level, self.locustlog_file)

    def get_options(self):
        options = {optname : self.__getattribute__(optname) for optname in self.get_available_options()}
        logger.debug("##### Locust plugin: get_options() : options = {}".format(options))
        return options

    def prepare_test(self):
        logger = logging.getLogger(__name__)


        try:
            logger.debug("######## DEBUG: looking for a console object")
            ### DEBUG: enable/disable Console
            console = self.core.get_plugin_of_type(ConsolePlugin)
        except Exception as ex:
            logger.debug("######## DEBUG: Console not found: %s", ex)
            console = None

        if console:
            logger.debug("######## DEBUG: console found")
            widget = LocustInfoWidget(self)
            console.add_info_widget(widget)
            logger.debug("######## DEBUG: locust widget added to console")


        try:

            locustfile = lm.find_locustfile(self.locustfile)
            if not locustfile:
                logger.error("##### Locust plugin: Could not find any locustfile! Ensure file ends in '.py' and see --help for available options.")
                sys.exit(1)

            if locustfile == "locust.py":
                logger.error("##### Locust plugin: The locustfile must not be named `locust.py`. Please rename the file and try again.")
                sys.exit(1)

            docstring, locusts = lm.load_locustfile(locustfile)

            logger.info("##### Locust plugin: locustfile = {}".format(locustfile))

            if not locusts:
                logger.error("##### Locust plugin: No Locust class found!")
                sys.exit(1)
            else:
                logger.info("##### Locust plugin: Locust classes found in {} : {}".format(locustfile, locusts))

            self._locustclasses = list(locusts.values())
            options = Opts(**self.get_options())
            self._options = options
            logger.debug("##### Locust plugin: main() : options = {}".format(options))


        except Exception as e:
            logger.error("##### Locust plugin: prepare_test() CRITICAL ERROR : %s", e)
            sys.exit(1)


    def is_any_slave_up(self):
        if self.master and self._locustslaves is not None:
            poll_slaves = [s.poll() for s in self._locustslaves]
            res = any([False if x is not None else True for x in poll_slaves])
            logger.debug("######## DEBUG: is_any_slave_up/any(res) = {}".format(res))
            return res
        elif self.master:
            logger.error("##### Locust plugin: no slave alive to poll")
            return False
        else:
            return False


    def start_test(self):

        # install SIGTERM handler
        def sig_term_handler():
            logger.info("##### Locust plugin: Got SIGTERM signal")
            self.shutdown(0)
            gevent.signal(signal.SIGTERM, sig_term_handler)

        def spawn_local_slaves(count):
            """
            Spawn *local* locust slaves : data aggregation will NOT work with *remote* slaves
            """
            try:
                args = ['locust']
                args.append('--locustfile={}'.format(str(self.locustfile)))
                args.append('--slave')
                args.append('--master-host={}'.format(self.master_host))
                args.append('--master-port={}'.format(self.master_port))
                args.append('--resplogfile={}'.format(self.locustlog_file))
                logger.info("##### Locust plugin: slave args = {}".format(args))

                # Spawning the slaves in shell processes (security warning with the use of 'shell=True')
                self._locustslaves = [subprocess.Popen(' '.join(args), shell=True, stdin=None,
                            stdout=open('{}/locust-slave-{}.log'.format(self.core.artifacts_dir, i), 'w'),
                            stderr=subprocess.STDOUT) for i in range(count)]
                #slaves = [SlaveLocustRunner(self._locustclasses, self._options) for _ in range(count)] # <-- WRONG: This will spawn slave running on the same CPU core as master
                time.sleep(1)

                logger.info("##### Locust plugin: Started {} new locust slave(s)".format(len(self._locustslaves)))
                logger.info("##### Locust plugin: locust slave(s) PID = {}".format(self._locustslaves))
            except socket.error as e:
                logger.error("##### Locust plugin: Failed to connect to the Locust master: %s", e)
                sys.exit(-1)
            except Exception as e:
                logger.error("##### Locust plugin: Failed to spawn locust slaves: %s", e)
                sys.exit(-1)

        try:
            logger.info("##### Locust plugin: Starting Locust %s" % version)

            # run the locust 

            ### FIXME
            #if self.csvfilebase:
            #    gevent.spawn(stats_writer, self.csvfilebase)
            ### /FIXME

            if self.run_time:
                if not self.no_web:
                    logger.error("##### Locust plugin: The --run-time argument can only be used together with --no-web")
                    sys.exit(1)
                try:
                    self.run_time = parse_timespan(self.run_time)
                except ValueError:
                    logger.error("##### Locust plugin: Valid --time-limit formats are: 20, 20s, 3m, 2h, 1h20m, 3h30m10s, etc.")
                    sys.exit(1)
                def spawn_run_time_limit_greenlet():
                    logger.info("##### Locust plugin: Run time limit set to %s seconds" % self.run_time)
                    def timelimit_stop():
                        logger.info("##### Locust plugin: Time limit reached. Stopping Locust.")
                        self._locustrunner.quit()
                        logger.debug("######## DEBUG: timelimit_stop()/self._locustrunner.quit() passed")
                    def on_greenlet_completion():
                        logger.debug("######## DEBUG: Locust plugin: on_greenlet_completion()")

                    #gevent.spawn_later(self.run_time, timelimit_stop)
                    gl = gevent.spawn_later(self.run_time, timelimit_stop)
                    # linking timelimit greenlet to main greenlet and get a feedback of its execution
                    #gl.link(on_greenlet_completion)



            # locust runner : web monitor
            if not self.no_web and not self.slave and self._locustrunner is None:
                # spawn web greenlet
                logger.info("##### Locust plugin: Starting web monitor at %s:%s" % (self.web_host or "*", self.port))
                main_greenlet = gevent.spawn(web.start, self._locustclasses, self._options)


            # locust runner : standalone
            if not self.master and not self.slave and self._locustrunner is None:
                logger.info("##### Locust plugin: LocalLocustRunner about to be launched")
                self._locustrunner = LocalLocustRunner(self._locustclasses, self._options)
                # spawn client spawning/hatching greenlet
                if self.no_web:
                    logger.info("##### Locust plugin: LocalLocustRunner.start_hatching()")
                    self._locustrunner.start_hatching(wait=True)
                    main_greenlet = self._locustrunner.greenlet
                if self.run_time:
                    logger.info("##### Locust plugin: spawn_run_time_limit_greenlet()")
                    spawn_run_time_limit_greenlet()
                    logger.info("##### Locust plugin: spawn_run_time_limit_greenlet() passed")

            # locust runner : master/slave mode (master here)
            elif self.master and self._locustrunner is None:
                self._locustrunner = MasterLocustRunner(self._locustclasses, self._options)
                logger.info("##### Locust plugin: MasterLocustRunner started")
                time.sleep(1)
                if self.no_web:
                    gevent.spawn(spawn_local_slaves(self.expect_slaves))
                    while len(self._locustrunner.clients.ready) < self.expect_slaves:
                        logger.info("##### Locust plugin: Waiting for slaves to be ready, %s of %s connected",
                                     len(self._locustrunner.clients.ready), self.expect_slaves)
                        time.sleep(1)
                    self._locustrunner.start_hatching(self.num_clients, self.hatch_rate)
                    logger.debug("######## DEBUG: MasterLocustRunner/start_hatching()")
                    main_greenlet = self._locustrunner.greenlet
                if self.run_time:
                    try:
                        spawn_run_time_limit_greenlet()
                    except Exception as e:
                        logger.error("##### Locust plugin: exception raised in spawn_run_time_limit_greenlet() = {}".format(e))

            # locust runner : master/slave mode (slave here)
            #elif self.slave and self._locustrunner is None:
            #    if self.run_time:
            #        logger.error("##### Locust plugin: --run-time should be specified on the master node, and not on slave nodes")
            #        sys.exit(1)
            #    try:
            #        self._locustrunner = SlaveLocustRunner(self._locustclasses, self._options)
            #        main_greenlet = self._locustrunner.greenlet
            #    except socket.error as e:
            #        logger.error("##### Locust plugin: Failed to connect to the Locust master: %s", e)
            #        sys.exit(-1)
            return self._locustrunner

            self._locustrunner.greenlet.join()
            code = 0
            if len(self._locustrunner.errors):
                code = 1
                self.shutdown(code=code)
        except KeyboardInterrupt as e:
            self.shutdown(0)


    def shutdown(self, code=0):
        """
        Shut down locust by firing quitting event, printing stats and exiting
        """


        logger.debug("######## DEBUG: shutdown()/_locustrunner = {}".format(self._locustrunner))
        logger.info("##### Locust plugin: Cleaning up runner...")
        if self._locustrunner is not None and self.is_any_slave_up():
            #if self.csvfilebase:
            #    write_stat_csvs(self.csvfilebase)
            retcode = self._locustrunner.quit()
            logger.debug("######## DEBUG: shutdown()/_locustrunner.quit() passed # retcode = {}".format(retcode))
        logger.info("##### Locust plugin: Running teardowns...")

        while not self.reader.is_stat_queue_empty():
            logger.info("##### Locust plugin: {} items remaining is stats queue".format(self.reader.stat_queue.qsize()))
            time.sleep(1)

        ### FIXME : possibly causing a greenlet looping infinitely
        #events.quitting.fire(reverse=True)
        print_stats(self._locustrunner.request_stats)
        print_percentile_stats(self._locustrunner.request_stats)
        print_error_report()
        self.reader.close()
        logger.info("##### Locust plugin: Shutting down (exit code %s), bye." % code)

    def is_test_finished(self):
        """
        Fetch locustrunner stats: min/max/median/avg response time, current RPS, fail ratio
        """
        if self._locustrunner:
            self._locuststats = self._locustrunner.stats.total

        """
        Fetch locustrunner status: 'ready', 'hatching', 'running', 'stopped' and returns status code
        """
        logger.debug("######## DEBUG: is_test_finished()? -> Fetching locust status")
        logger.debug("######## DEBUG: is_test_finished() -> self._locustrunner.state = {}".format(self._locustrunner.state))
        logger.debug("######## DEBUG: is_test_finished() -> is_any_slave_up() = {}".format(self.is_any_slave_up()))
        self._state = self._locustrunner.state
        if self._locustrunner.state == 'stopped' or self.master and not self.is_any_slave_up():
            self._user_count = 0
            return 0
        else:
            self._user_count = self._locustrunner.user_count
            return -1

    def end_test(self, retcode):
        if self.is_test_finished() < 0:
            logger.info("##### Locust plugin: Terminating Locust")
            self.shutdown(retcode)
        else:
            logger.info("##### Locust plugin: Locust has been terminated already")
            self.shutdown(retcode)
        return retcode