LFE Programming Rules and Conventions

5 Processes, Servers and Messages

5.1 Implement a process in one module

Code for implementing a single process should be contained in one module. A process can call functions in any library routine but the code for the "top loop" of the process should be contained in a single module. The code for the top loop of a process should not be split into several modules - this would make the flow of control extremely difficult to understand. This does not mean that one should not make use of generic server libraries, these are for helping structuring the control flow.

Conversely, code for no more than one kind of process should be implemented in a single module. Modules containing code for several different processes can be extremely difficult to understand. The code for each individual process should be broken out into a separate module.

5.2 Use processes for structuring the system

Processes are the basic system structuring elements. But don't use processes and message passing when a function call can be used instead.

5.3 Registered processes

Registered processes should be registered with the same name as the module. This makes it easy to find the code for a process.

Only register processes that should live a long time.

5.4 Assign exactly one parallel process to each true concurrent activity in the system

When deciding whether to implement libraries and applications using sequential or parallel processes, look to the nature of the problem the software is solving; when clearly analyzed, its structure will be clear. This structure, the one inherint in the solution, should be used.

The main rule is as follows:

"Use one parallel process to model each truly concurrent activity in the
real world"

If there is a one-to-one mapping between the number of parallel processes and the number of truly parallel activities in the real world, the program will be easy to understand.

5.5 Each process should only have one "role"

Processes can have different roles in the system. Take, for example, the client and server model: one role is to serve, responding to requests; the other role is to make requests when connecting to the server(s).

As far as possible, a process implementation should cleanly mirror the separation that exists in the conceptual model of the roles of a given system. A process should only have one role, e.g., it can be a client or a server, and should not combine or conflate these roles.

Other roles which process might have are:

  • Supervisor: watches other processes and restarts them if they fail.
  • Worker: a normal work process (can have errors).
  • Trusted Worker: not allowed to have errors.

5.6 Use generic functions for servers and protocol handlers wherever possible

In many circumstances it is a good idea to use generic server programs such as the generic server implemented in the standard libraries. Consistent use of a small set of generic servers will greatly simplify the total system structure.

The same is possible for most of the protocol handling software in the system.

5.7 Tag messages

All messages should be tagged. This makes the order in the receive statement less important and the implementation of new messages easier.

Don't program like this:

(defun loop (state)
  (receive
    ...
    ; don't do the following
    ((tuple mod func args)
      (apply mod func args)
      (loop state))
    ...))

The new message (tuple 'get-status-info, from, option) will introduce a conflict if it is placed below the (tuple mod func args) message.

If messages are synchronous, the return message should be tagged with a new atom, describing the returned message. Example: if the incoming message is tagged 'get-status-info, the returned message could be tagged 'status_info. One reason for choosing different tags is to make debugging easier.

This is a good solution:

(defun loop (state)
  (receive
    ...
    ((tuple 'execute mod func args) ; use a tagged message
      (apply mod func args)
      (loop state))
    ((tuple 'get-status-info from option)
      (! from (tuple 'status-info (get-status-info option state)))
      (loop state))
    ...))

5.8 Flush unknown messages

Every server should have an other alternative in at least one receive statement. This is to avoid filling up message queues.

Example:

(defun main-loop ()
  (receive
    ((tuple 'msg-1 message-1-data)
      ...
      (main-loop))
    ((tuple 'msg-2 message-2-data)
      ...
      (main-loop))
    (other ; flushes the message queue
      (error-logger:error-msg
        '"Error: Process ~w got unknown message ~w.~n"
        (list (self) other))
      (main-loop))))

5.9 Write tail-recursive servers

All servers must be tail-recursive, otherwise the server will consume memory until the system runs out of it.

Don't program like this:

(defun loop ()
  (receive
    ((tuple msg-1 message-1-data)
      ...
      (loop))
    ('stop
      'true)
    (other
      (error-logger:error-msg
        '"Error: Process ~w got unknown message ~w.~n"
        (list (self) other))
      (loop)))
  (io:format '"Server going down" '())) ; don't do this!
                                        ; this is not tail-recursive

This is a correct solution:

(defun loop ()
  (receive
    ((tuple msg-1 message-1-data)
      ...
      (loop))
    ('stop
      (io:format '"Server going down" '()))
    (other
      (error-logger:error-msg
        '"Error: Process ~w got unknown message ~w.~n"
        (list (self) other))
      (loop))))

If you use some kind of server library, for example generic, you automatically avoid doing this mistake.

5.10 Interface functions

Use functions for interfaces whenever possible, avoid sending messages directly. Encapsulate message passing into interface functions. There are cases where you can't do this.

The message protocol is internal information and should be hidden to other modules.

Example of interface function:

(defmodule
  (export
    (start 0)
    (stop 0)
    (open-file 1)
    ...))

(defun open-file (file-name)
  (! fileserver (tuple open-file-request filename))
  (receive
    ((tuple open-file-response result)
      result))
  ...)

5.11 Time-outs

Be careful when using (after ...) in (receive ...) statements. Make sure that you handle the case when the message arrives later.

(See previous section "Flush unknown messages".)

5.12 Trapping exits

As few processes as possible should trap exit signals. Processes should either trap exits or they should not. It is usually very bad practice for a process to "toggle" trapping exits.