Skip to content

Erlang PubNub Client and Chat

I was thoroughly impressed with PubNub, a publish/subscribe service, when I first read their articles and played around with it some in Javascript. But obviously I need an Erlang API if I’m going to really use it! So I’ve created ePubNub.

In the ePubNub README you’ll find information on some basic usage of the application. You don’t have to do anything more than use the epubnub.erl module to publish and subscribe (by either providing a PID to send messages to or a function handler to process each).

Here I’ve built a little more complicated app/release called epubnub_chat, and the source is also on github.

The first thing we need is the epubnub app as a dependency in the epubnub_chat.app file:

   {applications, [kernel, stdlib, epubnub]},

We’ll use a simple_one_for_one for supervising channel subscribed processes. In epubnub_chat_sup we have 3 API functions for the user to use (start_link is run by the _app.erl module on startup): connect/1, connect/2, disconnect/1:

connect(Channel) ->
    supervisor:start_child(?SERVER, [Channel]).

connect(EPN, Channel) ->
    supervisor:start_child(?SERVER, [EPN, Channel]).

disconnect(PID) ->
    epubnub_chat:stop(PID).

EPN is a record containing the necessary url and keys for talking to the PubNub service and is created with the new functions in epubnub:

-spec new() -> record(epn).
new() ->
    #epn{}.

-spec new(string()) -> record(epn).
new(Origin) ->
    #epn{origin=Origin}.

-spec new(string(), string()) -> record(epn).
new(PubKey, SubKey) ->
    #epn{pubkey=PubKey, subkey=SubKey}.

-spec new(string(), string(), string()) -> record(epn).
new(Origin, PubKey, SubKey) ->
    #epn{origin=Origin, pubkey=PubKey, subkey=SubKey}.

You can pass none to connect/1 and it will send none to the chat gen_server and it will use defaults of pubsub.pubnub.com, demo and demo, for the url, publish key and subscribe key respectively.

Now in the epubnub_chat gen_server we need the following API functions:

start_link(Channel) ->
    start_link(epubnub:new(), Channel).

start_link(EPN, Channel) ->
    gen_server:start_link(?MODULE, [EPN, Channel], []).

message(Server, Msg) ->
    gen_server:cast(Server, {message, Msg}).

stop(Server) ->
    gen_server:cast(Server, stop).

The start_link functions are run when the supervisor spawns a simple_one_for_one supervisor for the process. This returns {ok, PID}. This PID must be remembered so we can talk to the process we have started thats subscribed to a specific channel. We pass this PID to the message/2 and stop/1 functions, which we’ll see at the and when we use the program.

start_link/1 and /2 call init/1 with the provided arguments:

init([EPN, Channel]) ->
    {ok, PID} = epubnub_sup:subscribe(EPN, Channel, self()),
    {ok, #state{epn=EPN, pid=PID, channel=Channel}}.

Here we use the epubnub_sup subscribe/3 function and not epubnub:subscribe because we want it to be supervised. We store the PID for this process so we can terminate it later.

The epubnub subscribe process was given the PID, returned by self/1, of the current process which is the gen_server process and will send messages that are published to the channel to that process. We handle these messages in handle_info/2:

handle_info({message, Message}, State) ->
    io:format("~p~n", [Message]),
    {noreply, State}.

Lastly, we have to handle the messages from message/2 and stop/1:

handle_cast({message, Msg}, State=#state{epn=EPN, channel=Channel}) ->
    epubnub:publish(EPN, Channel, Msg),
    {noreply, State}.

terminate(_Reason, #state{pid=PID}) ->
    epubnub:unsubscribe(PID),
    ok.

The handle_cast/2 function published your message to the channel this process subscribed to with epubnub:publish/3 and terminate calls epubnub:unsubscribe/1 before this process ending which sends a terminate message to the subscibred process.

Now lets see this program in action:

[tristan@marx ~/Devel/epubnub_chat]
09:55 (master)$ sinan dist
starting: depends
starting: build
starting: release
starting: dist
[tristan@marx ~/Devel/epubnub_chat]
09:55 (master)$ sudo faxien ir
Password:
Do you want to install the release: /Users/tristan/Devel/epubnub_chat/_build/development/tar/epubnub_chat-0.0.1.tar.gz
Enter (y)es, (n)o, or yes to (a)ll? > ? y
Replacing existing executable file at: /usr/local/lib/erlang/bin/epubnub_chat
Replacing existing executable file at: /usr/local/lib/erlang/bin/5.8.2/epubnub_chat
Replacing existing executable file at: /usr/local/lib/erlang/bin/erlware_release_start_helper
Replacing existing executable file at: /usr/local/lib/erlang/bin/5.8.2/erlware_release_start_helper
ok
[tristan@marx ~/Devel/epubnub_chat]
09:56 (master)$ epubnub_chat
....
Eshell V5.8.2  (abort with ^G)
1> {ok, Server} = epubnub_chat_sup:connect("chat").
{ok,}
2>
=PROGRESS REPORT==== 10-Apr-2011::09:57:14 ===
          supervisor: {local,inet_gethost_native_sup}
             started: [{pid,},{mfa,{inet_gethost_native,init,[[]]}}]

=PROGRESS REPORT==== 10-Apr-2011::09:57:14 ===
          supervisor: {local,kernel_safe_sup}
             started: [{pid,},
                       {name,inet_gethost_native_sup},
                       {mfargs,{inet_gethost_native,start_link,[]}},
                       {restart_type,temporary},
                       {shutdown,1000},
                       {child_type,worker}]

2> epubnub_chat:message(Server, <"hello there!">).
ok
3> <"hello there!">
<"I'm from the webapp!">

3> q().
ok

You can go to the PubNub tutorial page to chat between yourself, or get someone else to join!

That’s it! Simple and quick global communication that scales for you!

I’ve really enjoyed playing with PubNub and hope I get to use it for a real project soon.

eCloudEdit Part 2: CouchDB

In my last post I showed the Webmachine backend to James Yu’s CloudEdit app in Backbone.js. What was left out was, where are the documents stored? Here I’ll show how this is done with CouchDB. And you can give the app a try at http://erlware.org:8080

First, a new Erlang app is needed that we’ll call ece_db.

ece_db/
├── doc
├── ebin
│   ├── ece_db.app
│   └── overview.edoc
├── include
└── src
    ├── ece_db.erl
    ├── ece_db_app.erl
    └── ece_db_sup.erl

Three modules are implemented, one that starts the app by calling the supervisor’s start function, the supervisor itself that sets up a simple_one_for_one and the gen_server that handles the frontends requests for creating and modifying documents in CouchDB.

We’ll ignore the ece_db_app.erl module for this post and start with ece_db_sup.erl. On requests from the Webmachine resource module we don’t want one to wait on the other, requests should be handled in parrallel. One option is to not create a process for the database backend and instead have all of the functions in the database interface run in the process of the Webmachine resource. However, two reasons to not do this are that there are multiple pieces of data that must be read out of a configuration file and used to setup what is needed to talk to CouchDB. We do not want to be doing this for every request! Furthermore a supervised gen_server allows us the possibility of retrying requests with no extra code and crashing without retrying but not taking down the resource’s process that is handling the user’s HTTP request. It is a lot nicer and easier to just let things fail when we can!

  {ece_db, [{server, "HOSTNAME"},
            {port, 80},
            {database, "ecloudedit"}]}
-spec init(list()) -> {ok, {SupFlags::any(), [ChildSpec::any()]}} |
                       ignore | {error, Reason::any()}.
init([]) ->
    RestartStrategy = simple_one_for_one,
    MaxRestarts = 0,
    MaxSecondsBetweenRestarts = 1,

    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},

    Restart = temporary,
    Shutdown = 2000,
    Type = worker,

    {ok, Server} = application:get_env(server),
    {ok, Port} = application:get_env(port),
    {ok, DB} = application:get_env(database),

    AChild = {ece_db, {ece_db, start_link, [Server, Port, DB]},
              Restart, Shutdown, Type, [ece_db]},

    {ok, {SupFlags, [AChild]}}.

First, we have the config entries in config/sys.config that will be read in by application:gen_env. Here we set the server URL, port number and name of the database to use. CouchDB can easily be run locally but for my running copy I use a database hosted by the great service Cloudant. Next is the init function for ece_db_sup.erl. A key thing to note for a simple_one_for_one is that NO process is started after the supervisor’s init function returns like with other types of supervisor children. Instead we must explicitly call start_child. This is how we are able to create a supervised gen_server process for every HTTP request. Below is the code in ece_db_sup for starting and stopping the process:

start_child() ->
    supervisor:start_child(?SERVER, []).

terminate_child(PID) ->
    ece_db:terminate_child(PID).

In the last post I showed the functions that start and stop the ece_db process. Here they are again:

init([]) ->
    {ok, PID} = ece_db_sup:start_child(),
    {ok, #ctx{db=PID}}.
finish_request(ReqData, Ctx) ->
    ece_db_sup:terminate_child(Ctx#ctx.db),
    {true, ReqData, Ctx}.

On each request Webmachine calls the init function for the resource that is matched in the dispatch table. In that init function we call start_child in ece_db_sup which returns {ok, PID}. The PID is the process id we’ll be sending messages. Now in ece_db we implement the API functions needed to start the process and to interact with it.

start_link(Server, Port, DB) ->
    gen_server:start_link(?MODULE, [Server, Port, DB], []).

all(PID) ->
    gen_server:call(PID, all).

find(PID, ID) ->
    gen_server:call(PID, {find, ID}).

create(PID, Doc) ->
    gen_server:call(PID, {create, Doc}).

update(PID, ID, JsonDoc) ->
    gen_server:call(PID, {update, ID, JsonDoc}).

terminate(PID) ->
    gen_server:call(PID, terminate).

Each function, besides the one for starting the process, takes a PID. This PID is the process gen_server:call to which it sends its message. To handle these messages we have the gen_server callbacks.

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

%% @private
init([Server, Port, DB]) ->
    CouchServer = couchbeam:server_connection(Server, Port, "", []),
    {ok, CouchDB} = couchbeam:open_db(CouchServer, DB),
    {ok, #state{db=CouchDB}}.

%% @private
handle_call(all, _From, #state{db=DB}=State) ->
    Docs = get_docs(DB, [{descending, true}]),
    {reply, mochijson2:encode(Docs), State};
handle_call({find, ID}, _From, #state{db=DB}=State) ->
    [Doc] = get_docs(DB, [{key, list_to_binary(ID)}]),
    {reply, mochijson2:encode(Doc), State};
handle_call({create, Doc}, _From, #state{db=DB}=State) ->
    {ok, Doc1} = couchbeam:save_doc(DB, Doc),
    {NewDoc} = couchbeam_doc:set_value(<<"id">>, couchbeam_doc:get_id(Doc1), Doc1),
    {reply, mochijson2:encode(NewDoc), State};
handle_call({update, ID, NewDoc}, _From, #state{db=DB}=State) ->
    IDBinary = list_to_binary(ID),
    {ok, Doc} = couchbeam:open_doc(DB, IDBinary),
    NewDoc2 = couchbeam_doc:set_value(<<"_id">>, IDBinary, {NewDoc}),
    NewDoc3 = couchbeam_doc:set_value(<<"_rev">>, couchbeam_doc:get_rev(Doc), NewDoc2),
    {ok, {Doc1}} = couchbeam:save_doc(DB, NewDoc3),
    {reply, mochijson2:encode(Doc1), State;
handle_call(terminate, _From, State) ->
    {stop, normal, State}.

init takes the server URL, the port number and the name of the database to store the documents as arguments and creates a CouchBeam database record. To handle the other messages we have the handle_call function to match on the different tuples sent to gen_server:call for the different function calls. all uses the internal function get_docs with the option to have the function in descending order (so the newest is first) and returns those after encoding to JSON. find uses the same function but only wants a certain document which in the case of CouchDB is specified with a key value. create first saves the new document which returns the document that is actually stored in CouchDB. CouchDB adds the _id and _rev key/value pairs it generates. Since our frontend expects the id to be the value of a key id and not _id we use couchbeam_doc:set_value to set a value of the new key id with the value of the documents id retrieved from the document with couchbeam_doc:get_id. For update we first open the document corresponding to the id passed in as an argument to get the _rev value. _rev needs to be set in the document we send to save_doc so CouchDB knows this is a valid update based on the previous version of the document. Thus we set the new documents _id and _rev values and send to save_doc.

Lastly, we have the get_docs function. Since the frontend expects the key id to exist and CouchDB stores the id with the key _id we use a simple view to return the documents with id containing the value of the documents _id. Views are defined with map/reduce functions that CouchDB runs across the databases documents and builds an index from. These indexes are then queried based on keys to find documents. Thus access is very quick, indexes are built with BTrees, and a map/reduce does not have to be run on the documents for every request. Additionally, when a new document is created the view's map/reduce functions only need to run over the new documents to update the BTree. Updates to the view's indexes are either done on each use of the view or can be manually told to run.

function (doc)
{
  emit(doc._id, {id : doc._id, title : doc.title, created_at : doc.created_at, body : doc.body});
}
get_docs(DB, Options) ->
    {ok, AllDocs} = couchbeam:view(DB, {"all", "find"}, Options),
    {ok, Results} = couchbeam_view:fetch(AllDocs),

    {[{<<"total_rows">>, _Total},
      {<<"offset">>, _Offset},
      {<<"rows">>, Rows}]} = Results,

    lists:map(fun({Row}) ->
                      {<<"value">>, {Value}} = lists:keyfind(<<"value">>, 1, Row),
                      Value
              end, Rows).

In get_docs the couchbeam:view function is used to construct a view request to be sent to the CouchDB server. couchbeam_view:fetch sends the view request and returns the results as Erlang terms. After this we extract the rows from the results and use lists:map to extract out just the value of each row to be returned.

That's it! There are places that could use improvement. One being the use of lists:map over every returned row to construct documents the frontend can deal with. Additionally, the need to duplicate _id as id for the frontends use could be removed through modifications on the frontend.

In the next installment I'll be updating the code -- and maybe making some of these performance enhancements -- with James Yu's recent changes in his Part 2.

eCloudEdit: Erlang, Webmachine and Backbone.js

To experiment with using a pure client-side rendering talking to an Erlang backend I’ve taken James Yu’s CloudEdit tutorial an app written with Backbone.js and Rails and converted the backend to use Webmachine and CouchDB. You can see eCloudEdit in action here. The Backbone.js code is the same so to understand that please see James’ post, here I’ll describe the Erlang backend.

To begin with we setup two applications, one for handling the web interaction and a second for handling the database interaction. We end up with this directory layout:

  |-eCloudEdit
   |-bin
   |-config
   |-doc
   |-lib
   |---ece_db
   |-----doc
   |-----ebin
   |-----include
   |-----src
   |---ece_web
   |-----doc
   |-----ebin
   |-----include
   |-----priv
   |-------images
   |-------javascripts
   |---------controllers
   |---------models
   |---------views
   |-------stylesheets
   |-----src

Under ece_web/priv is where all the html and Javascript from CloudEdit is placed, with nothing changed. To serve up the static content through Webmachine we use this resource which we won’t worry about in this article. Lets look at how webmachine deciced where to route requests. This is handled by a set of dispatch rules that are passed into Webmachine on startup. We can see how this is done by looking at ece_web_sup and how the supervisor loads Webmachine:

-spec init(list()) -> {ok, {SupFlags::any(), [ChildSpec::any()]}} |
                          ignore | {error, Reason::any()}.
init([]) ->
    WebChild = {webmachine_mochiweb,
                {webmachine_mochiweb, start, [config()]},
                permanent, 5000, worker, dynamic},

    RestartStrategy = one_for_one,
    MaxRestarts = 3,
    MaxSecondsBetweenRestarts = 10,
    SupFlags = {RestartStrategy, MaxRestarts, MaxSecondsBetweenRestarts},

    {ok, {SupFlags , [WebChild]}}.

config() ->
    {ok, IP} = application:get_env(webmachine_ip),
    {ok, Port} = application:get_env(webmachine_port),
    {ok, App}= application:get_application(),
    LogDir = code:priv_dir(App) ++ "/logs",
    {ok, Dispatch} = file:consult(filename:join([code:priv_dir(App), "dispatch"])),

    [{ip, IP},
     {port, Port},
     {log_dir, LogDir},
     {backlog, 128},
     {dispatch, Dispatch}].

The dispatch terms are loaded from a file dispatch in the application’s priv directory. For this app the dispatch file contains:

{["documents", id], ece_resource_documents, []}.
{["documents"], ece_resource_documents, []}.
{['*'], ece_resource_static, []}.

There are two resources, one for handling the requests for creating, updating and viewing documents and one for serving up all other requests (static html, js and css files). The first rule matches paths like, /documents/foo, id is set to foo and the request is sent to the documents resource. If there is nothing after /documents it is still sent to the documents resource but there is no id.

Webmachine is essentially a REST toolkit. You build resources by defining functions that handle the different possible HTTP requests. For this webapp we’ll only be dealing with GET, POST and PUT. GET is used if we’d like to retrieve information on documents, POST is for creating a new document and PUT is for updating a document.

-record(ctx, {db}).

init([]) ->
    {ok, PID} = ece_db_sup:start_child(),
    {ok, #ctx{db=PID}}.

allowed_methods(ReqData, Ctx) ->
    {['HEAD', 'GET', 'POST', 'PUT'], ReqData, Ctx}.

content_types_accepted(ReqData, Ctx) ->
    {[{"application/json", from_json}], ReqData, Ctx}.

content_types_provided(ReqData, Ctx) ->
    {[{"application/json", to_json}], ReqData, Ctx}.

finish_request(ReqData, Ctx) ->
    ece_db_sup:terminate_child(Ctx#ctx.db),
    {true, ReqData, Ctx}.

To handle GET requests we implemented the to_json function we specified in content_types_provided to handle GET requests for json data.

to_json(ReqData, Ctx) ->
    case wrq:path_info(id, ReqData) of
        undefined ->
            All = ece_db:all(Ctx#ctx.db),
            {All, ReqData, Ctx};
        ID ->
            JsonDoc = ece_db:find(Ctx#ctx.db, ID),
            {JsonDoc, ReqData, Ctx}
    end.

wrq:path_info is used to find the value of id, which we set in the dispatch file, if it is undefined we know the request is for all documents, while if it has a value we know to find the contents of the document with that id and return its contents. We’ll see the content of ece_db:all/1 and ece_db:find/2 in the next article. Just know they both return JSON data structures.

Now we must support POST for creating documents or we have nothing to return to a GET request.

process_post(ReqData, Ctx) ->
    [{JsonDoc, _}] = mochiweb_util:parse_qs(wrq:req_body(ReqData)),
    {struct, Doc} = mochijson2:decode(JsonDoc),
    NewDoc = ece_db:create(Ctx#ctx.db, {Doc}),
    ReqData2 = wrq:set_resp_body(NewDoc, ReqData),
    {true, ReqData2, Ctx}.

wrq:req_body/1 returns the contents of the body sent in the HTTP request. Here it is the conents of the document to store. We decode it to an Erlang data structure and pass it to the ece_db app for inserting into the database. After inserting to the database the create/2 function returns the new document with a new element id (in this case generated by CouchDB). This is required so we know the document’s id which is used by the Backbone.js frontend. In order to return it from the POST request we must set response body to the contents of the document with wrq:set_resp_body/2

Lastly, updating documents requires support for PUT. In contents_type_accepted/2 we’ve specified that PUT requests with JSON content is sent to the function from_json/2:

from_json(ReqData, Ctx) ->
    case wrq:path_info(id, ReqData) of
        undefined ->
            {false, ReqData, Ctx};
        ID ->
            JsonDoc = wrq:req_body(ReqData),
            {struct, Doc} = mochijson2:decode(JsonDoc),
            NewDoc = ece_db:update(Ctx#ctx.db, ID, Doc),
            ReqData2 = wrq:set_resp_body(NewDoc, ReqData),
            {true, ReqData2, Ctx}
    end.

If this request was not routed through the first rule in our dispatch file it does not have an id and thus can not be an update. When this happens we return false so the frontend is aware something has gone wrong. For requests containing an id we pass the contents of the requests body to ece_db‘s update/2 function.

In the next post I’ll show how ece_db is implemented with Couchbeam for reading and writing the documents to CouchDB on Cloudant.

Follow

Get every new post delivered to your Inbox.