While developing Depot we've run into a number of issues here and there. The latest one was quite frustrating but quite satisfying to solve. Haskell doesn't provide a library function for non blocking IO that is cross platform (hGetBufNonBlocking is posix only). What that means is that when a client connected to the depot server the server had to wait until either there was no content left OR the client disconnected. I.e. no forced timeouts.
That means that if a number of clients connected to depot at once and simply didn't transmit any data they could completely freeze the server-- bad news.
The hGetLine function reads input from a handle until it reaches a newline character. It's the ideal way to read in HTTP headers since they are terminated by CRLF (\r\n) characters. So I had to work out a non blocking version of hGetLine, here's what I came up with:
hGetLineNB h timeout = do
buff <- atomically newEmptyTMVar
tout <- atomically newEmptyTMVar
rthr <- forkIO $ hgl h buff
tthr <- forkIO $ countDown tout rthr
line <- atomically (waitForData buff tout)
(\e -> error toutmsg)
mapM_ (killThread) [rthr,tthr]
toutmsg = "Timed out reading line in " ++ show timeout ++ "ms"
hgl h buff = hGetLine h >>= atomically . putTMVar buff
countDown tout rthr = threadDelay (timeout * 1000) >>
atomically (putTMVar tout True)
getData buff = takeTMVar buff >>= return
checkTimeout tout = takeTMVar tout >> error "Time out"
waitForData buff tout = getData buff
orElse checkTimeout tout
What this function does is create two empty transaction variables: One for the line that is to be read in and one to hold a boolean timeout value; Then it starts two threads. One begins to read from the handle, putting the contents into the "buff" variable. The other waits for the length of time specified by the "timeout" value passed to the function.
The waitForData function checks the values of the two transaction variables, blocking the current thread of execution until one of them stops blocking. If the buff variable is filled then the contents are returned, if the tout variable is filled the checkTimeout function falls through and causes an error.
The waitForData call is wrapped in a "catch...finally" pair of functions that handle any exceptions by rethrowing them as timeout errors and ensure that both of the two threads that were started are killed before proceeding. The result is a reliable non-blocking getline function for Depot. I was quite happy with this although I'm sure that a more experienced Haskell programmer could point out ways to do this better.