Getting started with WSGI and Python as well as uwsgi application server

15 Jan 2023 - tsp
Last update 15 Jan 2023
Reading time 20 mins

Introduction

Disclaimer: My personal opinion is still that Python is not a suitable language for production environment and especially not for web development. When the author has the choice he would choose a more suitable language for the web such as Java with JavaEE or a language such as PHP (The author personally thinks Python is on par with PHP) - or depending on the application Elixir but there are situations where usage of Python might be a good idea - and then it’s still better than C# based stuff and ASP.NET anyways. And it’s always a good idea to know how all of that stuff works - since it’s about the same for every new hyped technology (one will see that WSGI web applications work just like any other CGI application anyways, there is no new black magic except transparent launching of the interpreter and many fancy names for the components implementing it - as usual).

So I’ve came around WSGI (the protocol as well as the module) lately especially in the context of Flask applications in a cloud environment and since I’ve been using a little bit more Python lately in work context I thought that most of the tutorial I’ve read had been way to complicated and did not really fit my view on the whole Internet infrastructure - and I also thought that it simply could not be that complicated since the network and the whole stack really works the same as in the early 90’th even today - so when it sounds complicated it’s usually just formulated in a complicate fashion or to sound fancy - but the principles are still the same and applications on the network also still work the same, there is no such thing as too quick movement and development even though the tools evolve and the languages change. So I thought to dive a little bit into the matter (and also configuration of uwsgi which turned out to be way more versatile than one will imagine after skimming over this blog article - basically it’s a great tool when one wants to build a cloud like horizontally scaling system - it offers much flexibility that one will not use for a typical small to medium scale deployment anyways - usually one could even just don’t use the tool anyways and deliver a micro HTTP server in ones application since the gain of using an application server is not that large when doing small to medium scale application development with Python - so I think the value it can provide is really underestimated in many cases).

Note that this blog article has been written from the viewpoint of someone who already developed many web applications in different frameworks and many different languages (but never a larger project in one of the Python frameworks for the web). So it’s of course heavily biased by previous experience and view of the whole infrastructure - it’s of course also affected from a system and network administration point of view as well as influenced by the knowledge of basic inner workings of the whole WWW infrastructure. So it’s not written from the point of view form a beginner for web development …

What is WSGI?

The Web Server Gateway Interface is something that’s pretty similar that most of developers for web applications remember from the early years - the common gateway interface (CGI). It mainly differs from pre-forked FastCGI in the design goal - CGI had been a specification to launch any external application, supply information about the requests in system environment variables and pass request payload via the standard input - as well as request output via standard output. WSGI works similar though it has been built for Python in particular (though implementations such as the uwsgi application server are perfectly capable of running applications written in Erlang, Ruby, Lua, Perl, Java, JavaScript, etc.). On one side it “specifies” that each application has to be:

The start_response callable buffers the parameters passed. As mentioned above - as long as output buffering has not started flushing - an error handler can call it a second time to replace existing buffered data by an error handlers output - though the author personally would not consider that a clean application design. In any other case start_response can be called more often.

As one can see this is pretty much the same thing that CGI also did.

The API for the Python side of WSGI has been specified in PEP 3333 and is pretty short. In my opinion it’s a good idea to read information about WSGI directly there to get known to the ideas behind this interface even when it looks really familiar to anyone who has ever written a CGI application in any programming language. WSGI is used by nearly every Python web framework such as Flask, Django.

The life cycle

In contrast to more major and more sophisticated specifications such as JavaEE the life cycle of a WSGI servlet is not specified in any way. This is container specific though most of them will load a module once and call the callable more than once. This is especially the case for horizontally scaling cloud infrastructure that spawns server processes on demand. Whenever the application gets replaced most containers support swapping the loaded module in a graceful way - i.e. processing existing running requests with the old code while hot reloading new code. There are containers though that work like legacy CGI without Pre-Forking an loads code on demand though that leads to a huge overhead with interpreted languages such as Python. Unfortunately most containers do not provide clean life cycle callbacks so that you cannot launch background tasks or allocate shared resources whenever the container is loaded (ok you could use __init__.py for that) and cleanly release them whenever the container gets evicted or replaced (this is the problem). For some application servers as uwsgi you are able to write extensions to provide some kind of background tasks - but then they have to be deployed independent of the application again. This is in my opinion one of the points where the whole specification needs major improvement before being really usable in a general sense.

Environment variables

The following environment variables are required to be present (or omitted if they would be empty):

In addition the dictionary also contains some other WSGI specific stuff:

The stream objects support read(size), readline(), readlines(hint) as well as __iter__() for the sources and write(str), writelines(seq) as well as flush() for the sinks.

Optionally a container might provide a wsgi.file_wrapper. This can be used to transmit file like objects from the filesystem using operating system facilities like sendfile.

WSGI containers

There is a number of dedicated WSGI containers that one can use. The most popular one being uwsgi which is basically a simple wrapper around one of the event handling libraries and the language plugin (Python being always present). It also allows for arbitrary plugins that might handle MQTT messages or other stuff.

Other popular containers are:

In addition there are browser plugins that speak the wsgi protocol such as mod_wsgi for the Apache httpd web server.

When using an application server one usually does not expose the application server directly to the outside world - one usually uses a web server, at least a load balancer or any other component that plays reverse proxy in front of it. Especially for serving statics, terminating SSL connections, etc. The most common layouts use web servers such as nginx or Apache http in front of the application servers. Also keep in mind that you usually don’t want to trust the pretty young and novel implementations of those Python application servers and take the usual precautions of partitioning your systems to isolate them in your environment as much as possible - but that’s a good practice anyways.

Installing uwsgi on FreeBSD

To get started one might use the uswgi application server. This works pretty fast especially for development and can also be configured for production environments. Note that this requires a little bit more considerations than mentioned here in the beginning - don’t use uswgi by simply launching your script on any production machine …

Installation is pretty simple and can be done through packages or ports (don’t install via pip though:

pkg install www/uwsgi

or

cd /usr/ports/www/uwsgi
make install clean

The latter approach of course allows one to set compile time options - uwsgi is pretty flexible. Note that this also installs the /usr/local/etc/rc.d/uwsgi init script that allows one to launch uswgi with the standard rc.conf framework.

The available /etc/rc.conf environment variables are:

At time of writing the package does not install any sample ini scripts though.

The most simple WSGI application

So let’s start with the most simple hello world WSGI application. This can be written either imperative or as a class implementation. The most simple way is the imperative structure - but it can be any callable named application:

import uwsgi

def application(env, start_response):
       start_response(
               "200 OK",
               [
                       ('Content-type', 'text/plain'),
                       ('Content-language', 'en')
               ]
       )
       return [
               b"Hello world!"
       ]

Running form a file

To launch a simple test version of this script on the local development machine one can directly launch it using the --wsgi-file parameter for uwsgi:

$ uwsgi --http :1234 --wsgi-file helloworld.py --need-app

The argument --http :1234 starts the application server listening to requests at http://localhost:1234. In addition to http uswgi is also capable of listening on:

Note that the http or https sockets don’t have to be real network sockets - one can also use a Unix domain socket which might be of special interest behind a reverse proxy (that one should use anyways) when one restricts network access of the container itself. This can be done by simply specifying the filename of the Unix domain socket:

$ uwsgi --http /path/to/socket.sock --wsgi-file helloworld.py --need-app

Running from a package

So running from a file is ok for simple demonstration purposes and the most simple applications but this might not be what one usually has in mind when deploying a Python application. More likely ones application will be packaged using setuptools such as any other Python application.

To build the package one requires again a simple pyproject.toml and a simple setup.cfg. For demonstration purposes the author used the following pyproject.toml:

[build-system]
requires = [
    "setuptools>=42",
    "setuptools-git-versioning",
    "wheel"
]
build-backend = "setuptools.build_meta"

[tool.setuptools-git-versioning]
enabled = true

and the following setup.cfg:

[metadata]
name = modulewsgihelloworld-tspspi
version = 0.0.1
author = Thomas Spielauer
author_email = pypipackages01@tspi.at
description = Just a demonstration hello world project on how to package WSGI applications
long_description = file: README.md
long_description_content_type = text/markdown
url = https://github.com/tspspi/modulewsgihelloworld
classifiers =
    Programming Language :: Python :: 3
    License :: OSI Approved :: BSD License
    Operating System :: OS Independent

[options]
package_dir =
    = src
packages = find:
python_requires = >=3.8
install_requires =
    matplotlib >= 3.4.1

[options.packages.find]
where = src

In a src/modulewsgihelloworld/helloworld.py the following source has been added:

import uwsgi

def mainhello(env, start_response):
	start_response(
		"200 OK",
		[
			('Content-type', 'text/plain'),
			('Content-language', 'en')
		]
	)
	return [
		b"Hello world!"
	]

To be a valid module src/modulewsgihelloworld/__init__.py is existing (but just an empty file) and to prevent problems when building the package with git versioning (though not releasing that one on PyPi or course) one can also add a gitignore to src/modulewsgihelloworld/.gitiginore ignoring the *.egg-info and a gitignore to dist/.gitignore that ignores and *.tar.gz and *.whl. This results in the following directory structure:

 |- pyproject.toml
 |- setup.cfg
 |- dist
 |  |- .gitignore
 |- src
    |- modulewsgihelloworld
	   |- __init__.py
	   |- helloworld.py

Then the module got built (see my previous blog article for details using

$python -m build

After installing the given module (pip install dist/*.whl) the module can be executed using the module option:

$ uwsgi --module modulewsgihelloworld.helloworld:mainhello --http :1234 --need-app

When using packages one is now really able to use either one’s own package repository, package files or even PyPi to distribute and upgrade applications using ones build automation system

Reading uwsgi configuration from an --ini or --yaml file

Above the configuration for uwsgi has been read from the command line. This is of course inconvenient especially when one launches it with more elaborate features (uwsgi supports clustering, local synchronized caches, various life cycle management algorithms, different event loops, filesystem mounting and unmounting when running a horizontally scalable cloud system; it supports reloading on different external mechanisms, monitoring via metrics and statistics, multicasting, async mode, lazy loading mode, different clock sources, static file serving, etc.). Most of the features are especially important when moving to a production system or writing more complex applications.

Some of the interesting options for a beginner that one should know of are:

All of those options can be set in an ini or yaml file that’s then passed to uwsgi. Using an ini this might look like the following:

[uwsgi]
http = :1234
https = :1235,mycert.crt,mykey.key
chdir = /my/app/directory

module = my.example.module.file:MyHandler()
virtualenv = /path/to/venv

master = true
processes = 4
threads = 8

One can then launch uwsgi using:

$ uwsgi --ini filename.ini

This article is tagged:


Data protection policy

Dipl.-Ing. Thomas Spielauer, Wien (webcomplains389t48957@tspi.at)

This webpage is also available via TOR at http://rh6v563nt2dnxd5h2vhhqkudmyvjaevgiv77c62xflas52d5omtkxuid.onion/

Valid HTML 4.01 Strict Powered by FreeBSD IPv6 support