A modifiable version of the "PLTP state diagram"
Can be found here
The paper on "Communicating Finite-State Machines" is
- Walled at ACM Digital Library
- But there is a free copy floating around, too.
Modeling a communication protocol between two (or more) processes is done by specifying a finite-state machine for each process, where an edge is traversed either "actively", sending a message to the other process or "passively" upon receipt of a message (via one or possibly more channels). Transitions are done alternatingly on both sides.
See:
- Wikipedia: Communicating Finite State Machine
The diagram as shown multiplies-out the state machines for the server and client into a single "protocol state machine". This is called a Channel System or Global State Transition Diagram.
See:
- Wikipedia: Channel System)
- Wikipedia: Reachability Analysis
Notes:
- This is an "alternating state machine": A BOLD transition is always followed by a SLASH transition and vice-versa.
- Pengine processing or idling is done while the machine is in one of the states.
- Better labels for the states might be:
- 0 = pengine_inexistent
- 1 = pengine_creating or pengine_destroying
- 2 = pengine_idling
- 3 = pengine_running
- 4 = pengine_outputting, waiting for client to pull text
- 5 = pengine_prompting, waiting for client to push text
- 6 = pengine_waiting_for_cmd
- 7 = pengine_stopping
- All the messages/events shown are synchronous but there are also asynchronous messages like "abort" to kick the pengine out of state 3 - so this diagram is missing a few elements.
- AFAIK, this machine is independent of the HTTP session state: You can close the HTTP session, and reconnect later using a previously obtained Pengine Id. But are messages in the queue delivered at that point? We need to test!
- Missing is the timeout event which tears down a Pengine after 3 minutes of idling.
- All the messages are HTTP messages, which may have their own internal chunking for large data blocks. (What happens if there is an error in the HTTP protocol?)
- Websockets or HTTP/2 woule be more flexible for such a back-and-forth than HTTP/1.1. And maybe even XMPP (but you cannot readily drive these from a web browser).
Example: Setting up local pengines
Here we create local engines (but they are not cleaned up properly, once 3 have been created, no more can be created even though the 3 exited after reaching their "idle timeout").
Jan recommends to not use pengines locally. Use engines instead:
Local pengines have been out of fashion for a while. In most cases where you might want to use them engines are probably a way more efficient choice. These didn’t exist when Pengines were invented …
:- use_module(library(pengines)).
:- use_module(library(settings)).
attempt :-
set_setting(pengine_sandbox:idle_limit, 3), % default is 5 mins, shorten to 3s
setting(pengine_sandbox:slave_limit,Max),
format("One can create ~d server/slave pengines~n",Max),
length(PengineIds,Max),
maplist(
[PengineId]>>pengine_create(
[
id(PengineId)
]),
PengineIds
),
maplist(
[PengineId]>>format("Created Pengine: ~q~n",PengineId),
PengineIds),
format("Current threads~n",[]),
forall(
thread_property(ThreadId,id(NumThreadId)),
format(" ~q ~q~n",[ThreadId,NumThreadId])),
aggregate_all(count, pengines:child(_,_), CountBefore),
format("There are ~d pengines~n",CountBefore),
sleep(5),
% It would be better to join the threads here.
% There will be 3 x "Adios" on the screen.
aggregate_all(count, pengines:child(_,_), CountAfter),
format("There are ~d pengines~n",CountAfter),
% But now it's no longer possible to start another pengine...
\+ pengine_create(./lib/swipl/library/http/web/js/pengines.js
[
id(PengineId),
at_exit(format('Adios!~n',[]))
]).
Example code in the SWI-Prolog distribution
We find the following:
The javascript library providong function to invoke a pengine on a remote HTTP server from a browser is in $SWIPL_HOME/lib/swipl/library/http/web/js/pengines.js. It is pulled in by the example .html pages.
The default options in that file are:
Pengine.options = {
format: "json",
server: "/pengine" // i.e. query the same server as the one serving the page
};
Under
$SWIPL_HOME/lib/swipl/doc/packages/examples/pengines/where$SWIPL_HOMEis the home of installed SWI-Prolog- or
$SWIPL_DISTRO/packages/pengines/examples/where$SWIPL_DISTROis the location of the to-be-compiled SWI-Prolog distribution
we find:
|
├── client.pl - Simple client-side demo code
├── server.pl - Server-side demo code
└── web - Various web pages to demonstrate access-from-browser
├── index.html - List them all
├── simple.html - Simple demo running a Prolog query
├── chunking.html - As above using paginated results
├── input_output.html - Run a dialogue with I/O
├── queens.html - The famous 8 queens (on the web!)
├── debugging.html - Debugging
├── hack.html - You can't run just *any* Prolog goal
├── pengine.html - Pengine (doesn't work with sleep/1!!)
├── queen.png
└── update-jquery
Examine server.pl
The code of server.pl which actually starts a pengine server
:- module(pengine_server,
[ server/1 % +Port
]).
:- use_module(library(http/thread_httpd)).
:- use_module(library(http/http_dispatch)).
:- use_module(library(http/http_server_files)).
:- use_module(library(http/http_files)).
:- use_module(library(pengines)).
:- use_module(pengine_sandbox:library(pengines)).
:- http_handler(/, http_reply_from_files(web, []), [prefix]).
server(Port) :-
http_server(http_dispatch, [port(Port)]).
- There is a directive that calls http_handler/3, registering the handler http_reply_from_files/3 with the origin-of-files directory set to
web, and this for any request start with/, i.e. for any and all requests.- I'm not sure how the relative path
webis resolved, I had to replace it with an absolute path to thewebdirectory. Otherwise the "internal server error" is given as response, which is just indication that a predicate is failing, in this case, an indication that the requested file cannot be found or accessed (a bit hard to judge, more logging and more fine-grained error reporting would be needed). - In any case, given the
Pathof an URL, the web server will try to serve a fileweb/Path, i.e. any of the .html files in the distribution. - It rejects requests for things like
../../../etc/passwd, which is perfect.
- I'm not sure how the relative path
- Predicate
server(Port)just calls http_server/2, the web-server predicate, with the the standard http_dispatch/1 fromlibrary(http_dispatch)and the TCP/IP port to use as arguments. - When
server(Port)is called, any HTTP request will be handed to http_reply_from_files/3 as previously registered. We now are running a web file server!
But where do the Pengines come into the picture?
It turns out that if you load library(pengines), the following HTTP handlers are declared via directives:
:- http_handler(root(pengine), http_404([]), [ id(pengines) ]).
:- http_handler(root(pengine/create), http_pengine_create, [ time_limit(infinite), spawn([]) ]).
:- http_handler(root(pengine/send), http_pengine_send, [ time_limit(infinite), spawn([]) ]).
:- http_handler(root(pengine/pull_response), http_pengine_pull_response, [ time_limit(infinite), spawn([]) ]).
:- http_handler(root(pengine/abort), http_pengine_abort, []).
:- http_handler(root(pengine/detach), http_pengine_detach, []).
:- http_handler(root(pengine/list), http_pengine_list, []).
:- http_handler(root(pengine/ping), http_pengine_ping, []).
:- http_handler(root(pengine/destroy_all), http_pengine_destroy_all, []).
:- http_handler(root(pengine/'pengines.js'), http_reply_file(library('http/web/js/pengines.js'), []), []).
:- http_handler(root(pengine/'plterm.css'), http_reply_file(library('http/web/css/plterm.css'), []), []).
So an infrastructure for handling pengine requests appears underneath URL /pengine and is available to http_server/2 under the above URLs. You are immediately good to go.
Running a pengine server and listing its pengines
Suppose you have the above server.pl at hand, with web replaced by the appropriate path.
Then:
?- [server]. true. ?- server(10001). % Started server at http://localhost:10001/ true.
Now access http://localhost:10001/chunking.html with your web browser and kick off pengine processing several times in different tabs.
You can list the running pengines with this code inspired by pengines.pl:
- use_module(library(pengines)).
% Resulting dict:
%
% pengine{
% self:Id, % Identifier of the pengine. This is the same as the first argument.
% module:Id, % Temporary module used for running the Pengine (always bound to the same value of the Identifier).
% parent:Parent, % Message queue to which the (local) pengine reports (what if it's not local?)
% application:Application, % Pengine runs the given application (module-without-source-name).
% destroy:Destroy, % Destroy is =true= if the pengine is destroyed automatically after completing the query.
% }
%
% The following are added depending on circumstances:
%
% alias:Alias, % Name is the alias name of the pengine, as provided through the `alias` option when creating the pengine.
% remote:IsRemote % One of true, false to indicate whether this is a remote or a local pengine
% url:ServerUrl % URL of the remote server if this is a remote pengine
% thread:Thread % Thread number of pengine (different from 0) if this is a local pengine
% source:source(SourceID,Source) % Source is the source code with the given SourceID. May be present if the setting `debug_info` is present.
% detached:When % Time when the Pengine was detached.
collect_one(Id,DictOut) :-
Dict = pengine{ self:Id, module:Id, parent:Parent, application:Application, destroy:Destroy },
pengines:current_pengine(Id,_,_,_,_,_), % Backtrackably enumerate Id (to make sure Id does not change on the 2nd call to current_pengine/6)
format("Checking out pengine ~q~n",[Id]),
pengines:current_pengine(Id,Parent,Thread,ServerUrl,Application,Destroy), % Get more info about Pengine Id (this should succeed in any case)
maybe_add_detached(Id,Dict,Dict1),
maybe_add_alias(Id,Dict1,Dict2),
maybe_add_remote(Thread,ServerUrl,Dict2,Dict3),
maybe_add_source(Id,Dict3,DictOut).
maybe_add_detached(Id,DictIn,DictOut) :-
pengines:pengine_detached(Id, When)
-> put_dict(_{detached:When},DictIn,DictOut)
; DictIn = DictOut.
maybe_add_alias(Id,DictIn,DictOut) :-
(pengines:child(Alias,Id),Alias\==Id)
-> put_dict(_{alias:Alias},DictIn,DictOut)
; DictIn = DictOut.
maybe_add_remote(Thread,ServerUrl,DictIn,DictOut) :-
Thread == 0
-> put_dict(_{remote:true,url:ServerUrl},DictIn,DictOut)
; put_dict(_{remote:false,thread:Thread},DictIn,DictOut).
maybe_add_source(Id,DictIn,DictOut) :-
pengines:pengine_data(Id, source(SourceID, Source))
-> put_dict(_{source:source(SourceID,Source)},DictIn,DictOut)
; DictIn = DictOut.
% Collection information on all pengines
collect_all(DictOut) :-
bagof(Id-DictEngine,collect_one(Id,DictEngine),BaggedPairs),
dict_pairs(DictOut,pengines,BaggedPairs).
In combination with a dict prettyprinter (which I still have to properly put into a package), you then get results like:
?- collect_all(D),dict_pp(D,_{border:true}).
Checking out pengine '17892012-ee6a-48cd-9a1a-f2570e9f8891'
Checking out pengine 'ffb1e780-a85b-47cc-be09-19970f024b53'
Checking out pengine 'b4b74d91-8492-4a22-a0af-903a83f6f658'
+-------------------------------------------------------------------------------------------+
| pengines |
+-------------------------------------------------------------------------------------------+
|17892012-ee6a-48cd-9a1a-f2570e9f8891 : +--------------------------------------------------+|
| | pengine ||
| +--------------------------------------------------+|
| |application : pengine_sandbox ||
| |destroy : true ||
| |module : 17892012-ee6a-48cd-9a1a-f2570e9f8891||
| |parent : <message_queue>(0x7f44540054e0) ||
| |remote : false ||
| |self : 17892012-ee6a-48cd-9a1a-f2570e9f8891||
| |thread : <thread>(11,0x7f445c0167a0) ||
| +--------------------------------------------------+|
|b4b74d91-8492-4a22-a0af-903a83f6f658 : +--------------------------------------------------+|
| | pengine ||
| +--------------------------------------------------+|
| |application : pengine_sandbox ||
| |destroy : true ||
| |module : b4b74d91-8492-4a22-a0af-903a83f6f658||
| |parent : <message_queue>(0x7f4454006a30) ||
| |remote : false ||
| |self : b4b74d91-8492-4a22-a0af-903a83f6f658||
| |thread : <thread>(13,0x7f445c01d8f0) ||
| +--------------------------------------------------+|
|ffb1e780-a85b-47cc-be09-19970f024b53 : +--------------------------------------------------+|
| | pengine ||
| +--------------------------------------------------+|
| |application : pengine_sandbox ||
| |destroy : true ||
| |module : ffb1e780-a85b-47cc-be09-19970f024b53||
| |parent : <message_queue>(0x7f4454003700) ||
| |remote : false ||
| |self : ffb1e780-a85b-47cc-be09-19970f024b53||
| |thread : <thread>(9,0x7f445c01b9e0) ||
| +--------------------------------------------------+|
+-------------------------------------------------------------------------------------------+