Designing long-running applications

A long-running application server will be reading requests from some kind of queue and formulating responses to those requests. In many cases, we leverage the HTTP protocol and build application servers into a web server framework. See Chapter 13, Transmitting and Sharing Objects, for details on how to implement RESTful web services following the Web Server Gateway Interface (WSGI) design pattern.

A desktop GUI application has a lot of features in common with a server. It reads events from a queue that includes mouse and keyboard actions. It handles each event and gives some kind of GUI response. In some cases, the response may be a small update to a text widget. In other cases, a file might get opened or closed, and the state of menu items may change.

In both cases, the central feature of the application is a loop that runs forever, handling events or requests. Because these loops are simple, they're often part of the framework. For a GUI application, we might have a loop like that in the following code:

root = Tkinter.Tk() 
app = Application(root) 
root.mainloop() 

For Tkinter applications, the top-level widget's mainloop() gets each GUI event and hands it to the appropriate framework component for handling. When the object handling events—the top-level widget, root, in the example—executes the quit() method, then the loop will be gracefully terminated.

For a WSGI-based web server framework, we might have a loop like the following code:

httpd = make_server('', 8080, debug) 
httpd.serve_forever() 

In this case, the server's serve_forever() method gets each request and hands it to the application—debug in this example—for handling. When the application executes the server's shutdown() method, the loop will be gracefully terminated.

We often have some additional requirements that distinguish long-running applications:

  • Robust: When dealing with external OS or network resources, there are timeouts and other errors that must be confronted successfully. An application framework that allows for plugins and extensions, enjoys the possibility of an extension component harboring an error that the overall framework must handle gracefully. Python's ordinary exception handling is perfectly adequate for writing robust servers. In Chapter 15, Design Principles and Patterns, we addressed some of the high-level considerations.
  • Auditable: A simple, centralized log is not always sufficient. In Chapter 16, The Logging and Warning Modules, we addressed techniques to create multiple logs to support the security or financial audit requirements.
  • Debuggable: Ordinary unit testing and integration testing reduces the need for complex debugging tools. However, external resources and software plugins or extensions create complexities that may be difficult to handle without providing some debugging support. More sophisticated logging can be helpful.
  • Configurable: Except for simple technology spikes, we want to be able to enable or disable the application features. Enabling or disabling debugging logs, for example, is a common configuration change. In some cases, we want to make these changes without completely stopping and restarting an application. In Chapter 14, Configuration Files and Persistence, we looked at some techniques to configure an application. In Chapter 18, Coping with the Command Line, we extended these techniques.
  • Controllable. A simplistic long-running server can simply be killed in order to restart it with a different configuration. In order to ensure that buffers are flushed properly and OS resources are released properly, it's better to use a signal other than SIGKILL to force termination. Python has signal-handling capabilities available in the signal module.

These last two requirements for a dynamic configuration and clean way to shut down a server, lead us to separate the primary input stream from a secondary control input. This control input can provide additional requests for configuration or shutdown.

We have a number of ways to provide asynchronous inputs through an additional channel:

  • One of the simplest ways is to create a queue using the multiprocessing module. In this case, a simple administrative client can interact with this queue to control or interrogate the server or GUI. For more examples of multiprocessing, see Chapter 13, Transmitting and Sharing Objects. We can transmit the control or status objects between the administrative client and the server.
  • Lower-level techniques are defined in the Networking and Interprocess Communication section of the Python Standard Library. These modules can also be used to coordinate with a long-running server or GUI application.
  • Use persistent storage of state so the long-running process can be killed and restarted with a different configuration. We looked at persistence techniques in Chapter 10, Serializing and Saving - JSON, YAML Pickle, CSV and Shelve; Chapter 11, Storing and Retrieving Objects via Shelve; and Chapter 12, Storing and Retrieving Objects via SQLite. Any of these can be used to save the state of a server leading to a seamless restart.

There are two common use cases for web-based servers:

  • Some servers provide a RESTful API.
  • Some servers are focused on providing the User Experience (UX).

The RESTful API servers are often used by mobile applications, and the UX is packaged separately. A RESTful API server generally maintains state in a persistent database. As part of server reliability engineering, there may be multiple copies of the server to share the workload, and software upgrades often happen by introducing the new release and shifting the workload from the old servers to the new servers. When there are multiple copies of a server, then shared persistent storage is required to keep the state of a user's transaction when each individual request could be handled by different servers. The dynamic configuration and control is handled by shifting workloads and stopping the old servers so the work is handled by new servers.

Providing a UX from a web server often requires maintaining session state on the server. In this case, killing the server means the user's session state is lost. We don't want to have angry users losing the contents of their shopping carts  because we reconfigured a server. If the session information is cached in a database, and the cookie sent to the user is nothing more than a database key, we can create very robust web servers. The FlaskSession project (more info available at https://pythonhosted.org/Flask-Session) provides a number of ways of saving session information in a cache so servers can be stopped and restarted.

The next section shows how to organize code into src, scripts, tests, and docs.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset