Messaging Patterns in Bash

Around the time when I was implementing queues in toolbox, I was reading Enterprise Integration Patterns, a book about messaging patterns, written by Gregor Hohpe (whom I worked with more than a decade ago) and Bobby Woolf. While reading the book, I thought how awesome it would be if I could use my queues to implement messaging in Bash — and that’s how I got inspired to write the uipc module. Because there is no documentation but an API reference, I decided to write a few articles about how the uipc module can be used to implement some of the messaging patterns from the book.

Point-to-Point Channels

The most basic communication that can be implemented with the uipc module is a direct message transfer from the sender to the receiver. The sender places messages in a queue, the channel, from where the receiver will pick them up. If there is more than one receiver, the channel guarantees that the message is picked up by only one of the receivers. This is what EIP calls a Point-to-Point Channel. This pattern is fairly easy to realize with toolbox/uipc, although the terminology is somewhat different.

At first, the script needs to source toolbox and include the uipc module, which contains the ipc_* functions (note that there is also a module called ipc, which is functionally equivalent but uses GPG to sign and verify messages).

. toolbox.sh
include "uipc"

To send messages, a process now needs to create an IPC endpoint like so:

priv_ep=$(ipc_endpoint_open)

This will open a private (or unnamed, or anonymous, or whatever you like to call it) endpoint. Because the address of a private endpoint is random, it’s only useful if you are the one who is initiating the communication.

If you want to receive messages from someone, you need an endpoint with an address that other processes can refer to. Thus, you open your endpoint by passing an address to ipc_endpoint_open() and you get a public endpoint.

pub_ep=$(ipc_endpoint_open "foobar")

A sender can now send messages to the public endpoint by passing its address to ipc_endpoint_send().

ipc_endpoint_send "$priv_ep" "foobar" "Hello world"

This places the message in the queue of the public endpoint, where the receiving process can pick it up with a call to ipc_endpoint_recv(). To put it short, a public endpoint in toolbox/uipc is a Point-to-Point Channel.

The following is what a simple sender looks like (for the sake of brevity, I have omitted any error handling).

. toolbox.sh
include "uipc"

channel="foobar"
message="Hello world"

endpoint=$(ipc_endpoint_open)
ipc_endpoint_send "$endpoint" "$channel" "$message"

And this is what the receiving end looks like.

. toolbox.sh
include "uipc"
include "log"

channel="foobar"
endpoint=$(ipc_endpoint_open "$channel")

while true; do
    message=$(ipc_endpoint_recv "$endpoint")
    ipc_msg_get_data "$message" | log_highlight "Message"
done

Competing Consumers

Endpoints also support multiple receivers as described in the pattern. You can open the same public endpoint from multiple processes, and every message is received by only one of the processes. Because each message is received by only one of the processes, multiple processes can listen on the same public endpoint to process requests in parallel. This is yet another pattern, called Competing Consumers, since the listening processes are competing to get the next message from the endpoint (or channel).

The following is how you would implement the pattern with toolbox/uipc (again, without error handling).

. toolbox.sh
include "uipc"

consumer() {
    local channel="$1"

    local endpoint
    local message

    endpoint=$(ipc_endpoint_open "$channel")

    while true; do
        message=$(ipc_endpoint_recv "$endpoint")
        handle_message "$message"
    done
}

channel="foobar"
num_cpus=$(nproc)

for (( i = 0; i < num_cpus; i++ )); do
    ( consumer "$channel" ) &
done
wait

The above example will spawn one consumer child process per CPU and wait until all of them have ended (which they never will). Of course, you could also have the parent process consume messages from the endpoint and execute the script multiple times. Endpoints can be shared by independent processes, so strictly speaking you could have multiple different scripts share the same endpoint. I’m not sure if that’s something one would actually want to do, though.