# 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:
1) Add **Dynoscale** addon to your Heroku app
2) Install **dynoscale** package
3) 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](https://github.com/Mjolnir-Software/dynoscale-python/tree/main/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:
```python
# `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():
```python
# `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:
```python
# 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:
```python
# `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
1. Add __dynoscale__ to your app on Heroku: `heroku addons:create dscale`
2. Install __dynoscale__: `python -m pip install dynoscale`
1. 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`):
1. If you want to wrap you app (let's look at Flask example):
```python
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 this
```python
from 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)
```
2. Or, if you'd prefer to use the hook, then change your `gunicorn.conf.py` accordingly instead:
```python
# This one line will do it for you:
from dynoscale.hooks.gunicorn import pre_request # noqa # pylint: disable=unused-import
```
If you already use the `pre_request` hook, alias ours and call it manually:
```python
# 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...
```
3. __Profit!__ _Literally, this will save you money! đ°đ°đ° đ_
## đ Complete ASGI example
1. Add __dynoscale__ to your app on Heroku: `heroku addons:create dscale`
2. Prepare your amazing webapp, we'll use **Starlette** served by **Gunicorn** with **Uvicorn** workers:
```python
# 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:
```python
# 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'
```
3. Install all the dependencies:
- `python -m pip install "uvicorn[standard]" gunicorn dynoscale`
4. Start it up with:
```bash
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!
5. 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`
# Changelog of `dynoscale` for Python
### 1.2.2 [TBD]
- updated test/dev dependencies
- adding support for TLS redis urls with self-signed certificates
### 1.2.1 [2023-03-01]
- Fix: Limit resource consumption while reporting on extreme numbers of pending tasks.
### 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](https://python-rq.org)
### 1.0.0 [2022-02-27]
First public release
Raw data
{
"_id": null,
"home_page": "https://dynoscale.net",
"name": "dynoscale",
"maintainer": "",
"docs_url": null,
"requires_python": ">=3.10",
"maintainer_email": "",
"keywords": "heroku,scaling,dyno,wsgi",
"author": "Ondrej Dolejsi",
"author_email": "ondrej.dolejsi@gmail.com",
"download_url": "https://files.pythonhosted.org/packages/c5/f9/d9355511dbc840cfa6745467c515b86091c8fa67f44f1a09b1c9805322d4/dynoscale-1.2.2.tar.gz",
"platform": null,
"description": "# Dynoscale Agent\n\n### Simple yet efficient scaling agent for Python apps on Heroku\n\nDynoscale Agent supports both **WSGI** and **ASGI** based apps and **RQ** workers _(DjangoQ and Celery support is coming\nsoon)_.\nThe easies way to use it in your project is import the included Gunicorn hook in your _gunicorn.conf.py_ but we'll\nexplain\nthe setup process in more detail below.\n\n### Note that for auto-scaling to work, your web/workers have to run on _Standard_ or _Performace_ dynos!\n\n## Getting started\n\nThere are generally 3 steps to set up autoscaling with Dynoscale:\n\n1) Add **Dynoscale** addon to your Heroku app\n2) Install **dynoscale** package\n3) Initialize **dynoscale** when you app starts\n\n### 1) Enabling Dynoscale add-on\n\nThere are two ways to add the Dynoscale add-on to your app. \nFirst one is to add the add-on through the Heroku dashboard by navigating to _your app_, then selecting the _resources_\ntab and finally searching for _dynoscale_ then select your plan and at this point your app will be restarted with the\naddon enabled.\n\nThe second option is to install it with _heroku cli tools_, using this command for example:\n\n heroku addons:create dscale:performance\n\n### 2) Installing dynoscale agent package\n\nThis is same as installing any other Python package, for example: `python -m pip install dynoscale`.\n\nIf you'd like to confirm it's installed by heroku, then run:\n\n heroku run python -c \"import dynoscale; print(dynoscale.__version__)\" \n\nwhich will print out the installed version (for example: `1.2.0`)\n\nIf you'd like to confirm that dynoscale found the right env vars run:\n\n heroku run python -c \"from dynoscale.config import Config; print(Config())\"\n\nand you'll likely see something like this:\n\n Running python -c \"from dynoscale.config import Config; print(Config())\" on \u2b22 your-app-name-here... up, run.9816 (Eco)\n {\"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\"}}\n\n### 3) Initialize dynoscale during the app startup\n\nThis can take multiple forms and depends on your app. Is your app WSGI or ASGI? How do you serve it? Do you have\nworkers? There are [examples](https://github.com/Mjolnir-Software/dynoscale-python/tree/main/examples) in the repo, take\na look! I hope you'll find something close to your setup.\n\nIf you have a WSGI app _(ex.: Bottle, Flask, CherryPy, Pylons, Django, ...)_ and you serve the app with **Gunicorn**\nthen in your `gunicorn.conf.py` just import the pre_request hook from dynoscale and that's it:\n\n```python\n# `gunicorn.conf.py` - Using Dynoscale Gunicorn Hook\nfrom dynoscale.hooks.gunicorn import pre_request # noqa # pylint: disable=unused-import\n```\n\nOr if you prefer you can **instead** pass your WSGI app into DynoscaleWsgiApp():\n\n```python\n# `web.py` - Flask Example\nfrom dynoscale.wsgi import DynoscaleWsgiApp\n\napp = Flask(__name__)\napp.wsgi_app = DynoscaleWsgiApp(app.wsgi_app)\n```\n\nDo you use Gunicorn with Uvicorn workers? Replace `uvicorn.workers.UvicornWorker`\nwith `dynoscale.DynoscaleUvicornWorker` like so:\n\n```python\n# Contents of gunicorn.conf.py\n...\n# worker_class = 'uvicorn.workers.UvicornWorker'\nworker_class = 'dynoscale.uvicorn.DynoscaleUvicornWorker'\n...\n```\n\n... and you're done!\n\nDo you serve you ASGI app some other way? (ex.: Starlette, Responder, FastAPI, Sanic, Django, Guillotina, ...)_ wrap\nyour ASGI app\nwith DynoscaleASGIApp:\n\n```python\n# `web.py` - Starlette Example\nimport os\n\nfrom starlette.applications import Starlette\nfrom starlette.responses import Response\nfrom starlette.routing import Route\n\nfrom dynoscale.asgi import DynoscaleAsgiApp\n\n\nasync def home(_):\n return Response(\"Hello from Starlette, scaled by Dynoscale!\", media_type='text/plain')\n\n\napp = DynoscaleAsgiApp(Starlette(debug=True, routes=[Route('/', endpoint=home, methods=['GET'])]))\n\nif __name__ == \"__main__\":\n import uvicorn\n\n uvicorn.run('web:app', host='0.0.0.0', port=int(os.getenv('PORT', '8000')), log_level=\"info\")\n```\n\n---\n\n## \ud83d\udcd6 Complete WSGI example\n\n1. Add __dynoscale__ to your app on Heroku: `heroku addons:create dscale`\n2. Install __dynoscale__: `python -m pip install dynoscale`\n 1. Add __dynoscale__ to your app, you can either wrap your app or if you use Gunicorn, you can also just use one of\n its hooks (`pre_request`):\n 1. If you want to wrap you app (let's look at Flask example):\n ```python\n import os\n \n from flask import Flask\n \n app = Flask(__name__)\n \n @app.route(\"/\")\n def index():\n return \"Hello from Flask!\"\n \n if __name__ == \"__main__\":\n app.run(host='0.0.0.0', port=int(os.getenv('PORT', '8000')), debug=True)\n ```\n then just wrap your WSGI app like this\n ```python\n from flask import Flask\n # FIRST, IMPORT DYNOSCALE\n from dynoscale.wsgi import DynoscaleWsgiApp\n \n app = Flask(__name__)\n \n @app.route(\"/\")\n def index():\n return \"Hello from Flask!\"\n \n if __name__ == \"__main__\":\n # THE CHANGE BELOW IS ALL YOU NEED TO DO\n app.wsgi_app = DynoscaleWsgiApp(app.wsgi_app)\n # YUP, WE KNOW, CAN'T GET SIMPLER THAN THAT :)\n app.run(host='127.0.0.1', port=3000, debug=True)\n ```\n 2. Or, if you'd prefer to use the hook, then change your `gunicorn.conf.py` accordingly instead:\n ```python\n # This one line will do it for you:\n from dynoscale.hooks.gunicorn import pre_request # noqa # pylint: disable=unused-import\n ``` \n If you already use the `pre_request` hook, alias ours and call it manually:\n ```python\n # Alias the import...\n from dynoscale.hooks.gunicorn import pre_request as hook\n \n # ...and remember to call ours first!\n def pre_request(worker, req):\n hook(worker, req)\n # ...do your own thing...\n ```\n3. __Profit!__ _Literally, this will save you money! \ud83d\udcb0\ud83d\udcb0\ud83d\udcb0 \ud83d\ude0f_\n\n## \ud83d\udcd6 Complete ASGI example\n\n1. Add __dynoscale__ to your app on Heroku: `heroku addons:create dscale`\n2. Prepare your amazing webapp, we'll use **Starlette** served by **Gunicorn** with **Uvicorn** workers:\n ```python\n # web.py\n import datetime\n from starlette.applications import Starlette\n from starlette.responses import Response\n from starlette.routing import Route\n \n \n async def home(_):\n return Response(\n \"Hello from \ud83c\udf1f Starlette \ud83c\udf1f served by Gunicorn using Uvicorn workers and scaled by Dynoscale!\\n\"\n f\"It's {datetime.datetime.now()} right now.\",\n media_type='text/plain'\n )\n \n \n app = Starlette(debug=True, routes=[Route('/', endpoint=home, methods=['GET'])])\n ```\n ... add Gunicorn config:\n ```python\n # gunicorn.conf.py\n import os\n # ENV vars\n PORT = int(os.getenv('PORT', '3000'))\n WEB_CONCURRENCY = int(os.getenv('WEB_CONCURRENCY', '10'))\n \n # Gunicorn config\n wsgi_app = \"web:app\"\n \n # \u250c---------- THIS HERE IS ALL OF DYNOSCALE SETUP ----------\u2510\n # | # worker_class = 'uvicorn.workers.UvicornWorker' |\n worker_class = 'dynoscale.uvicorn.DynoscaleUvicornWorker' # |\n # \u2514---------------------------------------------------------\u2518\n \n bind = f\"0.0.0.0:{PORT}\"\n preload_app = True\n \n workers = WEB_CONCURRENCY\n max_requests = 1000\n max_requests_jitter = 50\n \n accesslog = '-'\n loglevel = 'debug'\n ```\n3. Install all the dependencies:\n - `python -m pip install \"uvicorn[standard]\" gunicorn dynoscale`\n4. Start it up with:\n ```bash\n DYNO=web.1 DYNOSCALE_DEV_MODE=true DYNOSCALE_URL=https://some_request_bin_or_some_such.com gunicorn\n ```\n - On Heroku, DYNO and DYNOSCALE_URL will be set for you, you should only have `web: gunicorn` in your procfile.\n - In this example we start Dynoscale in dev mode to simulate random queue times, don't do this on Heroku!\n5. That's it you're done, now __Profit!__ _Literally, this will save you money! \ud83d\udcb0\ud83d\udcb0\ud83d\udcb0 \ud83d\ude0f_\n\n## \u2139\ufe0f Info\n\nYou should consider\nthe `dynoscale.wsgi.DynoscaleWsgiApp(wsgi_app)`, `dynoscale.hooks.gunicorn.pre_request(worker, req)`, `dynoscale.asgi.DynoscaleASGIApp(asgi_app)`\nand `dynoscale.uvicorn.DynoscaleUvicornWorker` the only parts of the public interface.\n\n## \ud83e\udd2f Examples\n\nPlease check out `./examples`, yes, we do have examples in the repository :)\n\n## \ud83d\udc69\u200d\ud83d\udcbb Contributing\n\nInstall development requirements:\n\n- `pip install -e \".[test]\"`\n\nYou can run _pytest_ from terminal: `pytest`\n\nYou can run _flake8_ from terminal: `flake8 ./src` \n\n# Changelog of `dynoscale` for Python\n\n### 1.2.2 [TBD]\n - updated test/dev dependencies\n - adding support for TLS redis urls with self-signed certificates\n\n### 1.2.1 [2023-03-01]\n - Fix: Limit resource consumption while reporting on extreme numbers of pending tasks.\n\n### 1.2.0 [2023-01-08]\n - dropping support for Python 3.7, 3.8, 3.9\n - adding support for Gunicorn with Uvicorn workers, use dynoscale.uvicorn.DynoscaleUnicornWorker\n\n### 1.1.3 [2023-01-13]\n\n- Added support for ASGI through DynoscaleAsgiApp class\n- Added options to control DS repository storage location with environment variables\n\n### 1.1.2 [2022-05-27]\n\n- Added logging to DynoscaleRQLogger\n\n### 1.1.1 [2022-05-12]\n\n- fixed issue when using GUNICORN hook (Incorrect key name in headers)\n\n### 1.1.0 [2022-03-25]\n\n- Support for [RQ](https://python-rq.org)\n\n### 1.0.0 [2022-02-27]\n\nFirst public release\n",
"bugtrack_url": null,
"license": "MIT",
"summary": "A simple yet efficient scaling agent for Python apps on Heroku",
"version": "1.2.2",
"project_urls": {
"Documentation": "http://dynoscale.net/documentation/category/general",
"Homepage": "https://dynoscale.net",
"Source": "https://github.com/Mjolnir-Software/dynoscale-python",
"Tracker": "https://github.com/Mjolnir-Software/dynoscale-python"
},
"split_keywords": [
"heroku",
"scaling",
"dyno",
"wsgi"
],
"urls": [
{
"comment_text": "",
"digests": {
"blake2b_256": "377e78a46a6f2fcfbf2dba476d9fda701e68e6cab63a2bafab373e87bdc0280f",
"md5": "29b833a4941b7d543b489fec0f98b8b2",
"sha256": "6dbcba134717dcf345e7134474cecfefbf6029e3795efb3ac18f273ac7e11d5f"
},
"downloads": -1,
"filename": "dynoscale-1.2.2-py3-none-any.whl",
"has_sig": false,
"md5_digest": "29b833a4941b7d543b489fec0f98b8b2",
"packagetype": "bdist_wheel",
"python_version": "py3",
"requires_python": ">=3.10",
"size": 21767,
"upload_time": "2023-05-17T18:41:10",
"upload_time_iso_8601": "2023-05-17T18:41:10.536522Z",
"url": "https://files.pythonhosted.org/packages/37/7e/78a46a6f2fcfbf2dba476d9fda701e68e6cab63a2bafab373e87bdc0280f/dynoscale-1.2.2-py3-none-any.whl",
"yanked": false,
"yanked_reason": null
},
{
"comment_text": "",
"digests": {
"blake2b_256": "c5f9d9355511dbc840cfa6745467c515b86091c8fa67f44f1a09b1c9805322d4",
"md5": "f09b5a3a4d9e9fd4cc7b0aada8494e0b",
"sha256": "1a36d86855a5ad0c2d7200dde7fc52b3c111e11752f226ec8a4cd2e85c3f2ec1"
},
"downloads": -1,
"filename": "dynoscale-1.2.2.tar.gz",
"has_sig": false,
"md5_digest": "f09b5a3a4d9e9fd4cc7b0aada8494e0b",
"packagetype": "sdist",
"python_version": "source",
"requires_python": ">=3.10",
"size": 29515,
"upload_time": "2023-05-17T18:41:12",
"upload_time_iso_8601": "2023-05-17T18:41:12.646639Z",
"url": "https://files.pythonhosted.org/packages/c5/f9/d9355511dbc840cfa6745467c515b86091c8fa67f44f1a09b1c9805322d4/dynoscale-1.2.2.tar.gz",
"yanked": false,
"yanked_reason": null
}
],
"upload_time": "2023-05-17 18:41:12",
"github": true,
"gitlab": false,
"bitbucket": false,
"codeberg": false,
"github_user": "Mjolnir-Software",
"github_project": "dynoscale-python",
"travis_ci": false,
"coveralls": false,
"github_actions": false,
"lcname": "dynoscale"
}