Tuesday, March 25, 2014
building a django app that uses ZeroMQ: an annotated webliography
I wanted to build a website that allows people to search their data against a database (in the not too distant future, when the website is live I'll link it and the source code and give more of an explanation. Edit: and here it is, the code, the ZeroMQ stuff is mostly at "/nmr/management/commands", and the website ). Each search takes a few seconds, so in order to be able to serve multiple clients at a time, and allow scaling, I wanted to build a system where the main wsgi process does not block, but passes the search request off to another process that puts it in a queue and executes requests in the queue one by one. I ended up following a simple approach using ZeroMQ. There is a scheduler that runs as a thread in the main wsgi process. When the search input view receives a search request, it writes the search parameters into the database and opens a connection to the scheduler thread and passes it the unique ID of the database record storing the search parameters. There are one or more worker processes each running as a subprocess. The workers are permanently attached via a socket to the scheduler. When a worker completes a job, the scheduler sends it the ID of the next job in the queue, the worker executes the job, writes the results in a database table, and tells the scheduler it is ready for another job. There can be many workers attached to the scheduler, so that multiple searches can be run concurrently.
Here then is a list of (some of) the websites I used for reference while writing this program.
Asycnronous job execution for Django apps:
Celery: this is the job queue that everyone recommends and is probably the way to go for asynchronous job execution, but I'm kind of stubborn and I don't like all of the dependencies so I'm making my own with zeromq instead.
How to spawn a child process that executes asynchronously from the calling script and closes when finished (doesn't become a stuck orphan) if the calling script has terminated (subprocess.Popen does not block).
A more detailed discussion of spawning non-blocking child processes from Django applications
RQ (Redis Queue) a simple job queue for python, depends on the redis database
brokest: another simple job queue, depends on pyzmq and cloud
To execute a script when Django initializes, call it from wsgi.py
To create a custom management command:
To call a management script programmatically, use django.core.management
To get a dict from a model instance, you can use .values() on a queryset object (gives a list of dicts, or more properly, an iterable that returns dicts when iterated over, if you want to modify those dicts, you have to convert it into a real, genuine, list of dicts first), or django.forms.models.model_to_dict
To get a set of objects with the same Many-to-One foreign key use _set as in One.many_set.all()
To make complex database queries (QuerySet filters) put individual criteria in Q objects (django.db.models.Q), and then combine those with parentheses(()), ampersands(&), and pipes(|)
JOIN-like queries, querying across relations, use __ to distinguish model and field
Python version of the zeromq guide. An extensive document describing zeromq usage. Includes lots of good, free to use examples:
PyZMQ documentation, not as extensive or as useful as the zeromq guide, but PyZMQ does provide some extra methods that may be helpful in some cases:
ØMQ and pyzmq Basics. A nice short tutorial on zmq using python examples.Quite a bit of overlap with the zeromq guide, but maybe the best place to start (I didn't run into it until after I'd already spent a couple days going over the guide though)
Use 127.0.0.1, not localhost (tested this with zmq 4, and it still seems to be true):
Use port * or 0 to find an arbitrary open port, then use getsockopt to query which port was found:
Example code for handling keyboard interrupt without locking:
To use keyboard interrupt to kill a script that is running zeromq or pyzmq use ctrl+break (yes, the key above Page Up that you've probably never touched before in your life) instead of ctrl+c (also, yes, that is my answer on that stack-overflow question):
Don't use a list as a queue instead use collections.dequeue : pop=popleft, push=append:
Register functions to run on exit (for example to clean up data and processes before closing) using the atexit module:
The decimal library is a nice way to represent numbers with fixed precision that you want to represent in a pretty way, and do predictable greater than or less than comparisons with. Use the quantize method to round.
<metablog>I enjoyed this. I think I'll make this a thing, despite my disappointment to find that someone else invented the word "webliography" long before I did.</metablog>