Package: dynoscale
Dynoscale AgentSimple yet efficient scaling agent for Python apps on HerokuDynoscale Agent supports bothWSGIandASGIbased apps andRQworkers(DjangoQ and Celery support is coming
soon).
The easies way to use it in your project is import the included Gunicorn hook in yourgunicorn.conf.pybut we'll
explain
the setup process in more detail below.Note that for auto-scaling to work, your web/workers have to run onStandardorPerformacedynos!Getting startedThere are generally 3 steps to set up autoscaling with Dynoscale:AddDynoscaleaddon to your Heroku appInstalldynoscalepackageInitializedynoscalewhen you app starts1) Enabling Dynoscale add-onThere 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 toyour app, then selecting theresourcestab and finally searching fordynoscalethen select your plan and at this point your app will be restarted with the
addon enabled.The second option is to install it withheroku cli tools, using this command for example:heroku addons:create dscale:performance2) Installing dynoscale agent packageThis 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 startupThis 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 areexamplesin 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 withGunicornthen in yourgunicorn.conf.pyjust import the pre_request hook from dynoscale and that's it:# `gunicorn.conf.py` - Using Dynoscale Gunicorn Hookfromdynoscale.hooks.gunicornimportpre_request# noqa # pylint: disable=unused-importOr if you prefer you caninsteadpass your WSGI app into DynoscaleWsgiApp():# `web.py` - Flask Examplefromdynoscale.wsgiimportDynoscaleWsgiAppapp=Flask(__name__)app.wsgi_app=DynoscaleWsgiApp(app.wsgi_app)Do you use Gunicorn with Uvicorn workers? Replaceuvicorn.workers.UvicornWorkerwithdynoscale.DynoscaleUvicornWorkerlike 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 Exampleimportosfromstarlette.applicationsimportStarlettefromstarlette.responsesimportResponsefromstarlette.routingimportRoutefromdynoscale.asgiimportDynoscaleAsgiAppasyncdefhome(_):returnResponse("Hello from Starlette, scaled by Dynoscale!",media_type='text/plain')app=DynoscaleAsgiApp(Starlette(debug=True,routes=[Route('/',endpoint=home,methods=['GET'])]))if__name__=="__main__":importuvicornuvicorn.run('web:app',host='0.0.0.0',port=int(os.getenv('PORT','8000')),log_level="info")📖 Complete WSGI exampleAdddynoscaleto your app on Heroku:heroku addons:create dscaleInstalldynoscale:python -m pip install dynoscaleAdddynoscaleto 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):importosfromflaskimportFlaskapp=Flask(__name__)@app.route("/")defindex():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 thisfromflaskimportFlask# FIRST, IMPORT DYNOSCALEfromdynoscale.wsgiimportDynoscaleWsgiAppapp=Flask(__name__)@app.route("/")defindex():return"Hello from Flask!"if__name__=="__main__":# THE CHANGE BELOW IS ALL YOU NEED TO DOapp.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 yourgunicorn.conf.pyaccordingly instead:# This one line will do it for you:fromdynoscale.hooks.gunicornimportpre_request# noqa # pylint: disable=unused-importIf you already use thepre_requesthook, alias ours and call it manually:# Alias the import...fromdynoscale.hooks.gunicornimportpre_requestashook# ...and remember to call ours first!defpre_request(worker,req):hook(worker,req)# ...do your own thing...Profit!Literally, this will save you money! 💰💰💰 😏📖 Complete ASGI exampleAdddynoscaleto your app on Heroku:heroku addons:create dscalePrepare your amazing webapp, we'll useStarletteserved byGunicornwithUvicornworkers:# web.pyimportdatetimefromstarlette.applicationsimportStarlettefromstarlette.responsesimportResponsefromstarlette.routingimportRouteasyncdefhome(_):returnResponse("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.pyimportos# ENV varsPORT=int(os.getenv('PORT','3000'))WEB_CONCURRENCY=int(os.getenv('WEB_CONCURRENCY','10'))# Gunicorn configwsgi_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=Trueworkers=WEB_CONCURRENCYmax_requests=1000max_requests_jitter=50accesslog='-'loglevel='debug'Install all the dependencies:python -m pip install "uvicorn[standard]" gunicorn dynoscaleStart it up with:DYNO=web.1DYNOSCALE_DEV_MODE=trueDYNOSCALE_URL=https://some_request_bin_or_some_such.comgunicornOn Heroku, DYNO and DYNOSCALE_URL will be set for you, you should only haveweb: gunicornin your procfile.In this example we start Dynoscale in dev mode to simulate random queue times, don't do this on Heroku!That's it you're done, nowProfit!Literally, this will save you money! 💰💰💰 😏ℹ️ InfoYou should consider
thedynoscale.wsgi.DynoscaleWsgiApp(wsgi_app),dynoscale.hooks.gunicorn.pre_request(worker, req),dynoscale.asgi.DynoscaleASGIApp(asgi_app)anddynoscale.uvicorn.DynoscaleUvicornWorkerthe only parts of the public interface.🤯 ExamplesPlease check out./examples, yes, we do have examples in the repository :)👩‍💻 ContributingInstall development requirements:pip install -e ".[test]"You can runpytestfrom terminal:pytestYou can runflake8from terminal:flake8 ./srcChangelog ofdynoscalefor Python1.2.2 [TBD]updated test/dev dependenciesadding support for TLS redis urls with self-signed certificates1.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.9adding support for Gunicorn with Uvicorn workers, use dynoscale.uvicorn.DynoscaleUnicornWorker1.1.3 [2023-01-13]Added support for ASGI through DynoscaleAsgiApp classAdded options to control DS repository storage location with environment variables1.1.2 [2022-05-27]Added logging to DynoscaleRQLogger1.1.1 [2022-05-12]fixed issue when using GUNICORN hook (Incorrect key name in headers)1.1.0 [2022-03-25]Support forRQ1.0.0 [2022-02-27]First public release