Last week I showed how a channel could link a producer and a consumer, and I idly speculated on how you’d set the depth of the queue, relative to the rate and variability of the producer and the consumer’s need for new input.
This weekend, I got to thinking about my next interesting Go project. It will be an HTTP proxy that can do some nifty tricks to offload a slow Internet link. One of the features I thought I might add would be something to send all the new HTML it sees through a secondary processing system in order to find extra assets it might want to cache. This is a case of a producer that wants to act totally independently of the consumer. If the secondary processing goroutine is slow, or hung inserting something into a database, or whatever, the producer (the HTTP reply) can’t hang — he needs to reply to the client.
The solution is to hook up a dispatcher between the producer and one or more consumers. If you just wanted several consumers working, they can just all read from the same channel, and safely take the next item. But if you want to give some kind of guarantee to the producer that it won’t hang, you need to put the dispatcher in the loop. (Of course, the producer can also check for itself that it’s not going to hang by doing a non-blocking write on the channel, then throw away the work unit if it would block. But if you don’t want things getting thrown away, the dispatcher solution is better.) The dispatcher is written so that it can’t block. It finds a consumer ready to take the work unit, and if it doesn’t, then it makes a new one and sends the work unit to the new consumer. If the consumers are too slow, instead of making more of them (risking to make the system suffer collapse) it panics. Not nice, but better to fail fast, and clearly.
The result is in the jra-go.googlecode.com/hg repository as a module named autocon. Have a look.
As usual, I want to point out some things I liked/disliked about how this turned out.
First, take a look at the autoConsumer type: it’s private. Why? Because the interface this thing gives to the outside world (the producer) is just a channel. The producer doesn’t get a pointer to an object, it just gets the channel it will be talking to. If you wanted to tune or monitor the dispatcher, this wouldn’t work. But I decided to start out with the simplest thing, and this is it. In fact, when it came time to write the tests cases in autocon_test.go I had a problem; I wanted to know what was happening inside, but I didn’t even have a pointer to the autoConsumer to go dig into it, because once NewAutoConsumer returns, the only thing in the entire system that has a pointer to the autoConsumer is run(). I solved that problem with lexically bound channels, so that my workers had access to the same channel that the test routine did. Then the test routine empties the channel looking to see if the right amount of garbage comes out of it (nothing more, nothing less).
One thing that’s not too nice is I had to add a runtime.Gosched in the test routines to get them to work. And as usual with scheduling problems, it’s sensitive. When I was debugging scheduling problems using fmt.Print, I got different behavior than when I debugged using println. Why? Because the fmt package uses a channel to implement a leaky bucket of unused pp structures, and the scheduling decisions are disrupted by popping and pushing them inside of fmt.Println. Lesson: beware of fmt when debugging scheduler problems.
A better solution to finessing the scheduler with runtime.Gosched would be to arrange for a positive signal to come back that the workers had run and done their job and were ready to have the work checked. But because channels don’t have a “wait until 10 are outstanding” kind of primitive, I would have instead needed to implement some kind of condition variable. It didn’t seem like the right place, down in a test routine, to be doing that!
Another thing I really don’t like about Go, but don’t have a solution for, is the boilerplate “if closed()” after “for x := range ch”. When a goroutine is blocked on a read on a channel, and another goroutine closes it, the channel becomes readable. The result it returns is a nil. If you don’t want to litter your code with a bunch of “if x != nil”, you need to catch that case early and bail out. Luckily, the closed() primitive tells you what you need to know. I think the behavior of closed channels is just right in Go as it is; it is the unavoidable reality check you get when you go from beautiful theory into ugly practice.
And while I am beating up on myself, let me point out that I’m using a label and a labeled goto. Using labels always seems to me like some kind of failure; like it’s just four little characters short of a goto. But the need is undeniable; when you call break inside of nested for loops, the language spec says that the innermost loop will be broken. In this case, I don’t need it, but it somehow improves readability, in that the label explains exactly which loop we are exiting and why. I am deeply conflicted about loop labels, and I suppose the only answer is to keep using them until I get over it, or until I break the habit.