Application Startup Sequence¶
The most common way to start up a Baseplate.py application is to run one of the the baseplate-serve script. This page explains exactly what’s going between that command and your application.
baseplate-script is another way to run code in a Baseplate.py
application that is generally useful for ephemeral jobs like periodic crons
or ad hoc tasks like migrations. That script follows a much abbreviated form
of this sequence.
The Python Interpreter¶
Because this is a Python application, before any code in Baseplate or your application can run, the Python interpreter itself must set itself up.
There are many many many steps involved in Python’s startup sequence but for our purposes the most important thing to highlight are a number of environment variables that can configure the interpreter.
Now that the interpeter is up, it runs the actual program we wanted it to
baseplate-serve) and the Baseplate.py startup sequence begins.
Before doing anything else, Baseplate.py monkeypatches the standard library to use Gevent, a library that transparently makes Python asynchronous. This allows us to simulate simultaneously processing many requests by interleaving their work and switching the CPU between them as they wait for IO operations like network requests. Monkeypatching replaces most of the APIs in the Python standard library that can block a process with ones provided by Gevent which take advantage of the blocking to swap to other work.
While Gevent gives us easy concurrency, it does not give us
parallelism. Python is still only fundamentally processing these requests in
one thread, one task at a time. Keep an eye out for code that would not
yield to other tasks, like CPU-bound loops, APIs that don’t have
asynchronous equivalents (like
flock()), or dropping into
gevent-unaware native extensions. See the
blocked_hub monitor in
Prometheus Exporter for a tool that can help debug this class of problem.
Monkeypatching is done as early as possible in the process to ensure that all other parts of the startup sequence use the monkeypatched IO primitives.
For more details on Gevent and how it works, see gevent For the Working Python Developer.
Python uses a list of directories, sourced from the environment variable
PYTHONPATH, to search for libraries when doing imports. Because it’s common
to want to run applications from the current directory, Baseplate.py adds the
current directory to the front of the path.
Listening for signals¶
Baseplate.py registers some handlers for
signals that allow
the outside system to interact with it once running. The following signals have
Dump a stack trace to
stdout. This can be useful for debugging if the process is not responsive.
Initiate graceful shutdown. The server will stop accepting new requests and shut down as soon as all currently in-flight requests are processed, or a timeout occurs.
SIGTERM. For use with Einhorn.
SIGTERM. For Ctrl-C on the command line.
Parsing command line arguments¶
Command line arguments are parsed using the Python-standard
baseplate-serve only requires one argument: a path to the configuration
file for your service. The optional arguments
--server-name control which sections of the config file are read. The
remaining options control the way the server runs.
Parsing the configuration file¶
Baseplate.py loads the configuration file from the path given in command line.
The raw file on disk is parsed using a
with interpolation disabled.
Configuration files are split up into sections that allow for one file to hold
configuration for multiple components. There are generally two types of section
in the config file: application configuration sections that look like
[app:foo] and server configuration sections that look like
[server:bar]. After parsing the configuration file, Baseplate.py uses the
section names specified in the
line arguments to determine which sections to pay attention to. If not
specified on the command line, the default section name is
baseplate-serve --app-name=foo would load the
[server:main] sections from the config file.
If you use multiple
server blocks you may find
yourself with a lot of repetition. You can move duplicated configuration to
a meta-section called
[DEFAULT] and it will automatically be inherited
in all other sections in the file (unless overridden locally).
The server configuration section is used to determine which server implementation to use and then the rest of the configuration is passed onto that server for instantiation. See Loading the server for more details. The application configuration section determines how to load your application and then the rest of the configuration is passed onto your code, see the Loading your code section for more details.
Next up, Baseplate.py configures Python’s
logging system. The default
Logs are written to
The default log level is
--debugcommand line argument was passed which changes the log level to
A baseline structured logging format is applied to log messages, see the logging observer’s documentation for details.
This configuration affects all messages emitted through
logging (but not
[loggers] section is present in your configuration file,
is given a chance to override configuration using the standard logging
config file format. This can be useful if you want
finer grain control of what messages get filtered out etc.
Loading your code¶
The next step is to load up your application code.
Baseplate.py looks inside the selected
[app:foo] section for a setting
factory. The value of this setting should be the full name of a
my.module:my_callable where the part before the colon is a
module to import and the part after is a name within that module. The
referenced module is imported with
importlib.import_module() and then
the referenced name is retrieved with
getattr() on that module object.
Once the callable is loaded, Baseplate.py passes in the parsed settings from
[app:foo] section and waits for the function to return an
application object. This is where your application can do all of its one-time
startup logic outside of request processing.
Binding listening sockets¶
Unless running under Einhorn, Baseplate.py needs to create and bind a socket
for the server to listen on. The address bound to is selected by the
option and defaults to
Two socket options are applied when binding a socket:
This allows us to bind the socket even when connections from previous incarnations are still lingering in
This allows multiple instances of our application to bind to the same socket and the kernel distributes connections to them according to a deterministic algorithm. See this explanation of SO_REUSEPORT for more information. This generally only is useful under Einhorn where multiple processes are run on the same host.
Loading the server¶
Baseplate.py now loads the actual server code that will run the main application loop from here on out.
This process is very similar to loading your application code. The
setting in the selected
[server:foo] section of the configuration file is
inspected to determine which code to load. This is generally one of the server
implementations in Baseplate.py but you can write your own in your application
if needed. Once loaded, the rest of the configuration is passed onto the loaded
The new server object has expectations of what kind of application object your
application factory returned. For example, an HTTP server expects a WSGI callable while the Thrift server expects a
Once everything is set up, Baseplate.py writes “Listening on <address>” to the log and hands off control to the server object which is expected to serve forever (unless one of the signals registered above is received) and use your application to handle requests.