Active modules

Author(s): Manuel Hermenegildo, Daniel Cabeza (original version before 1.16), Jose F. Morales (revised version).

Stability: [devel] Currently the subject of active development and/or research. Functionality may be limited and API and/or functionality may change without warning or deprecation period. Not recommended yet for use in production.


This version corresponds to a revised model for active modules. See Ciao version 1.15 for the original first design as described in [CH95].

An active module is an ordinary module whose instances (copies sharing the same code but different state or data) have computational resources attached (e.g., computation steps shared in a fair fashion).

Active modules provide a high-level model of concurrency suitable for distributed execution (and inter-process communication) that is similar to active objects and actors. Note that Ciao also offers lower-level primitives for concurrency and network-based communication.

Concurrency model and semantics

Each active module instance is internally composed of a local mailbox for queries (a queue of messages) and a query handler loop. Each active module instance is identified by a unique name (which can be provided or created automatically).

Calls to exported predicates of an active module are enqueued in the mailbox. The query handler loop is a deterministic loop that executes queries sequentially, sending back the results to the caller program (another active module instance) if needed. The composition of the (possibly) multiple answers from the callee and the caller is given be the query protocols defined below.

Query requests at the handler loop do not fail or leave choicepoints. When required, non-deterministic behaviour must be captured on the answer and treated on the callee by the query protocols.

Query protocols

There exist several query protocols depending on the expected answers of a predicate.

  • all solutions: all answers to each query are precomputed on the callee side and sent to the caller. Backtracking is supported by enumeration on the caller side. Note: calls to active module predicates with an infinite number of solutions will obviously not terminate with this query method.

  • cast: no answer is required from the callee (equivalent to message passing in distributed computation). It is useful when the query performs side-effects or it sends back the answers to the caller active module through another cast (a-la continuation-based programming).

  • (experimental) answers with suspensions: the callee may return a suspended computation and continue the execution on the caller site. Currently predicates must be declared as suspendable. The main focus of the current functionality is the implementation of RESTful applications via the (experimental) HTTP interface. Note: support is limited, recommended only for deterministic computations.

Further details on the semantics and concurrency model:

  • The cost of calls depends on the size of the messages (arguments, results, and the target location)
  • Deadlocks may happen due to the "message processing lock" (e.g., A calls B, B calls A). Use cast instead.

  • Query protocols will be changed or extended in the future, specially to optimize cost for particular cases.
  • Suspendable predicates rely on the experimental fibers package. This may change in the future.
  • Or-suspensions for lazily asking for more solutions are not currently implemented (they are in development).

Side-effects

All communication between active module instances should (in principle) happen through message passing. Instances should not share any global data. Sharing via dynamic/data predicates (or other global mechanisms) is seen as an impure side-effect w.r.t. this model and must be used with care (e.g., caching, hand-made optimizations, etc.).

Distributed

In a distributed setting active module instances may run on separate nodes that can interchange messages through the network. See actmod_dist for more details about the distribution protocol and how it can be extended.

Using active modules

Using active modules requires the use of the actmod package:

:- module(...,...,[actmod]).

This turns the current module into an active module and enables all the directives and features required to use other active modules.

Predicates exported by an active module can be accessed by other active modules using the use_module/3 declaration with the active option (see below).

Note that the process of using an active module does not involve transferring any code, but rather setting up things so that calls in the module using the active module are executed as remote procedure calls to the active module.

Running active modules

For spawning active module instances (dynamic creation) see actmod_process.

Active modules may implement a main/1 predicate, if they want to receive command-line arguments or use the directive :- dist_node to include a default main/1 for distributed nodes. See actmod_dist for more details.

Examples

The following command:

ciaoc simple_server.pl

compiles the simple server example that comes with the distribution (in the actmod/example directory). The simple_client_with_main example (in the same directory) can be compiled as usual:

ciaoc simple_client_with_main

Now, if the server is running when the client is executed it will connect with the server to access the predicate(s) that it imports from it.

An even simpler client simple_client.pl can be loaded into the top level and its predicates called as usual (and they will connect with the server if it is running).


Usage and interface

Documentation on new declarations

DECLARATIONuse_module/3

Usage::- use_module(ModSpec,Imports,Opts).

Import from ModSpec the predicates in Imports with options Opts. If Imports is a free variable, all predicates are imported.

Documentation on exports

import_opt(Opt)

Options for use_module/3:

  • active: import as an active module (which adds a dependency to the module interface, not its code; it allows actmod_spawn/3 and static named instances).
  • reg_protocol(RegProtocol): specify default name server protocol.
  • libexec: use libexec as spawning option by default.
  • binexec(Name): use binexec(Name) as spawning option by default.