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

Add Programs load test #28

Merged
merged 2 commits into from
Oct 28, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .pep8
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[pep8]
# E501: Line too long.
# See: http://pep8.readthedocs.org/en/latest/intro.html#error-codes
ignore=E501
exclude=.git,.pycharm_helpers
7 changes: 3 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,17 @@ language: python
python:
- "2.7"

# use docker container infrastructure
# Use docker container infrastructure.
sudo: false

# Cache the pip directory. "cache: pip" doesn't work due to install override. See https://github.com/travis-ci/travis-ci/issues/3239.
cache:
directories:
- $HOME/.cache/pip
install:
- pip install pip==6.1.0
- pip install -r dev-requirements.txt
- pip install --upgrade pip wheel pep8
script:
- pep8 .
- pep8 --config=.pep8 .
branches:
only:
- master
File renamed without changes.
61 changes: 44 additions & 17 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,28 +1,55 @@
This project is the single source for public load tests for edX software component. New tests should be developed here unless there is a very good reason why that cannot be the case. Old tests should be scrubbed and moved here over time.
edX Load Tests |Travis|_
=========================
.. |Travis| image:: https://travis-ci.org/edx/edx-load-tests.svg?branch=master
.. _Travis: https://travis-ci.org/edx/edx-load-tests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for doing this cleanup.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem.


This repository is home to public load tests for edX software components. New tests should be developed here. Old tests should be scrubbed and moved here over time.

Installation
------------
Getting Started
---------------

mkvirtualenv edx-load-tests
pip install -r locust/requirements.txt
cd locust/$TEST_DIR
locust --host="http://localhost" -f $csm
If you have not already done so, create and activate a `virtualenv <https://virtualenvwrapper.readthedocs.org/en/latest/>`_. Unless otherwise stated, assume all commands below are executed within said virtualenv.

Layout
------
Next, install load testing requirements.

Each set of tasks should be captured as a top-level locustfile named
for the particular set of endpoints being tested. This toplevel file
can be a flat python file (lms.py) or a directory (csm/). In the case
of a directory, the __init__.py file should have (or import) the Locust
subclass that defines the test.
.. code-block:: bash

$ pip install -r requirements.txt

Start Locust by providing the Locust CLI with a target host and pointing it to the location of your desired locustfile. For example,

.. code-block:: bash

$ locust --host=http://localhost:8009 -f programs

Repository Structure
--------------------

Tests are organized into top-level packages. For examples, see ``csm`` and ``programs``. A module called ``locustfile.py`` is included inside each test package, within which a subclass of the `Locust class <http://docs.locust.io/en/latest/writing-a-locustfile.html#the-locust-class>`_ is defined. This subclass is imported into the test package's ``__init__.py`` to facilitate discovery at runtime.

License
-------

The code in this repository is licensed under version 3 of the AGPL
unless otherwise noted. Please see the `LICENSE`_ file for details.
The code in this repository is licensed under the AGPLv3 unless otherwise noted. Please see `LICENSE.txt <https://github.com/edx/edx-load-tests/blob/master/LICENSE.txt>`_ for details.

How To Contribute
-----------------

Contributions are very welcome.

Please read `How To Contribute <https://github.com/edx/edx-platform/blob/master/CONTRIBUTING.rst>`_ for details.

Even though they were written with ``edx-platform`` in mind, the guidelines
should be followed for Open edX code in general.

Reporting Security Issues
-------------------------

Please do not report security issues in public. Please email security@edx.org.

Mailing List and IRC Channel
----------------------------

.. _LICENSE: https://github.com/edx/edx-load-tests/blob/master/LICENSE
You can discuss this code in the `edx-code Google Group`__ or in the ``#edx-code`` IRC channel on Freenode.

__ https://groups.google.com/forum/#!forum/edx-code
3 changes: 0 additions & 3 deletions dev-requirements.txt

This file was deleted.

58 changes: 58 additions & 0 deletions helpers/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Pair Locust and Slumber to allow easier load testing of REST APIs.

See: http://www.renzolucioni.com/pairing-locust-and-slumber/.
"""
from edx_rest_api_client import exceptions
from edx_rest_api_client.client import EdxRestApiClient
import requests
import slumber


class LocustResource(slumber.Resource):
"""Custom Slumber Resource which takes advantage of Locust's extended HttpSession."""
def _request(self, method, data=None, files=None, params=None):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional: add a **kwargs to the signature, to insulate from errors in case an update of slumber modifies the signature of this non-public method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jimabramson instead of silencing that kind of error, wouldn't we prefer this to fail loudly so we can investigate the change to Slumber and modify this accordingly?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rlucioni actually since this isn't a user-facing system I agree, that makes more sense.

serializer = self._store['serializer']
url = self.url()

headers = {'accept': serializer.get_content_type()}

if not files:
headers['content-type'] = serializer.get_content_type()
if data is not None:
data = serializer.dumps(data)

# An optional argument that can be used to specify a label to use in Locust's statistics
# instead of the actual URL. Can be used to group requests to the same API endpoint that vary
# only by resource ID included in the URL into a single entry in Locust's statistics. For example,
# requests to
# 'http://localhost:8002/api/v1/resource/1/'
# and
# 'http://localhost:8002/api/v1/resource/2/'
# might be grouped under the name:
# '/api/v1/resource/:id/'
# See: http://docs.locust.io/en/latest/api.html#httpsession-class/.
name = params.pop('name', None)

resp = self._store['session'].request(
method,
url,
data=data,
params=params,
files=files,
headers=headers,
name=name
)

if 400 <= resp.status_code <= 499:
exception_class = exceptions.HttpNotFoundError if resp.status_code == 404 else exceptions.HttpClientError
raise exception_class('Client Error %s: %s' % (resp.status_code, url), response=resp, content=resp.content)
elif 500 <= resp.status_code <= 599:
raise exceptions.HttpServerError('Server Error %s: %s' % (resp.status_code, url), response=resp, content=resp.content)

self._ = resp

return resp


class LocustEdxRestApiClient(EdxRestApiClient):
resource_class = LocustResource
38 changes: 38 additions & 0 deletions programs/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Programs Load Testing
=====================

This directory contains Locust tasks designed to exercise the `edX Programs Service <https://github.com/edx/programs>`_.

Getting Started
---------------

At this point, these load tests only require a running instance of the Programs service.

Configuration
-------------

The load tests rely on configuration which can be specified using environment variables.

==================== ========= ===========================================
Variable Required? Description
==================== ========= ===========================================
PROGRAMS_SERVICE_URL No URL root for the Programs service
PROGRAMS_API_URL No URL root for the Programs API
JWT_AUDIENCE No JWT audience claim (aud)
JWT_ISSUER No JWT issuer claim (iss)
JWT_EXPIRATION_DELTA No Number of days before generated JWTs expire
JWT_SECRET_KEY Yes Secret key used to sign JWTs

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in practice this will be the oauth2 client secret key

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but the Programs settings still require setting a value for JWT_SECRET_KEY, a name which I think makes more sense in this context. It should be pretty easy to track down the right value for this setting if you look at the settings on your Programs instance.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok.

==================== ========= ===========================================

If you want to use the defaults provided for these variables, make sure that these defaults are configured for your instance of the Programs service.

Running
-------

You can run the Programs load tests from the top level ``edx-load-tests`` directory by executing something like the following:

.. code-block:: bash

$ JWT_SECRET_KEY=replace-me locust --host=http://localhost:8009/ -f programs

As of this writing, there is a bug in Locust preventing tests from accessing hosts using SSL. This is attributable to an `issue with gevent <https://github.com/gevent/gevent/issues/477>`_ that appears to have been fixed in 1.0.2. However, Locust still `requires <https://github.com/locustio/locust/blob/master/setup.py#L50>`_ the broken 1.0.1. To get around this, use ``http`` as the protocol in the host URL, not ``https``.
1 change: 1 addition & 0 deletions programs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from programs.locustfile import ProgramsUser
26 changes: 26 additions & 0 deletions programs/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Configuration for Programs load testing."""
import os


# URL CONFIGURATION
PROGRAMS_SERVICE_URL = os.environ.get(
'PROGRAMS_SERVICE_URL',
'http://localhost:8004'
).strip('/')

PROGRAMS_API_URL = os.environ.get(
'PROGRAMS_API_URL',
'{}/api/v1/'.format(PROGRAMS_SERVICE_URL)
)
# END URL CONFIGURATION


# JWT CONFIGURATION
JWT_AUDIENCE = os.environ.get('JWT_AUDIENCE', 'replace-me')
JWT_ISSUER = os.environ.get('JWT_ISSUER', 'http://127.0.0.1:8000/oauth2')
JWT_EXPIRATION_DELTA = int(os.environ.get('JWT_EXPIRATION_DELTA', 1))
JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY')

if not JWT_SECRET_KEY:
raise RuntimeError('A JWT secret key is required to run Programs load tests.')
# END JWT CONFIGURATION
57 changes: 57 additions & 0 deletions programs/locustfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import datetime
import uuid

import jwt
from locust import TaskSet, task, HttpLocust
from locust.clients import HttpSession
from locust.exception import LocustError

from helpers.api import LocustEdxRestApiClient
from programs.config import PROGRAMS_API_URL, JWT_AUDIENCE, JWT_ISSUER, JWT_EXPIRATION_DELTA, JWT_SECRET_KEY


class ProgramsTaskSet(TaskSet):
"""Tasks exercising Programs functionality."""
@task
def list_programs(self):
self.client.programs.get()


class ProgramsUser(HttpLocust):
"""Representation of an HTTP "user" to be hatched.

Hatched users will be used to attack the system being load tested. This class
defines which TaskSet class should define the user's behavior and how long a simulated
user should wait between executing tasks. This class also provides a custom client used
to interface with edX REST APIs.
"""
USERNAME_PREFIX = 'load-test-'

task_set = ProgramsTaskSet
min_wait = 3 * 1000
max_wait = 5 * 1000

def __init__(self):
super(ProgramsUser, self).__init__()

if not self.host:
raise LocustError(
'You must specify a base host, either in the host attribute in the Locust class, '
'or on the command line using the --host option.'
)

self.client = LocustEdxRestApiClient(
PROGRAMS_API_URL,
session=HttpSession(base_url=self.host),
jwt=self._get_token()
)

def _get_token(self):
payload = {
'preferred_username': self.USERNAME_PREFIX + str(uuid.uuid4()),
'iss': JWT_ISSUER,
'aud': JWT_AUDIENCE,
'exp': datetime.datetime.utcnow() + datetime.timedelta(days=JWT_EXPIRATION_DELTA),
}

return jwt.encode(payload, JWT_SECRET_KEY)
8 changes: 1 addition & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
# Packages needed to run any loadtest in this repo

-e git+https://github.com/edx/locust.git@edx#egg=locustio[scipy]
-e .
jupyter
runipy
seaborn
-r requirements/dev.txt
6 changes: 6 additions & 0 deletions requirements/base.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Packages required to run load tests
-e git+https://github.com/edx/locust.git@edx#egg=locustio[scipy]
edx-rest-api-client==1.2.1
jupyter
runipy
seaborn
4 changes: 4 additions & 0 deletions requirements/dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Packages required to develop load tests
-r base.txt

pep8==1.6.2
5 changes: 0 additions & 5 deletions setup.cfg

This file was deleted.

13 changes: 0 additions & 13 deletions setup.py

This file was deleted.