While building closestack, a virtual machine management project, I tried to keep deployment simple and avoided using Celery or other heavier async stacks.
That worked for a while, but it was easy to predict that as the number of users grew, waiting times would grow too. So I needed a simpler asynchronous solution to improve concurrency without bringing in RabbitMQ, Redis, and Celery.
uWSGI already provides a built-in queue manager called spooler, which turned out to be a good fit.
Why not Celery?
Because once Celery enters the picture, a lot more infrastructure and operational concerns show up too.
The problem still needs solving
Without adding RabbitMQ, Redis, or Celery, uWSGI‘s built-in spooler can be used to handle queueing and asynchronous work.
Since most of my projects already use the classic Nginx + uWSGI + Django pattern, I looked more carefully through the uWSGI documentation and found that it also offers features such as shared queues, spoolers, and cron-like task management.
This post introduces the spooler approach. Full documentation is here:
http://uwsgi-docs-zh.readthedocs.io/zh_CN/latest/Spooler.html
Try it out
Everything below was tested in a Python 3.6 environment.
Build a simple uWSGI + Django service
- Create and activate a virtual environment:
1 | python3 -m venv demo_venv |
- Install Django and uWSGI:
1 | pip install django |
- Create a demo Django project:
1 | django-admin startproject demo |
This creates a project structure like:
1 | demo/ |
- Create a
uwsgi.inifile under the project directory:
1 | [uwsgi] |
Here I used http instead of socket, so Nginx is not required during testing.
- Start it:
1 | uwsgi uwsgi.ini |
Then open http://127.0.0.1:8080 to confirm the service is running.
Add async tasks
Create the spooler directory
According to the documentation, the spooler stores queued tasks as files inside a directory, and the spooler process watches those files to implement a task queue.
Add this to uwsgi.ini:
1 | spooler = %(chdir)/demo/tasks |
After restarting uWSGI, the tasks directory will be created automatically, and the startup log will contain something like:
1 | spawned the uWSGI spooler on dir /Users/knktc/tmp/demo/demo/tasks with pid 7363 |
Write tasks
Create a write_task endpoint that inserts a task into the queue:
1 | import uwsgi |
In Python 3, both keys and values passed to uwsgi.spool() must be bytes, otherwise uWSGI raises:
spooler callable dictionary must contains only bytes
Add the route in urls.py:
1 | from django.urls import path |
Restart and visit:
http://127.0.0.1:8080/write_task/
The browser should show done!, and you should see a spool file appear under tasks/.
Process tasks
Now create a worker in worker.py:
1 | import os |
This worker waits 5 seconds, then writes the current time and message body into log.txt.
uwsgi.SPOOL_OK means the task completed successfully and the spool file should be removed.
To activate this worker, add the following to uwsgi.ini:
1 | import = demo/worker.py |
After restarting uWSGI, you should see that the task files disappear and log.txt is created, for example:
1 | 2018-07-23 09:59:09.019494:b'hello world' |
If you hit the endpoint several times in a row, the HTTP requests complete quickly instead of waiting 5 seconds, which means the task handling is asynchronous.
Advanced usage
Multiple spooler workers
You can increase parallelism by setting:
1 | spooler-processes = 4 |
After restarting, uWSGI will spawn multiple spooler processes for the same queue, allowing tasks to be processed in parallel.
Task dispatching
The spooler itself is intentionally simple. By default, one worker function receives all queued tasks. If you need multiple task types, you can build your own dispatcher.
You can also define multiple spooler directories:
1 | spooler = %(chdir)/demo/tasks_a |
Then add two endpoints:
1 | urlpatterns = [ |
And write tasks into the corresponding spooler paths:
1 | def write_task_a(request): |
To process them differently, turn the worker into a dispatcher:
1 | import os |
Now the dispatcher examines each task body and routes the work to the correct handler.
After restarting uWSGI and calling write_task_a/ and write_task_b/ alternately, you will see separate tasks appear in the two spooler directories, and the final log.txt will show both task types being processed.
Reference
- A ready-made Django wrapper around uWSGI spooler already exists: https://github.com/Bahus/uwsgi_tasks