-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Gunicorn plumbing #357
Merged
Gunicorn plumbing #357
Changes from 7 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
a13a8f5
port notify load test from #354.
c0c0n3 960a148
make sure load test uses latest ql code and removes containers to avo…
c0c0n3 6b84e35
port gunicorn setup from #354.
c0c0n3 4c6f1b1
get rid of supervisord.
c0c0n3 076d880
move gunicorn dep to pipfile; flexible wgsi launcher; use gthread ins…
c0c0n3 92eb940
make test scripts give crate enough time to get 100% functional befor…
c0c0n3 8bc1d6e
polish gunicorn runner; make config easily overridable in docker cont…
c0c0n3 4815ff1
skip broken geocoding test as it has nothing to do w/ this pr.
c0c0n3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,15 @@ | ||
from utils.hosts import LOCAL | ||
import server.wsgi as flask | ||
import server.grunner as gunicorn | ||
from utils.cfgreader import EnvReader, BoolVar | ||
|
||
|
||
def use_flask() -> bool: | ||
env_var = BoolVar('USE_FLASK', False) | ||
return EnvReader().safe_read(env_var) | ||
|
||
|
||
if __name__ == '__main__': | ||
import connexion | ||
app = connexion.FlaskApp(__name__, specification_dir='../specification/') | ||
app.add_api('quantumleap.yml', | ||
arguments={'title': 'QuantumLeap V2 API'}, | ||
pythonic_params=True, | ||
# validate_responses=True, strict_validation=True | ||
) | ||
app.run(host=LOCAL, port=8668) | ||
if use_flask(): # dev mode, run the WSGI app in Flask dev server | ||
flask.run() | ||
else: # prod mode, run the WSGI app in Gunicorn | ||
gunicorn.run() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
|
||
DEFAULT_HOST = '0.0.0.0' # bind to all available network interfaces | ||
DEFAULT_PORT = 8668 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
# | ||
# Gunicorn settings to run QuantumLeap. | ||
# To make configuration more manageable, we keep all our Gunicorn settings | ||
# in this file and start Gunincorn from the Docker container without any | ||
# command line args except for the app module and the path to this config | ||
# file, e.g. | ||
# | ||
# gunicorn server.wsgi --config server/gconfig.py | ||
# | ||
# Settings spec: | ||
# - https://docs.gunicorn.org/en/stable/settings.html | ||
# | ||
|
||
import multiprocessing | ||
|
||
import server | ||
|
||
|
||
# | ||
# Server config section. | ||
# | ||
|
||
bind = f"{server.DEFAULT_HOST}:{server.DEFAULT_PORT}" | ||
|
||
|
||
# | ||
# Worker processes config section. | ||
# Read: https://docs.gunicorn.org/en/latest/design.html | ||
# | ||
|
||
|
||
# Number of worker processes for handling requests. | ||
# We set it to the max Gunicorn recommends. | ||
workers = multiprocessing.cpu_count() * 4 + 1 | ||
|
||
# QuantumLeap does alot of network IO, so we configure worker processes | ||
# to use multi-threading (`gthread`) to improve performance. With this | ||
# setting, each request gets handled in its own thread taken from a | ||
# thread pool. | ||
# In our tests, the `gthread` worker type had better throughput and | ||
# latency than `gevent` but `gevent` used up less memory, most likely | ||
# because of the difference in actual OS threads. So for now we go with | ||
# `gthread` and a low number of threads. This has the advantage of better | ||
# performance, reasonable memory consumption, and keeps us from accidentally | ||
# falling into the `gevent` monkey patching rabbit hole. Also notice that | ||
# according to Gunicorn docs, when using `gevent`, Psycopg (Timescale | ||
# driver) needs psycogreen properly configured to take full advantage | ||
# of async IO. (Not sure what to do for the Crate driver!) | ||
worker_class = 'gthread' | ||
|
||
# The size of each process's thread pool. | ||
# So here's the surprise. In our tests, w/r/t to throughput `gthread` | ||
# outperformed `gevent`---27% better. Latency was pretty much the same | ||
# though. But the funny thing is that we used exactly the same number | ||
# of worker processes and the default number of threads per process, | ||
# which is, wait for it, 1. Yes, 1. | ||
# | ||
# TODO: proper benchmarking. | ||
# We did some initial quick & dirty benchmarking to get these results. | ||
# We'll likely have to measure better and also understand better the | ||
# way the various Gunicorn worker types actually work. (Pun intended.) | ||
threads = 1 | ||
|
||
|
||
# | ||
# Logging config section. | ||
# | ||
|
||
loglevel = 'debug' | ||
|
||
|
||
# TODO: other settings. | ||
# Review gunicorn default settings with an eye on security and performance. | ||
# We might need to set more options than the above. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
import sys | ||
from typing import Any, Dict | ||
|
||
from gunicorn.app.base import Application | ||
from gunicorn.config import make_settings | ||
|
||
import server.gconfig | ||
from server.wsgi import application | ||
|
||
|
||
def quantumleap_base_config() -> Dict[str, Any]: | ||
""" | ||
Read the base QuantumLeap configuration from the ``server.gconfig`` | ||
module. | ||
|
||
:return: the dictionary with the base QuantumLeap settings. | ||
""" | ||
gunicorn_setting_names = [k for k, v in make_settings().items()] | ||
server_config_vars = vars(server.gconfig).items() | ||
return { | ||
k: v | ||
for k, v in server_config_vars if k in gunicorn_setting_names | ||
} | ||
|
||
|
||
class GuantumLeap(Application): | ||
""" | ||
Gunicorn server runner. | ||
|
||
This class is a fully-fledged Gunicorn server WSGI runner, just like | ||
``WSGIApplication`` from ``gunicorn.app.wsgiapp``, except the WSGI | ||
app to run is fixed (QuantumLeap) and the content of ``server.gconfig`` | ||
is used as initial configuration. Notice you can override these base | ||
config settings using CLI args or/and a config file as you'd normally | ||
do with Gunicorn, but you can't run any WSGI app other than QuantumLeap. | ||
""" | ||
|
||
def init(self, parser, opts, args): | ||
return quantumleap_base_config() | ||
|
||
def load(self): | ||
return application | ||
|
||
|
||
def run(): | ||
""" | ||
Start a fully-fledged Gunicorn server to run QuantumLeap. | ||
|
||
If you pass no CLI args, this is the same as running | ||
|
||
``$ gunicorn server.wsgi --config server/gconfig.py`` | ||
|
||
which starts Gunicorn to run the QuantumLeap WSGI Flask app with | ||
the Gunicorn settings in our ``server.gconfig`` module. | ||
You can specify any valid Gunicorn CLI option and it'll take | ||
precedence over any setting with the same name in ``server.gconfig``. | ||
Also you can specify a different config module and options in that | ||
module will override those with the same name in ``server.gconfig``. | ||
The only thing you won't be able to change is the WSGI app to run, | ||
QuantumLeap. | ||
""" | ||
gunicorn = GuantumLeap('%(prog)s [OPTIONS] [APP_MODULE]') # (1) | ||
sys.argv[0] = 'quantumleap' # (2) | ||
sys.exit(gunicorn.run()) # (2) | ||
|
||
# NOTE. | ||
# 1. We keep the same usage as in `gunicorn.app.wsgiapp.run`. | ||
# 2. We basically start Gunicorn in the same way as the Gunicorn launcher | ||
# would: | ||
# | ||
# $ cat $(which gunicorn) | ||
# | ||
# #!/Users/andrea/.local/share/virtualenvs/ngsi-timeseries-api-MeJ80LMF/bin/python | ||
# # -*- coding: utf-8 -*- | ||
# import re | ||
# import sys | ||
# from gunicorn.app.wsgiapp import run | ||
# if __name__ == '__main__': | ||
# sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) | ||
# sys.exit(run()) | ||
# |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
from connexion import FlaskApp | ||
|
||
import server | ||
|
||
|
||
SPEC_DIR = '../../specification/' | ||
SPEC = 'quantumleap.yml' | ||
|
||
|
||
def new_wrapper() -> FlaskApp: | ||
""" | ||
Factory function to build a Connexion wrapper to manage the Flask | ||
application in which QuantumLeap runs. | ||
|
||
:return: the Connexion wrapper. | ||
""" | ||
wrapper = FlaskApp(__name__, | ||
specification_dir=SPEC_DIR) | ||
wrapper.add_api(SPEC, | ||
arguments={'title': 'QuantumLeap V2 API'}, | ||
pythonic_params=True, | ||
# validate_responses=True, strict_validation=True | ||
) | ||
return wrapper | ||
|
||
|
||
quantumleap = new_wrapper() | ||
""" | ||
Singleton Connexion wrapper that manages the QuantumLeap Flask app. | ||
""" | ||
|
||
application = quantumleap.app | ||
""" | ||
The WSGI callable to run QuantumLeap in a WSGI container of your choice, | ||
e.g. Gunicorn, uWSGI. | ||
Notice that Gunicorn will look for a WSGI callable named `application` if | ||
no variable name follows the module name given on the command line. So one | ||
way to run QuantumLeap in Gunicorn would be | ||
|
||
gunicorn server.wsgi --config server/gconfig.py | ||
|
||
An even more convenient way is to use our Gunicorn standalone server, | ||
see `server.grunner` module. | ||
""" | ||
|
||
|
||
def run(): | ||
""" | ||
Runs the bare-bones QuantumLeap WSGI app. | ||
Notice the app will run in the Flask dev server, so it's only good | ||
for development, not prod! | ||
""" | ||
quantumleap.run(host=server.DEFAULT_HOST, | ||
port=server.DEFAULT_PORT) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import http from 'k6/http'; | ||
import { check, sleep } from 'k6'; | ||
|
||
export default function() { | ||
var url = 'http://192.0.0.1:8668/v2/notify'; | ||
const before = new Date().getTime(); | ||
const T = 30; // time needed to complete a VU iteration | ||
|
||
|
||
for (var i = 0; i < 100; i++){ | ||
var data = { | ||
"id": "Room:1", | ||
"type": "Room", | ||
"temperature": { | ||
"value": 23, | ||
"type": "Float" | ||
}, | ||
"pressure": { | ||
"value": 720, | ||
"type": "Integer" | ||
} | ||
} | ||
var array = []; | ||
array.push(data); | ||
|
||
var payload = { | ||
"data" : array | ||
} | ||
var payload = JSON.stringify(payload); | ||
|
||
var params = { | ||
headers: { | ||
'Content-Type': 'application/json', | ||
} | ||
}; | ||
let res = http.post(url, payload, params); | ||
check(res, { 'status was 200': r => r.status == 200 }); | ||
} | ||
const after = new Date().getTime(); | ||
const diff = (after - before) / 1000; | ||
const remainder = T - diff; | ||
if (remainder > 0) { | ||
sleep(remainder); | ||
} else { | ||
console.warn( | ||
`Timer exhausted! The execution time of the test took longer than ${T} seconds` | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
#!/usr/bin/env bash | ||
|
||
docker build --cache-from smartsdk/quantumleap -t smartsdk/quantumleap ../../ | ||
|
||
docker-compose up -d | ||
docker-compose stop orion | ||
docker-compose stop mongo | ||
sleep 10 | ||
|
||
docker run -i --rm loadimpact/k6 run --vus 10 --duration 60s - < notify-load-test.js | ||
|
||
sleep 10 | ||
|
||
docker run -i --rm loadimpact/k6 run --vus 100 --duration 120s - < notify-load-test.js | ||
|
||
sleep 10 | ||
|
||
docker-compose down |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this logging for gunicorn or also for the app?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's Gunicorn own log level, it doesn't affect QuantumLeap. That's the log level you had in #354 and I suggest we keep it that way until we get to know Gunicorn better :-)