Seeking advice on port "pattern" (long)

Garry Hodgson garry@REDACTED
Mon Jun 25 20:37:44 CEST 2001


okay, i've been using erlang for a while now, and have learned
enough to be dangerous.  i've got my first app working nicely, but have
this nagging feeling that i'm not doing things in the most elegant
way.  so i'm seeking advice on good practice for using the port
mechanism
to attach to legacy applications.  in particular, if there are standard
patterns for how to do this cleanly, i'd appreciate some clue.
pointers to code examples would be especially helpful.

the application is a server that provides a single point of contact for
several other servers on different machines, each offering different
kinds
of network information queries.  i've got a registered allocator
process,
which maintains a list of idle and busy nodes, a list of active servers
running on those nodes, and maps a given request to a server who can 
handle it, starting a fresh one if needed.  this is done via a
spawn_link
of a port server, which starts the appropriate legacy app (more on this
later).

requests come into the master, which asks the allocator for the
approriate
server, then passes the request on to that server.

the complication comes in the port process.  some of the legacy code is
multithreaded, and can potentially have a mix of long running database
queries and others that respond quickly.  each message and corresponding
response is tagged with a transaction id, so i can associate it with the
correct caller on return.  because i can't rewrite the legacy apps, i'm
communicating with simple ascii strings (no length prefix), and must
accumulate
data until i get a complete response, as some of the queries return
lengthy
replies, and don't accumulate the whole thing before replying.

so, my port module, absent some detail, is included below.  it seems to
me that
it's much more complicated than it ought to be.  spawning the separate
alarm 
processes seems clumsy, vs. spawning a process per transaction with a
receive/after/end
statement.  but either way, i've got to keep a list of 'em, so i can
send the reply
to the right one once i get a complete message.

i'm (dimly) aware of the gen_server behavior, which i've avoided in 
favor of rolling my own for learning purposes.  i'm open to going that
route now if that's the right thing to do.

so, that's more or less where i am.  i love this language, and am just
beginning
to feel fluent in it.  but i could use come pointers to gain
Enlightenment.  does
anyone have any sage words of advice?

thanks very much.

% ----------------- code follows ------------------

% alarm process to handle timeouts, spawned when call comes in

setalarm( Id ) ->
  spawn( port, doalarm, [ self(), 10000, Id ] ).

doalarm( Caller, Timeout, Id ) ->
  receive
    cancel ->  true
  after Timeout ->  Caller ! { timeout, Id }
  end.

% port:start()
start( Service, ExtPrg ) -> init( Service, ExtPrg ).

init( Service, ExtPrg ) ->
  Port = open_port( {spawn, ExtPrg}, [] ),
  loop( Service, Port, [], 1000, [] ).

loop( Service, Port, Pending, NextTrans, Buffered ) ->
  receive
    { call, Caller, Request, Args } ->
      Job = { NextTrans, Caller, setalarm( NextTrans ) },
      Port ! {self(), {command, encode( NextTrans, Request, Args ) } },
      loop( Service, Port, [ Job|Pending ], NextTrans + 1, Buffered );

    { Port, { data, Data } } ->
      AllData = Buffered++Data,
      case decode( AllData ) of
        { Id, Req, Result } ->
          case lists:keysearch( Id, 1, Pending ) of
            { value, { Id, Caller, Alarm } } ->
              Caller ! { ok, { Service, { Req, Result } } },
              Alarm ! cancel,
              loop( Service, Port, lists:keydelete( Id, 1, Pending ),
NextTrans, Buffered );
            false ->
              screamloudly( "Got data for nonexistent transaction ~w", [
Id ] ),
              loop( Service, Port, Pending, NextTrans, Buffered )
          end;
        { parseError, Details } ->
          io:format( "huh? got ~p ~n.  i'll wait for more...~n",
[Details] ),
          loop( Service, Port, Pending, NextTrans, AllData )
      end;

    { timeout, Id } ->
      io:format( "port: timeout on Id ~w", [ Id ] ),
      case lists:keysearch( Id, 1, Pending ) of
        { value, { Id, Caller, Alarm } } ->
          Caller ! { error, timeout },
        %  loop( Service, Port, lists:keydelete( Id, 1, Pending ),
NextTrans, [] );
          exit( { error, "Application timed out" } );
        false ->
          screamloudly( "Got timeout for nonexistent transaction ~w", [
Id ] ),
          loop( Service, Port, Pending, NextTrans, [] )
      end;

    stop ->
      Port ! { self(), close },
      receive
        {Port, closed} ->
          exit( normal )
      end
  end.




---
Garry Hodgson                   sometimes we ride on your horses
Senior Hacker                   sometimes we walk alone
Software Innovation Services    sometimes the songs that we hear
AT&T Labs                       are just songs of our own
garry@REDACTED



More information about the erlang-questions mailing list