💾 Archived View for separateconcerns.com › 2014-09-20-iris.gmi captured on 2024-02-05 at 09:37:13. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2023-09-28)

-=-=-=-=-=-=-

Iris

published 2014-09-20

Iris [1] is a "decentralized Cloud messaging" middleware that I have really started looking into with the recent release of version 0.3.0. It had struck me as interesting when I first heard of it at FOSDEM 2014 [2]. The reason for that, beyond the great presentation skills of its author, is that it implements principles I think are sound to build SOA upon.

In Iris, the logical and physical layers of services are cleanly separated. Each box in the system runs a local instance of the Iris broker ("Iris node"). The broker is written in Go, and all broker instances in the system converse in a proprietary protocol.

To provide or consume a service, you never have to open a connection to a remote machine: you just talk to your local broker using the Iris relay protocol. This means you can use any programming language you want as long as you implement this protocol, which is much simpler than the protocol used between brokers. There are official clients for the relay protocol (Iris calls them "bindings", but I think that name is confusing) in Go, Erlang and Java. I have written one in Lua [3] that I will use for examples in the rest of this post (if you prefer, you will find corresponding code in Go for most examples here [4]).

In Iris, services are represented by names. To provide a service you register with its name, to consume one you address it by its name. That means you never dial into a specific instance of a service explicitly. From the consumer's point of view, the service could be provided by a single box as well as hundreds across different datacenters. Boxes can go up or down, and you will almost never have to care about it when writing code: it is an operational concern. The way Iris' author puts it is that in Iris the smallest logical entity is a cluster, there is no concept of individual machine or process.

Iris supports three usual basic patterns for communication: Request/Response, Publish/Subscribe and Broadcast. A fourth one, Tunnel, is slightly more complicated.

1: https://github.com/project-iris/iris

2: https://www.youtube.com/watch?v=WTRORimPvHE

3: https://github.com/catwell/cw-lua/tree/master/iris-lua

4: https://github.com/catwell/cw-lua/tree/2e74fcb3546ac4ef1a2f659e8012ff8a78fa7a36/iris-lua/examples

Request/Response

This is the pattern everybody knows about from HTTP, and probably the workhorse of most SOAs. A client sends a request to a cluster, and a single node of the cluster answers it. Iris just does the messaging here: the request and the response are binary blobs, there is no notion of headers.

Here is how it looks in Lua on the client side:

local iris = require "iris"

local c = iris.new()
assert(c:handshake(""))

local req = c:request("echo", "hello", 1000)
c:process_one()
local r = assert(req:response())

c:teardown()

print("reply arrived: " .. r)

and on the service side:

local iris = require "iris"

local c = iris.new()
assert(c:handshake("echo"))

c.handlers.request = function(req)
    print("request arrived: " .. req)
    return req
end

for i=1,5 do c:process_one() end

c:teardown()

Publish/Subscribe

Another well-known pattern. Consumers subscribe to a channel identified by a name, producers send messages into the channel and all subscribers receive them. There is no response from the subscriber. Channels are completely independent from clusters, i.e. channel "X" and cluster "X" can coexist and have nothing in common, and there is no restriction that a channel only works within a given cluster (any entity on the same Iris network can access it).

Here is a publisher to channel "somechan" in Lua:

local iris = require "iris"
local socket = require "socket"

local c = iris.new()
assert(c:handshake(""))

for i=1,5 do
    c:publish("somechan", "message " .. i)
    socket.sleep(1)
end

c:teardown()

and a consumer:

local iris = require "iris"
local socket = require "socket"

local c = iris.new()
assert(c:handshake(""))

c.handlers.pubsub.somechan = function(msg)
    print("message arrived: " .. msg)
end

c:subscribe("somechan")

for i=1,5 do c:process_one() end

c:teardown()

Broadcast

We have seen that requests are sent to a single instance of a cluster. What if you want to notify all members of a cluster of some event? This is what Broadcast is for. It is kind of redundant with Publish/Subscribe (you could have all members of each cluster subscribe to a channel) but it is somehow cleaner to have a separate message type for this. You could use it to build more complicated things on top, for instance a full-fledged service bus.

Here is how to send a broadcast message in Lua:

local iris = require "iris"

local c = iris.new()
assert(c:handshake(""))

c:broadcast("bcst", "hello")
c:teardown()

and how to handle them:

local iris = require "iris"

local c = iris.new()
assert(c:handshake("bcst"))

c.handlers.broadcast = function(msg)
    print("message arrived: " .. msg)
end

for i=1,5 do c:process_one() end

c:teardown()

Tunnel

Tunnels are the last and most complicated way to communicate provided by Iris. To understand why we need them, remember how requests work.

The fact that any member of a cluster can answer a request prevents you from doing anything stateful, because you cannot know if you are talking to the same instance or not in two separate requests. Moreover, the nature of the request and response does not allow any of them to be composed of multiple parts sent in separate messages. A request cannot trigger multiple responses.

Tunnels are a solution to all that. When you open a tunnel, you address a cluster, but what you obtain is a persistent, bidirectional, ordered pipe to a specific instance of that cluster. You can think of it as a TCP socket or a circuit if you come from a telecom background (although there are no latency guarantees). Tunnels implement a throttling algorithm so that both ends of the tunnel can specify a maximum size for their input buffer.

For now, here is what I think of tunnels in the context of a SOA: they are very powerful but tricky to use. If you can avoid them, you should. If not, you should not use raw tunnels anyway: you should define your protocol, write an abstraction for it and never expose the tunnel itself to the application.

One valid use case I can see for tunnels, and which is used in Iris examples, is streaming. But this can go much further. Like the name indicates, you could tunnel most protocols into them, so they could serve as the foundation for some kind of VPN.

Anyway, here is an example client with multiple responses and state:

local iris = require "iris"

local c = iris.new()
assert(c:handshake(""))

local tun = c:tunnel("tunnel", 1000)
tun:confirm()
tun:allow(1024)

tun.handlers.message = function(msg)
    if msg == "data" then
        print("got data")
    elseif msg == "continue" then
        print("got continue, sending request")
        tun:cosend("request")
    elseif msg == "bye" then
        print("got bye, quitting")
        tun:close()
    else
        print("got invalid message: " .. msg)
        tun:close()
    end
end

local xfer = tun:transfer("request")
while not xfer:run() do c:process_one() end

local op
while op ~= iris.OP.TUN_CLOSE do op = c:process_one() end

c:teardown()

and the server on the other end:

local iris = require "iris"

local c = iris.new()
assert(c:handshake("tunnel"))

local MAX_COUNT = 3
local count = 0

c.handlers.tunnel = function(tun)
    tun:allow(1024)
    tun.handlers.message = function(msg)
        if msg == "request" then
            if count < MAX_COUNT then
                count = count + 1
                print(string.format(
                    "got request, sending %dx data + continue",
                    count
                ))
                for i=1,count do tun:cosend("data") end
                tun:cosend("continue")
            else
                print("got request, sending bye")
                tun:cosend("bye")
            end
        else
            print("got invalid message: " .. msg)
            tun:close()
        end
    end
end

local op
while op ~= iris.OP.TUN_CLOSE do op = c:process_one() end

c:teardown()

Conclusion

Iris is a very interesting piece of software which implements a vision of systems that I like and cannot find in any other product out of the box. It is still very young and probably not ready for serious production yet, although I have found it stable during the (limited) testing I have done. Even though my current job does not involve SOA I will probably come back to it someday, so Iris will join the list of tools whose progress I follow attentively.

On a side note, Iris was built for its authors' thesis, and he is now looking for a sponsor to allow him to continue working on it. If you have the power to make that happen, give it a look. Think how VMWare / Pivotal must be happy of the deal they did with Antirez [5] :)

Finally, if you try my Iris client [6] and find bugs, do not hesitate to report them on Github! I have no real world Iris system to try it on so there are certainly plenty.

5: http://invece.org/

6: https://github.com/catwell/iris-lua