A Short Adventure with simpleHTTP

Paul R. Brown @ 2008-02-08T05:51:28Z

I'll blog separately about adding support for some JavaScript-free del.icio.us and Flickr chrome to perpubplat (look at the sidebar and the bottom of entries in the detail view), but like the experiment with the del.icio.us JSON API, it had an interesting (to me at least) and unexpected turn with the Haskell Network.HTTP library not closing its connections on my development box and laptop but running just fine on a Linux server.

Too Many Open Files?

I fired up a local build of perpubplat and let it run for a bit. The hit rate was a little more aggressive than del.icio.us was happy with (999 response codes), so I tapered it back and let the service run for a while. After a couple of hours, the service crashed with a "too many open files" exception, and the only thing that could mean was that the connections to del.icio.us weren't getting closed properly. A quick restart, a little wait, and there are a bunch of open connections to del.icio.us hanging around; here's a representative pair of open connections:

$ sudo lsof -u _www | grep perpubpla
[...]
perpubpla 52499 _www    6u  IPv4 0xa642e64        0t0      TCP \
  coresaplenty:49364->badges1.del.vip.re1.yahoo.net:http (LAST_ACK)
[...]
perpubpla 52499 _www   22u  IPv4 0x9fa7270        0t0      TCP \
  coresaplenty:49457->badges1.del.vip.re1.yahoo.net:http (TIME_WAIT)
[...]

(My local Apache2 runs as the _www user.) A quick check showed that the number is steadily increasing:

$ while true; do sudo lsof -u _www | grep del.vip | wc -l; sleep 10; done
[...]
80
83
[...]
150
152
[...]

My first thought was that I was being overly lazy in the code that connects to the remote services. I added some strictness annotations in strategic places (enough to ensure that the response body was fully read), tinkered with relevant HTTP headers (e.g., Connection: close), and turned on debugging in the Network.HTTP library, which just reported that it was closing streams.

I polled folks on #haskell, but it didn't appear that others were having the same issue.

Detour into TCP

Rather than suspect the Network.HTTP library (which is where the problem was), the hung connections led me to initially suspect the network layer, and I dug into the TCP exchange. Here's a state chart from RFC 793 that provides a non-normative explanation of proper TCP behavior:

                              +---------+ ---------\      active OPEN  
                              |  CLOSED |            \    -----------  
                              +---------+<---------\   \   create TCB  
                                |     ^              \   \  snd SYN    
                   passive OPEN |     |   CLOSE        \   \           
                   ------------ |     | ----------       \   \         
                    create TCB  |     | delete TCB         \   \       
                                V     |                      \   \     
                              +---------+            CLOSE    |    \   
                              |  LISTEN |          ---------- |     |  
                              +---------+          delete TCB |     |  
                   rcv SYN      |     |     SEND              |     |  
                  -----------   |     |    -------            |     V  
 +---------+      snd SYN,ACK  /       \   snd SYN          +---------+
 |         |<-----------------           ------------------>|         |
 |   SYN   |                    rcv SYN                     |   SYN   |
 |   RCVD  |<-----------------------------------------------|   SENT  |
 |         |                    snd ACK                     |         |
 |         |------------------           -------------------|         |
 +---------+   rcv ACK of SYN  \       /  rcv SYN,ACK       +---------+
   |           --------------   |     |   -----------                  
   |                  x         |     |     snd ACK                    
   |                            V     V                                
   |  CLOSE                   +---------+                              
   | -------                  |  ESTAB  |                              
   | snd FIN                  +---------+                              
   |                   CLOSE    |     |    rcv FIN                     
   V                  -------   |     |    -------                     
 +---------+          snd FIN  /       \   snd ACK          +---------+
 |  FIN    |<-----------------           ------------------>|  CLOSE  |
 | WAIT-1  |------------------                              |   WAIT  |
 +---------+          rcv FIN  \                            +---------+
   | rcv ACK of FIN   -------   |                            CLOSE  |  
   | --------------   snd ACK   |                           ------- |  
   V        x                   V                           snd FIN V  
 +---------+                  +---------+                   +---------+
 |FINWAIT-2|                  | CLOSING |                   | LAST-ACK|
 +---------+                  +---------+                   +---------+
   |                rcv ACK of FIN |                 rcv ACK of FIN |  
   |  rcv FIN       -------------- |    Timeout=2MSL -------------- |  
   |  -------              x       V    ------------        x       V  
    \ snd ACK                 +---------+delete TCB         +---------+
     ------------------------>|TIME WAIT|------------------>| CLOSED  |
                              +---------+                   +---------+

Without any additional digging, the fact that the connections appeared to be hung in either of the final two states (TIME_WAIT or LAST_ACK) should have alerted me to the actual problem, as should the fact that the connection in the TIME_WAIT state failed to time out even after a long period of time (e.g., 30 minutes). But I didn't see it yet, so I kept digging.

With the aid of tcpdump and wireshark, the interaction that hangs in the LAST_ACK state:

perpubpla 52499 _www    6u  IPv4 0xa642e64        0t0      TCP \
  coresaplenty:49364->badges1.del.vip.re1.yahoo.net:http (LAST_ACK)
|29.402   |         SYN       |                   |Seq = 0 Ack = 3900749235
|         |(49364)  ------------------>  (80)     |
|29.500   |         SYN, ACK  |                   |Seq = 0 Ack = 1
|         |(49364)  <------------------  (80)     |
|29.500   |         ACK       |                   |Seq = 1 Ack = 1
|         |(49364)  ------------------>  (80)     |
|29.501   |         PSH, ACK - Len: 120           |Seq = 1 Ack = 1
|         |(49364)  ------------------>  (80)     |
|29.620   |         PSH, ACK - Len: 373           |Seq = 1 Ack = 121
|         |(49364)  <------------------  (80)     |
|29.620   |         PSH, ACK - Len: 5             |Seq = 374 Ack = 121
|         |(49364)  <------------------  (80)     |
|29.620   |         FIN, ACK  |                   |Seq = 379 Ack = 121
|         |(49364)  <------------------  (80)     |
|29.620   |         ACK       |                   |Seq = 121 Ack = 374
|         |(49364)  ------------------>  (80)     |
|29.620   |         ACK       |                   |Seq = 121 Ack = 379
|         |(49364)  ------------------>  (80)     |
|29.620   |         ACK       |                   |Seq = 121 Ack = 380
|         |(49364)  ------------------>  (80)     |
|29.621   |         FIN, ACK  |                   |Seq = 121 Ack = 380
|         |(49364)  ------------------>  (80)     |
|29.722   |         ACK       |                   |Seq = 380 Ack = 122
|         |(49364)  <------------------  (80)     |

According to the statechart and the RFC, this looks acceptable.

As for the one that hangs in the TIME_WAIT state:

perpubpla 52499 _www   22u  IPv4 0x9fa7270        0t0      TCP \
  coresaplenty:49457->badges1.del.vip.re1.yahoo.net:http (TIME_WAIT)
|189.205  |         SYN       |                   |Seq = 0 Ack = 1062078327
|         |(49457)  ------------------>  (80)     |
|189.308  |         SYN, ACK  |                   |Seq = 0 Ack = 1
|         |(49457)  <------------------  (80)     |
|189.308  |         ACK       |                   |Seq = 1 Ack = 1
|         |(49457)  ------------------>  (80)     |
|189.308  |         PSH, ACK - Len: 120           |Seq = 1 Ack = 1
|         |(49457)  ------------------>  (80)     |
|189.507  |         ACK       |                   |Seq = 1 Ack = 121
|         |(49457)  <------------------  (80)     |
|189.529  |         PSH, ACK - Len: 373           |Seq = 1 Ack = 121
|         |(49457)  <------------------  (80)     |
|189.529  |         PSH, ACK - Len: 5             |Seq = 374 Ack = 121
|         |(49457)  <------------------  (80)     |
|189.529  |         FIN, ACK  |                   |Seq = 379 Ack = 121
|         |(49457)  <------------------  (80)     |
|189.529  |         ACK       |                   |Seq = 121 Ack = 374
|         |(49457)  ------------------>  (80)     |
|189.529  |         ACK       |                   |Seq = 121 Ack = 379
|         |(49457)  ------------------>  (80)     |
|189.529  |         ACK       |                   |Seq = 121 Ack = 379
|         |(49457)  ------------------>  (80)     |
|189.529  |         FIN, ACK  |                   |Seq = 121 Ack = 379
|         |(49457)  ------------------>  (80)     |
|189.529  |         FIN, ACK  |                   |Seq = 121 Ack = 380
|         |(49457)  ------------------>  (80)     |
|189.632  |         ACK       |                   |Seq = 380 Ack = 122
|         |(49457)  <------------------  (80)     |

As with the connection hung in the LAST_ACK state, this also looks to be within the behavior prescribed by the statechart and RFC.

This issues occurs both on my primary development box at home and on my laptop, both Intel hardware with Mac OS X 10.5.1, but on the (virtual) server that hosts this blog (Linux 2.6 kernel), the same code, libraries, and GHC version exhibit no problems.

The Actual Problem

Finger-tracing the code for the simpleHTTP function in the Network.HTTP module leads down into the Network.TCP module, and when I finally read the code, I kicked myself for the detour with packet sniffing. Here is the full text for the close function for the TCP stream with the issue highlighted in red:

close ref = 
    do { c <- readIORef (getRef ref)
       ; closeConn c `Exception.catch` (\_ -> return ())
       ; writeIORef (getRef ref) ConnClosed
       }
    where
        -- Be kind to peer & close gracefully.
        closeConn (ConnClosed) = return ()
        closeConn (MkConn sk addr [] _) =
            do { shutdown sk ShutdownSend
               ; suck ref
               ; shutdown sk ShutdownReceive
               ; sClose sk
               }

        suck :: Connection -> IO ()
        suck cn = readLine cn >>= 
                  either (\_ -> return ()) -- catch errors & ignore
                         (\x -> if null x then return () else suck cn)

The Exception.catch will trap any exception that occurs during connClose, but that doesn't mean that the connClose function has completed fully and closed the socket with sClose.

Here's a quick experiment to confirm my suspicions. First, some finer grained exception trapping and reporting:

close ref = 
    do { c <- readIORef (getRef ref)
       ; closeConn c `Exception.catch` (flag "0")
       ; writeIORef (getRef ref) ConnClosed
       }
    where
        -- Be kind to peer & close gracefully.
        closeConn (ConnClosed) = return ()
        closeConn (MkConn sk addr [] _) =
            do { ( shutdown sk ShutdownSend >> suck ref) `Exception.catch` (flag "1")
               ; shutdown sk ShutdownReceive `Exception.catch` (flag "2")
               ; sClose sk
               }

        flag s e = print $ s ++ ":" ++ show e

        suck :: Connection -> IO ()
        suck cn = readLine cn >>= 
                  either (\_ -> return ()) -- catch errors & ignore
                         (\x -> if null x then return () else suck cn)

And a ghci session to try it out:

$ cd ~/work/haskell-http/HTTP-3001.0.4
$ ghci Network.HTTP
GHCi, version 6.8.2: http://www.haskell.org/ghc/  :? for help
[.. startup stuff ...]
*Network.HTTP> :set prompt "> "
> :m + Data.Maybe Network.URI
> let teh_goog = fromJust $ parseURI "http://www.google.com"
> resp <- simpleHTTP $ Request teh_goog GET [] ""
"2:shutdown: invalid argument (Socket is not connected)"
Right HTTP/1.1 200 OK 
Cache-Control: private
Content-Type: text/html; charset=ISO-8859-1
Set-Cookie: PREF=ID=[...]:TM=[...]:LM=[...]:S=[...]; \
  expires=Sun, 07-Feb-2010 05:02:22 GMT; path=/; domain=.google.com
Server: gws
Transfer-Encoding: chunked
Date: Fri, 08 Feb 2008 05:02:22 GMT
Connection: Close
Content-Length: 5367


Aha — the highlighted line shows that the code is exiting before closing the socket with sClose. Without the change to trap the exceptions within the closeConn function, the connection to Google would have remained open, but now:

> :! lsof | grep ghc | grep goog
>

With a little clean-up, we've got a patch ready to submit:

close ref = 
    do { c <- readIORef (getRef ref)
       ; closeConn c
       ; writeIORef (getRef ref) ConnClosed
       }
    where
        -- Be kind to peer & close gracefully.
        closeConn (ConnClosed) = return ()
        closeConn (MkConn sk addr [] _) =
            mapM_ (flip Exception.catch $ \_ -> return ())
                  [ shutdown sk ShutdownSend
                  , suck ref
                  , shutdown sk ShutdownReceive
                  , sClose sk ]

        suck :: Connection -> IO ()
        suck cn = readLine cn >>= 
                  either (\_ -> return ()) -- catch errors & ignore
                         (\x -> if null x then return () else suck cn)
(comment bubbles) 2 comments

GHC 6.6 and Mac OS X Readline Quick Fix

Paul Brown @ 2006-10-17T06:59:00Z

If you build vanilla GHC 6.6 from the source tarball on Mac OS X (non-Intel), it will build straight through with no issues, but the ghci commandline interface will drive you crazy because it lacks support for editing. This is due to a damaged readline that ships with Mac OS X, but here's a quick fix. The first step is installing an up-to-date version of GNU readline into /usr/local with the usual recipe:

cd ~/work
mkdir gnu-readline
cd !$
wget ftp://ftp.cwru.edu/pub/bash/readline-5.2.tar.gz
tar xzvf readline-5.2.tat.gz
cd readline-5.2
./configure
make && sudo make install

(I keep a copy of the source for any modules that I build from scratch.) The second step is courtesy of Andy Gill:

cd ~/work
mkdir ghc
cd !$
wget http://www.haskell.org/ghc/dist/6.6/ghc-6.6-src.tar.bz2
wget http://www.haskell.org/ghc/dist/6.6/ghc-6.6-src-extralibs.tar.bz2
tar xjvf ghc-6.6-src.tar.bz2
tar xjvf ghc-6.6-src-extralibs.tar.bz2
cd ghc-6.6
./configure --with-readline-includes=/usr/local/include \
            --with-readline-libraries=/usr/local/lib
make -j && sudo make install

This is one of those little joys of configure: flags are passed down to the libraries from the master build, but available options are not bubbled up to ./configure --help. You'd have no idea about the availability of the readline-related flags unless you looked at the library.

Of course, you can also use Fink or DarwinPorts and pass the appropriate directories to configure instead.

Building from source takes a long time, no matter what kind of machine you've got. If you've got a multi-processor or multi-core machine, make -j (as many concurrent jobs as make feels like running) will help cut the time down. It takes under an hour on my four-way G5 with -j and several hours without. (Don't bother with -j without also cranking up the number of processes, e.g., ulimit -u 512, which beats the anemic default of 100 on Mac OS.)

(comment bubbles) 2 comments

To Each His Own Window Navigation Paradigm

Paul Brown @ 2006-08-04T07:26:28Z

Tim Bray on ALT-TAB:

[...] It’s only taken me a day or two to decide that the Windows/Gnome view that a window is a window is a window, and alt-tab works at a window at a time, and you don’t have to bear in mind the difference between an app an a window, is right, and OS X is wrong about this. [...]

Funny. As I've been bouncing between Linuxes, Windows, and Mac OS X, I've come to the opposite conclusion because, e.g., I can't tell from the ALT-TAB display which terminal window I'll get, and if I then get it wrong, I have to go through the stack of windows again and make another guess.

(comment bubbles) 0 comments

More is Less and Less is More for Windows

Paul Brown @ 2006-07-14T05:55:10Z

Jon is writing simple text in full screen, and Cedric's trying (really) to get comfortable with managing windows on Mac OS. Merlin Mann has been regularly riffing about simplifying window management and increasing focus by getting away from multiple open apps. Little bits of chrome and a few icons aren't that distracting for me, so I'm happy with my version of simple text in full screen, which is Aquamacs with a nice theme and all other applications hidden. (WriteRoom apparently doesn't work on two-headed environments anyway, as it only blacks-out the primary display.)

Being disciplined about keeping non-foreground apps hidden is key. If I want a different application, I hide the one I'm working in (CMD-h), call-up Quicksilver (ALT-ALT), and type the name of the application that I want to either un-hide or start. To paraphrase Bob Marley, no mousin', no cry. As the screenshot suggests, that means that my extra display is little more than a fancy picture frame a good amount of the time, although it's quite useful for reference material in a browser or PDF viewer if I'm writing code (as opposed to prose).

On the topic of multiple monitors, via Lifehacker, an article in the New York Times about multi-monitor setups:

Survey after survey shows that whether you measure your productivity in facts researched, alien spaceships vaporized, or articles written, adding an extra monitor will give your output a considerable boost — 20 percent to 30 percent, according to a survey by Jon Peddie Research.

The actual study cites a 42% average increase in productivity, and I have to say that I'm incredulous but not sufficiently curious to pay for the report.

(comment bubbles) 2 comments

Pretty PDF Annotation with OmniGraffle

Paul Brown @ 2006-07-09T07:15:13Z

The Mac OS Preview application is good for lots of things, but the annotation functionality is not that great, i.e., crappy. So, I decided to try OmniGraffle instead, and that approach has its pluses and minuses. On the plus side, OmniGraffle supports importing a multi-page PDF document. On the minus side, at least for me (OmniGraffle Pro 4.1.1), it doesn't actually import any of the PDF but just creates a document with the right number of pages...

Fortunately, Melissa O'Neill from the CS Department at Harvey Mudd College cooked up an app called PDFtoKeynote that will export a multi-page PDF document either as a Keynote presentation or as an OmniGraffle document. (Check out her rant at the bottom of the page that was inspired by Keynote's XML file format, ending with “And remember, in the time it takes to write a DTD, you could have written a grammar specification for a parser generator like Yacc or Antlr.” Amen, sister.)

Here's my recipe for using OmniGraffle for annotations:

  1. Use Preview to scale the document to 75% of its original size by changing the scaling factor in the page setup and exporting to PDF. This will give you a nice border to scribble in. (Or have the author(s) leave a big margin for you in their draft.)
  2. Open the scaled-down document in PDFtoKeynote, select the 612x792 dimensions, and save as an OmniGraffle document.
  3. Open the document in OmniGraffle. Hit CMD-A CMD-L (select all, lock) to lock all of the pages in place.
  4. Draw on the document with OmniGraffle.
(comment bubbles) 0 comments

More on Erlang Performance and Threading

Paul Brown @ 2006-05-11T23:34:00Z

After I saw Robert Sayre's results, I thought that I'd give Rickard Green's Erlang exerciserbig.erl” a go on my four-core (two 2.5GHz G5 processors with two cores each) PowerMac (MacOS X 10.4.6) to see the effect of different numbers of schedulers. (Joe Armstrong posted some benchmark information in his blog, but I don't have a means to reproduce them for direct comparison.)

Eye-Grabbing Plots

There's code below, so as an amuse bouche, here are a couple of plots that illustrate the results. (I used HippoDraw to draw the plots.) This first graphic shows the time to execute the benchmark plotted against the number of processes.

#SchedulersColor
1 orange 
2 red 
4 green 
8 blue 
16 magenta 
  

The green plot illustrates that four schedulers breaks even with one or two schedulers at 800 processes and wins from there out. (I did try a 32 scheduler run but ditched it part way through because the performance was so poor.) Here's another plot that provides an alternative visualization.

In the plot, lighter is faster, and as the number of processes increases, it's visually apparent that the four scheduler sequence is superior.

Interpretation

OK — so what gives here?

In comparison with Robert's results (look for the graph), multiple schedulers provided better performance but much less dramatically versus a single scheduler, and performance degraded much more rapidly with more than the optimum number of schedulers. More than likely, the root cause lies down deep in the core of the MacOS X kernel. Apple has a technote that explains threading in MacOS X, and a cursory read suggests that the application-level pthread threading model is deeply layered over the low-level kernel threading model. My interpretation would be that Mach is doing extra work to spread load across lower-level threads when relatively few schedulers are used, so it wouldn't be surprising if a single scheduler manages to use slightly more than one of the cores.

In terms of what SMP (a.k.a. “symmetric multi-processing”) means for Erlang, MT (for “multi-threaded”) would be a better term. The current version of Erlang, R10B, uses a single scheduler thread to process a queue of runnables, and Erlang R11B uses multiple scheduler threads to manage the same queue. (See, e.g., this presentation.) Under (naively) ideal circumstances, a threads works so hard that it fully consumes the attention of a processor and then other threads are forced onto other processors (i.e., number of threads converges to number of processors), but as this benchmark illustrates, the strength of that convergence is determined by the extent to which the operating system kernel cooperates.

Code Snippets

Here's a little snippet of Erlang to make running the benchmark with different numbers of processes easier and dump data in a convenient format:

-module(bmark).
-export([go/0]).

n() -> element(1,string:to_integer(
                  lists:nth(1,init:get_plain_arguments()))).

plur(1) -> "";
plur(_) -> "s".

runbmark([]) -> done;
runbmark([Head|Tail]) ->
    io:format("~4w ~4w ~6.1f~n",
              [n(), 
               Head, 
               trunc(big:bang(Head)/100000)/10]),
    runbmark(Tail).

go() ->
    N = n(),
    io:format("// Running with ~w scheduler~s.~n",
              [N,plur(N)]),
    runbmark(lists:seq(50,1500,50)),
    io:format("~n",[]),
    halt().

And here's some bash to run 1, 2, 4, 8, and 16 schedulers in succession:

for ((i=0 ; i<5 ; ++i )); do \
 path/to/otp_src_R11B_2006-05-08/bin/erl -smp +S$((1<<$i)) \
-noshell -eval 'bmark:go()' -- $((1<<$i)); echo; done
(comment bubbles) 1 comment

Damn Ergonomics

Paul Brown @ 2006-04-10T00:36:00Z

After having my hard drive renamed “iywemsndfgouy” a few times, I got a recommendation for AlphaBaby from Sam (thanks!), and the kid likes it. A lot. AlphaBaby displays shapes and letters and plays sounds in response to keypresses but otherwise locks the machine down. (Nonetheless, Emme has managed to hit the magic key combination ctrl-alt-cmd-q a few times with her palm and middle finger and get back to renaming desktop items.) My old 17“ G4 Powerbook is a downstairs email terminal about 50% of the time and running AlphaBaby for Emme the other 50%. (Fortunately, she's not talking yet to ask for a MacBook Pro...) She has a grand time massaging the keyboard and watching the shapes. She has actually started to peck at the keys with a finger as her manual dexterity improves, but it's never very long before she notices the power button on the upper right — round, shiny, textured, and inviting. It just looks like it should be pushed, so it gets pushed. Repeatedly. Half the time, she suspends the machine; half the time, she shuts it down. We'll see how long it takes for her to get over the urge to push it.

(comment bubbles) 2 comments

Posts tagged ["macosx"] contains 9 items in 2 pages of 7 items each:
1 2