Skip to content

Limmen/erl_pengine

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

67 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

erl_pengine (v0.1.1)

Build Status

Hex pm

Overview

ErlangPengine

Erlang client to prolog pengine server.

To make full use of erl_pengine you must know the general protocol, its possibilities and limitations, see the following links for more information:

EDoc link:

Features

Pengines is short for Prolog Engines, it's a package which allows you to talk to remote prolog servers in a simple and effective way.

With a pengines client you can access pretty much the full prolog power remotely. You write your pengine-server and make it export the predicates you desire as accessible from remote and then you can access it.

If you are looking for just basic prolog access, you should look at erlog which is a prolog in interpreter for a subset of the prolog standard in erlang.

If you need access to full prolog-power like access to a CLP-solver, a semantic-web server or similar, a pengine-client is a good way to do it.

Other pengine clients:

Useful Modules

  • erl_pengine.erl: entry point module, application callback module, start with application:start(erl_pengine)
  • pengine.erl: main API for a created slave-pengine
  • pengine_master: API for administering active pengines and also creating new ones

Table of Contents

QuickStart Usage

Add to rebar3 project

The package is published at hex.pm You can add it to your project by putting the following to your list of dependencies in rebar.config:

  {deps, [
         {erl_pengine, "0.1.1"}
  ]}.

Or add it as a github dependency:

 {deps, [
        {erl_pengine, {git, "https://github.com/Limmen/erl_pengine", {branch, "master"}}}
 ]}.

Start application

Starts application together with dependencies

application:ensure_all_started(erl_pengine).

Basic Usage

Connect to prolog-pengine server at http://127.0.0.1:4000/pengine and create a slave-pengine with default create-options and issue queries

%% create pengine with default options #{}, Pid = pid of pengine process, Id = pengine unique binary identifier.
%% as default no create-query is sent upon create request
{{ok, {Pid, Id}}, {no_create_query}} = pengine_master:create_pengine("http://127.0.0.1:4000/pengine", #{}).      

%% Query pengine_master of list of active slave pengines
[{Pid, Id}] = pengine_master:list_pengines().

%% query pengine for a solution to member(X, [1,2]), MoreSolutions is a boolean indicating if more solutions exists.
{success, Id, [[1]], MoreSolutions = true} = pengine:ask(Pid, "member(X, [1,2])", #{template => "[X]", chunk => "1"}).

%% ask pengine for next and last solution. Default option when pengine is created is that it will destroy itself upon
%% query completion. This can be configured in create options.
{{success, Id, [[2]], false}, {pengine_destroyed, Reason}} = pengine:next(Pid).

%% When remote-pengine is destroyed also the erlang-process representing the pengine terminates
[] = pengine_master:list_pengines().          

Architecture

architecture

pengine_master contains the API for creating new pengines, aborting pengines or getting a list of active slave-pengines.

Each remote slave_pengine is represented by a erlang pengine-process which contains the API for querying or destroying the pengine.

Pengines are identified by their erlang-pids but you can also lookup pengines by their id from the pengine_master with lookup_pengine(Id) or list_pengines().

API

Create

create_options

%% pengine create options
-type pengine_create_options():: #{
                              application => binary() | string(),
                              ask => binary() | string(),
                              template => binary() | string(),
                              chunk => integer(),
                              destroy => boolean(),
                              format => binary() | string(),
                              src_text => binary() | string(),
                              src_url => binary() | string()
                             }.
                             
%% default options
#{application => "pengine_sandbox", chunk => 1, destroy => true, format => json}.

create_response

%% response to a create request
-type create_response()::{{ok, {PengineProcess :: pid(), Id :: binary()}}, {no_create_query}} |
                         {{ok, {PengineProcess :: pengine_destroyed, Id :: binary()}}, {no_create_query}} |
                         {{ok, {PengineProcess :: pid(), Id :: binary()}}, {create_query, ask_response()}} |
                         {{ok, {PengineProcess :: pengine_destroyed, Id :: binary()}}, {create_query, ask_response()}} |
                         {{error, {max_limit, Reason :: any()}}, destroy_response()}.

pengine_master:create_pengine/2

-spec create_pengine(string(), pengine:pengine_create_options()) ->
                            pengine:create_response() | pengine:error_response().
create_pengine(Server, CreateOptions) ->
    ...   

The pengine server can specify the max number of pengines a single client is allowed to create. This is not enforced by the server, it's up to the client implementation so its more of a "hint" by the server. This is however enforced by erl_pengine if you try to create a pengine whilst having more than the max-limit number of active pengines it will destroy the pengine and return an error.

To save one roundtrip its possible to issue a query directly upon creation of the pengine, e.g:

%% Options to be passed to pengine upon creation
Options = #{destroy => true, application => "pengine_sandbox", chunk => 2, format => json, ask => "member(X, [1,2])", template => "[X]"}.

%% Ask query upon creation, chunking the result to max chunk size 2. Pengine is destroyed after query completion.
{{ok, {pengine_destroyed, Id}}, {create_query, {{success, Id, [[1],[2]], false}, {pengine_destroyed, _Reason}}}} = 
 pengine_master:create_pengine("http://127.0.0.1:4000/pengine", Options). 

It is also possible to inject prolog source-code into the pengine upon creation.

%% Options to be passed to pengine upon creation
Options = #{destroy => false, application => "pengine_sandbox", chunk => 1, format => json, src_text => "pengine(pingu).\n"}.

%% Create slave_pengine with Options and inject the source
{{ok, {Pid, Id}}, {no_create_query}} = pengine_master:create_pengine("http://127.0.0.1:4000/pengine", Options).

%% Query pengine of the injected source
{success, Id1, [[<<"pingu">>]], MoreSolutions = false} = pengine:ask(P1, "pengine(X)", #{template => "[X]", chunk => "1"}).

A typical example of injecting source is of course to inject whole source-files or point to a source-url.

src_text.pl:

pengine_child(pingu).
pengine_child(pongi).
pengine_child(pingo).
pengine_child(pinga).

pengine_master(papa, pingu).
pengine_master(mama, pongi).

Creating pengine injected with the source from src_text.pl:

%% Read prolog source into binary
{ok, File} = file:read_file("src_text.pl").

%% Options to be passed to pengine upon creation
Options = #{destroy => false, application => "pengine_sandbox", chunk => 1, format => json, src_text => File}.

%% Inject prolog source upon creation
{{ok, {Pid, Id}}, {no_create_query}} = pengine_master:create_pengine("http://127.0.0.1:4000/pengine", Options)

%% Ask pengine a query of the injected source
{success, Id, [[<<"pingu">>], [<<"pongi">>], [<<"pingo">>], [<<"pinga">>]], false} = 
 pengine:ask(Pid, "pengine_child(X)", #{template => "[X]", chunk => "10"}).
 
 %% It is also possible to inject source by specifying a url, just send a options like the following:
 Options = #{destroy => true, application => "pengine_sandbox", chunk => 1, format => json, src_url => "http://127.0.0.1:4000/src_url.pl"}

Destroy

destroy_response

%% response if pengine to send request to was destroyed after the request
-type destroy_response()::{pengine_destroyed, Reason :: any()}.

pengine:destroy/1

-spec destroy(pid()) -> destroy_response() | error_response().
destroy(Pengine) ->
 ...

The destroy/1 function takes a pid of a erlang process representing the slave-pengine and sends a request to the prolog server to destroy the slave-pengine and once that is done the erlang process will also terminate. The destroy function is typically only necessary if the pengine was created with option destroy=false otherwise the slave-pengine will destroy itself after query completion and so will the erlang-process.

%% Create pengine with default options
{{ok, {Pid, Id}}, {no_create_query}} = pengine_master:create_pengine("http://127.0.0.1:4000/pengine", #{}).

%% destroy pengine
{pengine_destroyed, _Reply} = pengine:destroy(Pid).

Ask

query_options

%% query_options to the ask() function.
-type query_options():: #{
                     template := string(),
                     chunk := integer()
                    }.

ask_response

%% response to a ask-request
-type ask_response():: {query_response(), destroy_response()} |
                       query_response() |
                       {output_response(), destroy_response()} |
                       output_response() |
                       {prompt_response() | destroy_response()} |
                       prompt_response() |
                       died_response().
                       
%% response to a query
-type query_response()::{failure, Id :: binary()} |
                        {success, Id :: binary(), Data :: list(), More :: boolean()}.

pengine:ask/3

-spec ask(pid(), string(), query_options()) -> ask_response() | error_response().
ask(Pengine, Query, Options) ->
 ...

The ask/3 function takes a pengine process, a query and query-options as input and will send a query request to the slave_pengine.

%% Ask pengine for one solution at a time
{success, Id, [[1]], MoreSolutions = true} = pengine:ask(Pid, "member(X, [1,2])", #{template => "[X]", chunk => "1"}).

pengine:next/1

-spec next(pid()) -> ask_response() | error_response().
next(Pengine) ->
...

The next/1 function will ask the pengine for more solutions to the currently active query

%% Ask pengine for next solution of the active query (if you call next() with no active query you'll get a protocol error)
 {{success, Id, [[2]], MoreSolutions = false}, {pengine_destroyed, _}} = pengine:next(Pid).

Ping

ping_response

%% response for ping request
-type ping_response():: {ping_response, Id :: binary(), Data :: map()} |
                        {ping_interval_set, Interval :: integer()} |
                        died_response().

pengine:ping/2

-spec ping(pid(), integer()) -> ping_response() | error_response().
ping(Pengine, Interval) ->
...

Sends a ping request to the pengine

If Interval = 0, send a single ping. If Interval > 0, set/change periodic ping event, if 0, clear periodic interval.

Slave-pengines are destroyed by the server after ~5min to avoid runaway computations and stacking up pengines. Periodic pinging of a slave-pengine is a way to have the state of active pengines of pengine_master more up to date since it will let you notice as soon as a pengine is aborted and can then update the state and terminate also the erlang process (by default there is no open connection between pengine-slaves and their masters after query completion/creation and pengine-slaves will not notify the master upon abortion).

%% Send a single ping request to the pengine, to ping it periodically set interval > 0
16> pengine:ping(Pid, 0).
{ping_response,<<"bbe8836f-8eae-45d5-853e-e55ea3b92f6b">>,
               #{<<"id">> => 11,
                 <<"stacks">> =>
                     #{<<"global">> =>
                           #{<<"allocated">> => 61424,
                             <<"limit">> => 268435456,
                             <<"name">> => <<"global">>,<<"usage">> => 2056},
                       <<"local">> =>
                           #{<<"allocated">> => 28672,
                             <<"limit">> => 268435456,
                             <<"name">> => <<"local">>,<<"usage">> => 1408},
                       <<"total">> =>
                           #{<<"allocated">> => 120808,
                             <<"limit">> => 805306368,
                             <<"name">> => <<"stacks">>,<<"usage">> => 4128},
                       <<"trail">> =>
                           #{<<"allocated">> => 30712,
                             <<"limit">> => 268435456,
                             <<"name">> => <<"trail">>,<<"usage">> => 664}},
                 <<"status">> => <<"running">>,
                 <<"time">> =>
                     #{<<"cpu">> => 0.001505787,
                       <<"epoch">> => 1496089942.8789568,
                       <<"inferences">> => 197}}}

Receive Pengine Output

Slave-Pengines can send output back to their masters by using the prolog predicate pengine_output/1

output_response

%% response to pengine-output
-type output_response()::{{output, PrologOutput :: any()}, {pull_response, ask_response()}}.

Example two prolog outputs from the pengine:

output_test:-
    pengine_output(output_test_success).
    
output_test2(done):-
    pengine_output(output_test2_first),
    pengine_output(output_test2_second).

When a slave-pengine responds with output the erl_pengine will automatically call pull_response to receive all outputs since there might be multiple.

%% ask pengine for output_test which will just return a single output and no solution
    {
      {
        output,<<"output_test_success">>
      },
      {
        pull_response,
        {
          {
            success,
            Id,
            [[]],
            false
          },
          {
            pengine_destroyed, 
            _Reason1
          }
        }
      }
    } 
        = pengine:ask(Pid, "output_test", #{template => "[]", chunk => "1"}).
     
%% ask pengine for output_test2 which will return two outputs and also a single solution 'done'.
    {
      {output,<<"output_test2_first">>},
      {
        pull_response,
       {
         {
           output,<<"output_test2_second">>},
         {
           pull_response,
          {
            {
              success,
              Id,
              [[<<"done">>]],
              false
            },
         {
           pengine_destroyed,
          _Reason2
         }
          }
         }
       }
      }
    } 
        = pengine:ask(Pid, "output_test2(X)", #{template => "[X]", chunk => "1"}).

Respond to Pengine Prompt

prompt_response

%% response to a pengine-prompt
-type prompt_response()::{prompt, Id :: binary(), Data :: binary()}.

respond/2

-spec respond(pid(), list()) -> ask_response() | error_response().
respond(Pengine, PrologTerm) ->
 ...

respond/2 lets you respond to a pengine-slave that has sent you a prompt waiting for input with the prolog predicate pengine_input/2

Example:

prolog-prompt from the pengine:

prompt_test(prompt_test_success(Input)):-
	pengine_input(prompt_test, Input).

Response:

%% Receive prompt from slave-pengine
{prompt, Id, <<"prompt_test">>} = pengine:ask(Pid, "prompt_test(X)", #{template => "[X]", chunk => "1"}).

%% Respond to prompt with prolog atom 'pengine'
{{success, Id1, 
      [[
        #{
           <<"args">> := [<<"pengine">>], 
           <<"functor">> := <<"prompt_test_success">>
         }
       ]], MoreSolutions = false}, {pengine_destroyed, _Reason}} = pengine:respond(Pid, "pengine").

pengine_master

pengine_master manages active pengines and contains some useful utility functions.

abort/stop

abort_response
%% response to abort-request
-type abort_response()::{aborted, Id :: binary()} |
                        {{aborted, Id :: binary()}, destroy_response()} |
                        died_response().
stop_response
%% response to stop-request
-type stop_response()::{stopped, Id :: binary()} |
                       died_response().
pengine_master:abort/1 and pengine_masterstop/1

Sometimes a query to a pengine might take long time and you dont want to wait for the solution but want to interrupt the busy pengine and free your erlang-process which will be stuck waiting for a response.

stop/1 gently tries to ask the pengine to stop and abort/1 terminates the pengine's query by force.

-spec abort(binary(), string() | binary()) -> pengine:abort_response() | pengine:error_response().
abort(Id, Server) ->
 ...
-spec stop(binary(), string() | binary()) -> pengine:stop_response() | pengine:error_response().
stop(Id, Server) ->
 ...

Example:

%% Attempt to stop pengine-query
    spawn(fun() -> 
                  pengine:ask(P1, "member(X, [1,2])", #{template => "[X]", chunk => "1"})
          end),
    Res = pengine_master:stop(Id1, "http://127.0.0.1:4000/pengine"),
    case Res of 
        {stopped, Id1} -> ok;        
        {error, Id1, _Reason, _Code} -> ok %% error if nothing to stop      
    end.

%% Abort pengine by force (should always succeed)
    Self = self(),
    spawn(fun() -> 
                  {{aborted, Id1}, {pengine_destroyed, _Reason1}} = pengine:ask(P1, "long_query(X)", #{template => "[X]", chunk => "1"}),
                  Self ! aborted
          end),    
    {pengine_died,_Reason} = pengine_master:abort(Id1, "http://127.0.0.1:4000/pengine"),
    receive
        aborted ->
            ok    
    end.

list and lookup active pengines

pengine_master:list_pengines/1

-spec list_pengines() -> list().
list_pengines()->
...

pengine_master:lookup_pengine/1

-spec lookup_pengine(binary()) -> pid().
lookup_pengine(Id)->
...

Examples

%% Ask pengine_master for a list of active slave pengines.
%% Note that the remote slave_pengines need not necessarily be active, if you dont ping the slave_pengine periodically
%% you don't know if it has died.
[{Pid1, Id1}, {Pid2, Id2}] = pengine_master:list_pengines()

%% Pengines are identified by their Pid in erl_pengine, you can lookup a Pid given a slave_pengine Id
Pid = pengine_master:lookup_pengine(Id)

kill all pengines

You can always kill pengines manually by their Pids and pengine:destroy/1 but for convenience pengine master exports a function to kill all pengines in one go (it will kill both erlang-process and the remote slave-pengine)

pengine_master:kill_all_pengines/0

-spec kill_all_pengines() -> ok.
kill_all_pengines()->
...
ok = pengine_master:kill_all_pengines().

Errors

A common source of error from the pengien is if the Prolog Transport Protocol (PLTP) and its associated finite state machine and transitions is violated. Another source of error is for example if you try to query a slave-pengine with a specified ID and that slave-pengine does not exist anymore.

error_response

%% response when pengine signaling that some error occurred
-type error_response()::{error, Id :: binary(), Reason :: binary(), Code :: binary()}.

died_response

%% response if pengine to send request to was dead before request could be handled
-type died_response()::{pengine_died, Reason :: any()}.

Examples

protocol_error:

%% attempt to respond to a prompt that does'nt exist
{error, Id2, _Reason2, <<"protocol_error">>} = pengine:respond(P2, "pengine"). %% no prompt left to respond

existence_error:

 %% send request to slave-pengine which has already terminated.
3 > pengine:destroy(Pid).
{error,<<"f1d9eeb1-889b-4ae5-969c-aa3d83fafdd4">>,
       <<"pengine `'f1d9eeb1-889b-4ae5-969c-aa3d83fafdd4'' does not exist">>,
       <<"existence_error">>}

pengine_died as a result of request:

%% abort a pengine by force will also kill it.
{pengine_died,_Reason2} = pengine_master:abort(Id1, "http://127.0.0.1:4000/pengine").

econnrefused, connection to pengine server was lost:

1> pengine_master:create_pengine("http://127.0.0.1:4000/pengine", #{}).
{error, {{error,econnrefused},
 "Connection with pengine server could not be established, terminating pengine-process"}}
 
2> pengine:ask(P1, "member(X, [1,2])", #{template => "[X]", chunk => "1"}).
{error, {{error,econnrefused},
 "Connection with pengine server could not be established, terminating pengine-process"}}

Examples

See /examples for two simple example projects, one project uses prolog as a constraint-solver for the sudoku-problem and the other project uses prolog as a rdf triple-store.

%% solve_sudoku will create pengine with the given source and query it for a solution of the sudoku-problem specified in Src
sudoku_solver:solve_sudoku(Src).
 [[9,8,7,6,5,4,3,2,1],
  [2,4,6,1,7,3,9,8,5],
  [3,5,1,9,2,8,7,4,6],
  [1,2,8,5,3,7,6,9,4],
  [6,3,4,8,9,2,1,5,7],
  [7,9,5,4,6,1,8,3,2],
  [5,1,9,2,8,6,4,7,3],
  [4,7,2,3,1,9,5,6,8],
  [8,6,3,7,4,5,2,1,9]]
 4> 

 %% supervises will create pengine and query it for supervises(X,Y).
2> semweb:supervises("X", "Y").
[{<<"http://www.limmen.kth.se/ontologies/erl_pengine#pengine_sup">>,
  supervises,
  <<"http://www.limmen.kth.se/ontologies/erl_pengine#pengine">>},
 {<<"http://www.limmen.kth.se/ontologies/erl_pengine#erl_pengine_sup">>,
  supervises,
  <<"http://www.limmen.kth.se/ontologies/erl_pengine#table_mngr">>},
 {<<"http://www.limmen.kth.se/ontologies/erl_pengine#erl_pengine_sup">>,
  supervises,
  <<"http://www.limmen.kth.se/ontologies/erl_pengine#pengine_sup">>},
 {<<"http://www.limmen.kth.se/ontologies/erl_pengine#erl_pengine_sup">>,
  supervises,
  <<"http://www.limmen.kth.se/ontologies/erl_pengine#pengine_master">>}]
  
3> semweb:supervises("'http://www.limmen.kth.se/ontologies/erl_pengine#pengine_sup'", "Y").
[{"'http://www.limmen.kth.se/ontologies/erl_pengine#pengine_sup'",
  supervises,
  <<"http://www.limmen.kth.se/ontologies/erl_pengine#pengine">>}]
  
4> semweb:supervises("'http://www.limmen.kth.se/ontologies/erl_pengine#erl_pengine_sup'", "Y").
[{"'http://www.limmen.kth.se/ontologies/erl_pengine#erl_pengine_sup'",
  supervises,
  <<"http://www.limmen.kth.se/ontologies/erl_pengine#table_mngr">>},
 {"'http://www.limmen.kth.se/ontologies/erl_pengine#erl_pengine_sup'",
  supervises,
  <<"http://www.limmen.kth.se/ontologies/erl_pengine#pengine_sup">>},
 {"'http://www.limmen.kth.se/ontologies/erl_pengine#erl_pengine_sup'",
  supervises,
  <<"http://www.limmen.kth.se/ontologies/erl_pengine#pengine_master">>}]
  
5> semweb:supervises("X", "'http://www.limmen.kth.se/ontologies/erl_pengine#pengine'").
[{<<"http://www.limmen.kth.se/ontologies/erl_pengine#pengine_sup">>,
  supervises,
  "'http://www.limmen.kth.se/ontologies/erl_pengine#pengine'"}]
6> 

Contribute

Contributions are welcome, for bugreports please use github issues.

It's hard to think of all edge-cases and cover with tests so please if you find a bug, open up a issue or fork and create a PR.

Most useful project commands

It's a rebar3 project so all commands listed here: rebar3 commands are available.

# build
$ ./rebar3 compile

# remove temporary files
$ ./rebar3 clean

# run unit tests
$ ./rebar3 eunit

# run system tests, note that the test-suite uses absolute path to a prolog-pengine server.
$ ./rebar3 ct 

# run static code analysis
$ ./rebar3 dialyzer 

# alias to run all tests
$ ./rebar3 testall

# alias to run the ci-check which travis will run upon git push
$ ./rebar3 ci

# validate codebase, runs: tests, linters, static code analysis
$ ./rebar3 validate

# Generate documentation with edoc
$ ./rebar3 edoc

# Start shell with application loaded
$ ./rebar3 shell

# Run release
$ ./rebar3 run

Make sure that any PR first passes dialyzer, linter and tests.

Author & Maintainer

Kim Hammar kimham@kth.se

Copyright and license

LICENSE

MIT

(C) 2017, Kim Hammar