Erlang Central

Writing an Erlang Port using OTP Principles

Revision as of 14:44, 21 June 2007 by SyaJoi (Talk | contribs)

Contents

Author

Pete Kazmier

Overview

Note: This HOW-TO was converted to a wiki format from its original format. The original, which is easier to read, can be found here. At some point, I'll clean up the wiki version.

Introduction

There are several different mechanisms that are available when trying to make Erlang interact with the external world. These are described in the official Erlang documentation in the Tutorial. This is a tutorial on how to use one of those techniques, the Erlang port. The source code for the tutorial can be found here: [1]

The tutorial demonstrates how to communicate to an external program using a standard Erlang port. A simple echo server, written in Python, will be used in the example. The port will enable us to send Erlang messages to the echo server via its standard input and standard output. The echo server will read data from standard input and echo it back, along with a time stamp, to our Erlang port.

In addition, the example utilizes several standard OTP behaviors such as gen_servers, supervisors, and applications for completeness. We want to ensure that our application is robust in the event that the external echo server crashes or does not respond in a timely manner. The use of standard OTP behaviors enables us to do so with minimal effort.

The Big Picture

A high-level overview of the components is presented in this section. It is intended to be the road map for the rest of the document. Subsequent chapters provide additional detail on each of the components as well as the source code for each. Lets begin with a diagram illustrating the big picture.

Figure 1.1: The Big Picture

The diagram depicts two OS-level processes, an Erlang virtual machine and a Python interpreter, communicating with each other using standard OS pipes via stdin and stdout. These two processes are completely independent of each other in all other respects and are treated individually by the OS. On the other hand, all of the Erlang processes reside within the same OS-level process, the Erlang virtual machine.

Note:

         An OS-level process should not be confused with an Erlang
         process. 

Within the Erlang virtual machine, there are three client processes, a supervisor, a gen_server, and a port to the external Python process. You'll also notice that three of the components have been co-located with each other in the box labeled echo_app. These components represent the OTP application that we are going to be build. The use of an OTP application allows us to start and stop all of the processes associated with our echo server as a single entity. In addition, it'll enable others to incorporate our application in other systems with minimal effort.

Because we are implementing a centralized server that is going to handle and respond to client requests, the use of the OTP gen_server behavior will greatly simplify our implementation. The gen_server behavior provides a framework for modeling client-server relationships where a server is managing a resource that is going to be shared between one or more clients. In this case, the gen_server will be responsible for managing access to the port that is communicating with the external Python process.

Ports can be viewed as external Erlang processes. They can be used to communicate with external OS-level processes or access any opened file descriptors in use by the Erlang VM. Messages can be sent and received from ports just as if they were any other Erlang process. In this tutorial, messages sent to the port will be sent to our external Python process. Likewise, when the Python process sends data to the Erlang VM, a message will be sent back to the port's controlling process, the process that opened the port. In this implementation, the gen_server is responsible for the opening and closing of the port. This will ensure that all messages sent from the port arrive at the gen_server process.

By default, external processes spawned via the opening of an port communicate to the Erlang VM via standard input and standard output. Our Python process will read lines of data via its stdin, and then write responses back via stdout.

Note:

         Care must be taken when communicating via OS-level pipes
         using stdin and stdout as both streams are buffered by the C
         stdio library.  More details will be described in the
         chapter on the implementation of the Python echo server.

Clients will use a specified API, echo/1, to make requests to our echo server. By using this API, clients will be insulated from the implementation details of our echo server. The client does not need to know it is communicating with another process. This allows us to change our implementation later if we choose.

Finally, in order to provide a reliable service, we will employ the use of a supervisor to monitor our gen_server process. Within the OTP framework, supervisors monitor the behavior of worker processes, and can restart them in the event something goes wrong. As you will see, our gen_server process is linked to the port, and as a result, if the port (or the external Python process) terminates abnormally, our gen_server process will also terminate, forcing the supervisor to restart the gen_server and thus the external Python process.

Subsequent chapters will explore each of these components and their implementations in detail. Before moving on, we should take a brief moment to discuss the standard directory layout of an OTP application and where each of the components that are discussed in this tutorial should reside. Here is the layout for this project:

Code listing 1.1: Directory layout of the application

`-- echo_app-1.0
    |-- ebin
    |   |-- echo.beam
    |   |-- echo_app.app
    |   |-- echo_app.beam
    |   `-- echo_sup.beam
    |-- priv
    |   `-- echo.py
    `-- src
        |-- echo.erl
        |-- echo_app.erl
        `-- echo_sup.erl
        

All of the Erlang source code should reside in the src directory, the Python program should reside in the priv directory, and the compiled sources and application definition file should be in the ebin directory. It's now time to start digging into the details of the tutorial.

Note:

         Each chapter going forward begins with a discussion on the
         design, followed by a section that walks through the actual
         implementation.  The design section is meant to be read
         without access to the source code.

External Python Echo Server

Design

Lets start with the design of the external Python echo server. As the previous chapter already mentioned, the Erlang VM will communicate with this process via an Erlang port, which by default will use the standard input and standard output of the Python process to communicate to the port. Requests will be processed one at a time, and will yield a response before the next request is processed. Communication is therefore synchronous.

There are a few things to consider when implementing our external Python process. First, we have decided to use the standard IO mechanisms of Python which utilize the stdio library. Therefore, we must be aware that stdin and stdout will be fully buffered because we are communicating with Erlang via standard OS-level pipes. For those that do not recall, the stdio library uses line buffering on file objects associated with a terminal device, and fully buffers everything else with the exception that stderr is never buffered.

The second item to consider is the communication protocol used between the Erlang port and the Python process. Although we have already decided that communication will occur over stdin and stdout, we have yet to discuss the exact format of the requests that will arrive on stdin, and the exact format of the responses that will be sent back via stdout. We will explore this topic in the next section.

Third, what should we do in the event of an error? Do we try and write a robust Python server that catches every single possible error? Or do we let our Python process terminate upon any error? For the purpose of this tutorial, we will allow our Python process to terminate upon any error because we will design our Erlang port to terminate our gen_server process which will then force the supervisor to restart everything including the external Python process. I'm new to Erlang, but it seems that this is the most common approach to programming within Erlang, let things fail, and let your supervisors correct the problem.

In the next section, the communication protocol between the external server and port is discussed in detail.

Communication Protocol

The communication protocol used between our Erlang application and this external Python process will be line oriented. Requests will arrive on stdin as a single line per request. The server will read and process one request at a time. After the request has been processed, one or more lines will be sent back via stdout. The Erlang process must continue reading data from the process until a line with a single OK has been returned. This will indicate the end of the current response.

For the purpose of this tutorial, the Python server will simply respond with three lines of data: the original request, the current time, and a single OK. It should be noted that we are not limited to three lines of output, and that the protocol could be used with some other process that adheres to it. Here is a sample interaction with the Python echo server (the emphasized lines are user input):

Code listing 2.1: Sample Interaction with the Python Echo Server

kaz@coco:lib/echo_app-1.0/priv$ ./echo.py
Hello there!
Received Hello there!
Current time is Mon Jan 23 16:51:43 2006
OK
Is anyone home?
Received Is anyone home?
Current time is Mon Jan 23 16:51:48 2006
OK
^D  # Control-D was pressed to close stdin
        

As you can see, the Python echo server lives up to its name. It simply echoes back whatever it received along with some additional data to make the example a little more interesting by forcing us to read an arbitrary amount of response data. With that said, lets move on to the implementation of the echo server.

Implementation

Here is the full implementation of the Python echo server. Do not forget to place this source file in the priv directory of your directory layout. Although Python was chosen as the implementation language, even those that are not familiar with Python should not have any problem understanding this trivial program.

Code listing 2.2: Implementation of echo.py

#!/usr/bin/python

import sys
import time

while 1:
    line = sys.stdin.readline()
    if not line: break

    # Send back the received line
    print "Received", line,

    # Lets send back the current time just so we have more than one
    # line of output.
    print "Current time is", time.ctime()

    # Now send our terminating string to end the transaction.
    print "OK"

    # And finally, lets flush stdout because we are communicating with
    # Erlang via a pipe which is normally fully buffered.
    sys.stdout.flush()
        

As you can see, the Python process sits in a loop forever reading one line at a time from stdin. The line is then echoed back along with a time stamp followed by our terminator string. There are two items of importance, both of which deal with buffering issues.

First, note the call to sys.stdout.flush(). This forces the stdio library to flush the current stdout buffer. It ensures that Erlang will receive a response to the request. If this was not provided, our Erlang server may hang indefinitely if we did not code it in a robust manner (which we will do to protect ourselves from this scenario should it happen).

Second, which is a bit more subtle, and only of interest to those that are familiar with Python's idiom of reading lines for a file object. The normal way to process one line at a time is with the following idiom:

Code listing 2.3: Python Idiom for Reading Lines from a File Object

#!/usr/bin/python

import sys

for line in sys.stdin:
    # Do something with line here
    print line
        

Python uses an internal buffering when reading from file-object iterators such as the one above. What does this mean? Even though the Erlang VM may have sent a line, the Python process may be buffering it internally, thus blocking forever. This internal buffer can be avoided by using the technique in our implementation of the Python echo server. This ensures that Python has access to the data as soon as Erlang sends it across the pipe. Again, this is an implementation note to those that may already know Python.

An Aside

Although the Python echo server is trivial, it was used as an example because it could be easily replaced with any number of other programs that follow the same communication protocol. One such example is [2] which happens to be the author's original motivation for writing this tutorial. In fact, as you'll see, which external program to be spawned can be specified as a system configuration parameter to our OTP application.

Client API

Design

Clients will access the server through an API call to echo:echo/1 instead of directly sending messages to the gen_server. The function takes a single string argument which will eventually be processed by our external Python echo server. By providing an API call, we provide a layer of indirection that will hide implementation details the client need not be aware of.

This function will return a list of lists. Each line of output generated by our Python echo server will yield a list of one or more strings depending on whether the line of output exceeds the buffer size specified when the port is opened. This will be explained in more detail in the next chapter. Recall that the Python server sends back a line containing the original request, as well as a line containing the current time stamp. Lets look at some output from the use of echo:echo/1:

Code listing 3.1: Sample Output of Client API

kaz@coco$ erl -boot echo -boot_var MYAPPS ~/port_example
Erlang (BEAM) emulator version 5.4.6 [source] [threads:0]

Eshell V5.4.6  (abort with ^G)
1