Skip to content

Commit

Permalink
Merge pull request #1 from locustio/master
Browse files Browse the repository at this point in the history
Sync with fork
  • Loading branch information
ajt89 authored Oct 27, 2019
2 parents 64f96aa + 784fd20 commit 88168a3
Show file tree
Hide file tree
Showing 21 changed files with 475 additions and 162 deletions.
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Release Process

* Run github_changelog_generator to update `CHANGELOG.md`
- `github_changelog_generator -u Locustio -p Locust -t $CHANGELOG_GITHUB_TOKEN`
- `github_changelog_generator -t $CHANGELOG_GITHUB_TOKEN locustio/locust`
* Add highlights to changelog in docs: `locust/docs/changelog.rst`
* Update `locust/__init__.py` with new version number: `__version__ = "VERSION"`
* Tag master as "VERSION" in git
Expand Down
68 changes: 33 additions & 35 deletions CHANGELOG.md

Large diffs are not rendered by default.

204 changes: 109 additions & 95 deletions docs/changelog.rst

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions docs/running-locust-distributed.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ processor core, on the slave machines.
Both the master and each slave machine, must have a copy of the locust test scripts
when running Locust distributed.

.. note::
It's recommended that you start a number of simulated users that are greater than
``number of locust classes * number of slaves`` when running Locust distributed.

Otherwise - due to the current implementation -
you might end up with a distribution of the Locust classes that doesn't correspond to the
Locust classes' ``weight`` attribute. And if the hatch rate is lower than the number of slave
nodes, the hatching would occur in "bursts" where all slave node would hatch a single user and
then sleep for multiple seconds, hatch another user, sleep and repeat.


Example
=======
Expand Down
6 changes: 3 additions & 3 deletions docs/writing-a-locustfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ One can mark requests as failed, even when the response code is OK, by using the

.. code-block:: python
with client.get("/", catch_response=True) as response:
with self.client.get("/", catch_response=True) as response:
if response.content != b"Success":
response.failure("Got wrong response")
Expand All @@ -431,7 +431,7 @@ be reported as a success in the statistics:

.. code-block:: python
with client.get("/does_not_exist/", catch_response=True) as response:
with self.client.get("/does_not_exist/", catch_response=True) as response:
if response.status_code == 404:
response.success()
Expand All @@ -450,7 +450,7 @@ Example:
# Statistics for these requests will be grouped under: /blog/?id=[id]
for i in range(10):
client.get("/blog?id=%i" % i, name="/blog?id=[id]")
self.client.get("/blog?id=%i" % i, name="/blog?id=[id]")
Common libraries
=================
Expand Down
24 changes: 24 additions & 0 deletions examples/fast_http_locust.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from locust import HttpLocust, TaskSet, task
from locust.contrib.fasthttp import FastHttpLocust


class UserTasks(TaskSet):
@task
def index(self):
self.client.get("/")

@task
def stats(self):
self.client.get("/stats/requests")


class WebsiteUser(FastHttpLocust):
"""
Locust user class that does requests to the locust web server running on localhost,
using the fast HTTP client
"""
host = "http://127.0.0.1:8089"
min_wait = 2000
max_wait = 5000
task_set = UserTasks

26 changes: 26 additions & 0 deletions examples/nested_inline_tasksets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from locust import HttpLocust, TaskSet, task


class WebsiteUser(HttpLocust):
"""
Example of the ability of inline nested TaskSet classes
"""
host = "http://127.0.0.1:8089"
min_wait = 2000
max_wait = 5000

class task_set(TaskSet):
@task
class IndexTaskSet(TaskSet):
@task(10)
def index(self):
self.client.get("/")

@task(1)
def stop(self):
self.interrupt()

@task
def stats(self):
self.client.get("/stats/requests")

2 changes: 1 addition & 1 deletion locust/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .core import HttpLocust, Locust, TaskSet, TaskSequence, task, seq_task
from .exception import InterruptTaskSet, ResponseError, RescheduleTaskImmediately

__version__ = "0.12.1"
__version__ = "0.12.2"
8 changes: 7 additions & 1 deletion locust/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from .clients import HttpSession
from .exception import (InterruptTaskSet, LocustError, RescheduleTask,
RescheduleTaskImmediately, StopLocust)
from .runners import STATE_CLEANUP
from .runners import STATE_CLEANUP, LOCUST_STATE_RUNNING, LOCUST_STATE_STOPPING, LOCUST_STATE_WAITING
logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -134,6 +134,7 @@ class Locust(object):
_setup_has_run = False # Internal state to see if we have already run
_teardown_is_set = False # Internal state to see if we have already run
_lock = gevent.lock.Semaphore() # Lock to make sure setup is only run once
_state = False

def __init__(self):
super(Locust, self).__init__()
Expand Down Expand Up @@ -360,6 +361,9 @@ def run(self, *args, **kwargs):

while (True):
try:
if self.locust._state == LOCUST_STATE_STOPPING:
raise GreenletExit()

if self.locust.stop_timeout is not None and time() - self._time_start > self.locust.stop_timeout:
return

Expand Down Expand Up @@ -432,7 +436,9 @@ def get_wait_secs(self):
return millis / 1000.0

def wait(self):
self.locust._state = LOCUST_STATE_WAITING
self._sleep(self.get_wait_secs())
self.locust._state = LOCUST_STATE_RUNNING

def _sleep(self, seconds):
gevent.sleep(seconds)
Expand Down
9 changes: 9 additions & 0 deletions locust/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ def parse_options():
help="sets the exit code to post on error"
)

parser.add_argument(
'-s', '--stop-timeout',
action='store',
type=int,
dest='stop_timeout',
default=None,
help="number of seconds to wait for a simulated user to complete any executing task before exiting. Default is to terminate immediately."
)

parser.add_argument(
'locust_classes',
nargs='*',
Expand Down
6 changes: 5 additions & 1 deletion locust/rpc/zmqrpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ def recv_from_client(self):
class Server(BaseSocket):
def __init__(self, host, port):
BaseSocket.__init__(self, zmq.ROUTER)
self.socket.bind("tcp://%s:%i" % (host, port))
if port == 0:
self.port = self.socket.bind_to_random_port("tcp://%s" % host)
else:
self.socket.bind("tcp://%s:%i" % (host, port))
self.port = port

class Client(BaseSocket):
def __init__(self, host, port, identity):
Expand Down
17 changes: 14 additions & 3 deletions locust/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
STATE_INIT, STATE_HATCHING, STATE_RUNNING, STATE_CLEANUP, STATE_STOPPING, STATE_STOPPED, STATE_MISSING = ["ready", "hatching", "running", "cleanup", "stopping", "stopped", "missing"]
SLAVE_REPORT_INTERVAL = 3.0

LOCUST_STATE_RUNNING, LOCUST_STATE_WAITING, LOCUST_STATE_STOPPING = ["running", "waiting", "stopping"]

class LocustRunner(object):
def __init__(self, locust_classes, options):
Expand Down Expand Up @@ -125,12 +126,13 @@ def hatch():

locust = bucket.pop(random.randint(0, len(bucket)-1))
occurrence_count[locust.__name__] += 1
new_locust = locust()
def start_locust(_):
try:
locust().run(runner=self)
new_locust.run(runner=self)
except GreenletExit:
pass
new_locust = self.locusts.spawn(start_locust, locust)
self.locusts.spawn(start_locust, new_locust)
if len(self.locusts) % 10 == 0:
logger.debug("%i locusts hatched" % len(self.locusts))
gevent.sleep(sleep_time)
Expand All @@ -151,7 +153,7 @@ def kill_locusts(self, kill_count):
dying = []
for g in self.locusts:
for l in bucket:
if l == g.args[0]:
if l == type(g.args[0]):
dying.append(g)
bucket.remove(l)
break
Expand Down Expand Up @@ -193,6 +195,15 @@ def stop(self):
# if we are currently hatching locusts we need to kill the hatching greenlet first
if self.hatching_greenlet and not self.hatching_greenlet.ready():
self.hatching_greenlet.kill(block=True)
if self.options.stop_timeout:
for locust_greenlet in self.locusts:
locust = locust_greenlet.args[0]
if locust._state == LOCUST_STATE_WAITING:
locust_greenlet.kill()
else:
locust._state = LOCUST_STATE_STOPPING
if not self.locusts.join(timeout=self.options.stop_timeout):
logger.info("Not all locusts finished their tasks & terminated in %s seconds. Killing them..." % self.options.stop_timeout)
self.locusts.kill(block=True)
self.state = STATE_STOPPED
events.locust_stop_hatching.fire()
Expand Down
1 change: 1 addition & 0 deletions locust/static/locust.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ var report;

function renderTable(report) {
var totalRow = report.stats.pop();
totalRow.is_aggregated = true;
var sortedStats = (report.stats).sort(sortBy(sortAttribute, desc));
sortedStats.push(totalRow);
$('#stats tbody').empty();
Expand Down
26 changes: 21 additions & 5 deletions locust/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ def calculate_response_time_percentile(response_times, num_requests, percent):
processed_count += response_times[response_time]
if(num_requests - processed_count <= num_of_request):
return response_time
# if all response times were None
return 0


def diff_response_time_dicts(latest, old):
Expand All @@ -74,13 +76,17 @@ class RequestStats(object):
def __init__(self):
self.entries = {}
self.errors = {}
self.total = StatsEntry(self, "Total", None, use_response_times_cache=True)
self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=True)
self.start_time = None

@property
def num_requests(self):
return self.total.num_requests

@property
def num_none_requests(self):
return self.total.num_none_requests

@property
def num_failures(self):
return self.total.num_failures
Expand Down Expand Up @@ -129,7 +135,7 @@ def clear_all(self):
"""
Remove all stats entries and errors
"""
self.total = StatsEntry(self, "Total", None, use_response_times_cache=True)
self.total = StatsEntry(self, "Aggregated", None, use_response_times_cache=True)
self.entries = {}
self.errors = {}
self.start_time = None
Expand All @@ -155,6 +161,9 @@ class StatsEntry(object):
num_requests = None
""" The number of requests made """

num_none_requests = None
""" The number of requests made with a None response time (typically async requests) """

num_failures = None
""" Number of failed request """

Expand Down Expand Up @@ -214,6 +223,7 @@ def __init__(self, stats, name, method, use_response_times_cache=False):
def reset(self):
self.start_time = time.time()
self.num_requests = 0
self.num_none_requests = 0
self.num_failures = 0
self.total_response_time = 0
self.response_times = {}
Expand Down Expand Up @@ -246,6 +256,9 @@ def _log_time_of_request(self, t):
self.last_request_timestamp = t

def _log_response_time(self, response_time):
if response_time is None:
self.num_none_requests += 1
return

self.total_response_time += response_time

Expand Down Expand Up @@ -287,15 +300,15 @@ def fail_ratio(self):
@property
def avg_response_time(self):
try:
return float(self.total_response_time) / self.num_requests
return float(self.total_response_time) / (self.num_requests - self.num_none_requests)
except ZeroDivisionError:
return 0

@property
def median_response_time(self):
if not self.response_times:
return 0
median = median_from_dict(self.num_requests, self.response_times)
median = median_from_dict(self.num_requests - self.num_none_requests, self.response_times) or 0

# Since we only use two digits of precision when calculating the median response time
# while still using the exact values for min and max response times, the following checks
Expand Down Expand Up @@ -340,6 +353,7 @@ def extend(self, other):
self.start_time = min(self.start_time, other.start_time)

self.num_requests = self.num_requests + other.num_requests
self.num_none_requests = self.num_none_requests + other.num_none_requests
self.num_failures = self.num_failures + other.num_failures
self.total_response_time = self.total_response_time + other.total_response_time
self.max_response_time = max(self.max_response_time, other.max_response_time)
Expand All @@ -362,6 +376,7 @@ def serialize(self):
"last_request_timestamp": self.last_request_timestamp,
"start_time": self.start_time,
"num_requests": self.num_requests,
"num_none_requests": self.num_none_requests,
"num_failures": self.num_failures,
"total_response_time": self.total_response_time,
"max_response_time": self.max_response_time,
Expand All @@ -378,6 +393,7 @@ def unserialize(cls, data):
"last_request_timestamp",
"start_time",
"num_requests",
"num_none_requests",
"num_failures",
"total_response_time",
"max_response_time",
Expand Down Expand Up @@ -633,7 +649,7 @@ def print_stats(stats):
except ZeroDivisionError:
fail_percent = 0

console_logger.info((" %-" + str(STATS_NAME_WIDTH) + "s %7d %12s %42.2f") % ('Total', total_reqs, "%d(%.2f%%)" % (total_failures, fail_percent), total_rps))
console_logger.info((" %-" + str(STATS_NAME_WIDTH) + "s %7d %12s %42.2f") % ('Aggregated', total_reqs, "%d(%.2f%%)" % (total_failures, fail_percent), total_rps))
console_logger.info("")

def print_percentile_stats(stats):
Expand Down
4 changes: 2 additions & 2 deletions locust/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -217,9 +217,9 @@ <h2>Version <a href="https://github.com/locustio/locust/releases/tag/{{version}}
<script type="text/javascript" src="./static/vintage.js"></script>
<script type="text/x-jqote-template" id="stats-template">
<![CDATA[
<tr class="<%=(alternate ? "dark" : "")%> <%=(this.name == "Total" ? "total" : "")%>">
<tr class="<%=(alternate ? "dark" : "")%> <%=(this.is_aggregated ? "total" : "")%>">
<td><%= (this.method ? this.method : "") %></td>
<td class="name" title="<%= this.name %>"><%= this.name %></td>
<td class="name" title="<%= this.name %>"><%= this.safe_name %></td>
<td class="numeric"><%= this.num_requests %></td>
<td class="numeric"><%= this.num_failures %></td>
<td class="numeric"><%= Math.round(this.median_response_time) %></td>
Expand Down
Loading

0 comments on commit 88168a3

Please sign in to comment.