Steadfastness of a remote answer collector, Web Prolog style

The example uses pengin_spawn/2, pengine_ask/2, pengine_next/1 and receive/1. Where and how is a pengine/webprolog deallocated? What would happen if I do, i.e. ask main/1 with a closed and shorter list. Would main/1 be steadfast and deallocate a pengine/webprolog at the same time?

Welcome to SWI Web Prolog!

?- main([A,B]).

Are there some setup_call_cleanup/3 patterns possible/impossible or necessary/unnecessary for pengines/webprolog? setup_call_cleanup/3 has been introduced in many Prolog systems, since it helps accessing or modifying resources.

There is even a draft standard:
https://www.complang.tuwien.ac.at/ulrich/iso-prolog/cleanup

The typical pattern to access a resource such as a server, is the following programming pattern, which has the advantage that it can be used via backtracking and also works nicely with surrounding cut:

fetch(X) :-
    setup_call_cleanup(server_open(Y),
         server_fetch(Y,X),
         server_close(Y)).

It might not be the prefered programming pattern if between backtracking or cutting away the fetch/1 call, a lot of time can pass, and therefore the server connection would be open for a long time.

But nevertheless, it assures that resources are freed. The cleanup part is also called in case an exception happens in the call part or in the continuation.

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

I’m not sure I can answer all your questions, but here goes:

Allocation and deallocation of pengines and other actors is performed by (what I refer to as) the actor manager. The actor manager is briefly described in the Erlang’19 paper.

Well, let’s see what happens, but let’s first pass the monitor option so that we can check the end result:

main(List) :-
    pengine_spawn(Pid, [
        node('http://localhost:3060'),
        monitor(true),
        src_text("
            q(X) :- p(X).
            p(a). p(b). p(c).
        ")
    ]),
    pengine_ask(Pid, q(_X)),
    collect_and_print(Pid, List).
    
collect_and_print(Pid, List) :-
    receive({
        success(Pid, [X], false) -> 
            List = [X] ;
        success(Pid, [X], true) ->
            List = [X|Rest],
            pengine_next(Pid),
            collect_and_print(Pid, Rest)
    }).

A test run:

Welcome to SWI Web Prolog!

?- main([A,B]).
false.

?- flush.
Shell got down('61931180'@'http://localhost:3060',exit)
true.

?- 

Whether this told you everything you want to know, I’m not sure.

I’m sure some are possible, but I don’t know if there are any impossible ones. Any ideas?

BTW, for answers to such detailed questions, you may want to download and test the PoC yourself? Bug reports (especially for design bugs) are most welcome!

Yes, that’s the one, mentioned in my first post in the first topic related to Web Prolog. But let me provide you with a collection of links, so that you don’t have to dig around in order to “RTFM”:

  1. The “Intro to Web Prolog for Erlangers” paper.

  2. A first draft of a manual.

  3. The longer (> 170 pages) manuscript “Web Prolog and the programmable Prolog Web”.

  4. A chapter on Web Prolog as a scripting language for SCXML.

  5. A chapter on marketing ideas for Prolog.

The chapters 4) and 5) are extracted from the next draft of “Web Prolog and the programmable Prolog Web” that I’m working on. This draft is too messy to distribute at this point in time, but will be announced when I think it’s ready for public consumption.


So equipped with this, let's move on to your other questions:

No, it has nothing to do with links. Unfortunately, I noticed that documentation for flush/0 is missing from the manual. But it’s very simple. Here’s a quote from the intro paper:

Calling the utility predicate flush/0 - also borrowed from Erlang - allowed us to inspect the content of the top-level mailbox.

It means that the shell (or rather the pengine to which the shell is attached) received a message informing you that the pengine which was spawned no longer exists. Erlang has such messages too.

flush/0 is implemented like so:

flush :-
    receive({
        Message -> 
            format("Shell got ~q~n",[Message]),
            flush
    },[ 
        timeout(0)
    ]).

No, as you can see, it’s much simpler than that.

Yes, Web Prolog has exit/2 too, with the same meaning. There’s pengine_exit/2 as well, which is simply an alias for exit/2.

pengine_exit/2 means the same as pengine_destroy/2 in library(pengines). The idea behind the name change is to stick to Erlang-ish naming, as long as it makes sense.


So let’s replace “xxx” with “exit” (and add another argument) in your program, like so:

main(List) :-
    setup_call_cleanup(
        pengine_spawn(Pid, [
        node('http://localhost:3060'),
        monitor(true),
        src_text("
              q(X) :- p(X).
              p(a). p(b). p(c).
          ")
       ]),
      (pengine_ask(Pid, q(_X)),
      collect_in_list(Pid, List)),
      pengine_exit(Pid, hello)).
    
collect_in_list(Pid, List) :-
    receive({
        success(Pid, [X], false) -> 
            List = [X] ;
        success(Pid, [X], true) ->
            List = [X|Rest],
            pengine_next(Pid),
            collect_in_list(Pid, Rest)
    }).

This works, for both ?-main(List) and ?-main([A,B]), just as before. However, I don’t think the use of setup_call_cleanup/3 makes any sense here, since pengine_exit/2 will always run, but won’t do anything at all since the pid at this point in time points to a pengine which is already gone.

The calls in the second and third argument to setup_call_cleanup/3 are deterministic and they don’t fail or throw exceptions, not even if the spawned pengine for some reason is already dead or if its creation didn’t succeed in the first place. There is a pid, but it doesn’t point to an active pengine. This too is Erlang-ish behaviour.

The only thing that might happen is if the call to collect_in_list/2 hangs, i.e. if no success messages show up. There can be a network problem, for example.

So what can be done about that? Here’s one way to handle the situation, assuming we want ?-main(List) to always terminate, and the spawned pengine to die:

main(List) :-
    pengine_spawn(Pid, [
        node('http://localhost:3060'),
        monitor(true),
        src_text("
            q(X) :- p(X).
            p(a). p(b). p(c).
        ")
    ]),
    pengine_ask(Pid, q(_X)),
    collect_in_list(Pid, List).
    
collect_in_list(Pid, List) :-
    receive({
        success(Pid, [X], false) -> 
            List = [X] ;
        success(Pid, [X], true) ->
            List = [X|Rest],
            pengine_next(Pid),
            collect_in_list(Pid, Rest)
    }, [
         timeout(2),
         on_timeout(pengine_exit(Pid, hello))
    ]).

Now, if the success messages take too long to arrive, the pengine will be killed. And since we are monitoring it, the following down message will be sent to the mailbox of the calling process.

Shell got down('6131580'@'http://localhost:3060',hello)

Yes, if you pass monitor(false), which is the default, no down message would be sent.

So now you know. But I wouldn’t mind if you RTFM anyway. :wink:

BTW, given the above explanations, you should be able to understand how the implementation of rpc/2-3 works. Here it is:

rpc(URI, Query) :-
    rpc(URI, Query, []).

rpc(URI, Query, Options) :-
    pengine_spawn(Pid, [
         node(URI),
         exit(true),
         monitor(false)
       | Options
    ]),
    pengine_ask(Pid, Query, Options),
    wait_answer(Query, Pid).

wait_answer(Query, Pid) :-
    receive({
        failure(Pid) -> fail;            
        error(Pid, Exception) -> 
            throw(Exception);                  
        success(Pid, Solutions, true) -> 
            (   member(Query, Solutions)
            ;   pengine_next(Pid), 
                wait_answer(Query, Pid)
            );
        success(Pid, Solutions, false) -> 
            member(Query, Solutions)
    }).

And here’s a test:

?- rpc('http://localhost:3060', q(X), [
       src_text("
            q(X) :- p(X).
            p(a). p(b). p(c).
       ")
   ]).
X = a ;
X = b ;
X = c.

?-

(post withdrawn by author, will be automatically deleted in 24 hours unless flagged)

Sure, my first (crappy) implementation used posix threads (thread_create/3 and friends) only (just like library(pengines)), the current PoC uses Paul Tarau’s engines and fewer threads. An implementation in Erlang might use Erlang processes. An implementation in Java might use something else. What? Well, I leave that to you to propose. The point is that there are many way to implement Web Prolog, and I’m not yet sure which way is the best. I’m still in the specification phase, and would prefer to remain there for a while.

I’m not sure where you think setup_call_cleanup/3 would help. Can you elaborate?

exit(true) tells the pengine to die after having run the query to completion, wheras exit(false) means you can keep using it for more querying.

That could and probably should be added, but I still don’t understand how setup_call_cleanup/3 would help, so I would use pengine_exit/2 in combination with a timeout as in the previous example.

If you know the first solution is all you want, I would propose to use once(q(X)) as a goal. :wink:

But hey, I’m aware of the problem, and I’m not sure a 100% satisfying solutions exists for immediately killing (remote) pengines when they are no longer needed. I have a few ideas, and one of the best might be the following. A Web Prolog node can (and should) be equipped with a stateless (think RESTful) HTTP API as well as a WebSocket API. rpc/2-3, as implemented above, has to use the WebSocket API, but rpc/2-3 can also be implemented using HTTP as transport (one might use an option transport to choose between the two implementations). Here’s a somewhat ugly implementation:

rpc(URI, Query, Options) :-
    option(limit(Limit), Options, 1),
    rpc(URI, Query, 0, Limit).
    
rpc(URI, Query, Offset, Limit) :-
    parse_url(URI, Parts),
    format(atom(QueryAtom), "(~p)", [Query]),
    rpc(Query, Offset, Limit, QueryAtom, Parts).
    
rpc(Query, Offset, Limit, QueryAtom, Parts) :-    
    parse_url(ExpandedURI, [ path('/ask'),
                             search([ query=QueryAtom,
                                      offset=Offset,
                                      limit=Limit,
                                      format=prolog
                                    ])
                           | Parts]), 
    setup_call_cleanup(
        http_open(ExpandedURI, Stream, []),
        read(Stream, Answer), 
        close(Stream)),
    wait_answer(Answer, Query, Offset, Limit, QueryAtom, Parts).

wait_answer(error(anonymous, Error), _, _, _, _, _) :-
    throw(Error).
wait_answer(failure(anonymous), _, _, _, _, _) :-
    fail.
wait_answer(success(anonymous, Solutions, false), Query, _, _, _, _) :- 
    !,
    member(Query, Solutions).
wait_answer(success(anonymous, Solutions, true), 
            Query, Offset0, Limit, QueryAtom, Parts) :-
    (   member(Query, Solutions)
    ;   Offset is Offset0 + Limit,
        rpc(Query, Offset, Limit, QueryAtom, Parts)
    ).

To understand how the stateless HTTP is supposed to work, you should first look at section 4.9 in the paper, and if you want to know (much!) more, at section 6.3 - 6.5 in the longer manuscript. The tutorial in the PoC provides a couple of examples.

Initially you might feel setup_call_cleanup/3 is an ugly construct. But you can check the source code of SWI-Prolog, you will find that Jan W. uses it heavily. Sometimes multiple cascaded setup_call_cleanup/3 construct invocations.

I have one feature request for setup_call_cleanup/2. It would be more useful if the setup part would be interruptible. Like this pattern in Java:

 Y = server_open();
 try {
   X = server_fetch(Y);
 } finally {
   server_close(Y);
 }

In Java server_open() can be interruptible. But if server_open() doesn’t abort, an interrupt will not kill Java statement sequencing ( ; ) so that the try/finally handler will be installed.

Traditionally a simple setup_call_cleanup/3 implementation in Prolog simply masks interruptes for the setup part until the cleanup handler is installed. Now I wonder, whether could do something better, since I now inline disjunction and have a new call/1 in my system.

So basically in the next release of my system there will be no more meta predicate (,)/2 that might be interrupted, so that even a Prolog sequencing through conjunction might not anymore interrupted. Maybe we can then get a better setup_call_cleanup/3.

Edit 07.09.2019:
The requirement is a little contradictory, since Prolog interrupts work differently than Java interrupts. Prolog code is usually viewed as anytime interruptible, we want to externally timeout search procedures etc… For example by call_with_time_limit/2.

On the other hand, ordinary Java code is not interruptible. A Jave search procedure that does not poll an interrupt flag cannot be interrupted. So I dont know yet how to create a setup_call_cleanup/3 that is interruptible in the first argument. Only some vague ideas…

1 Like

For an implementation of threaded engines, see https://logtalk.org/manuals/userman/threads.html#threaded-engines

The API was developed in parallel with the SWI-Prolog engines API and after some discussion with Jan Wielemaker. The APIs share a similar interface. For some examples, see https://github.com/LogtalkDotOrg/logtalk3/tree/master/examples/engines

The tbbt example could probably be converted into a fun demo for Web Prolog using two nodes, one for each player.

1 Like

Thanks, both of you! One has to eat too, and that’s what I’m going to do now. :slight_smile: I’ll get back to you tomorrow, or perhaps not until Monday. Have a nice evening!

I’m back! Couldn’t resist…

Let’s see if I can deliver a decent response to each of you before it’s time to hit the sack. :slight_smile:

Jan B, I thought I knew enough about setup_call_cleanup/2 to use it once in a while when it’s appropriate (when opening files, for example), but who knows, I may be missing something. I’m sure it would deserve a place in Web Prolog, but I still don’t get what kind of role it can play in solving the problems we’ve discussed above. I understand that call_with_time_limit/2 might be useful (instead of the solution based on receive/1-2 and timeouts), but you keep talking about setup_call_cleanup/2 all the time, and that makes me confused. I’m happy to make another attempt to understand. :slight_smile:

Paulo, API looks great!. So it does indeed seem like Logtalk might be a suitable implementation language for a Web Prolog node. :wink: And yes, game-based demos are effective, so I’d love to find out if the tbbt example can be rewritten in Web Prolog.

(Now off to say hej to our new Swedish friend Håkan before going to bed…)

(Strange, BTW, I just noticed that there’s a limit on how many answers one may write to the same post in a row. Didn’t know that until know, and that’s why you get my responses to you both in the same post.)

I am only talking about setup_call_cleanup/3 in this thread because the threads title is “Web prolog error handling”. But error handling here means not catch/3, but rather passing along errors safely. setup_call_cleanup/3 was especially created for this purpose.

Paul Brown has one Article tagged setup_call_cleanup/3
https://prologhub.pl/?tag=setup_call_cleanup/3

It is generally recommended in multithreaded programming that a thread, before it exits, cleans up all its resources it has acquired. Of course there is much more two it. An end-point might also transmit errors, since we want error handling to work distributed over multiple nodes.

For example I dont know exactly what this here means:

        error(Pid, Exception) -> 
            throw(Exception);                  

I guess this is a RTFM case again. But it could mean that while executing ask or next, the pengine experience an exception. This is fine, this is in the spirit of Paul Taraus engine/iterators. But according to this state transition diagram:

pltpsynch

https://www.swi-prolog.org/pldoc/doc_for?object=section%28%27packages/pengines.html%27%29

The pengine is not automatically destroyed after an exception. The pengine is a context which is durable in the sense that it survives errors, etc… It only goes from state 3 to state 2, and can be reused for new queries.

So judging from the state transition diagram to get back into state 0 from state 2, you would need to do something. Also if the response is success(true), then you are in state 6 and you need to do two things to get back into state 0.

So pengines have quite a complicated state transition graph, and I am not yet sure what the pengine_xxx command should be, that can be placed into the cleanup part.

4 posts were merged into an existing topic: Merrits of Erlang style for real-time systems

If you want to do a panorama discussion about Erlang, you should create a new thread. Also I don’t know whether Pmoura has addressed my question. It gets more and more foggy, since this thread is hijacked for I dunno what.

I agree about the need for nice names, but they’re hard to come up with.

But they are equal, aren’t they, as they both fail (given that there are three solutions)?

Both might fail. And one might destroy a pengine and the other might not destroy a pengin (since it stays in state s4). “steadfastness” was formulated by Richard O’Keefe as follows:

Richard O’Keefe, 2009:
“As the inventor of the term “steadfast” in Prolog programming,
I ought to be in favour of it. Steadfastness basically
means that you cannot force a predicate down the wrong path
by filling in output arguments wrongly.”

Thats what this thread is all about, getting pengine always destroyed. I wrote “Would main/1 be steadfast and deallocate a pengine/webprolog at the same time?”.

Edit 08.09.2019:
Another solution would be to use an uninstantiated exception in the main/1 predicate. Check whether the first argument is a variable. Thus drop steadfastness.

Maybe should edit the title of the thread. I don’t know how this could be done, I don’t see some swi-prolog.discourse.group control to edit the title. Its not about a panorama discussion of Erlang and Web-Prolog. Maybe the title should read “steadfastness” something.

Also I don’t know whether Pmoura has addressed my question. It is only getting more and more foggy.

Alternatively could try to split away the original question again.

“Steadfastness of a remote answer collector, Web Prolog style”

I already made a comment, concerning stale pengines see below. Scriptkiddies would love it. I think watchdogs and the like is not really a problem/solution pair for steadfastness. Thats not part of my question, more part of the greater panorama.

In my non-malicious main/1 example all that happens, is that the flow of the program is such, that there is a stale pengine. But the requirement would be that we want no stale pengines at all, that they are deallocated immediately after collecting results.

How can this be solved?

Edit 08.09.2019:
In the above I guess the remote penguine would go into state s2, and the damage would already be made. But I do not assume that a solution should also adress such malicious code, its just a nice illustration of state s2.

I assume a damage is done, since state s2 says WORKING. So I guess the child pengine will be a new bomb. BTW: This was sometimes a Google LLC question for job applicants, what is the shortest line of code to program a bomb that exhausts all pids.

I don’t think this answer settles the problem. Since it doesn’t use Web Prolog. But the title of the thread now reads “…, Web prolog style”, making it unnecessary to wander around.

The solution there uses http_open/3 and close/1 for setup and cleanup. It doesn’t show the server side, I guess the server has even no dedicated pengine, rather a fresh cursor for every request block.

But the example has it:

main(List) :-
    pengine_spawn(Pid, [
        node('http://localhost:3060'),
        monitor(true),
        src_text("
            q(X) :- p(X).
            p(a). p(b). p(c).
        ")
    ]),
    ...

So we do function shipping, and do not access some data which is already on the server. So pooling of pengines is not out of question, but poses itself a little differently.