Prolog main program vs Prolog HTTP server thread difference?

I’m using: SWI-Prolog version ??? 8.2.4 on MacOS 10.14.6

I want the code to: start and interact with other processes in an http server thread.

Is there a fundamental problem with starting a new process and interacting with it by pipes from within an HTTP server thread?

I have a 3rd party app that works interactively on its std in and std out, let’s call it “iapp”.

I have a predicate “open_iapp” that uses create_process to start iapp with pipes connected to its stdin and stdout.

I have a predicate “command_iapp” that sends a command to iapp and receives the response via the pipes.

I encapsulate open_iapp and command_iapp in a predicate “interact_iapp”.

I also have a predicate “close_iapp” that nicely closes the pipes and terminates the iapp process.

The predicate interact_iapp works as expected when invoked within a standalone Prolog program.

When I add an HTTP API to the program and start http_server in the program and call interact_iapp from the http handler the interact_iapp hangs up.
The server works fine for requests that don’t call interact_iapp.

Might be wise to update Prolog. 8.2.4 is pretty old. 99% of the time the latest development release is the best :slight_smile: The new version may make a difference (but possibly not).

In theory there is no reason why you can’t call external programs from an HTTP handler. There are plenty of examples for that. For example, SWISH calls dot (graphviz) for rendering graphs

It should be noted that HTTP requests are handled by a pool of workers and thus different requests are typically handled by different threads. So, does “iapp” do its job for a single HTTP request or is the interaction spread over multiple HTTP requests? In the latter case, where do you store the I/O streams that talk to the pipes?

Is the Prolog server running in the background as a service, or in a terminal? Note that rights and environment variables may differ.

2 Likes

Thank you for the response, Jan.

I have updated SWI Prolog to the latest stable version for Mac OS as suggested. This has not solved the problem.

The short answers to your questions:
The interaction with “iapp” is spread over multiple client HTTP requests.
I run the Prolog server in a terminal which serves as a message console.

The Prolog server program starts http_server and the main program then sleeps (for a long time, repeatedly) or optionally returns to Prolog top level so I can do makes while developing/debugging and can examine data structures.

For the ongoing interaction of clients with instances of iapp I have an iapp session.
When an HTTP request is received that requires a session with “iapp”, the server starts an “iapp” process and successfully exchange several commands and responses with it over the pipes.
Other clients of the server can similarly initiate distinct sessions with other instances of “iapp”.

The “iapp” process session is left to continue as it must be used again for future HTTP calls from the same client for which it was created and the process id of iapp is returned to the client as an identifier for future interaction with the iapp session.

The process id of iapp along with the pipe descriptors to communicate with it are stored in a unit clause
of a dynamic predicate.

When a client makes an HTTP request that requires interaction with “iapp” the caller provides the process id of the previously established session.

The server thread handling the request looks up (in the dynamic predicate) the in and out streams associated with the relevant iapp process id.

The server thread then attempts to continue the dialog with iapp on behalf of the client.

This is where the problem occurs.

The interaction with iapp hangs on the first read.

Subsequent HTTP requests to the server complete successfully, provided they do not involve interaction of the server thread with “iapp”.

In the case of the version that enters top level interactive loop after starting the http_server,
the hang is on the very first read on the in pipe from iapp. It seems like the outstanding read
from the terminal in the main thread is interfering with the read on the pipe by the server thread.
But doing some interaction from the keyboard unfreezes that hang (once).
Then the interactions with iapp proceed normally for the initial Http request.
Subsequent http requests that do not involve interaction with iapp complete properly.

However, the next http request that requires interaction with iapp hangs and the interactive terminal
session also becomes unresponsive. Curiously though the http server continues to respond
to “innocuous” requests. I.e. requests that do not involve interaction with an instance of iapp
succeed, including ones that initiate entirely new instances of iapp!

But, in the same way, new requests for followup actions with the new instances of iapp also hang.

A related question: it appears that i have to reopen connection (socket, setsockopt, connect from C program) for every interaction (HTTP api call) with server.

Thanks for the description. Roughly this should work. SWISH does something similar when connecting to R through Rserve (although using sockets rather than a process_create/3 pipe. One remark about the toplevel interaction may be relevant. If two threads read from the same stream, part of the data will go to one thread and part to the other. To be more precise, any of the Prolog input predicates will get a lock on the stream, do the read and release the lock. So, two threads doing get_code/2 will see half the characters going one way and the other the other way, while two threads doing read_term/3 will see Prolog terms going to either thread. So, if somehow you may end up with two threads interacting with the same iapp you’ll get this type of behavior. Some debug/3 statements may help. If you are familiar with debugging tools like lldb you may also connect and check the stack traces of the various threads to see what is going on.

SWI-Prolog does Keep-alive. That requires you to send an HTTP 1.1 request (e.g., with a request header). Not sure whether Keep-alive is default. In any case, be aware that either party may disconnect at any time for a keep-alive connection. The default time SWI-Prolog waits for an idle keep-alive connection is just 1 second. That is enough to avoid reconnecting during a burst of requests and keeps the number of waiting threads and sockets low.

1 Like

Jan,

I have distilled the problem down to two files, a C client and the Prolog server (client.c 150 lines, and server.pl 121 lines)
from about 10,000 lines. In this example the backend app is the unix bc (basic calculator). I’ve reduced the exchange
between the server and the client from a JSON exchange to a simple text exchange, as is the exchange between
the server and the backend, which thinks it’s having an interactive session with a user.

The client main() uses 4 C functions to interact with the server.
These 3: session_start(), session_query(), session_stop() involve interaction with the backend app.
Another function ping(), just sends the server a message and gets a reply without any interaction with the backend app.
Each of these functions opens a socket to the server and closes it when the exchange is done, every time they are called.
I discovered a while ago that I couldn’t just open the connection and leave it open for the duration of the client’s multiple
interactions with the server.

There is a flag at the top of the server file that determines whether the main prolog control flow will return
to the Prolog top level or sleep after starting the http server. I find the former useful for reloading the server code with
make while it is running for testing and debugging and the latter for a production server run.

session_start() does an http call to the endpoint api/session_start, and the others do api/session_query, api/session_stop
and api/ping respectively. The http handlers for these are api_session_start, api_session_query, api_session_stop and api_ping.
Some of these have auxiliary predicates that have some of the logic, but all are small in this distillation.

session_start does a process_create for the backend application (bc), converts the Pid to a session id Sid and
keeps this along with the pipe descriptors in a clause of the dynamic predicate backend_session(Sid,ToBackend,FromBackend).
It also sends a command to bc “sid=Sid” which distinguishes it from other instances of bc.
Later a session_query of this instance with the command “sid” bc will print out the value of the variable sid which is the process id.
session_start returns the Sid, which the C function session_start() stores away for later use by the client.

The session_id is used by the client side as a parameter to session_query and session_stop, which look it up in backend_session/3.
session_query also takes a command parameter, which it sends to the backend over the “to” pipe descriptor and reads the response
from the “from” pipe, which it in turn returns to the client.

session_stop uses the session_id parameter to lookup the pid and pipe descriptors and then retracts the backend_session entry for Sid.
It sends a “quit” command to the bc instance and then closes the pipes and waits for the process to quit before returning to the client.

main() in the client does the sequence session_start(), ping(), session_query(“sid”), session_stop().
Sometimes it will run through this sequence without a problem. Not reliably. More often than not it will hang somewhere
during the open_server_comm()/close_server_comm() interval in session_query. Oddly, when am at Prolog top level
after starting the http server, if I do something like “nl.” it breaks the hang and the server and client complete their exchanges.
The victory is short-lived though because the Prolog top level becomes unresponsive, even double CTRL-Cs don’t recover it
and I end up having to kill it.

Rance

The files are attached.

server.pl (3.76 KB)

client.c (4.75 KB)

If you remove the with_tty_raw/1 call from session_get_resp/2, all seems to work fine. with_tty_raw/1 interacts with the terminal. That interferes with the main thread reading from the terminal. There is no need. You want to read from a pipe, not a terminal and surely not the Prolog console. with_tty_raw/1 switches the terminal temporary to “raw” mode as opposed to “cooked” mode that allows the user to edit the input line and make it available to the process on return.

A small note on keeping the connection open. As is, you send GET /path. That is an HTTP 1.0 request and is replied to by the server using a document and closing the stream. Alternatively, send an HTTP 1.1 request as in

GET /path HTTP/1.1
Host: localhost
(optionally, more headers)

(two newlines)

As a reply you get an HTTP header that contains Content-length: <bytes>, you read the header (up to the empty line that terminates it) and then read <bytes> bytes of reply. The connection remains open for a while (default 1 second). So, when sending the next request you reuse the open connection, but you must be prepared for the possibility that it is closed and you get an error. In that case close your side, re-open and retry.

1 Like

Jan,
Thank you SO much for spotting that misuse of with_tty_raw!!!
While originally writing the interaction with the existing app over pipes I found,
somewhere in the documentation the magic incantation:

with_tty_raw((fill_buffer(S),read_pending_codes(S,Codes,)))

Though the semantics was a bit mysterious it worked for me while using in the main execution path so I didn’t suspect a problem, which only showed up much later when I started using it in a thread.

The moral of the story, I suppose, is not to mindlessly repeat a magic incantation in a strange language if you do not know its full meaning!

Next, I will implement your suggestion to use HTTP/1.1 for longer lasting connections, with reestablishment if necessary. Thanks for that explanation.
If we ever meet again, I owe you a beer or a Genever.