alice
library
manual.

Alice Project

The Remote structure


________ Synopsis ____________________________________________________

    signature REMOTE
    structure Remote : REMOTE

This structure provides support for the implementation of distributed applications. This includes exporting values from and importing values into sites, performing remote function application and starting a new site. See the distribution overview for an introduction to the distribution facilities provided by Alice.

Communication between sites is performed by cloning data structures. Cloning is defined by pickling.

For more background, see the overview of distribution.

See also: COMPONENT_MANAGER, Url, Pickle, Socket, Lock.Sync


________ Import ______________________________________________________

    import structure Remote from "x-alice:/lib/distribution/Remote"
    import signature REMOTE from "x-alice:/lib/distribution/REMOTE-sig"

________ Interface ___________________________________________________

    signature REMOTE =
    sig
	type ticket = string

	exception Proxy of exn
	exception SitedArgument
	exception SitedResult
	exception Protocol of string
	exception Ticket
	exception Port
	exception Remote of exn
	exception Connection
	exception Exit of OS.Process.status

	val port : unit -> Socket.port option
	val setPort : Socket.port -> unit

	val proxy : ('a -> 'b) -> ('a -> 'b)

	val offer : package -> ticket
	val take :  ticket -> package

	val eval : string * Component.t -> package
	val run :  string * Component.t -> package

	functor Proxy (signature S  structure X : S) : S

	functor Offer (signature S  structure X : S) : (val ticket : ticket)
	functor Take (val ticket : ticket  signature S) : S

	functor Eval (val host : string
			 signature S
			 functor F : COMPONENT_MANAGER -> S) : S
	functor Run (val host : string
			 signature S
			 functor F : COMPONENT_MANAGER -> S) : S
    end

________ Description _________________________________________________

type ticket = string

The type of tickets representing offered values. Tickets are short strings suitable for communication, for instance, over voice lines or by email. Tickets are suitable for parsing as URLs.

exception Port

raised by setPort if the port has already been set, either explicitly or implicitly.

exception Ticket

indicates that a ticket was not well-formed or referred to a site or value that could not be accessed.

exception Proxy of exn

indicates that a call through a proxy has failed. The argument exception describes the specific cause of failure.

exception SitedArgument
exception SitedResult
exception Protocol of string

the former two exceptions indicate that the argument or result of a call to a proxy, or to eval or run contained a sited value. The third exception indicates an internal protocol error during execution of excute or a proxy call, where the string describes the error condition in text form. For proxy calls, these exceptions are never raised directly, but occur only as the argument of a Proxy exception.

exception Remote of exn

indicates that remote execution has failed. The argument exception describes the specific cause of failure.

exception Connection
exception Exit of OS.Process.status

the former indicates that a connection for remote exection could not be established; the latter indicates that a remote process has called OS.Process.exit, or been terminated by other means, before evaluation of its root component has finished.

setPort p

sets the port used by the system to offer packages and accept incoming proxy calls. If no port is set, it will be chosen automatically when the first call to either offer or proxy is performed. However, firewall configuration may require a fixed choice.

This function may only be called once, and must be called before the first call to offer or proxy. In violation of this restriction, the exception Port will be raised.

If the port number is invalid or the port unavailable, all consecutive calls to offer or proxy will raise an IO.Io exception.

port ()

returns the port set by setPort or chosen automatically by the system, or NONE when no port has been chosen yet. Raises Io if the chosen port was invalid or unavailable, or another error occured during the attempt to bind it.

proxy f

returns a proxy for f. The proxy differs from f in that:

The cloning performed when applying proxies implies that any future in the argument value or result is requested. That may raise an exception due to a failed future, which then becomes the result of the call. In either case, this is considered regular execution of the proxy call.

But proxy calls may also fail, for a number of reasons. In each case, the exception Proxy is raised to indicate failed execution of the call, with the actual cause as an argument. The following causes are possible:

Other causes for raising Proxy are possible.

Proxy (signature S = S structure X = X)

returns a wrapper of structure X where all fields of function type are replaced by a proxy for the respective function. A function that returns a function will return a proxy, respectively (i.e. the wrapping is recursively performed for curried functions).

If X is a functor, then a proxy for the functor is created. When the functor is applied, the resulting module will be wrapped recursively (i.e. the wrapping is recursively performed for curried functors and any resulting structure).

Example:

      Proxy (signature S = (val x : int val f : int -> int val g : int -> int -> int)
             structure X = (val x = 5   fun f n = n + 1    fun g n m = n + m))

returns a structure that is equivalent to

      struct
         val x = 5 
	 val f = proxy (fn n => n + 1)
         val g = proxy (fn n => proxy (fn m => n + m))
      end

Similarly,

      Proxy (signature S = fct () -> (val x : int val f : int -> int -> int)
             structure X = fct () => (val x = 5   fun f n m = n + m))

returns a proxy for a functor equivalent to

      fct () => (val x = 5  val f = proxy (fn n => proxy (fn m => n + m)))

Note that structure fields of non-function type will not be wrapped, even if they contain function values. For example,

      Proxy (signature S = (val p : (int -> int) * (int -> int))
             structure X = (val p = (fn n => n, fn n => n + 1)))

returns X unchanged.

offer package

makes package available to other sites for taking. Returns a ticket suitable for take or Take. Raises Sited if the package contains sited values, or IO.Io when the port set by setPort was invalid or unavailable. An offered package can be taken any number of times. If the package contains mutable data, then each take will return a clone of a snapshot of the package made when offer is executed.

Tickets are URIs of the form

	x-alice://ip:port/offer/n

where ip is the local IP address or domain, port the port set by setPort or chosen by the system, and n a running number, starting with 1. By using setPort, the URI is made deterministic, which may be important for server applications.

take ticket

imports the data structure denoted by ticket, which must have been created by offer or Offer. Raises Ticket if the ticket is invalid or the site on which it was created no longer exists. Raises IO.Io if retrieving the package fails for other reasons.

Offer (signature S = S structure X = X)

makes module X available to other sites for taking with signature S. Returns a ticket suitable for take or Take. Equivalent to

	(val ticket = offer (pack X : S))
Take (val ticket = ticket signature S = S)

imports the module denoted by ticket, which must have been created by offer or Offer, under a sub-signature of S. Raises Ticket if the ticket is invalid or the site on which it was created no longer exists. Raises Package.Mismatch if the module was not exported with a signature matching S. Raises IO.Io if retrieving the module fails for other reasons. Equivalent to

	unpack (take ticket) : S
eval (host, component)
run (host, component)

create a new site on host using SSH, transfer a clone of component to the new site, on which it is evaluated using the local component manager. A clone of the component's export module is transferred back to the caller as a package. If evaluation of the component terminates with an exception, that exception is re-raised in the caller's thread.

Like proxy calls, remote execution may fail for a number of reasons. In each case, the exception Remote is raised, carrying a secondary exception as an indication of the cause of failure:

With eval, the remote process will exit after evaluation of the component, regardless of any concurrent processes that may have been spawned by evaluation. With run, the remote process stays alive after the result is returned to the caller, i.e. the process is not terminated automatically (not even if the parent process terminates!). This allows concurrent threads to continue running; in particular, it enables the remote process to create proxies and return them to the caller who can therewith call into the remote process later. It also allows a process to migrate to another site, by initiating a remote process and then terminating the original one. To terminate the remote process, an explicit call to OS.Process.exit on the remote site is required. If evaluation of the component raises an exception, the process is terminated in both cases.

Note that the ssh command must be available on the local site, you must be able to login on the remote site via SSH without entering a password (e.g. by using an SSH daemon), and the Alice System's bin directory must be in PATH on the remote site. On Linux, the latter can optionally be achieved by setting ALICE_REMOTE_PATH=<alice-bin-dir> into your ~/.ssh/environment file, if enabled. If that is not possible, you can fall back to having a script named aliceremote in your remote home directory, which sets paths appropriately and forwards its arguments to the alicerun command of the Alice System.

If the environment variable ALICE_REMOTE_LOG is set on the remote machine then its value will be interpreted as a file name and some logging information about the remote execution will be written to the file (without truncating it).

Eval (val host = host signature S = S functor F = F)
Run (val host = host signature S = S functor F = F)

construct a component with export signature S from F and transfer a clone of it to a newly created site on host using SSH, on which it is evaluated using the local component manager. A clone of the resulting structure is transferred back to the caller of and returned as the resulting structure. Equivalent, respectively, to

	let
	    structure C = Component.Create (signature S = S structure F = F)
	in
	    unpack (eval (host, C.component)) : S
	end

and

	let
	    structure C = Component.Create (signature S = S structure F = F)
	in
	    unpack (run (host, C.component)) : S
	end

________ Examples ____________________________________________________

Remote file access

Here is an example of a simple program that creates a component to read the content of a file on a remote site. Note how the mobile component refers to dynamically acquired data, namely the respective file name:

import structure Remote  from "x-alice:/lib/distribution/Remote"
import signature TEXT_IO from "x-alice:/lib/system/TEXT_IO-sig"

structure Main =
struct
   val (hostname, filename) =
	case CommandLine.arguments ()
	 of [s1, s2] => (s1, s2)
	  | _ => (TextIO.output (TextIO.stdErr, "usage: fetch  \n");
		  OS.Process.exit OS.Process.failure)

   val component =
       comp
           import structure TextIO : TEXT_IO from "x-alice:/lib/system/TextIO"
       in
           val content : string
       with
           val file = TextIO.openIn filename
           val content = TextIO.inputAll file
           do TextIO.closeIn file
       end

   structure Result =
       unpack Remote.eval (hostname, component) : (val content : string)

   do TextIO.print Result.content
   do OS.Process.exit OS.Process.success
end

Chat room

A more interesting example is a simple client/server application with bidirectional communication: a simplistic chat room consisting of a chat server to which multiple clients can connect.

The application consists of two programs: one for the server and one for clients. Both have to agree on the server interface, which we define in a shared component:

(*) SERVER-sig.aml

signature SERVER =
sig
   val register : (string -> unit) -> unit
   val broadcast : string -> unit
end

Clients that register with the server will receive all messages sent by other clients, and they can broadcast messages themselves.

Here is the code for the server:

import structure Remote from "x-alice:/lib/distribution/Remote"
import signature SERVER from "SERVER-sig.aml"

val clients = ref nil
fun register client = clients := clients :: !clients
fun broadcast message =
    List.app (fn receive => spawn receive message) (!clients)

structure Server =
struct
   val register = Remote.proxy (Lock.sync (Lock.lock ()) register)
   val broadcast = Remote.proxy broadcast
end

val url = Remote.offer (pack Server : SERVER)
do TextIO.print (url ^ "\n")

The server simply keeps a list of registered clients (represented by their receive functions); broadcasting iterates over this list and forwards the message to each client. In order to avoid having to wait for each client in turn to receive its message, forwarding happens asynchronously, using a future created with spawn.

Moreover, since the client list is stateful, we have to avoid race conditions when updating it during potentially concurrent calls to register from multiple clients. The exported register function hence synchronises using a fresh mutex lock (note that we do not need to synchronise broadcast, since it accesses the reference atomically).

On startup, the server prints a URL for clients to connect. The code for clients is even simpler:

import structure Remote from "x-alice:/lib/distribution/Remote"
import signature SERVER from "SERVER-sig.aml"

val [url, name] = CommandLine.arguments ()

structure Server = unpack Remote.take url : SERVER
do Server.register (Remote.proxy TextIO.print)

fun loop () = case TextIO.inputLine TextIO.stdIn of
              | NONE => OS.Process.exit OS.Process.success
              | SOME line => (Server.broadcast (name ^ ": " ^ line); loop ())
do loop ()

It expects a server URL and a user name on the command line, registers with the server, and simply forwards everything typed by the user to the server. Note that the call to register passes a proxy to the local print as argument, so that printing happens on the right site.

This completes the application. Obviously, the implementation is quite simplistic: a user will see an echo of everything he types. More seriously, there is no error handling, and disconnecting clients do not notify the server (it will simply go on creating forwarding threads that fail with an Io exception). However, it should be clear how the program could be refined in that respect.



last modified 2007/Mar/30 17:12