[
  {
    "path": ".gitignore",
    "content": "*.pyc\n"
  },
  {
    "path": "README.md",
    "content": "Async, non-blocking Flask & SQLAlchemy example\n==============================================\n\n> [!WARNING]  \n> This code is really old at this point. Use it for edification but not production!\n\n## Overview\n\nThis code shows how to use the following menagerie of compontents\ntogether in a completely non-blocking manner:\n\n* [Flask](http://flask.pocoo.org/), for the web application framework;\n* [SQLAlchemy](http://www.sqlalchemy.org/), for the object relational mapper (via [Flask-SQLAlchemy](https://github.com/mitsuhiko/flask-sqlalchemy));\n* [Postgresql](http://www.postgresql.org/), for the database;\n* [Psycopg2](http://initd.org/psycopg/), for the SQLAlchemy-Postgresql adapter;\n* [Gunicorn](http://gunicorn.org/), for the WSGI server; and,\n* [Gevent](http://www.gevent.org/), for the networking library.\n\nThe file `server.py` defines a small Flask application that has\ntwo routes: one that triggers a `time.sleep(5)` in Python and one that\ntriggers a `pg_sleep(5)` in Postgres.  Both of these sleeps are normally\nblocking operations.  By running the server using the Gevent\nworker for Gunicorn, we can make the Python sleep non-blocking.\nBy configuring Psycopg2's co-routine support (via\n[psycogreen](https://bitbucket.org/dvarrazzo/psycogreen)) we \ncan make the Postgres sleep non-blocking.\n\n\n## Installation\n\nClone the repo:\n\n\tgit clone https://github.com/kljensen/async-flask-sqlalchemy-example.git\n\nInstall the requirements\n\n\tpip install -r requirements.txt\n\nMake sure you've got the required database\n\n\tcreatedb fsppgg_test\n\nCreate the required tables in this database\n\n\tpython ./server.py -c\n\n\n## Running the code\n\nYou can test three situations with this code:\n * Gunicorn blocking with SQLAlchemy/Psycopg2 blocking;\n * Gunicorn non-blocking with SQLAlchemy/Psycopg2 blocking; and,\n * Gunicorn non-blocking with SQLAlchemy/Psycopg2 non-blocking.\n\n### Gunicorn blocking with SQLAlchemy blocking\n\nRun the server (which is the Flask application) like\n\n\tgunicorn server:app\n\nThen, in a separate shell, run the client like\n\n\tpython ./client.py\n\nYou should see output like\n\n\tSending 5 requests for http://localhost:8000/sleep/python/...\n\t\t@  5.05s got response [200]\n\t\t@ 10.05s got response [200]\n\t\t@ 15.07s got response [200]\n\t\t@ 20.07s got response [200]\n\t\t@ 25.08s got response [200]\n\t\t= 25.09s TOTAL\n\tSending 5 requests for http://localhost:8000/sleep/postgres/...\n\t\t@  5.02s got response [200]\n\t\t@ 10.02s got response [200]\n\t\t@ 15.03s got response [200]\n\t\t@ 20.04s got response [200]\n\t\t@ 25.05s got response [200]\n\t\t= 25.05s TOTAL\n\t------------------------------------------\n\tSUM TOTAL = 50.15s\n\n\n### Gunicorn non-blocking with SQLAlchemy blocking\n\nRun the server like\n\n\tgunicorn server:app -k gevent\n\nand run the client again.   You should see output like\n\n\tSending 5 requests for http://localhost:8000/sleep/python/...\n\t\t@  5.05s got response [200]\n\t\t@  5.06s got response [200]\n\t\t@  5.06s got response [200]\n\t\t@  5.06s got response [200]\n\t\t@  5.07s got response [200]\n\t\t=  5.08s TOTAL\n\tSending 5 requests for http://localhost:8000/sleep/postgres/...\n\t\t@  5.01s got response [200]\n\t\t@ 10.02s got response [200]\n\t\t@ 15.04s got response [200]\n\t\t@ 20.05s got response [200]\n\t\t@ 25.06s got response [200]\n\t\t= 25.06s TOTAL\n\t------------------------------------------\n\tSUM TOTAL = 30.14s\n\t \n\n### Gunicorn non-blocking with SQLAlchemy non-blocking\n\nRun the server like\n\n\tPSYCOGREEN=true gunicorn server:app  -k gevent \n\nand run the client again.   You should see output like\n\n\tSending 5 requests for http://localhost:8000/sleep/python/...\n\t\t@  5.03s got response [200]\n\t\t@  5.03s got response [200]\n\t\t@  5.03s got response [200]\n\t\t@  5.04s got response [200]\n\t\t@  5.03s got response [200]\n\t\t=  5.04s TOTAL\n\tSending 5 requests for http://localhost:8000/sleep/postgres/...\n\t\t@  5.02s got response [200]\n\t\t@  5.03s got response [200]\n\t\t@  5.03s got response [200]\n\t\t@  5.03s got response [200]\n\t\t@  5.03s got response [200]\n\t\t=  5.03s TOTAL\n\t------------------------------------------\n\tSUM TOTAL = 10.07s\n\n\n## Warnings (I lied, it actually does block)\n\nIf you increase the number of requests made in `client.py` you'll notice\nthat SQLAlchemy/Psycopg2 start to block again.  Try, e.g.\n\n\tpython ./client.py 100\n\nwhen running the server in fully non-blocking mode.  You'll notice the `/sleep/postgres/` \nresponses come back in sets of 15. (Well, probably 15, you could have your\nenvironment configured differently than I.)  This because SQLAlchemy uses\n[connection pooling](http://docs.sqlalchemy.org/en/latest/core/pooling.html)\nand, by default, the [QueuePool](http://docs.sqlalchemy.org/en/latest/core/pooling.html#sqlalchemy.pool.QueuePool)\nwhich limits the number of connections to some configuration parameter\n`pool_size` plus a possible \"burst\" of `max_overflow`.  (If you're using \nthe [Flask-SQLAlchemy](https://github.com/mitsuhiko/flask-sqlalchemy)\nextension, `pool_size` is set by your Flask app's configuration variable\n`SQLALCHEMY_POOL_SIZE`.  It is 5 by default.  `max_overflow` is 10 by\ndefault and cannot be specified by a Flask configuration variable, you need\nto set it on the pool yourself.) Once you get over\n`pool_size + max_overflow` needed connections, the SQLAlchemy operations\nwill block.  You can get around this by disabling pooling via SQLAlchemy's\n[SQLAlchemy's NullPool](http://docs.sqlalchemy.org/en/latest/core/pooling.html#sqlalchemy.pool.NullPool);\nhowever, you probably don't want to do that for two reasons.  \n\n1.  Postgresql has a configuration parameter `max_connections` that, drumroll, limits the\nnumber of connections.  If `pool_size + max_overflow` exceeds `max_connections`,\nany new connection requests will be declined by your Postgresql instance.\nEach unique connection will cause Postgresql to use a non-trival amount of\nRAM.  Therefore, unless you have a ton of RAM, you should keep `max_connections`\nto some reasonable value.\n\n2.  If you used the `NullPool`, you'd create a new TCP connection every\ntime you use SQLAlchemy to talk to the database.  Thus, you'll encur an\noverhead  associated with the TCP handshake, etc.\n\nSo, in effect, the concurrency for Postgresql operations is always\nlimited by `max_connections` and how much RAM you have.\n\n\n## Results\n\nStuff gets faster, shizzle works fine.  Your mileage may vary in production.  \n\n\n## License (MIT)\n\nCopyright (c) 2013 Kyle L. Jensen (kljensen@gmail.com)\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "client.py",
    "content": "import sys\nimport gevent\nimport time\nfrom gevent import monkey\nmonkey.patch_all()\nimport urllib2\n\n\ndef fetch_url(url):\n    \"\"\" Fetch a URL and return the total amount of time required.\n    \"\"\"\n    t0 = time.time()\n    try:\n        resp = urllib2.urlopen(url)\n        resp_code = resp.code\n    except urllib2.HTTPError, e:\n        resp_code = e.code\n\n    t1 = time.time()\n    print(\"\\t@ %5.2fs got response [%d]\" % (t1 - t0, resp_code))\n    return t1 - t0\n\n\ndef time_fetch_urls(url, num_jobs):\n    \"\"\" Fetch a URL `num_jobs` times in parallel and return the\n        total amount of time required.\n    \"\"\"\n    print(\"Sending %d requests for %s...\" % (num_jobs, url))\n    t0 = time.time()\n    jobs = [gevent.spawn(fetch_url, url) for i in range(num_jobs)]\n    gevent.joinall(jobs)\n    t1 = time.time()\n    print(\"\\t= %5.2fs TOTAL\" % (t1 - t0))\n    return t1 - t0\n\n\nif __name__ == '__main__':\n\n    try:\n        num_requests = int(sys.argv[1])\n    except IndexError:\n        num_requests = 5\n\n    # Fetch the URL that blocks with a `time.sleep`\n    t0 = time_fetch_urls(\"http://localhost:8000/sleep/python/\", num_requests)\n\n    # Fetch the URL that blocks with a `pg_sleep`\n    t1 = time_fetch_urls(\"http://localhost:8000/sleep/postgres/\", num_requests)\n\n    print(\"------------------------------------------\")\n    print(\"SUM TOTAL = %.2fs\" % (t0 + t1))\n"
  },
  {
    "path": "config.py",
    "content": "SQLALCHEMY_DATABASE_URI = 'postgresql+psycopg2://localhost/fsppgg_test'\nSQLALCHEMY_ECHO = False\nSECRET_KEY = '\\xfb\\x12\\xdf\\xa1@i\\xd6>V\\xc0\\xbb\\x8fp\\x16#Z\\x0b\\x81\\xeb\\x16'\nDEBUG = True\n"
  },
  {
    "path": "requirements.txt",
    "content": "Flask-SQLAlchemy==0.16\npsycopg2==2.4.6\npsycogreen==1.0\ngevent==0.13.8\ngunicorn==0.17.2"
  },
  {
    "path": "server.py",
    "content": "import sys\nimport os\nimport time\nfrom flask import Flask, jsonify\nfrom flask.ext.sqlalchemy import SQLAlchemy\n\n\n# Optionally, set up psycopg2 & SQLAlchemy to be greenlet-friendly.\n# Note: psycogreen does not really monkey patch psycopg2 in the\n# manner that gevent monkey patches socket.\n#\nif \"PSYCOGREEN\" in os.environ:\n\n    # Do our monkey patching\n    #\n    from gevent.monkey import patch_all\n    patch_all()\n    from psycogreen.gevent import patch_psycopg\n    patch_psycopg()\n\n    using_gevent = True\nelse:\n    using_gevent = False\n\n\n# Create our Flask app\n#\napp = Flask(__name__)\napp.config.from_pyfile('config.py')\n\n\n# Create our Flask-SQLAlchemy instance\n#\ndb = SQLAlchemy(app)\nif using_gevent:\n\n    # Assuming that gevent monkey patched the builtin\n    # threading library, we're likely good to use\n    # SQLAlchemy's QueuePool, which is the default\n    # pool class.  However, we need to make it use\n    # threadlocal connections\n    #\n    #\n    db.engine.pool._use_threadlocal = True\n\n\nclass Todo(db.Model):\n    \"\"\" Small example model just to show you that SQLAlchemy is\n        doing everything it should be doing.\n    \"\"\"\n    id = db.Column(db.Integer, primary_key=True)\n    title = db.Column(db.String(60))\n    done = db.Column(db.Boolean)\n    priority = db.Column(db.Integer)\n\n    def as_dict(self):\n        \"\"\" Return an individual Todo as a dictionary.\n        \"\"\"\n        return {\n            'id': self.id,\n            'title': self.title,\n            'done': self.done,\n            'priority': self.priority\n        }\n\n    @classmethod\n    def jsonify_all(cls):\n        \"\"\" Returns all Todo instances in a JSON\n            Flask response.\n        \"\"\"\n        return jsonify(todos=[todo.as_dict() for todo in cls.query.all()])\n\n\n@app.route('/sleep/postgres/')\ndef sleep_postgres():\n    \"\"\" This handler asks Postgres to sleep for 5s and will\n        block for 5s unless psycopg2 is set up (above) to be\n        gevent-friendly.\n    \"\"\"\n    db.session.execute('SELECT pg_sleep(5)')\n    return Todo.jsonify_all()\n\n\n@app.route('/sleep/python/')\ndef sleep_python():\n    \"\"\" This handler sleeps for 5s and will block for 5s unless\n        gunicorn is using the gevent worker class.\n    \"\"\"\n    time.sleep(5)\n    return Todo.jsonify_all()\n\n\n# Create the tables and populate it with some dummy data\n#\ndef create_data():\n    \"\"\" A helper function to create our tables and some Todo objects.\n    \"\"\"\n    db.create_all()\n    todos = []\n    for i in range(50):\n        todo = Todo(\n            title=\"Slave for the man {0}\".format(i),\n            done=(i % 2 == 0),\n            priority=(i % 5)\n        )\n        todos.append(todo)\n    db.session.add_all(todos)\n    db.session.commit()\n\n\nif __name__ == '__main__':\n\n    if '-c' in sys.argv:\n        create_data()\n    else:\n        app.run()\n"
  }
]