records generated from UBF

Ulf Wiger etxuwig@REDACTED
Tue Apr 8 10:50:15 CEST 2003


Is anybody else out there playing around with UBF?

I wrote a small utility yesterday that generates a .hrl file
from a UBF specification. It generates a record
specification for each type that looks like a tagged tuple,
where the tag matches the type name, and all other
elements are type references. For example:

  person() = {person, name(), age(), address(), phone()};

becomes

  -record(person, {name, age, address, phone}).

The UBF contract checker will type check all incoming and
outgoing messages, so you can trust that all attributes of a
record are type correct.

This makes it quite easy to put together external interfaces
with a very nice programming style for the back-end.

...

I will take the opportunity to extoll the virtues of
UBF-oriented programming by providing a "small" example.


The server in question handles administrative tasks for a
Track & Field competition, and also serves spectators with
updates on what is going on (which, as everyone who's been
to a Track&Field event knows, is quite a lot).

The network is typically WLAN. The client is written in
Java, and the server in Erlang. I use UBF, mnesia, rdbms,
and will probably later use xmerl, inets and perhaps
erlguten. Who knows - maybe we'll throw in sowap
as well. ;) This is an Open Source project.

The UBF contract specifies some basic types, e.g.

club_id()               = constant();
location()              = string();
club()                  = {club, club_id(), name(),
                           location(), info()};

Using the utility described above, "compiling" the contract
results in, among other things, a .hrl file with record
declarations to match:

-record(club, {club_id,
               name,
               location,
               info}).

The contract also specifies valid messages:

qNewClub()              = {new_club, club()};
qListClubs()            = list_clubs;
rListClubs()            = club_list() | rError();
qOpenClub()             = {open_club, club_id()};
rOpenClub()             = club() | rError();
qControlClub()          = {control_club, club_id()};
qEndControlClub()       = {end_control_club, club_id()};
qUpdateClub()           = {update_club, club()};
qDeleteClub()           = {delete_club, club_id()};

And two states, 'administrator' and 'viewer', where valid
messages are listed:

+STATE administrator
        ...
        qNewClub()      => rReply() & administrator;
        qControlClub()  => rReply() & administrator;
        qEndControlClub() => rReply() & administrator;
        qUpdateClub()     => rReply() & administrator;
        qDeleteClub()     => rReply() & administrator;
        ...

(actually, messages valid both for 'administrator' and
'viewer' are listed under ANYSTATE:)

+ANYSTATE
        qListClubs()    => rListClubs();
        qOpenClub()     => rOpenClub();


I have one plugin serving both administrators and viewers,
since the viewer information is a subset of the
administrator information. I start two instances of the
plugin, one in 'administrator' state and one in 'viewer'
state. After this, the contract checker makes sure that only
messages valid for each state are admitted, and I don't
have to worry much about checking stuff in my own code.

The code in the plugin looks like this:

========

handlerRpc(S, list_clubs, D, Env) ->
    reply(catch {{club_list, club:list_clubs()},
          S, D}, S, D);

handlerRpc(S, {open_club, ClubId}, D, Env) ->
    reply(catch {club:open(ClubId), S, D}, S, D);

handlerRpc(S, {new_club, Club}, D, Env) ->
    reply(catch {club:new(Club), S, D}, S, D);

========

And the code in the different modules (club, athlete, event,
etc.) looks sort of like this:

========

-module(cath.server.club).

-export([list_clubs/0,
         new/1,
         open/1,
         update/1,
         delete/1]).

-export([schema/1]).

-include("cath_plugin.hrl"). % generated .hrl file


schema([]) ->
    [
     {club, [{attributes, record_info(fields, club)},
             {record_name, club},
             {disc_copies, db:writer_nodes()},
             {ram_copies, db:reader_nodes()}]}
    ].

list_clubs() ->
    db:list_items(club).

new(#club{club_id = Id} = Club) ->
    db:new(club, Id, Club).

open(ClubID) ->
    db:open(club, ClubID).

update(#club{club_id = Id} = Club) ->
    db:update(club, Id, Club).

delete(Id) ->
    db:delete(club, Id).


The db.erl module is a database wrapper, which uses rdbms as
the activity module. Rdbms makes sure e.g. that one cannot
add an athlete to a non-existant club, or that all athletes
belonging to a club are deleted if the club is deleted --
basic referential integrity, that is.

Error handling is simple. Each user gets a session (a
process). UBF will exit and close the session if the
protocol is violated, and mnesia/rdbms will abort and exit
if anything goes wrong inside a transaction; transaction
aborts lead to error messages. If the protocol doesn't allow
for an error message in response to a certain query, it will
complain, and close the session. Perhaps not the most
elegant way to do it, but it works.

A more complex module, e.g. athlete, might have to handle
dependencies:

========

-module(cath.server.athlete).

-export([list_athletes/1,
         open/2,
         new/2,
         update/2,
         delete/2]).

-export([schema/1]).

-include("cath_plugin.hrl").


schema(Contest) ->
    TabName = contest:table_name(athlete, Contest),
    [
     {TabName,
      %% mnesia options
      [
       {disc_copies, db:writer_nodes()},
       {ram_copies, db:reader_nodes()},
       {attributes, record_info(fields, athlete)},
       {record_name, athlete}
      ],
      %% rdbms properties
      [
       %% when deleting a club, also delete its athletes
       {{attr, {club, club_id}, references},
        [{TabName,club,{full,cascade,ignore}}]},
       {{attr, {TabName,club}, references},
        [{club, club_id, {full,ignore,no_action}}]}
      ]
     }
    ].

list_athletes(Contest) ->
    lists:map(
      fun(#athlete{club = Id} = A) ->
              A#athlete{club = club:open(Id)}
      end, db:list_items(athlete, Contest)).


new(#athlete{athlete_id = Id} = Ath, Contest) ->
    db:new(athlete, Id, Ath, Contest).

========

That is, the referential integrity checks done by rdbms
ensure that the new athlete actually belongs to an existing
club, and UBF has already checked that the #athlete{} record
contains valid types.

Not all functionality has been implemented yet, but the
server runs all day during testing, and it never crashes
(of course, it's Erlang).

Performance? I haven't tested it yet, but I'm sure it's more
than sufficient. The application is designed to take
advantage of distribution, if I add a load sharing front-end
to it (I vaguely recall this having been done before...;)


Life is good.

/Uffe

-- 
Ulf Wiger, Senior Specialist,
   / / /   Architecture & Design of Carrier-Class Software
  / / /    Strategic Product & System Management
 / / /     Ericsson AB, Connectivity and Control Nodes



More information about the erlang-questions mailing list