-
Notifications
You must be signed in to change notification settings - Fork 10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Use handler-bind instead of handler-case #8
Comments
attach-errback now returns a promise, finished with the return value of the errback. introducing `promisify` which turns any val (or multiple values) into a promise object, catching errors as it goes along: (promisify (values 1 2 3)) => promise with values (1 2 3) (promisify (error "oh crap")) => promise with errored T improving error chaining. previously, errors were not propagating like values were. this has been updated so that an errback at the bottom (or top in lisp) of the chain can catch errors for the entire chain. introducing `catcher` macro, which essentially replaces promise-handler-case. instead of wrapping lexical forms, we now rely on error chaining. this is nicer because we dont just need access to the lexical form, any other form that returns a promise can be caaught. introducing `finally` macro, which runs its body form whether the given promise is finished or errored, allowing for easy cleanup in a chain no matter the outcome. introducing `with-promise` macro. separates promise creation and resolving from consumption: (with-promise (resolve reject) (as:with-delay (1) (resolve 123))) this is important not just because of separation of creation and consumption, but because with-promise catches errors in the creation process and signals them on the returned promise. adding utility functions: - amap - all - areduce adding chaining helper: (chain 4 (:then (x) (+ 4 x)) (:then (x) (format t "x is ~a~%" x))) yields form: (attach (promisify (attach (promisify 4) (lambda (x) (+ 4 x)))) (lambda (x) (format t "x is ~a~%" x))) (chain 4 (:then (x) (+ x 9)) (:then (x) (list x x x)) (:map (x) (+ x 5)) (:reduce (acc x 0) (+ acc x)) (:then (final) (format t "value is ~a~%" final)) (:catch (e) (format t "error! ~a~%" e))) prints: value is 54 (chain 4 (:then (x) (* x 13)) (:then (y) (list y (1+ y) (+ 5 y))) (:map (x) (+ x 'tst)) (:then (x) (format t "x is ~a~%" x)) (:catch (e) (format t "error! ~a~%" e)) (:finally () (format t "done~%"))) prints: error! The value TST is not of the expected type NUMBER. done introducing the concept of naming promises, although this will probably go away and be replaced with something a bit more helpful.
Thanks @orthecreedence, it'd be really good as this is the main blocker for me when considering your libraries, which otherwise appear to be excellent. |
So I'm trying to wrap my head around handler-bind. It either needs to handle a restart or do a local exit from the handler in order for an error to be "handled." I am new to this construct and before jumping in and implementing something that will probably end up looking a lot like handler-case, I'm wondering what you see the handler-bind forms looking like. Is your primary goal to run restarts? If so, does it make sense to do something like: (block 'blackbird-handler
(handler-bind ((condition (lambda (e) ...your handler/restart code here...))
;; no restarts were run, signal the error
(condition (lambda (e) (signal-error promise e) (return-from blackbird-handler)))
...do stuff...)) Any input you give would be very useful! |
@orthecreedence here's what I know in the form of answers to some passages of what you wrote:
My main goal is to be able to run restarts, yes. If the conditions are errors, the default behaviour, after your lisp implementation sees them at the very top level, is to run the debugger stored in In my program logic, I might also want to run restarts automatically, by wrapping my own But perhaps the most important thing, and from reading your replies (and your code) you might be missing it, is that if you don't resignal I can do a third thing. I can see the restarts established by So what does this rant mean (assuming it means something)? That while choose to do the javascript or ruby-like thing of letting users of your library pass closures as args named When is it a bad thing? In production code of course, where do want the error to be demoted to a warning for example. That's why hunchentoot and some other libraries have those This is all to say that no, I don't think it makes sense to do something as you did there. I think you have to redesign your API's slightly to say instead of:
say
Maybe you can do something that makes the two cases more or less compatible. |
I think I am starting to understand. So cl-async will still take As far as blackbird goes, any Thank you for bearing with me! |
@orthecreedence, yes I think you're on the right track. I planned on expanding my previous post with some examples, but xmas got in the way :-D The
I still wanted to understand exactly why But even then there should be zero resignalling, so that the handler runs as deep in the stack as the condition originated. And if you implement But all this talk is worthless without some actual code. Tell me how and where you planning to make the first changes, then I can have a look. In the meantime I will also have a look at the cl-async and blackbird code... |
@orthecreedence, actually maybe scratch that plan. I gotta try
|
i am starting to see your point with the event-cb stuff. and i also understand christmas timing, i barely get a few minutes here and there to reply =). i think one case where traditional error handling fails is the fact the stack is unwound after each cl-async action is triggered. so within the event loop i may open a socket and want to be able to see events on that particular socket and act on them in the context of that code itself. you won't be able to do this without having your handler-bind code outside of (wrapping) the event loop because the stack unwinds and removes your handlers otherwise. this is why i favor being able to do some callback-based "event" handling, as opposed to having everything handled in a set of handlers outside the event loop. although you're correct that non-cl-async conditions should not be getting caught and passed to any event-cb. also, once again, i may be missing something obvious here so feel free to correct me. |
I believe this is fixed in c4ea241. There's now an exported symbol, I am very open to any changes if you feel anything was implemented incorrectly here. Also, I'm pushing out some changes to cl-async soon that include the changes we discussed above (this may be a day or two later as I review things, I'm a bit more cautious with the codebase because more people rely on it). |
this can be considered a breaking change, per issue #108 as well as discussions at orthecreedence/blackbird#8. the idea being that cl-async no longer catches all errors and routes them to the event-cb callbacks, but instead now looks for event-info conditions (and all derivatives) and sends *only those* to the event-cb, leaving all other errors to bubble to the top (debugger). note that errors only bubble to the top if :catch-app-errors is nil! if t, *all* errors are still caught, with one important change: if :caught-errors is given (a function of one argument) when starting the event loop, then all caught errors are sent to this function to do what you will with them. in other words, event-cb is no longer the dumping ground for all conditions when catching errors, now there is a separate place. note that :caught-errors is only used when :catch-app-errors is T.
Ok, please see orthecreedence/cl-async#108. I decided to keep event-cb because it is used to catch events from libuv on a per-connection basis, but other than the conditions created by libuv itself, all conditions are now allowed to bubble up without being touched or passed into event-cb (and enter the debugger) if There are more details in the cl-async issue. Please give it a shot, and let me know if you need changes! |
I tried loading the very bleeding edge of cl-async and its dependencies and after some effort, I managed to do it. I was trying to run this very basic example
and make variations on it so I could check if error handling is now cool and if I can make my But I crashed into this, and didn't debug any further, sorry. Perhaps you can help me?
|
Seems like an outdated version of libuv. Can you confirm you have libuv >= 1.0.0? |
No I can't, because I don't :-). Homebrew on Mac OSX gives me libuv 0.10.21. Building from head right now, will be back with some more info soon. Shouldn't cffi and such warn early when compiling bindings? |
After much effort (had to trash quicklisp for some reason), my little example now yields another error. Using a pristine quicklisp, sbcl 1.2.2, master cl-async, cl-libuv and fast-io
But we already have an illustration of the problems that I think still exist with cl-async. (with-event-loop (:catch-app-errors nil)
(tcp-connect "www.google.com" 80
(lambda (socket data)
(format t "Done processing!~%")
(close-socket socket))
(lambda (error) (error error))
:data (format nil "GET /~c~c" #\return #\newline))) BIG UPDATE. I didn't read the doc or you didn't write it yet, but I've noticed that |
You're right, that memory alignment error should not be getting passed to the event-cb. That is a special case I forgot about when doing the conversion. I'll review each of the subsystems' C callbacks today and make sure that non-libuv conditions get thrown instead of passed to event-cb. |
Oh also, I'll spin up my mac (or bsd, similar) VM today and try to track down that actual DNS error. I might just cave in the next few days and rebuild cl-libuv via cffi-groveler so this type of junk doesn't happen anymore. |
I fixed that DNS throwing issue (the error is now thrown) and checked all the other systems to make sure conditions are being treated properly. Here's the breakdown: any libuv status that comes through a libuv callback that is not a "success" will have an event condition created for it and be passed to the attached event-cb. Any libuv status that comes back from calling a libuv function directly (such as writing to a socket) that is not a "success" will have an error thrown for it. If an error is not thrown, then it's a bug in cl-async. Any errors that occur in non-cl-async code (but were spawned via cl-async) are not touched by cl-async at all, unless In other words, the only thing event-cb is used for now is things like "socket timed out" or "dns not found" etc. Operational errors ("writing to socket returned an error" et al) throw errors. All thrown errors are untouched. However, I still have yet to track down the root cause of the DNS error you're seeing. I tried |
This part I almost understand, untill the part where you talk about "passing it to the attached event-cb". Why not let it bubble up to top level? In other words, why not make it behave like the next category of unexpected behaviour, which I'll comment on shortly?
This is quite OK. But is the caught by some (in-package :capitaomorte)
(cl-async:some-good-function #'process-success
:error-handling #'log-error)) , or by wrapping my own handler-case around some (in-package :capitaomorte)
(handler-case
(cl-async:some-good-function #'process-success)
(cl-async:fiasco (f) (log-error f)) Or even better, if (in-package :capitaomorte)
(handler-bind
((cl-async:fiasco (lambda (f)
(when (find-restart 'cl-async:log-it-and-continue)
(invoke-restart 'cl-async:log-it-and-continue))
;; must be some more serious fiasco
;; that doesn't provide this restart so
;; let it bubble up to someone that knows how
;; to handle it
))
;; In this scenario, I imagine that this one may be thrown
;; by PROCESS-SUCCESS. In the others I would need
;; similar
(capitaomorte:usual-f*ckup
(lambda (f)
(if (capitaomorte:production-mode-p)
(invoke-restart 'capitaomorte:muffle-it)))))
(cl-async:some-good-function #'process-success))
I can help track this down too and give my socket-fu a polish. |
My reasoning for this is to be able to handle events differently on a per-object basis. For instance (as:with-event-loop ()
(as:tcp-connect
"www.google.com" 80
(lambda (sock data)
(my-app:do-stuff data))
(lambda (ev)
(when (typep ev 'as:socket-eof)
(my-app:we-got-booted (as:tcp-socket ev)))))) I like this because a socket hanging up isn't worth exiting the event loop, IMO. In order to achieve the same thing with (handler-bind
((as:socket-eof
(lambda (ev)
(my-app:we-got-booted (as:tcp-socket ev))
(let ((restart (find-restart 'cl-async:continue-event-loop ev)))
(when restart (invoke-restart restart))))))
(as:with-event-loop ()
(as:tcp-connect
"www.google.com" 80
(lambda (sock data)
(my-app:do-stuff data))))) This seems like a lot of boilerplate and mental overhead to handle something as simple as a socket EOFing, which is fairly commonplace. And consider this scenario: (as:with-event-loop ()
;; analytics are nice but not critical
(as:tcp-connect
"my-analytics.com" 80
(lambda (sock data) ...)
(lambda (e)
(when (typep e 'as:socket-refused)
(format t "call to analytics server failed, no big deal ~a~%" e))))
;; we need this
(as:tcp-connect
"launch-the-missle.net" 80
(lambda (sock data) ...)
(lambda (e)
(when (typep e 'as:socket-refused)
(error "Nuke launch failed!"))))) Would this even have a direct translation in a On top of this, from my understanding (helped quite a bit by talking to you) one of the main benefits for letting errors bubble up is to a) get backtraces and b) drop into the debugger. A backtrace here is useless because it's only a few levels deep under In other words, I'm just not sure it would be useful to completely abolish event-cb in cases where libuv is actually handing us events having to do with the corresponding objects we're operating on. And in doing so, it may create a lot more overhead by forcing all error handling to the outside of the event loop (we can't use handler-case or handler-bind around async operations so we need to put them outside the event loop) as well as making it a challenge to identify which errors are occuring on which objects.
Yes, this would be the calling code's responsibility. All errors thrown should have the base (handler-case
(as:with-event-loop ()
(my-app:do-things-that-might-throw-errors ...))
(as:event-error (e)
(my-app:log-error e))) The one exception I know of is the DNS error you found, which is actually more of a panic than anything else, and is thrown with the type As mentioned over in orthecreedence/cl-async#108, In fact, I'm starting to wonder if (handler-bind
((error (lambda (c)
(let ((res (find-restart 'cl-async-util:continue-event-loop c)))
(when res (invoke-restart res))))))
(as:with-event-loop (:catch-app-errors nil
:caught-errors
(lambda (e)
(format t "- caught: ~a~%" e)))
(as:with-delay ()
(format t "x is ~a~%" (+ 4 'five)))))
|
The "boilerplate" argument doesn't hold much for me in common lisp, because macros. Also note that I'm not saying that that parameter should be extinct, only optional.
I see your point, indeed. The :event-cb is useful there indeed, and now I understand the intrinsic need for "attaching" handlers. But why not let it have Tangentially, I personally also hate the fact that I have to use (defmethod my-app:handle-event ((event as:socket-refused))
(if (string= (url-of event)) "analytics") ... ...)) And then pass Actually
There is also the benefit that any restarts either
Yes, this argument is made. Don't abolish it. Optionally give it
I assume you meant "all errors thrown by
I guess. But if someday you feel that
Yes exactly. But I don't find it horrible, keep it. It's nice to pass it the value returned by some function Or, I'd make it a generalized boolean and get rid of This very same behaviour would go for the fourth argument of functions such as So to summarize, here's what I'd like to see:
How backward incompatible is all this? The new Sorry for the long-windedness and What do you think? |
Sorry in advance for the great wall of text.
True =].
What I'm saying is that any event condition that is even passed to event-cb originates from a cl-async callback. What this means is that any handler-bind that catches it must be outside the event loop. Keep in mind, that as of the last few commits, cl-async does not catch errors inside of user-supplied callbacks by default, they are allowed to bubble up. event-cb is used to notify the user only in cases where libuv gives any status other than success on a specific operation. For instance: (as:with-event-loop ()
(as:with-delay ()
(+ 4 'five))) This triggers an error that passes all the way through cl-async to the top level/debugger, with full stack/restarts. In previous versions, the above error would be caught and rethrown inside of cl-async, erasing any stack or restarts. That said, here's my understanding of what you want: ;; cl-async's event-handling code
(catch 'event-handling-done
(handler-bind
((error (lambda (e) (funcall event-cb e))))
(error (make-instance 'tcp-eof :socket the-socket))))
;; event-cb
(lambda (e)
(when (some-logic-here)
(throw 'event-handling-done))) Here's how it currently works (and the event-cb that would essentially do the same thing) ;; cl-async's event-handling code
(funcall event-cb (make-instance 'tcp-eof :socket the-socket))
;; event-cb
(lambda (e)
(unless (some-logic-here)
(error e))) I'm really not opposed to implementing the event handling from the first example and by default
True, but calling
What I meant was I'd be inclined to change the actual I hope that my days of catching and rethrowing errors are over =].
Will do!
I really like the idea of being able to call
cl-async handles all these conditions internally. In other words, when it calls event-cb with a Actually though, now that I think about it, providing a generic function that gets called when I do like that idea. So, for your final suggestions:
As much as it pains me to change the
I still think this may need a bit more discussion. If the event handling code examples I posted above are indeed what you envision, then I'm fine with implementing this, but am still curious as to what use it would be.
I like it.
Actually my point (and I hope I made this clearer in the code examples above) was that event-cb doesn't catch user-generated errors at all anymore, it now only receives cl-async event conditions. One thing I've been kicking around is a
Do you mean |
I will read the rest of you reply later, but right now, I just wanted to clarify this:
I just wanted to clarify that I meant the error handlers supplied by the user should have "handler-bind semantics". I.e. when you detect that a user passed one, just |
I think i get it: ;; current
(tcp-connect "somewhere.com" 80 'my-read-cb :event-cb 'do-something-with-error)
;; what I understand you are saying
(handler-bind
((error 'do-something-with-error))
(tcp-connect "somewhere.com" 80 'my-read-cb)) ;; no :event-cb, handler is in handler-bind Is that what you mean? |
@orthecreedence, no I don't think so. I'm saying something much simpler I think. I'm saying keep supporting Without
With
|
But your previous example is also correct. I also mean that. |
Here's a quick example (using the latest version of cl-async): (handler-bind
((error
(lambda (e)
(let ((res (find-restart 'cl-async-util:continue-event-loop e)))
(when res (invoke-restart res))))))
(as:with-event-loop (:catch-app-errors nil
:send-errors-to-eventcb t)
(as:delay
(lambda () (format t "result: ~a~" (+ 4 'five)))
:event-cb
(lambda (e) (format t "got err: ~a~%" e))))) I think what you're saying is you want something like this: (as:with-event-loop (:catch-app-errors nil
:send-errors-to-eventcb: t)
(as:delay
(lambda () (format t "result: ~a~%" (+ 4 'five)))
:event-cb
(lambda (e)
(format t "got err: ~a~%" e)
(throw 'cl-async-util:event-handling-done)))) The current version lets the error bubble to outside the loop, where the I'm fairly certain this can be done in a backwards compatible manner with (yet another) switch somewhere (such as |
doesn't unwind the stack when handling errors
The text was updated successfully, but these errors were encountered: