[racket] tcp exceptions and connection reestablishment...
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