A simple yet efficient scaling agent for Python apps on Heroku
Project description
Dynoscale Agent
Simple yet efficient scaling agent for Python apps on Heroku
Dynoscale Agent supports both WSGI and ASGI based apps and RQ workers (DjangoQ and Celery support is coming soon). The easies way to use it in your project is import the included Gunicorn hook in your gunicorn.conf.py but we'll explain the setup process in more detail below.
Note that for auto-scaling to work, your web/workers have to run on Standard or Performace dynos!
Getting started
There are generally 3 steps to set up autoscaling with Dynoscale:
- Add Dynoscale addon to your Heroku app
- Install dynoscale package
- Initialize dynoscale when you app starts
1) Enabling Dynoscale add-on
There are two ways to add the Dynoscale add-on to your app.
First one is to add the add-on through the Heroku dashboard by navigating to your app, then selecting the resources
tab and finally searching for dynoscale then select your plan and at this point your app will be restarted with the
addon enabled.
The second option is to install it with heroku cli tools, using this command for example:
heroku addons:create dscale:performance
2) Installing dynoscale agent package
This is same as installing any other Python package, for example: python -m pip install dynoscale
.
If you'd like to confirm it's installed by heroku, then run:
heroku run python -c "import dynoscale; print(dynoscale.__version__)"
which will print out the installed version (for example: 1.2.0
)
If you'd like to confirm that dynoscale found the right env vars run:
heroku run python -c "from dynoscale.config import Config; print(Config())"
and you'll likely see something like this:
Running python -c "from dynoscale.config import Config; print(Config())" on ⬢ your-app-name-here... up, run.9816 (Eco)
{"DYNO": "run.9816", "DYNOSCALE_DEV_MODE": false, "DYNOSCALE_URL": "https://dynoscale.net/api/v1/report/yoursecretdynoscalehash", "redis_urls": {"REDISCLOUD_URL": "redis://default:anothersecrethere@redis-12345.c258.us-east-1-4.ec2.cloud.redislabs.com:12345"}}
3) Initialize dynoscale during the app startup
This can take multiple forms and depends on your app. Is your app WSGI or ASGI? How do you serve it? Do you have workers? There are examples in the repo, take a look! I hope you'll find something close to your setup.
If you have a WSGI app (ex.: Bottle, Flask, CherryPy, Pylons, Django, ...) and you serve the app with Gunicorn
then in your gunicorn.conf.py
just import the pre_request hook from dynoscale and that's it:
# `gunicorn.conf.py` - Using Dynoscale Gunicorn Hook
from dynoscale.hooks.gunicorn import pre_request # noqa # pylint: disable=unused-import
Or if you prefer you can instead pass your WSGI app into DynoscaleWsgiApp():
# `web.py` - Flask Example
from dynoscale.wsgi import DynoscaleWsgiApp
app = Flask(__name__)
app.wsgi_app = DynoscaleWsgiApp(app.wsgi_app)
Do you use Gunicorn with Uvicorn workers? Replace uvicorn.workers.UvicornWorker
with dynoscale.DynoscaleUvicornWorker
like so:
# Contents of gunicorn.conf.py
...
# worker_class = 'uvicorn.workers.UvicornWorker'
worker_class = 'dynoscale.uvicorn.DynoscaleUvicornWorker'
...
... and you're done!
Do you serve you ASGI app some other way? (ex.: Starlette, Responder, FastAPI, Sanic, Django, Guillotina, ...)_ wrap your ASGI app with DynoscaleASGIApp:
# `web.py` - Starlette Example
import os
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Route
from dynoscale.asgi import DynoscaleAsgiApp
async def home(_):
return Response("Hello from Starlette, scaled by Dynoscale!", media_type='text/plain')
app = DynoscaleAsgiApp(Starlette(debug=True, routes=[Route('/', endpoint=home, methods=['GET'])]))
if __name__ == "__main__":
import uvicorn
uvicorn.run('web:app', host='0.0.0.0', port=int(os.getenv('PORT', '8000')), log_level="info")
📖 Complete WSGI example
- Add dynoscale to your app on Heroku:
heroku addons:create dscale
- Install dynoscale:
python -m pip install dynoscale
- Add dynoscale to your app, you can either wrap your app or if you use Gunicorn, you can also just use one of
its hooks (
pre_request
):- If you want to wrap you app (let's look at Flask example):
import os from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "Hello from Flask!" if __name__ == "__main__": app.run(host='0.0.0.0', port=int(os.getenv('PORT', '8000')), debug=True)
then just wrap your WSGI app like thisfrom flask import Flask # FIRST, IMPORT DYNOSCALE from dynoscale.wsgi import DynoscaleWsgiApp app = Flask(__name__) @app.route("/") def index(): return "Hello from Flask!" if __name__ == "__main__": # THE CHANGE BELOW IS ALL YOU NEED TO DO app.wsgi_app = DynoscaleWsgiApp(app.wsgi_app) # YUP, WE KNOW, CAN'T GET SIMPLER THAN THAT :) app.run(host='127.0.0.1', port=3000, debug=True)
- Or, if you'd prefer to use the hook, then change your
gunicorn.conf.py
accordingly instead:# This one line will do it for you: from dynoscale.hooks.gunicorn import pre_request # noqa # pylint: disable=unused-import
If you already use thepre_request
hook, alias ours and call it manually:# Alias the import... from dynoscale.hooks.gunicorn import pre_request as hook # ...and remember to call ours first! def pre_request(worker, req): hook(worker, req) # ...do your own thing...
- Add dynoscale to your app, you can either wrap your app or if you use Gunicorn, you can also just use one of
its hooks (
- Profit! Literally, this will save you money! 💰💰💰 😏
📖 Complete ASGI example
- Add dynoscale to your app on Heroku:
heroku addons:create dscale
- Prepare your amazing webapp, we'll use Starlette served by Gunicorn with Uvicorn workers:
# web.py import datetime from starlette.applications import Starlette from starlette.responses import Response from starlette.routing import Route async def home(_): return Response( "Hello from 🌟 Starlette 🌟 served by Gunicorn using Uvicorn workers and scaled by Dynoscale!\n" f"It's {datetime.datetime.now()} right now.", media_type='text/plain' ) app = Starlette(debug=True, routes=[Route('/', endpoint=home, methods=['GET'])])
... add Gunicorn config:# gunicorn.conf.py import os # ENV vars PORT = int(os.getenv('PORT', '3000')) WEB_CONCURRENCY = int(os.getenv('WEB_CONCURRENCY', '10')) # Gunicorn config wsgi_app = "web:app" # ┌---------- THIS HERE IS ALL OF DYNOSCALE SETUP ----------┐ # | # worker_class = 'uvicorn.workers.UvicornWorker' | worker_class = 'dynoscale.uvicorn.DynoscaleUvicornWorker' # | # └---------------------------------------------------------┘ bind = f"0.0.0.0:{PORT}" preload_app = True workers = WEB_CONCURRENCY max_requests = 1000 max_requests_jitter = 50 accesslog = '-' loglevel = 'debug'
- Install all the dependencies:
python -m pip install "uvicorn[standard]" gunicorn dynoscale
- Start it up with:
DYNO=web.1 DYNOSCALE_DEV_MODE=true DYNOSCALE_URL=https://some_request_bin_or_some_such.com gunicorn
- On Heroku, DYNO and DYNOSCALE_URL will be set for you, you should only have
web: gunicorn
in your procfile. - In this example we start Dynoscale in dev mode to simulate random queue times, don't do this on Heroku!
- On Heroku, DYNO and DYNOSCALE_URL will be set for you, you should only have
- That's it you're done, now Profit! Literally, this will save you money! 💰💰💰 😏
ℹ️ Info
You should consider
the dynoscale.wsgi.DynoscaleWsgiApp(wsgi_app)
, dynoscale.hooks.gunicorn.pre_request(worker, req)
, dynoscale.asgi.DynoscaleASGIApp(asgi_app)
and dynoscale.uvicorn.DynoscaleUvicornWorker
the only parts of the public interface.
🤯 Examples
Please check out ./examples
, yes, we do have examples in the repository :)
👩💻 Contributing
Install development requirements:
pip install -e ".[test]"
You can run pytest from terminal: pytest
You can run flake8 from terminal: flake8 ./src
Change Log of dynoscale
for Python
1.2.0 [2023-01-08]
- dropping support for Python 3.7, 3.8, 3.9
- adding support for Gunicorn with Uvicorn workers, use dynoscale.uvicorn.DynoscaleUnicornWorker
1.1.3 [2023-01-13]
- Added support for ASGI through DynoscaleAsgiApp class
- Added options to control DS repository storage location with environment variables
1.1.2 [2022-05-27]
- Added logging to DynoscaleRQLogger
1.1.1 [2022-05-12]
- fixed issue when using GUNICORN hook (Incorrect key name in headers)
1.1.0 [2022-03-25]
- Support for RQ
1.0.0 [2022-02-27]
First public release
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file dynoscale-1.2.0.tar.gz
.
File metadata
- Download URL: dynoscale-1.2.0.tar.gz
- Upload date:
- Size: 29.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.10.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | b87dbe0e13efc8b2b053b9886bda250d5d5420959fbc4c66c5741558e6cbce11 |
|
MD5 | d71c9c87494d8c935e225527eb45211f |
|
BLAKE2b-256 | 63839993dd1320b8878a0b23f32a2b68326368ecbd647f23b14356d03d4cf69a |
File details
Details for the file dynoscale-1.2.0-py3-none-any.whl
.
File metadata
- Download URL: dynoscale-1.2.0-py3-none-any.whl
- Upload date:
- Size: 21.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/4.0.2 CPython/3.10.10
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 5aabb62a6897143d121bf2cff1bfcb520847e19173fa5eec3f8cde5064bf45ed |
|
MD5 | 5602bea38488c903c2f2806eef5cebf5 |
|
BLAKE2b-256 | e73692dcf148852d0fec6168da91f9671d3bea0beb4c8a5329bbb79e5e15f73a |