[racket] tcp exceptions and connection reestablishment...

From: Rüdiger Asche (rac at ruediger-asche.de)
Date: Mon Jul 2 11:50:13 EDT 2012

actually, i start to get a little bit uneasy about the exception handling 
strategy here. On the BSD socket API, there is no exception handling; each 
call to a socket related function returns some status code (admittedly, not 
always meaningful) that one can evaluate and respond to accordingly.

Now the way the Racket wrappers were designed make that kind of thing 
somewhat awkward because it is not always predictable whether the control 
flow returns or throws exceptions. Consider:

> (let loop-connect ()
>   (log-debug "Attempting connection..")
>   (with-handlers ((exn:fail:network?
>                    (lambda (exn)
>                      (log-warning (string-append "network error: "
>                                                  (exn-message exn)))
>                      (let ((sleep-seconds (+ 3 (random 3))))
>                        (log-debug
>                         (format
>                          "sleeping for ~A seconds before reconnect 
> attempt"
>                          sleep-seconds))
>                        (sleep sleep-seconds)))))
>    (let-values (((inport outport)
>                  (tcp-connect somehostname somehostport)))
>      (let loop-while-connected ()
>        ....
>        (read-something inport)
>        ....
>        (write-something outport)
>        ...
>        (loop-while-connected))))
>   (loop-connect))

If the connection goes down during the protocol exchange, the exception 
handler is invoked (worse, there are some scenarios in which a connection 
takedown my NOT invoke the handler but instead return #eof which makes it 
hard to come up with a uniform handling strategy). Naturally, I must shut 
down an established connection gracefully (otherwise, as pointed out to me 
in the response to my other question on WSAEALREADY, I end up with a socket 
in half open state which is bad). Unfortunately, in the above code fragment 
inport and outport are not visible within the scope of the handler, so I 
can't call close-input-port and close-output-port within the handler. Of 
course I could switch the code around:

>    (let-values (((inport outport)
>                  (tcp-connect somehostname somehostport)))
>   (with-handlers ((exn:fail:network?
>                    (lambda (exn)
>                      (log-warning (string-append "network error: "
>                                                  (exn-message exn)))
>                      (let ((sleep-seconds (+ 3 (random 3))))
>                        (log-debug
>                         (format
>                          "sleeping for ~A seconds before reconnect 
> attempt"
>                          sleep-seconds))
>                        (sleep sleep-seconds)))))
...

but then exceptions raised during the tcp-connect ITSELF won't be caught 
(plus, a continuation mark would explicitly have to be established for the 
tcp-connect call), and consequently, a return to a connection 
reestablishment point requires nested exception handlers which doesn't sound 
natural to me.

Of course, there's always

   (let ((inport 0)
             (outport 0))
  (with-handlers ((exn:fail:network?
                    (lambda (exn)
                        (if (port? inport)(close-input-port inport) #f)
                        (if (port? outport)(close-output-port outport) #f)
                        ))))
                 (let-values ((i o) (tcp-connect somehostname 
somehostport)))
                        (set! inport i)
                          (set! outport o)
                          ...
                          ; regular code path exit
                          (close-input-port i)
                          (close-output-port o)
                         (set! inport 0)
                         (set! outport 0)


but we all know why we'd like to avoid that kinda code.

 In this case I'd much prefer a "linear" control flow, or can somebody point 
me to a coding strategy that takes care of all the possible cases while 
still maintaining the racket charme we all love? ;-)

Thanks!

----- Original Message ----- 
From: "Neil Van Dyke" <neil at neilvandyke.org>
To: "Rüdiger Asche" <rac at ruediger-asche.de>
Cc: <users at racket-lang.org>
Sent: Wednesday, June 20, 2012 4:50 PM
Subject: Re: [racket] tcp exceptions and connection reestablishment...


> Rüdiger Asche wrote at 06/20/2012 09:21 AM:
>> I have tried call-with-exception-handler and handlers in different 
>> variations but haven't been able to produce code that allows me to 
>> gracefully return to reestablishing the connection. Does anyone have a 
>> code snippet that helps me?
>
> This is quick off-the-cuff suggestion to consider, not the only way to do 
> this (nor any kind of canonical recipe for doing this sort of thing, nor 
> necessarily the best way):
>
> (let loop-connect ()
>   (log-debug "Attempting connection..")
>   (with-handlers ((exn:fail:network?
>                    (lambda (exn)
>                      (log-warning (string-append "network error: "
>                                                  (exn-message exn)))
>                      (let ((sleep-seconds (+ 3 (random 3))))
>                        (log-debug
>                         (format
>                          "sleeping for ~A seconds before reconnect 
> attempt"
>                          sleep-seconds))
>                        (sleep sleep-seconds)))))
>    (let-values (((inport outport)
>                  (tcp-connect somehostname somehostport)))
>      (let loop-while-connected ()
>        ....
>        (read-something inport)
>        ....
>        (write-something outport)
>        ...
>        (loop-while-connected))))
>   (loop-connect))
>
> If you're hardcore: might want to also catch "exn:fail:network" within the 
> "letrec-values", too, and try to close ports (perhaps in a different 
> thread to close, or start a new thread for the new connection?) if not 
> already closed, and then perhaps re-raise the exception so the handler 
> above (within "loop-connect") attempts a new connection.  As you know, but 
> I'll mention it for the record: trying to close the ports might be a good 
> idea, especially if your TCP stack is not great about timing them out, but 
> if you have particular failure scenarios in an embedded system or similar, 
> might help to test the scenarios and look at the IP traffic and how many 
> times you fail to reconnect.  You might also want to increase your retry 
> delay on failures, in case you can flood the network or exhaust open ports 
> on client or server.  Say, each failure increments a counter by 2, and a 
> successful read-write loop decrements the counter if greater than 1, then 
> you use that number as the exponent in delay, or similar.  (This 
> complexity is not specific to Racket; the Racket-specific part is that you 
> can catch a network error with "with-handlers".)  BTW, if you have nothing 
> else running on this processor, you might as well get in a GC cycle or two 
> before sleeping.  There are probably more details that could be covered if 
> needed for this app, but one would have to work through it more closely.
>
> Side comment on the internal-"define" controversy: I left the named-"let"s 
> without arguments in this example, but one might find arguments to put in 
> them by the time the program is finished and robust, such as for retry 
> delay info, buffer management info, etc.  Converting the named-"let"s to 
> internal-"define", if the reader is so inclined, is left as an exercise 
> for the reader.  But perhaps internal-"define" people can see some of the 
> appeal of named-"let" in this case.
>
> Neil V.
> 


Posted on the users mailing list.