[Cuis-dev] Exception handler blocks with non-local returns

Phil B pbpublist at gmail.com
Tue Nov 5 12:00:50 PST 2019


(trying again... hit the wrong key)

On Tue, Nov 5, 2019 at 1:15 AM Andres Valloud via Cuis-dev <
cuis-dev at lists.cuis.st> wrote:

> I'm more familiar with how a message such as #receive would be
> implemented in some socket class, than how applications tend to write
> code that uses that functionality.  So, suppose you had a method like this:
> doTheHttpThing
>         | data |
>         self prepareToDoTheHttpThing.
>         data := [self socket receive] on: SocketError do: [:ex | ^nil].
>         data preprocess filter blah blah.
>         ^data
Many times, you're not dealing with a single http request, but a series of
interrelated requests that represent a conversation.  This is somewhat
subjective in terms of how one answers the question 'when does it make
sense to break this out into it's own method?' but I might end up with
something like:

    | intermediateData almostDoneData encounteredBlah |
    self prepareToDoTheHttpThing.
    encounteredBlah := false.
    intermediateData _ [
    self httpGet: primaryUrl ]
        on: SomeAllowableError
        do: [ :ex |
            ex isExpected
                ifTrue: [
                    encounteredBlah := true.
                    self httpGet: alternateUrl ]
                ifFalse: [
                    "Something is probably wrong on the server, ignore it
and move on"
                    ^ nil ] ].
    almostDoneData := intermediateData preprocess filter blah blah
extractDataWeReallyCareAbout collect: [ :eaElement |
        [ self httpGet: (self makeSecondaryUrlWith: eaElement) ]
            on: SomeOtherAllowableError
            do: [ :ex |
                ex meh
                    ifTrue: [
                        "Yeah, this happens from time to time.  Maybe we
should handle it better, maybe not.  For now, whatever, the data is
*probably* mostly good..."
                        someBadResultToken ]
                    ifFalse: [
                             ifTrue: [ "OK, this is related to issue #1234
and we got  garbage data back.  Can't do anything but file a bug report to
the service and hope they fix it... so far they haven't.  Dump the data and
move on..."
                                 self processingLog: 'Note #1234'.
                                 ^ nil ]
                             ifFalse: [ "This should not have happened..."
                                 ex pass ]]]].
    almostDoneData ... <and so on>

This may go on a couple levels deeper, and may be wrapped by session
management etc, but you get the idea: a sequence of related requests that
form a one-off conversation.

So now the problem that the on:do: is dealing with is what happens when
> the socket disconnects from under the client code.  Then #receive fails,
> there is no recourse, and the answer should be nil.  One could do the
> wrap around like this:
>         data := [self socket receive] on: SocketError do: [:ex | nil].
>         data isNil ifTrue: [^nil].
> and I'd be tempted to write it that way now because who knows that ^nil
> does today.  But suppose that no, that one would rather not have extra
> statements.  Then one might want something like [:ex | ex methodReturn:
> nil] instead.
> In this case, why couldn't that be written like this?
>         | data |
>         ^[
>                 self prepareToDoTheHttpThing.
>                 data := self socket receive.
>                 data preprocess filter blah blah.
>                 data
>         ] on: SocketError do: [:ex | nil]

Sure, I can't think of a case where you *must* use a non-local return in an
exception handler.  It's an intent/stylistic/convenience thing.  It's
similar to how it used to be important to some to optimize functions for
tail call optimization.  Potentially a big performance win... but
(re-)structuring your code for it could be a pain.

> Now there's no non-local return as far as exceptions are concerned, and
> by the way the code is faster in all cases because non-local return is
> expensive.

That would be premature optimization.  When you're dealing with network
requests (which often take on the order of tens to hundreds of
milliseconds) and putting together/processing the results (which can take
even longer), the cost of non-local returns is not even rounding error.
But your larger point is taken and ties in to something else below...

> Once I wrote a multi-process web spider, and I remember writing the code
> so that the actual networking interaction happened in a very small place
> that could be controlled easily with patterns such as the above.  Is the
> problem that application code doesn't always factor that nicely to allow
> that implementation strategy?  I do not have enough of a sample to tell.

The short answer is yes.  The main difference is that handling an isolated
http request is a nicely compartmentalized, widely used, bit of
functionality.  Application logic tends not to be... and there's a lot of
it winding down a bunch of essential, but rarely followed, paths.  That
code often ends up utilizing #on:do: in very situation-specific ways.

> > For example, I have a periodic task which involves ~10k http requests.
> > A couple hundred of those will fail each run for various reasons:
> > network errors, server down, server errors, page errors etc.  There are
> > exception handlers at different levels of connection/request handling
> > that decide if the error is something that needs to be specifically
> > handled / retried or if terminating the request (or perhaps it was
> > terminated on us) and returning some specific value to the sender is the
> > appropriate result.  These are almost always non-local returns since
> > we're failing right in the middle of processing a request which is no
> > longer valid.
> I suspect this is one of those cases where the code just doesn't factor
> nicely, right?

Right, or it might not be worth factoring.

> > 3) FFI code and dealing with the 'outside world' more generally.
> > Similar situation as 2: something fails either in an expected place or
> > in an expected way.  The current method can't continue but the sender
> > can.  Examples: a non-critical log file or database can't be found or
> > has a problem... (maybe) log something to transcript and return from the
> > method in question.  A backup file can't be copied to a secondary backup
> > location... we probably aren't connected to the network so ignore it and
> > try again the next time we get called.  I have several of these: fatal
> > to the current method but the sender doesn't care.
> >
> > The thing all of these examples have in common is that they are all
> > essentially resumable, fatal exceptions... that need to resume somewhere
> > else.
> Maybe more like critical exceptions that are not fatal simply because of
> the context in which they occur.

Well usually it's critical and fatal to the code that raised it (i.e. since
if failed back to us) and the decision I see the non-local return as making
and signaling is 'yes, it is fatal to us as well... but the sender should
be able to proceed.'  To me that's an important bit: signaling to the
exception framework how the exception was resolved.

Related to your earlier point about non-local returns being expensive: it
is entirely possible that for some applications, they impact performance
significantly.  In those cases, I could see being able to instrument the
exception framework to provide statistics about how exceptions are resolved
could be useful and even help identify where you should focus your
optimization efforts.

> You know, it just occurred to me that having such exceptions implement a
> #defaultAction method isn't really useful, either.  Suppose you could
> arrange the code so that the non-fatal yet critical exceptions were
> modeled by a particular class.  Instead of a million exception handlers
> everywhere doing something like
>         [...] on: NonFatalIOError do: [:ex | ex methodReturn: nil]
> one would like #defaultAction to do that.  However, #defaultAction
> cannot do that now because a non-local return in the #defaultAction
> method of the exception does not do what is required.
> If exceptions implemented #methodReturn: instead, then it might be
> possible for #defaultAction to be written like this:
> NonFatalIOError>>defaultAction
>         self methodReturn: nil
> This is not perfect yet, and one still needs to mark the return point
> with on:do:, but maybe there's a way to write even less code here.

Interesting idea.  We'd have to play around with an implementation in the
wild to get a feel for how that would actually play out.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.cuis.st/mailman/archives/cuis-dev/attachments/20191105/26071952/attachment.htm>

More information about the Cuis-dev mailing list