Thursday, May 1, 2025

It Still Sucks

Don’t get me wrong. I”m not saying that the alternatives are any better or even any different.

Unix has been around more than forty years and it is still susceptible to I/O deadlock when you try to run a subprocess and stream input to it and output from it. The processes run just fine for a while, then they hang indefinitely waiting for input and output from some buffer to synchronize.

I’m trying to store data in a database. There aren't any good database bindings I could find, so I wrote a small program that reads a record from stdin and writes it to the database. I launch this program from Common Lisp and write records to the input of the program. It works for about twenty records and then hangs. I've tried to be careful to flush and drain all streams from both ends, to no avail.

I have a workaround: start the program, write one record, and quit the program. This doesn’t hang and reliably writes a record to the database, but it isn’t fast and it is constantly initializing and creating a database connection and tearing it back down for each record.

You'd think that subprocesses communicating via stream of characters would be simple.

8 comments:

Joe Marshall said...

A reader is having problems with comments. (apparently several readers have had problems. Not sure what to do about this.)
Anyway, Dave says:
You have to give us more than that! Which UNIX is this? Which database? How does the program work? What's its buffering strategy? Does it use select or poll or some such, or does it just loop, reading some data then writing some data? What's it doing when it hangs? What kind of streams are we talking about? Give us a clue!

Ubuntu 24.04 running on WSL2.
Program is a Golang program that reads three lines from stdin with a bufio.NewScanner and then unmarshals the string as JSON and does a collection.InsertOne into a MongoDB.
No output is printed. Program loops reading from stdin and writing to MongoDB until it reads a blank line at which point it exits.

CommonLisp end runs uiop:launch-program with :stream for stdin, stdout, and stderr. It then formats json to the stdin of the subprocess.

As I mentioned, it goes swimmingly for about 20 records, and then it hangs. Golang program prints no output, so it should not hang waiting on buffered output. Golang program reads input completely, so it should not hang on input.

I've reduced my program to output one record and then a blank line, killing the golang program, then restarting it on the next iteration. This works but makes me feel dirty.

Anonymous said...

I also tried to comment earlier. Would a named pipe work for sending multiple records to the go program? Or writing a temporary JSON file with many records?

Joe Marshall said...

A named pipe may work, as might a file. I don't want to use a file because I want it to be incremental and not particularly buffered.

Anonymous said...

If you just run the Common Lisp program, I bet it'll output to your terminal the JSON formatted records as fast as you can enter them. I don't think there is any buffering issue here. Maybe there's a WSL issue, or a Golang issue, I don't know.

But, since you are only communicating in one direction, buffering deadlock of some sort doesn't come into the picture. If, on the other hand, you needed to communicate back to the Common Lisp program from the Golang program, then there could be a problem were one program is waiting for input that another program isn't giving.

In general, in UNIX, reading from stdin, and wriitng to stdout "just works." So I think there might be something unique in your situation.

Good luck sir. If you figure it out, we'd all be interested to know the outcome.

Anonymous said...

Could be an WSL2 issue. Even scp (secure copy) onto an Debian running on WSL2 from an external IP, never returns the prompt, even though the file has completely transferred.

The Software Bin said...

Hi, I suggest to check if stderr is maybe interfering. I remember a case where I wasn't able to obtain new stdout until I consumed/discarded stderr.

Marco Antoniotti said...

On a completely different dimension (don't ask) I just followed a lengthy discussion about UN*X "cooperative" locking via flock. People were pining for Windows (and especially other OS's) exclusive file locks. Maybe this is relevant.

Anonymous said...

"CommonLisp end runs uiop:launch-program with :stream for stdin,
stdout, and stderr." Why are you using :stream for either stdout or
stderr? Those arguments cause the child's stdout or stderr
respectively to be distinct pipes whose other ends are in the Lisp
process. This creates the possibility of a deadlock: if the child
writes to either of those channels and the parent doesn't read from
them (at all, or frequently enough), the child will eventually block
in one of those writes.

Further, you say about the Golang program that "No output is
printed". I'll assume this means you don't explicitly have your
program write anything to stdout. If that's true there's no point
giving the child a pipe for its stdout: supply :OUTPUT NIL.

So that leaves stderr as a potential culprit. I don't know Golang
customs about how chatty to be on stderr, but perhaps something's
writing diagnostics there. Does your Lisp program mean to do anything
with any such diagnostics? If not, supply :ERROR NIL. If so, my
suggestion would be to have the child use a regular file for its
stderr by supplying a pathname designator as the :ERROR
argument. Modulo a lot of technical and non-technical "whatabouts",
the Lisp program ought to be able to open and read from that regular
file periodically while it's doing its write loop, and the deadlock
potential should be eliminated (since the child won't block writing to
its stderr, and the parent would get an EOF rather than hang when it
gets to the current end of the regular file).