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" <[email protected]>
To: "Rüdiger Asche" <[email protected]>
Cc: <[email protected]>
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

Reply via email to