Starting a HTTP server and suspending the toplevel

Recently I had to write a simple http service in prolog. A HTTP service just has to indefinitely listen for requests to come in and handle them. I found this surprisingly difficult to set up right though.

SWI-Prolog comes with a HTTP server bundled in:

❯ swipl -q
?- use_module(library(http/http_server)).
true.

?- http_server([port(4444)]).
true.

Now a HTTP server is running on port 4444. Great. However, it runs on a set of background threads, which go away the moment the toplevel finishes.
This means I can’t just start a HTTP server and do nothing else, as then the toplevel just immediately halts and takes the HTTP server with it:

❯ swipl -g 'use_module(library(http/http_server))' -g 'http_server:http_server([port(4444)])' -t halt
% Started server at http://localhost:4444/
>

instead I have to do something silly, like

❯ swipl -g 'use_module(library(http/http_server))' -g 'http_server:http_server([port(4444)])' -t 'sleep(10000000000)'

That will keep the service going for the next 300 years. But that can’t be the right way to do this.

Another trick (used by terminusdb) is to take one of the worker threads and thread_join it like so:

main(_) :-
  http_server([port(4444)]),
  once(http_current_worker(4444, ThreadId)),
  thread_join(ThreadId, _).

Since the worker threads presumably run indefinitely, so will the toplevel. Again, I can’t really say this sparks joy.

Worth mentioning here is probably SWI-Prolog -- library(http/http_unix_daemon): Run SWI-Prolog HTTP server as a Unix system daemon. This allows you to start a SWI-Prolog HTTP server which is immediately detached from the current console, allowing it to keep running after logout, and notably, not requiring any tricks to keep the process alive (though I don’t know how this is actually implemented, I assume it’s something with SWI-Prolog -- detach_IO/1).
This style of unix daemon is not really how things are now done in modern daemon management, cause we now have either something like systemd to manage a daemon, or daemons get containerized and run as PID1.

What I’m really looking for is a way to just suspend a thread, until a signal arrives. I’d like to do

main(_) :-
  http_server([port(4444)]),
  thread_suspend.

or

❯ swipl -g 'use_module(library(http/http_server))' -g 'http_server:http_server([port(4444)])' -t 'thread_suspend'

Does this exist? Or is there an easy technique that allows me to implement it myself with just prolog code?

One more trick:

pipe(In, _), read(In, _).

this practically works like a suspend at the cost of some streams.

True. It implements --no-fork though, which keeps the server in the foreground. That is what you need with systemd or docker. Most stuff at swi-prolog.org runs this way. When I wrote this, daemonizing was the default :slight_smile: Now, many similar systems have a -d flag to fork …

The “standard” trick, also used by http_unix_daemon.pl is to call thread_get_message/1. As long as you do not send any messages to the main thread, this waits. Next, http_unix_daemon.pl does use timeouts and sends messages to make the main thread deal with several tasks, such as running make on SIGHUP, clean termination on SIGTERM and regular tasks such as log rotation.

On Unix systems, just using http_unix_daemon.pl is probably the way to go. It also deals with HTTPS, etc.

edit the services at swi-prolog.org do have some nasty shell tricks to deal with Docker health checks. That still needs to be done more gracefully :frowning:

That’s it! That is the option I didn’t notice!

?- http_daemon([port(4444),fork(false)]).
% Started server at http://localhost:4444/
...hangs...

This is the behavior I was looking for. One downside to this one is that interrupting it in a prolog toplevel doesn’t return me to a prompt but still halts the process. Still, that doesn’t matter for anything actually being deployed.

For the more generic problem, thread_get_message/1 is doing exactly what I want.
Thank you!

Can you elaborate? Why don’t things just work with docker?

If you want interactive, pass --interactive instead of --no-fork. That starts the toplevel in addition to all the other stuff the library does. There is no way to move from --no-fork to interactive. Could be added. I have some doubts about using SIGINT for that. Anything better? Maybe SIGINT and verify we are connected to a terminal is enough?

I wanted to use Docker HEALTHCHECK. See plweb/docker at master · SWI-Prolog/plweb · GitHub. Possibly I made it far too complex. In that case, please let me know :slight_smile: The main reason for this is to try and avoid disruption of service in case the server gets into a deadlock or some other state where it does not die, but also does not function correctly.

I don’t think it is really necessary. If the use case is people doing development on their prolog http service, it’s much better served with --interactive, or interactive(true) when calling http_daemon/1 from a prolog toplevel.

Doesn’t seem to crazy. Some things that stand out

  • an auth file is read but it doesn’t seem to do anything
  • the timeout+tail trick to wait for the process to go down should probably be waitpid instead, assuming that’s available in that container
  • Startup check is a little awkward

It would be good if we had a more generic way to signal that a swipl-based daemon is still starting, or that it is ready to handle whatever it is supposed to handle.

I like how it’s done in systemd and wonder if its approach can be used more generically. Basically, well-behaved systemd daemons check for a NOTIFY_SOCKET environment variable, and if it is there, they can use it to send status updates in the format READY=1, STOPPING=1, etc. This allows a daemon manager like systemd, but presumably also a simple docker wrapper, to actively monitor what is going on with a service and react to events.

I’ll see if I can develop a proof of concept library around sd_notify.

It also looks like just using the http/http_unix_daemon is enough to set up an initializer that starts the server based on the command line arguments. Therefore, it is possible to have a simple hello world like so:

#!/usr/bin/env swipl
:- use_module([
                  library(http/http_unix_daemon),
                  library(http/http_server)
              ]).

:- http_handler(root(.), handler, []).

handler(Request) :-
    http_parameters(Request, [name(Name, [default(stranger)])]),
    format("Status: 200~n~nHello, ~s!~n", [Name]).

It can then be started with

> ./hello.pl --no-fork --port 8080
% Started server at http://localhost:8080/
❯ curl -G 'http://localhost:8080' --data-urlencode 'name=Hootie McOwlface'
Hello, Hootie McOwlface!

This is very minimal setup to get a runnable HTTP service with a whole bunch of different startup options. I wish I had known about all this before, but I’m glad I know about it now :slight_smile:

1 Like

You’re welcome :slight_smile: As far as I’m concerned, this can be in the http_unix_daemon.pl as it already is concerned with e.g., logging and listening to signals. I didn’t know about this part of systemd. If we can reuse this for a clean Docker health check, much better. Note that there is a fairly new library http/http_health.pl that serves /health by default, producing a configurable JSON document about the status of the Prolog process.