{ "info": { "author": "Byron Ruth", "author_email": "b@devel.io", "bugtrack_url": null, "classifiers": [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4" ], "description": "# HTTP Task Queue (htq)\n\nThe motivation for this project is to provide a standard interface for sending a request in the background to a service that performs some task with a standard mechanism for signaling it to be *cancelled*.\n\nIn this context, a *task* is classified as something that may take longer than what is appropriate for a typical HTTP response or something that is allowed to be eventually completed. Examples include executing a database query, running an analysis on some data, and injesting/scraping data from websites or other services. One side effect of the client-server model is that the server may not be aware if the client aborts the request. The server will continue to perform the task using up resources that other tasks or request handlers could be using. The mechanism for signaling a cancellation is for a subsequent DELETE request to be sent to the service which can be handled to interrupt the first request. This of course requires services to support the DELETE method and implement the logic for cancelling the task being performed. See below for a working example of a [service](#service-example) implementing this interface.\n\nFor a detailed introduction, read the [tutorial](#tutorial) below. \n\n## Dependencies\n\n- Python 3.3+\n- Redis 2.4+\n\n## Command-line Interface\n\n```\n$ htq -h\nHTTP Task Queue (htq) command-line interface\n\nUsage:\n htq server [--host ] [--port ] [--redis ] [--debug]\n htq worker [--threads ] [--redis ] [--debug]\n\nOptions:\n -h --help Show this screen.\n -v --version Show version.\n --debug Turns on debug logging.\n --host Host of the HTTP service [default: localhost].\n --port Port of the HTTP service [default: 5000].\n --redis Host/port of the Redis server [default: localhost:6379].\n --threads Number of threads a worker should spawn [default: 10].\n```\n\nRun the server for the HTTP REST interface.\n\n```\nhtq server\n```\n\nRun the worker to send requests and receive responses.\n\n```\nhtq worker\n```\n\n## API\n\n- `GET /` - Gets all queued requests.\n- `POST /` - Sends (queues) a request\n- `GET //` - Gets a request by UUID\n- `DELETE //` - Cancels a request, deleting it's response if already received\n- `GET //response/` - Gets a request's response if it has been received\n- `DELETE //response/` - Delete a request's response to clear up space\n\n## Tutorial\n\nStart the HTQ server:\n\n```bash\n$ htq server\nStarting htq REST server...\n * Running on http://localhost:5000/\n```\n\nSend a POST to the service with a JSON encoded structure of the request to be sent. This immediately returns a `303 See Other` response with the `Location` header to the request\n\n```bash\n$ curl -i -X POST -H 'Content-Type: application/json' http://localhost:5000 -d '{\"url\": \"http://httpbin.org/ip\"}'\nHTTP/1.0 303 SEE OTHER\nContent-Type: text/html; charset=utf-8\nContent-Length: 0\nLocation: http://localhost:5000/a936e1a1-68d8-4433-a0c0-4f4b2111670d/\nServer: Werkzeug/0.9.6 Python/3.4.1\nDate: Mon, 29 Sep 2014 17:18:49 GMT\n```\n\nSee what the request resource looks like (use the URL from the `Location` header in your output):\n\n```bash\n$ curl -i http://localhost:5000/a936e1a1-68d8-4433-a0c0-4f4b2111670d/\nHTTP/1.0 200 OK\nContent-Type: application/json\nContent-Length: 185\nLink: ; rel=\"response\",\n ; rel=\"self\",\n ; rel=\"status\"\nServer: Werkzeug/0.9.6 Python/3.4.1\nDate: Mon, 29 Sep 2014 17:26:05 GMT\n\n{\n \"timeout\": 60,\n \"status\": \"queued\",\n \"data\": null,\n \"headers\": {},\n \"url\": \"http://httpbin.org/ip\",\n \"method\": \"get\",\n \"uuid\": \"a936e1a1-68d8-4433-a0c0-4f4b2111670d\",\n \"time\": 1412011537783\n}\n```\n\nThe above response shows the details of the request such as the URL, method, headers, and request data. Additionally metadata has been captured when the request was queued including the UUID, the time (in milliseconds) when the request was queued, the status of the request and a timeout. The `Link` header shows the related links from this resource including one to the response and the status. The status link is a lightweight:\n\n\n```bash\n$ curl -i http://localhost:5000/a936e1a1-68d8-4433-a0c0-4f4b2111670d/status/\nHTTP/1.0 200 OK\nContent-Type: application/json\nContent-Length: 20\nServer: Werkzeug/0.9.6 Python/3.4.1\nDate: Mon, 29 Sep 2014 17:30:21 GMT\n\n{\"status\": \"queued\"}\n```\n\nIf we were to `curl` the response link, it would block until the response is ready. Since we have not started a worker yet, this would be forever. Let's start a worker to actual send the request. Execute this in a separate shell:\n\n```bash\n$ htq worker --debug\nStarted 10 workers...\n[a936e1a1-68d8-4433-a0c0-4f4b2111670d] sending request...\n[a936e1a1-68d8-4433-a0c0-4f4b2111670d] response received\n```\n\nStarting the worker daemon immediately starts consuming the queue and sending the requests. As you can see, the one sent above has been sent and the response received. Let's check the status of our request.\n\n```bash\n$ curl -i http://localhost:5000/a936e1a1-68d8-4433-a0c0-4f4b2111670d/status/\nHTTP/1.0 200 OK\nContent-Type: application/json\nContent-Length: 21\nServer: Werkzeug/0.9.6 Python/3.4.1\nDate: Mon, 29 Sep 2014 17:36:56 GMT\n\n{\"status\": \"success\"}\n```\n\nSuccess! Now let's use the link to the response itself.\n\n```bash\n$ curl -i http://localhost:5000/a936e1a1-68d8-4433-a0c0-4f4b2111670d/response/\nHTTP/1.0 200 OK\nContent-Type: application/json\nContent-Length: 19291\nLink: ; rel=\"request\",\n ; rel=\"self\"\nServer: Werkzeug/0.9.6 Python/3.4.1\nDate: Mon, 29 Sep 2014 17:38:22 GMT\n\n{\n \"status\": \"success\",\n \"time\": 1412012019457,\n \"code\": 200,\n \"reason\": \"OK\",\n \"elapsed\": 79.81,\n \"uuid\": \"62db48a4-e511-4c8e-9c11-32b39758d1ff\",\n \"headers\": {\n \"Age\": \"0\",\n \"Content-Length\": \"32\",\n \"Connection\": \"Keep-Alive\",\n \"Access-Control-Allow-Origin\": \"*\",\n \"Server\": \"gunicorn/18.0\",\n \"Access-Control-Allow-Credentials\": \"true\",\n \"Date\": \"Mon, 29 Sep 2014 17:50:29 GMT\",\n \"Content-Type\": \"application/json\"\n },\n \"data\": \"{\\n \\\"origin\\\": \\\"159.14.243.254\\\"\\n}\"\n}\n```\n\nThe response contains all the elements of an HTTP response including code, reason, headers, and the data (which has been removed for brevity). In addition, the time (in milliseconds) the response was received and the elapsed time (in milliseconds) join the UUID and status metadata.\n\n### Canceling a request\n\nHTQ defines an interface for services to implement for allowing requests to be canceled. For example, if I send a request that is taking longer than I expect (delayed for 30 seconds):\n\n```bash\n$ curl -i -X POST -H Content-Type:application/json http://localhost:5000 -d '{\"url\": \"http://httpbin.org/delay/30\"}'\nHTTP/1.0 303 SEE OTHER\nContent-Type: text/html; charset=utf-8\nContent-Length: 0\nLocation: http://localhost:5000/1686e1b7-3b05-4d45-95e8-caf934f540aa/\nServer: Werkzeug/0.9.6 Python/3.4.1\nDate: Mon, 29 Sep 2014 18:00:28 GMT\n```\n\nThen I can send a DELETE to the request URL:\n\n```bash\n$ curl -i -X DELETE http://localhost:5000/1686e1b7-3b05-4d45-95e8-caf934f540aa/\nHTTP/1.0 204 NO CONTENT\nContent-Type: text/html; charset=utf-8\nContent-Length: 0\nServer: Werkzeug/0.9.6 Python/3.4.1\nDate: Mon, 29 Sep 2014 18:00:41 GMT\n```\n\nNow if we check the status, we should see the status has changed to `canceled` (the response is also empty).\n\n```bash\n$ curl -i http://localhost:5000/9619b267-760d-4f0a-9f15-eb8ad99cd1c4/status/\nHTTP/1.0 200 OK\nContent-Type: application/json\nContent-Length: 22\nServer: Werkzeug/0.9.6 Python/3.4.1\nDate: Mon, 29 Sep 2014 18:01:23 GMT\n\n{\"status\": \"canceled\"}\n```\n\nInternally this interrupts the request, but also sends a DELETE request to the endpoint (in this `http://httpbin.org/delay/30`). Implementors of services can support the DELETE request to cancel the underlying processing that is occurring. Of course this is specific to the underlying task being performed, but this simple service-level contract provides a consistent mechanism for signaling the the cancellation.\n\n## Service Example\n\nBelow is a working example of a service that implements the interface HTQ requires for receiving the DELETE to signal the cancellation. This service takes a JSON-encoded statement and parameters and executes them in a local PostgreSQL instance (for simplicity, the database settings are hard-coded).\n\n```python\nimport json\nimport logging\nimport psycopg2\nfrom flask import Flask, abort, request\n\nlogger = logging.getLogger(__name__)\nlogger.setLevel(logging.DEBUG)\nhandler = logging.StreamHandler()\nlogger.addHandler(handler)\n\napp = Flask(__name__)\n\n# Database connection settings\nsettings = {\n 'dbname': 'example',\n 'host': 'localhost',\n}\n\n# Currently running tasks by UUID mapped to the\n# connection process ID\ntasks = {}\n\n\n@app.route('//', methods=['POST'])\ndef run(uuid):\n data = request.json\n\n conn = psycopg2.connect(**settings)\n c = conn.cursor()\n\n # Store the PID of the connection execute this task\n pid = conn.get_backend_pid()\n tasks[uuid] = pid\n\n try:\n logger.debug('[{}] executing query for {}...'.format(pid, uuid))\n c.execute(data['statement'], data.get('parameters'))\n except:\n conn.cancel()\n abort(500)\n finally:\n tasks.pop(uuid, None)\n\n return json.dumps(c.fetchall()), 200\n\n\n@app.route('//', methods=['DELETE'])\ndef cancel(uuid):\n if uuid not in tasks:\n abort(404)\n\n pid = tasks.pop(uuid)\n\n # Open new connection to cancel the query\n conn = psycopg2.connect(**settings)\n c = conn.cursor()\n\n c.execute('select pg_terminate_backend(%s)', (pid,))\n logger.debug('[{}] canceled query for {}'.format(pid, uuid))\n\n return '', 204\n\n\nif __name__ == '__main__':\n app.run(threaded=True)\n```\n\nThe interaction looks as follows:\n\n```bash\n% python example_service.py\n * Running on http://127.0.0.1:5000/\n[57819] executing query for abc123...\n[57819] canceled query for abc123\n127.0.0.1 - - [30/Sep/2014 11:16:01] \"DELETE /abc123/ HTTP/1.1\" 204 -\n127.0.0.1 - - [30/Sep/2014 11:16:02] \"POST /abc123/ HTTP/1.1\" 500 -\n```\n\nA POST request was first sent to execute a query. A DELETE request was sent shortly after to cancel the query and returns. When the POST request does respond it is a 500 complaining the connection was closed (which is what we wanted).\n", "description_content_type": null, "docs_url": null, "download_url": "UNKNOWN", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/cbmi/htq/", "keywords": "http task queue", "license": "BSD", "maintainer": null, "maintainer_email": null, "name": "htq", "package_url": "https://pypi.org/project/htq/", "platform": "UNKNOWN", "project_url": "https://pypi.org/project/htq/", "project_urls": { "Download": "UNKNOWN", "Homepage": "https://github.com/cbmi/htq/" }, "release_url": "https://pypi.org/project/htq/0.1.0/", "requires_dist": null, "requires_python": null, "summary": "HTTP Task Queue", "version": "0.1.0" }, "last_serial": 1243349, "releases": { "0.1.0": [ { "comment_text": "", "digests": { "md5": "63ee2f8acd6df6ddf3acc4ad8b3bd3d4", "sha256": "56d72b8dfe6f1d2998dcaad6186f60914e1ffdfe69948dee5246959646534329" }, "downloads": -1, "filename": "htq-0.1.0.tar.gz", "has_sig": false, "md5_digest": "63ee2f8acd6df6ddf3acc4ad8b3bd3d4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10560, "upload_time": "2014-09-30T16:40:42", "url": "https://files.pythonhosted.org/packages/26/fc/cac96ae8bf210d0f0fa108f077030b4daeae827cdeb8e8afe06bf15efe3a/htq-0.1.0.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "63ee2f8acd6df6ddf3acc4ad8b3bd3d4", "sha256": "56d72b8dfe6f1d2998dcaad6186f60914e1ffdfe69948dee5246959646534329" }, "downloads": -1, "filename": "htq-0.1.0.tar.gz", "has_sig": false, "md5_digest": "63ee2f8acd6df6ddf3acc4ad8b3bd3d4", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 10560, "upload_time": "2014-09-30T16:40:42", "url": "https://files.pythonhosted.org/packages/26/fc/cac96ae8bf210d0f0fa108f077030b4daeae827cdeb8e8afe06bf15efe3a/htq-0.1.0.tar.gz" } ] }