Iris through Erlang... API v1

Bringing simplicity back to distributed services

Péter Szilágyi

European Institute for Innovation and Technology

Eötvös Loránd University, Budapest, Hungary

Babeş-Bolyai University, Cluj-Napoca, Romania

Note, these are the offline slides of the presentation. For executable codes, please check playground availability at http://iris.karalabe.com/talks.

What the heck is Iris?

Decentralized cloud messaging

I.e. If starting a batch of VMs on Google Compute Engine is a one liner,

gcutil addinstance vm-1 vm-2 ... vm-N

Assembling them into a distributed system should be a one liner too!

iris -net <service name> -rsa <private key>

What can it do?

It communicates, of course!

What makes it special? Simplicity!

Instances consolidated based on responsibility

Clusters are the smallest logical units

⊕ Harder to abuse, fewer moving components, self organization

Meaningful and non-dynamic addressing

Routing based on semantic addressing

⊕ Implicit failovers, automatic load balancing, simpler client code

Implicit security while trusting the trustworthy

Security at service level

⊕ Decoupled system, optimized encryption, out of the box

Show me the code!

Challenge #0 – Boilerplate

Assemble a network of micro services:

Solution #0 – Behavior iris_server

-behaviour(iris_server).
-export([init/2, handle_broadcast/2, handle_request/3, handle_tunnel/2,
    handle_drop/2, terminate/2]).

% Implement the startup and cleanup methods of iris_server.
init(_Conn, your_init_args) -> {ok, your_state}.
terminate(_Reason, _State)  -> ok.

main() ->
    % Register a micro-service instance into the network
    {ok, Server} = iris_server:start(55555, "Erlang Service", ?MODULE, your_init_args),

    % Unregister the service
    ok = iris_server:stop(Server).

% Remaining callbacks methods, not used in this demo
handle_broadcast(_Message, State)      -> {stop, not_implemented, State}.
handle_request(_Request, _From, State) -> {stop, not_implemented, State}.
handle_tunnel(_Tunnel, State)          -> {stop, not_implemented, State}.
handle_drop(_Reason, State)            -> {stop, not_implemented, State}.

Note: Connecting as a simple client is also supported.

Behavior life-cycle highlights

iris_server:start(Port::int(), Cluster::string(), Module::atom(), Args::term()) → Service
Module:init(Client :: iris:client(), Args :: term()) → {ok, State :: term()}

Termination was omitted, but is completely analogous to gen_server.

Challenge #1 – Web requests

Simulate a system for handling web requests:

Solution #1 – Browser

% Connect to the network as a simple client
{ok, Client} = iris_client:start(55555),

% Issue a dummy request every second
lists:foreach(fun(Index) ->
    Request = iolist_to_binary(io_lib:format("Request #~p", [Index])),
    case iris_client:request(Client, "webserver", Request, 1000) of
        {ok, Reply}     -> io:format("Web reply: ~s~n", [Reply]);
        {error, Reason} -> io:format("Request failed: ~s~n", [Reason])
    end,
    timer:sleep(1000)
end, lists:seq(1, 60)),

% Disconnect from the network
ok = iris_client:stop(Client).

Hint: Start some webservers and check back 😉

Solution #1 – Web server

%% Format each request a bit and return as the reply
handle_request(Request, _From, Id) ->
    Response = <<Id/binary, ": ", Request/binary>>,
    {reply, {ok, Response}, Id}.

main() ->
    % Generate a random ID for the web server
    random:seed(erlang:now()),
    Id = list_to_binary(io_lib:format("erl-www-~p", [random:uniform(100)])),

    % Register a webserver micro-service into the network
    {ok, Server} = iris_server:start(55555, "webserver", ?MODULE, Id),

    io:format("Waiting for inbound requests..."),
    timer:sleep(60 * 1000),

    ok = iris_server:stop(Server).

The presentation supports only one active demo process per window. Open new tab?

Request / Reply highlights

iris_client:request(Client, Cluster::string(), Request::binary(), timeout::int()) → Reply
Module:handle_request(Request :: binary(), State :: term()) → {reply, Response, State}

Challenge #2 – Aperture Science Enrichment Center 😈

Implement the comlink for Aperture Laboratories¹:

¹ http://en.wikipedia.org/wiki/Portal_(video_game)

Solution #2 – GLaDOS

% Connect to the Iris network as GLaDOS
{ok, Client} = iris_client:start(55555),

io:format("GLaDOS is online, sending wishes...~n"),

lists:foreach(fun(_) ->
    % Pick a random wish
    Wish = lists:nth(random:uniform(count()), wishes()),

    ok = iris_client:publish(Client, "official", <<"GLaDOS: ", Wish/binary>>),
    timer:sleep(5000)
end, lists:seq(1, 100)),

ok = iris_client:stop(Client).

Hint: Boot GLaDOS and let the experiment begin 😉

Solution #2 – Chell

-behavior(iris_topic).
-export([init/1, handle_event/2, terminate/2]).

%% Implement the startup and cleanup methods of iris_topic.
init(nil)                  -> {ok, nil}.
terminate(_Reason, _State) -> ok.

%% Print all events arriving on a subscription
handle_event(Event, State) ->
    io:format("~s~n~n", [Event]),
    {noreply, State}.

main() ->
    {ok, Client} = iris_client:start(55555),

    io:format("Tuning in to Aperture channels...~n"),
    iris_client:subscribe(Client, "official", ?MODULE, nil),

    timer:sleep(60 * 1000),
    ok = iris_client:stop(Client).

Hint: Maybe there is an "unofficial" channel? 😉

Publish / Subscribe highlights

iris_client:subscribe(Client, Topic :: string(), Module :: atom(), Args :: term())
iris_client:publish(Client, Topic :: string(), Event :: binary())
Module:handle_event(Event :: binary(), State :: term()) → {noreply, State}

Challenge #3 – Data repository

Implement a data distribution system:

Solution #3 – Client

main() ->
    {ok, Client} = iris_client:start(55555),

    % Open an outbound tunnel to a data store
    case iris_client:tunnel(Client, "repository", 1000) of
        {error, Reason} -> io:format("Tunneling failed: ~p~n", [Reason]);
        {ok, Tunnel}    ->
            % Request a file and retrieve the multi-part response
            iris_tunnel:send(Tunnel, <<"some file">>, 1000),
            fetch(Tunnel)
    end,
    ok = iris_client:stop(Client).

fetch(Tunnel) ->
    case iris_tunnel:recv(Tunnel, 1000) of
        {ok, Data} -> io:format("~s~n", [Data]), fetch(Tunnel);
        _Otherwise -> ok = iris_tunnel:close(Tunnel)
    end.

Hint: Start some data repositories 😉

Solution #3 – Data store

% Callback of iris_server, invoked when a tunnel is inbound
handle_tunnel(Tunnel, Id) ->
    % Fetch the file name
    {ok, Name} = iris_tunnel:recv(Tunnel, 1000),

    % Simulate sending some multi-part data stream
    lists:foreach(fun(Part) ->
        Piece  = io_lib:format("Erlang repo #~p: <~s> part #~p", [Id, Name, Part]),
        Binary = iolist_to_binary(Piece),

        ok = iris_tunnel:send(Tunnel, Binary, 1000)
    end, lists:seq(1, 10)),

    ok = iris_tunnel:close(Tunnel),
    {noreply, Id}.

The presentation supports only one active demo process per window. Open new tab?

Tunnel highlights

iris_client:tunnel(Client, Cluster :: string(), Timeout :: int()) → Tunnel
iris_tunnel:send(Tunnel, Message :: binary, Timeout :: int())
iris_tunnel:recv(Tunnel, Timeout :: int()) → Message

How does this all work?

Sneak behind the scenes

Iris nodes do the heavy lifting (one/host):

Thin clients bathe in the glory:

Where to go next?

Iris resources

Iris community:

Thank you

Péter Szilágyi

European Institute for Innovation and Technology

Eötvös Loránd University, Budapest, Hungary

Babeş-Bolyai University, Cluj-Napoca, Romania