Skip to content

Commit

Permalink
Merge branch 'master' into environment-refactor
Browse files Browse the repository at this point in the history
  • Loading branch information
heyman committed Apr 16, 2020
2 parents e32332d + ea588c7 commit 6feb9a7
Show file tree
Hide file tree
Showing 18 changed files with 117 additions and 191 deletions.
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
FROM python:3.6-alpine as builder
FROM python:3.8-alpine as builder

RUN apk --no-cache add g++ zeromq-dev libffi-dev
RUN apk --no-cache add g++ zeromq-dev libffi-dev file make gcc musl-dev
COPY . /src
WORKDIR /src
RUN pip install .

FROM python:3.6-alpine
FROM python:3.8-alpine

RUN apk --no-cache add zeromq && adduser -s /bin/false -D locust
COPY --from=builder /usr/local/lib/python3.6/site-packages /usr/local/lib/python3.6/site-packages
COPY --from=builder /usr/local/lib/python3.8/site-packages /usr/local/lib/python3.8/site-packages
COPY --from=builder /usr/local/bin/locust /usr/local/bin/locust
COPY docker_start.sh docker_start.sh
RUN chmod +x docker_start.sh
Expand Down
6 changes: 2 additions & 4 deletions docs/writing-a-locustfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,7 @@ Here is an example:

.. code-block:: python
from locust import Locust, task
from locust.wait_time import constant
from locust import Locust, task, constant
class MyLocust(Locust):
wait_time = constant(1)
Expand All @@ -135,8 +134,7 @@ the following example *task2* will have twice the chance of being picked as *tas

.. code-block:: python
from locust import Locust, task
from locust.wait_time import between
from locust import Locust, task, between
class MyLocust(Locust):
wait_time = between(5, 15)
Expand Down
6 changes: 3 additions & 3 deletions examples/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,17 @@ class WebsiteUser(HttpLocust):
stats = {"content-length":0}

@events.init.add_listener
def locust_init(environment, web_ui, **kwargs):
def locust_init(environment, **kwargs):
"""
We need somewhere to store the stats.
On the master node stats will contain the aggregated sum of all content-lengths,
while on the worker nodes this will be the sum of the content-lengths since the
last stats report was sent to the master
"""
if web_ui:
if environment.web_ui:
# this code is only run on the master node (the web_ui instance doesn't exist on workers)
@web_ui.app.route("/content-length")
@environment.web_ui.app.route("/content-length")
def total_content_length():
"""
Add a route to the Locust web app, where we can see the total content-length
Expand Down
1 change: 0 additions & 1 deletion locust/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from .core import HttpLocust, Locust, TaskSet, task
from .event import Events
from .exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately
from .sequential_taskset import SequentialTaskSet
from .wait_time import between, constant, constant_pacing

Expand Down
7 changes: 1 addition & 6 deletions locust/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,7 @@ class ForumPage(TaskSet):
instantiated. Useful for nested TaskSet classes.
"""

def __init__(self, parent):
# check if deprecated wait API is used
deprecation.check_for_deprecated_wait_api(self)

def __init__(self, parent):
self._task_queue = []
self._time_start = time()

Expand Down Expand Up @@ -465,8 +462,6 @@ class ForumPage(TaskSet):

def __init__(self, environment):
super(Locust, self).__init__()
# check if deprecated wait API is used
deprecation.check_for_deprecated_wait_api(self)
self.environment = environment

def on_start(self):
Expand Down
30 changes: 24 additions & 6 deletions locust/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,10 @@ def hatching(self):
@property
def running(self):
return self.get_by_state(STATE_RUNNING)

@property
def missing(self):
return self.get_by_state(STATE_MISSING)

self.clients = WorkerNodesDict()
self.server = rpc.Server(master_bind_host, master_bind_port)
Expand Down Expand Up @@ -462,10 +466,11 @@ def start(self, locust_count, hatch_rate):
self.state = STATE_HATCHING

def stop(self):
self.state = STATE_STOPPING
for client in self.clients.all:
self.server.send_to_client(Message("stop", None, client.id))
self.environment.events.test_stop.fire(environment=self.environment)
if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]:
self.state = STATE_STOPPING
for client in self.clients.all:
self.server.send_to_client(Message("stop", None, client.id))
self.environment.events.test_stop.fire(environment=self.environment)

def quit(self):
if self.state not in [STATE_INIT, STATE_STOPPED, STATE_STOPPING]:
Expand All @@ -476,18 +481,28 @@ def quit(self):
self.server.send_to_client(Message("quit", None, client.id))
gevent.sleep(0.5) # wait for final stats report from all workers
self.greenlet.kill(block=True)

def check_stopped(self):
if not self.state == STATE_INIT and all(map(lambda x: x.state != STATE_RUNNING and x.state != STATE_HATCHING, self.clients.all)):
self.state = STATE_STOPPED


def heartbeat_worker(self):
while True:
gevent.sleep(HEARTBEAT_INTERVAL)
if self.connection_broken:
self.reset_connection()
continue

for client in self.clients.all:
if client.heartbeat < 0 and client.state != STATE_MISSING:
logger.info('Worker %s failed to send heartbeat, setting state to missing.' % str(client.id))
client.state = STATE_MISSING
client.user_count = 0
if self.worker_count - len(self.clients.missing) <= 0:
logger.info("The last worker went missing, stopping test.")
self.stop()
self.check_stopped()
else:
client.heartbeat -= 1

Expand Down Expand Up @@ -547,11 +562,14 @@ def client_listener(self):
if msg.node_id in self.clients:
del self.clients[msg.node_id]
logger.info("Client %r quit. Currently %i clients connected." % (msg.node_id, len(self.clients.ready)))
if self.worker_count - len(self.clients.missing) <= 0:
logger.info("The last worker quit, stopping test.")
self.stop()
elif msg.type == "exception":
self.log_exception(msg.node_id, msg.data["msg"], msg.data["traceback"])

if not self.state == STATE_INIT and all(map(lambda x: x.state != STATE_RUNNING and x.state != STATE_HATCHING, self.clients.all)):
self.state = STATE_STOPPED
self.check_stopped()


@property
def worker_count(self):
Expand Down
16 changes: 11 additions & 5 deletions locust/static/locust.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ $(window).ready(function() {
}
});

$("#box_stop a.stop-button").click(function(event) {
event.preventDefault();
$.get($(this).attr("href"));
$("body").attr("class", "stopped");
function appearStopped() {
$(".box_stop").hide();
$("a.new_test").show();
$("a.edit_test").hide();
$(".user_count").hide();
}

$("#box_stop a.stop-button").click(function(event) {
event.preventDefault();
$.get($(this).attr("href"));
$("body").attr("class", "stopped");
appearStopped()
});

$("#box_stop a.reset-button").click(function(event) {
Expand Down Expand Up @@ -173,6 +177,8 @@ function updateStats() {
rpsChart.addValue([total.current_rps, total.current_fail_per_sec]);
responseTimeChart.addValue([report.current_response_time_percentile_50, report.current_response_time_percentile_95]);
usersChart.addValue([report.user_count]);
} else {
appearStopped();
}

setTimeout(updateStats, 2000);
Expand All @@ -187,4 +193,4 @@ function updateExceptions() {
setTimeout(updateExceptions, 5000);
});
}
updateExceptions();
updateExceptions();
22 changes: 11 additions & 11 deletions locust/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ a:hover {
}
.hatching .boxes .box_running {display: block;}
.running .boxes .box_running {display: block;}
.stopped .boxes .box_running {display: block;}
.stopped .boxes .box_stop {display: none;}
.stopped .boxes .box_running, .stopping .boxes .box_running {display: block;}
.stopped .boxes .box_stop, .stopping .boxes .box_stop {display: none;}

.container {
max-width: 1800px;
Expand Down Expand Up @@ -206,7 +206,7 @@ a:hover {
}


.stopped .start {
.stopped .start, .stopping .start {
display: none;
border-radius: 5px;
-moz-border-radius: 5px;
Expand All @@ -216,7 +216,7 @@ a:hover {
box-shadow: 0 0 60px rgba(0,0,0,0.3);
}

.stopped .edit {display: none;}
.stopped .edit, .stopping .edit {display: none;}
.running .edit, .hatching .edit {
display: none;
border-radius: 5px;
Expand All @@ -232,25 +232,25 @@ a:hover {
.ready .start {display: block;}

.running .status, .hatching .status {display: block;}
.stopped .status {display: block;}
.stopped .status, .stopping .status {display: block;}
.ready .status {display: none;}

.stopped .boxes .edit_test, .ready .boxes .edit_test {display: none;}
.stopped .boxes .user_count, .ready .boxes .user_count {display: none;}
.stopped .boxes .edit_test, .stopping .boxes .edit_test, .ready .boxes .edit_test {display: none;}
.stopped .boxes .user_count, .stopping .boxes .user_count, .ready .boxes .user_count {display: none;}

.running a.new_test, .ready a.new_test, .hatching a.new_test {display: none;}
.running a.new_test {display: none;}
.stopped a.new_test {display: block;}
.stopped a.new_test, .stopping a.new_test {display: block;}

.start a.close_link, .edit a.close_link{
position: absolute;
right: 10px;
top: 10px;
}
.stopped .start a.close_link {display: inline;}
.stopped .start a.close_link, .stopping .start a.close_link {display: inline;}
.running .start a.close_link, .ready .start a.close_link, .hatching .start a.close_link {display: none;}

.stopped .edit a.close_link, .ready .edit a.close_link {display: none;}
.stopped .edit a.close_link, .stopping .edit a.close_link, .ready .edit a.close_link {display: none;}
.running .edit a.close_link, .hatching .edit a.close_link {display: inline;}

.stats_label {
Expand Down Expand Up @@ -461,7 +461,7 @@ ul.tabs li a.current:after {
}

.running .hostname, .hatching .hostname {display: block;}
.stopped .hostname {display: block;}
.stopped .hostname, .stopping .hostname {display: block;}
.ready .hostname {display: none;}

.footer {
Expand Down
7 changes: 4 additions & 3 deletions locust/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ class RequestStats(object):
"""
def __init__(self, use_response_times_cache=True):
"""
The value of use_response_times_cache will be set for each StatsEntry() when they are created.
Settings it to False saves some memory and CPU cycles which we can do on worker nodes where
the response_times_cache is not needed.
:param use_response_times_cache: The value of use_response_times_cache will be set for each StatsEntry()
when they are created. Settings it to False saves some memory and CPU
cycles which we can do on Worker nodes where the response_times_cache
is not needed.
"""
self.use_response_times_cache = use_response_times_cache
self.entries = {}
Expand Down
5 changes: 2 additions & 3 deletions locust/test/test_locust_class.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
from gevent import sleep
from gevent.pool import Group

from locust import InterruptTaskSet, ResponseError
from locust.core import HttpLocust, Locust, TaskSet, task
from locust.exception import InterruptTaskSet, ResponseError
from locust import HttpLocust, Locust, TaskSet, task, between, constant
from locust.env import Environment
from locust.exception import (CatchResponseError, LocustError, RescheduleTask,
RescheduleTaskImmediately, StopLocust)

from locust.wait_time import between, constant
from .testcases import LocustTestCase, WebserverTestCase


Expand Down
5 changes: 2 additions & 3 deletions locust/test/test_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,9 +128,8 @@ def my_task(self):
"--headless",
"--logfile", log_file_path,
], stderr=subprocess.STDOUT).decode("utf-8")
except Exception as e:
import code
code.interact("Error!", local=locals())
except subprocess.CalledProcessError as e:
raise AssertionError("Running locust command failed. Output was:\n\n%s" % e.stdout.decode("utf-8")) from e

with open(log_file_path, encoding="utf-8") as f:
log_content = f.read()
Expand Down
Loading

0 comments on commit 6feb9a7

Please sign in to comment.