[racket] tcp exceptions and connection reestablishment...

From: Matthew Flatt (mflatt at cs.utah.edu)
Date: Mon Jul 2 14:07:12 EDT 2012

Another approach is to use a custodian, which makes it easy to release
resources without having a binding to each resource:

  ;; try/shutdown : (-> any/c) (-> any/c) -> any/c
  ;;  Calls `thunk', and shuts down any resource created during
  ;; `thunk' on either normal return or on exception; further,
  ;; on an exception, log an error and tail-call `fail-thunk'.
  (define (try/shutdown thunk fail-thunk)
    (define c (make-custodian))
    (with-handlers* ([exn:fail? (lambda (exn)
                                  (log-error (exn-message exn))
                                  (custodian-shutdown-all c)
                                  (fail-thunk))])
      (parameterize ([current-custodian c])
        (begin0
         (thunk)
         (custodian-shutdown-all c)))))

  (define (go)
    (try/shutdown
     ;; try:
     (lambda ()
       (define-values (inport outport) 
         (tcp-connect somehostname somehostport))
       ....
       (read-something inport)
       ....
       (write-something outport))
     ;; on failure, try again:
     (lambda () (go))))

At Mon, 2 Jul 2012 17:50:13 +0200, Rüdiger Asche wrote:
> 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.
> > 
> 
> ____________________
>   Racket Users list:
>   http://lists.racket-lang.org/users


Posted on the users mailing list.