Skip to content
This repository has been archived by the owner on Aug 28, 2023. It is now read-only.

Commit

Permalink
Merge pull request #281 from flibbertigibbet/feature/load-testing
Browse files Browse the repository at this point in the history
Feature/load testing
  • Loading branch information
flibbertigibbet authored Feb 22, 2017
2 parents d496b14 + e292640 commit 366b4a4
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 0 deletions.
22 changes: 22 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ Run Django tests with::
./scripts/console django './manage.py test --settings climate_change_api.settings_test'


Load Testing
------------

The ``loadtest`` Docker container can be used to test API query response times using `locust <http://locust.io/>`_.

First set the environment variable ``API_TOKEN`` within the VM to a valid user token with::

export API_TOKEN=<user token>

Optionally, the target server to test may be configured to target the local instance with::

export API_HOST=http://localhost:8082

By default, the staging server will be targeted.

Then start the Docker container with::

docker-compose up loadtest

Naviagate to http://localhost:8089 and start tests by setting the swarm and hatch rate (1 for each is fine). To stop tests, click the red button in the web UI (or halt the container).


Documentation
-------------

Expand Down
3 changes: 3 additions & 0 deletions Vagrantfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ Vagrant.configure(2) do |config|
config.vm.network :forwarded_port, guest: 8080, host: Integer(ENV.fetch("CC_PORT_8080", 8080))
config.vm.network :forwarded_port, guest: 8088, host: Integer(ENV.fetch("CC_PORT_8088", 8088))

# locust port
config.vm.network :forwarded_port, guest: 8089, host: Integer(ENV.fetch("CC_PORT_8080", 8089))

# django runserver/debugging
config.vm.network :forwarded_port, guest: 8082, host: Integer(ENV.fetch("CC_PORT_8082", 8082))

Expand Down
12 changes: 12 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,15 @@ services:
- ./nginx/etc/nginx/nginx.conf:/etc/nginx/nginx.conf
- ./nginx/etc/nginx/includes/:/etc/nginx/includes/
- ./nginx/etc/nginx/conf.d/app/:/etc/nginx/conf.d/

loadtest:
build:
context: ./loadtest
dockerfile: Dockerfile
ports:
- "8089:8089"
environment:
- API_TOKEN
- API_HOST=https://api.staging.futurefeelslike.com
volumes:
- .:/opt
22 changes: 22 additions & 0 deletions loadtest/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM python:3-slim

MAINTAINER Azavea

RUN apt-get update && apt-get install -y --no-install-recommends \
ipython \
python3-pip \
build-essential \
&& rm -rf /var/lib/apt/lists/*

COPY requirements.txt /tmp/
RUN pip3 install --upgrade pip && pip3 install --no-cache-dir -r /tmp/requirements.txt

COPY ./ /opt/loadtest

WORKDIR /opt/loadtest

EXPOSE 8089

ENTRYPOINT ["/bin/bash"]

CMD ["-c", "locust --host=${API_HOST}"]
117 changes: 117 additions & 0 deletions loadtest/locustfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/python3

from functools import partial
import os
import uuid

from locust import HttpLocust, TaskSet, task

CITY_ID = os.getenv('LOAD_TEST_CITY_ID') or 1
SCENARIO = os.getenv('LOAD_TEST_SCENARIO') or 'RCP85'

DEFAULT_THRESHOLD_PARAMS = {
'threshold': 100,
'threshold_comparator': 'lte',
'threshold_units': 'F'
}


def general_indicator_query(locust_object, indicator, params):
# append a randomized extra parameter to the query to defeat caching
params['random'] = uuid.uuid4()
locust_object.client.get('/api/climate-data/{city}/{scenario}/indicator/{indicator}'.format(
city=CITY_ID, scenario=SCENARIO, indicator=indicator),
headers=locust_object.headers,
params=params,
name='{indicator} {agg}'.format(indicator=indicator,
agg=params.get('time_aggregation', '')))


class UserBehavior(TaskSet):

def get_api_url(self, url):
""" Helper for querying API with authorization header and random parameter to defeat cache
"""
self.client.get(url, headers=self.headers, params={'random': uuid.uuid4()}, name=url)

def build_indicator_queries(self):
"""
Add a load test query for each indciator / time aggregation
"""
indicators = self.client.get('/api/indicator/',
headers=self.headers).json()
for indicator in indicators:
indicator_name = indicator['name']
if indicator_name.endswith('threshold'):
params = DEFAULT_THRESHOLD_PARAMS
if indicator_name.find('precepitation') > -1:
params['threshold_units'] = 'in'
self.tasks.append(partial(general_indicator_query,
indicator=indicator_name,
params=params))
else:
# for non-threshold indicators, test all non-custom aggregation levels
for agg in indicator['valid_aggregations']:
if agg != 'custom':
self.tasks.append(partial(general_indicator_query,
indicator=indicator_name,
params={'time_aggregation': agg}))

def on_start(self):
""" on_start is called when a Locust start before any task is scheduled """
print('starting locust...')
token = os.getenv('API_TOKEN')
if not token:
raise ValueError('Must set API_TOKEN on environment to run load tests.')
self.headers = {'Authorization': 'Token {token}'.format(token=token)}
print('adding indciator queries...')
indicator_tasks = TaskSet(self)
self.build_indicator_queries()
print('ready to go!')

@task(1)
def index(self):
self.get_api_url('/')

@task(1)
def api_main(self):
self.get_api_url('/api/')

@task(1)
def scenarios(self):
self.get_api_url('/api/scenario/')

@task(1)
def scenario_details(self):
self.get_api_url('/api/scenario/{scenario}/'.format(scenario=SCENARIO))

@task(1)
def cities(self):
self.get_api_url('/api/city/')

@task(1)
def city_data(self):
self.get_api_url('/api/climate-data/{city}/{scenario}/'.format(city=CITY_ID,
scenario=SCENARIO))

@task(1)
def projects(self):
self.get_api_url('/api/project/')

@task(1)
def climate_models(self):
self.get_api_url('/api/climate-model/')

@task(1)
def climate_model_detail(self):
self.get_api_url('/api/climate-model/ACCESS1-0/')

@task(1)
def indicator_list(self):
self.get_api_url('/api/indicator/')


class WebsiteUser(HttpLocust):
task_set = UserBehavior
min_wait = 5000
max_wait = 9000
3 changes: 3 additions & 0 deletions loadtest/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
locustio==0.8a2
ipdb==0.10.2
ipython==5.2.2

0 comments on commit 366b4a4

Please sign in to comment.