Pyruvate WSGI server
.. image:: https://gitlab.com/tschorr/pyruvate/badges/main/pipeline.svg
:target: https://gitlab.com/tschorr/pyruvate
.. image:: https://codecov.io/gl/tschorr/pyruvate/branch/main/graph/badge.svg
:target: https://codecov.io/gl/tschorr/pyruvate
.. image:: http://img.shields.io/pypi/v/pyruvate.svg
:target: https://pypi.org/project/pyruvate
Pyruvate is a fast, multithreaded WSGI <https://www.python.org/dev/peps/pep-3333>
_ server implemented in Rust <https://www.rust-lang.org/>
_.
Features
- Non-blocking read/write using
mio <https://github.com/tokio-rs/mio>
_ - Request parsing using
httparse <https://github.com/seanmonstar/httparse>
_ rust-cpython <https://github.com/dgrunwald/rust-cpython>
_ based Python interface- Worker pool based on
threadpool <https://github.com/rust-threadpool/rust-threadpool>
_ PasteDeploy <https://pastedeploy.readthedocs.io/en/latest/>
_ entry point
Installation
If you are on Linux and use a recent Python version,
.. code-block::
$ pip install pyruvate
is probably all you need to do.
Binary Packages
+++++++++++++++
manylinux_2_17 <https://peps.python.org/pep-0600/>
_ and musllinux_1_1 <https://peps.python.org/pep-0656/>
_ wheels are available for the x86_64
architecture and active Python 3 versions (currently 3.8-3.12).
Source Installation
+++++++++++++++++++
On macOS or if for any other reason you want to install the source tarball (e.g. using pip install --no-binary
) you will need to install Rust <https://doc.rust-lang.org/book/ch01-01-installation.html>
_ first.
Development Installation
++++++++++++++++++++++++
-
Install Rust <https://doc.rust-lang.org/book/ch01-01-installation.html>
__
-
Install and activate a Python 3 (>= 3.8) virtualenv <https://docs.python.org/3/tutorial/venv.html>
_
-
Install setuptools_rust <https://github.com/PyO3/setuptools-rust>
_ using pip::
$ pip install setuptools_rust
-
Install Pyruvate, e.g. using pip::
$ pip install -e git+https://gitlab.com/tschorr/pyruvate.git#egg=pyruvate[test]
Using Pyruvate in your WSGI application
From Python using a TCP port
++++++++++++++++++++++++++++
A hello world WSGI application using Pyruvate listening on 127.0.0.1:7878 and using 2 worker threads looks like this:
.. code-block:: python
import pyruvate
def application(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers, None)
return [b"Hello world!\n"]
pyruvate.serve(application, "127.0.0.1:7878", 2)
From Python using a Unix socket
+++++++++++++++++++++++++++++++
A hello world WSGI application using Pyruvate listening on unix:/tmp/pyruvate.socket and using 2 worker threads looks like this:
.. code-block:: python
import pyruvate
def application(environ, start_response):
"""Simplest possible application object"""
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers, None)
return [b"Hello world!\n"]
pyruvate.serve(application, "/tmp/pyruvate.socket", 2)
Using PasteDeploy
+++++++++++++++++
Again listening on 127.0.0.1:7878 and using 2 worker threads::
[server:main]
use = egg:pyruvate#main
socket = 127.0.0.1:7878
workers = 2
Configuration Options
+++++++++++++++++++++
socket
Required: The TCP socket Pyruvate should bind to.
Pyruvate
also supports systemd socket activation <https://www.freedesktop.org/software/systemd/man/systemd.socket.html>
_
If you specify None
as the socket value, Pyruvate
will try to acquire a socket bound by systemd
.
workers
Required: Number of worker threads to use.
async_logging
Optional: Log asynchronously using a dedicated thread.
Defaults to True
.
chunked_transfer
Optional: Whether to use chunked transfer encoding if no Content-Length header is present.
Defaults to False
.
keepalive_timeout
Optional: Specify a timeout in integer seconds for keepalive connection.
The persistent connection will be closed after the timeout expires.
Defaults to 60 seconds.
max_number_headers
Optional: Maximum number of request headers that will be parsed.
If a request contains more headers than configured, request processing will stop with an error indicating an incomplete request.
The default is 32 headers
max_reuse_count
Optional: Specify how often to reuse an existing connection.
Setting this parameter to 0 will effectively disable keep-alive connections.
This is the default.
qmon_warn_threshold
Optional: Warning threshold for the number of requests in the request queue.
A warning will be logged if the number of queued requests reaches this value.
The value must be a positive integer.
The default is None
which disables the queue monitor.
send_timeout
Optional: Time to wait for a client connection to become available for
writing after EAGAIN, in seconds. Connections that do not receive data
within this time are closed.
The value must be a positive integer.
The default is 60 seconds.
Logging
+++++++
Pyruvate uses the standard Python logging facility <https://docs.python.org/3/library/logging.html>
.
The logger name is pyruvate
.
See the Python documentation (logging <https://docs.python.org/3/library/logging.html>
, logging.config <https://docs.python.org/3/library/logging.config.html>
_) for configuration options.
Example Configurations
Django
++++++
After installing Pyruvate in your Django virtualenv, create or modify your wsgi.py
file (one worker listening on 127.0.0.1:8000):
.. code-block:: python
import os
import pyruvate
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_django_application.settings")
application = get_wsgi_application()
pyruvate.serve(application, "127.0.0.1:8000", 1)
You can now start Django + Pyruvate with::
$ python wsgi.py
Override settings by using the DJANGO_SETTINGS_MODULE
environment variable when appropriate.
Tested with Django 4.1.x, 3.2.x, 2.2.x <https://www.djangoproject.com/>
_.
MapProxy
++++++++
First create a basic WSGI configuration following the MapProxy deployment documentation <https://mapproxy.org/docs/latest/deployment.html#server-script>
_.
Then modify config.py
so it is using Pyruvate (2 workers listening on 127.0.0.1:8005):
.. code-block:: python
import os.path
import pyruvate
from mapproxy.wsgiapp import make_wsgi_app
application = make_wsgi_app(r'/path/to/mapproxy/mapproxy.yaml')
pyruvate.serve(application, "127.0.0.1:8005", 2)
Start from your virtualenv::
$ python config.py
Tested with Mapproxy 1.15.x, 1.13.x, 1.12.x <https://mapproxy.org/>
_.
Plone
+++++
Using pip
After installing Pyruvate in your Plone virtualenv, change the `server` section in your `zope.ini` file (located in `instance/etc` if you are using `mkwsgiinstance` to create the instance)::
[server:main]
use = egg:pyruvate#main
socket = localhost:7878
workers = 2
Using `zc.buildout`
Using zc.buildout <https://pypi.org/project/zc.buildout/>
_ and plone.recipe.zope2instance <https://pypi.org/project/plone.recipe.zope2instance>
_ you can define an instance part using Pyruvate's PasteDeploy <https://pastedeploy.readthedocs.io/en/latest/>
_ entry point::
[instance]
recipe = plone.recipe.zope2instance
http-address = 127.0.0.1:8080
eggs =
Plone
pyruvate
wsgi-ini-template = ${buildout:directory}/templates/pyruvate.ini.in
The server
section of the template provided with the wsgi-ini-template <https://pypi.org/project/plone.recipe.zope2instance/#advanced-options>
_ option should look like this (3 workers listening on http-address
as specified in the buildout [instance]
part)::
[server:main]
use = egg:pyruvate#main
socket = %(http_address)s
workers = 3
There is a minimal buildout example configuration for Plone 5.2 in the examples directory <https://gitlab.com/tschorr/pyruvate/-/tree/main/examples/plone52>
_ of the package.
Tested with Plone 6.0.x, 5.2.x <https://plone.org/>
_.
Pyramid
+++++++
Install Pyruvate in your Pyramid virtualenv using pip::
$ pip install pyruvate
Modify the server section in your .ini
file to use Pyruvate's PasteDeploy <https://pastedeploy.readthedocs.io/en/latest/>
_ entry point (listening on 127.0.0.1:7878 and using 5 workers)::
[server:main]
use = egg:pyruvate#main
socket = 127.0.0.1:7878
workers = 5
Start your application as usual using pserve
::
$ pserve path/to/your/configfile.ini
Tested with Pyramid 2.0, 1.10.x <https://trypyramid.com/>
_.
Radicale
++++++++
You can find an example configuration for Radicale <https://radicale.org>
_ in the examples directory <https://gitlab.com/tschorr/pyruvate/-/tree/main/examples/plone52>
_ of the package.
Tested with Radicale 3.1.8 <https://radicale.org>
_.
Nginx settings
++++++++++++++
Like other WSGI servers Pyruvate should be used behind a reverse proxy, e.g. Nginx::
....
location / {
proxy_pass http://localhost:7878;
...
}
...
Nginx doesn't use keepalive connections by default so you will need to modify your configuration <https://nginx.org/en/docs/http/ngx_http_upstream_module.html#keepalive>
_ if you want persistent connections.
Changelog
1.3.0 (2024-07-04)
- Switch back to rust-cpython (
#29 <https://gitlab.com/tschorr/pyruvate/-/issues/29>
_)
1.3.0-rc1 (2023-12-28)
- Replace rust-cpython with PyO3 (
#28 <https://gitlab.com/tschorr/pyruvate/-/issues/28>
_) - Add support for Python 3.12
- Drop support for Python 3.7
1.2.2 (2023-07-02)
- Document Unix Domain Socket usage (
#27 <https://gitlab.com/tschorr/pyruvate/-/issues/27>
_) - Provide legacy manylinux wheel names (
#26 <https://gitlab.com/tschorr/pyruvate/-/issues/26>
_)
1.2.1 (2022-12-22)
- Track and remove unfinished responses that did not otherwise error (
#23 <https://gitlab.com/tschorr/pyruvate/-/issues/23>
_) - Build musllinux_1_1 wheels (
#24 <https://gitlab.com/tschorr/pyruvate/-/issues/24>
_)
1.2.0 (2022-10-26)
- Support Python 3.11 and discontinue Python 3.6, switch to manylinux2014 for building wheels (
#19 <https://gitlab.com/tschorr/pyruvate/-/issues/19>
_) - Add a request queue monitor (
#17 <https://gitlab.com/tschorr/pyruvate/-/issues/17>
_) - Remove blocking worker (
#18 <https://gitlab.com/tschorr/pyruvate/-/issues/18>
_) - Improve parsing of Content-Length header (
#20 <https://gitlab.com/tschorr/pyruvate/-/issues/20>
_)
1.1.4 (2022-04-19)
- Fix handling of empty list responses (
#14 <https://gitlab.com/tschorr/pyruvate/-/issues/14>
_) - Support hostnames in socket addresses (
#15 <https://gitlab.com/tschorr/pyruvate/-/issues/15>
_)
1.1.3 (2022-04-11)
- Simplify response writing and improve performance (
#12 <https://gitlab.com/tschorr/pyruvate/-/issues/12>
_) - Improve signal handling (
#13 <https://gitlab.com/tschorr/pyruvate/-/issues/13>
_)
1.1.2 (2022-01-10)
- Migrate to Rust 2021
- Use codecov binary uploader
- Add CONTRIBUTING.rst
- Fixed: The wrk benchmarking tool could make pyruvate hang when there is no Content-Length header (
#11 <https://gitlab.com/tschorr/pyruvate/-/issues/11>
_)
1.1.1 (2021-10-12)
1.1.0 (2021-09-14)
- Refactor FileWrapper and improve its performance
- Increase the default maximum number of headers
- Add
Radicale <https://radicale.org>
_ example configuration - Update development status
1.0.3 (2021-06-05)
- HEAD request: Do not complain about content length mismatch (
#4 <https://gitlab.com/tschorr/pyruvate/-/issues/4>
_) - More appropriate log level for client side connection termination (
#5 <https://gitlab.com/tschorr/pyruvate/-/issues/5>
_) - Simplify request parsing
1.0.2 (2021-05-02)
- Close connection and log an error in the case where the actual content length is
less than the Content-Length header provided by the application
- Fix readme
1.0.1 (2021-04-28)
- Fix decoding of URLs that contain non-ascii characters
- Raise Python exception when response contains objects other than bytestrings
instead of simply logging the error.
1.0.0 (2021-03-24)
- Improve query string handling
0.9.2 (2021-01-30)
- Better support for HTTP 1.1 Expect/Continue
- Improve documentation
0.9.1 (2021-01-13)
- Improve GIL handling
- Propagate worker thread name to Python logging
- Do not report broken pipe as error
- PasteDeploy entry point: fix option handling
0.9.0 (2021-01-06)
- Reusable connections
- Chunked transfer-encoding
- Support macOS
0.8.4 (2020-12-12)
0.8.3 (2020-11-26)
- Clean wheel build directories
- Fix some test isolation problems
- Remove a println
0.8.2 (2020-11-17)
- Fix blocksize handling for sendfile case
- Format unix stream peer address
- Use latest mio
0.8.1 (2020-11-10)
- Receiver in non-blocking worker must not block when channel is empty
0.8.0 (2020-11-07)
- Logging overhaul
- New async_logging option
- Some performance improvements
- Support Python 3.9
- Switch to manylinux2010 platform tag
0.7.1 (2020-09-16)
- Raise Python exception when socket is unavailable
- Add Pyramid configuration example in readme
0.7.0 (2020-08-30)
- Use Python logging
- Display server info on startup
- Fix socket activation for unix domain sockets
0.6.2 (2020-08-12)
- Improved logging
- PasteDeploy entry point now also uses at most 24 headers by default
0.6.1 (2020-08-10)
- Improve request parsing
- Increase default maximum number of headers to 24
0.6.0 (2020-07-29)
- Support unix domain sockets
- Improve sendfile usage
0.5.3 (2020-07-15)
- Fix testing for completed sendfile call in case of EAGAIN
0.5.2 (2020-07-15)
- Fix testing for completed response in case of EAGAIN
- Cargo update
0.5.1 (2020-07-07)
- Fix handling of read events
- Fix changelog
- Cargo update
- 'Interrupted' error is not a todo
- Remove unused code
0.5.0 (2020-06-07)
- Add support for systemd socket activation
0.4.0 (2020-06-29)
- Add a new worker that does nonblocking write
- Add default arguments
- Add option to configure maximum number of request headers
- Add Via header
0.3.0 (2020-06-16)
- Switch to rust-cpython
- Fix passing of tcp connections to worker threads
0.2.0 (2020-03-10)
- Added some Python tests (using py.test and tox)
- Improve handling of HTTP headers
- Respect content length header when using sendfile
0.1.0 (2020-02-10)