5 ms·
I don't like the "central exception handler" idea -- it is invariably too far away from the error site to do anything intelligent. I do like Erlang's approach
by atombender 9y ago
I don't like the "central exception handler" idea -- it is invariably too far away from the error site to do anything intelligent.
I do like Erlang's approach with hierarchical error domains ("processes" and "supervisor trees"), and I honestly think Go made a mistake here: The semi-recent introduction of Context acknowledges that a control hierarchy is present and an important part of a concurrent, distributed application and that there are aspects of this that need to flow through everything. The Context exists to permit a caller to call functions and launch goroutines that inherit deadlines, the ability to cancel their work, and so on. But this is a rather blunt and intrusive tool that could have been better solved by baking termination into the goroutine scheduling runtime. Of course, one reason goroutines cannot be forcibly terminated is because everything is mutable all over the place, but you could still have termination induce system-level errors that the goroutine would be forced to handle (or maybe you could safely induce a panic)... but that ship seems to have sailed a long time ago.
And yet a hierarchical approach similar to supervisor trees would have been awesome to have in Go. Managing hordes of complex, "crash only" goroutines in Go gets gnarly fast, and you tend to end up with your own ad-hoc type of supervisor trees that uses waitgroups and maps and what not to corral trees of composed workers to ensure they are all able to cancel, retry + back off, and time out appropriately. Even something like retry with backoff isn't part of the standard library, even though it's extremely core to distributed, concurrent programming.
I think I agree with Go's authors that exceptions are bad, but exceptions in an explicitly hierarchical, supervisor-type process model, where processes are "crash only", actually make sense. It means you tend to design your system into discrete parts consisting of dumb workers (that give up and crash on the smallest error) and slightly less dumb supervisors (that react to said crashes). It's not a panacea, but it's better than the loose, somewhat impoverished set of tools that Go gives you.
- KallDrexx 9y agoI don't think a central exception handler means ONLY handle exception in one very specific point. It means that if you can handle an error handle it, otherwise let the central exception handler handle it. I really like Elixir and the Erlang VM but supervisor trees are not a perfect solution either. For all the talk about how in Erlang you just let things fail and don't have to do defensive coding, any non-trivial application requires a lot of very thorough design on how to handle error scenarios and keep your application consistent. If a process fails too many times too often (say your web service it's trying to ping is inaccessible) it can take out your whole supervisor. If that supervisor is holding an ETS table that holds important coordination data (but not data that requires persistence) that ETS table (and all it's data) is now completely gone, and unless that case has been handled you have a lot of processes running but not doing what they should be doing, and your application is inconsistent until you completely shut it down and restart it. And if you don't plan for a lot of these scenarios then you will be caught completely surprised when it happens in production.
- regulation_d 9y agoIf you need an ETS table to have greater resiliency move it up higher in the supervision tree. Nothing prevents you from starting it at the application level. Also you have a lot of control of your supervision tree. You can specify the quantity and frequency of failures that would cause a supervisor to fail. For some use cases, 3 failures in 5 seconds might be reason enough to restart that supervisor. For other use cases, a much higher failure rate might be justified.
- KallDrexx 9y agoRight, so you have to make a lot of decisions on what things can fail, how often they should be allowed to fail, what side effects can be tolerated with a major failure, what happens if a bad process fails when it comes back up (thus risking causing the supervisor itself to cascade restart itself), etc... These decisions are not trivial and are exacerbated because of the Actor style of flow due to the independent pieces. So from my experience you have non-BEAM languages where a single error can take down your whole application if you don't plan your failure scenarios properly, where as in BEAM languages if you don't plan your failure scenarios properly you are more likely to end up with an inconsistent system. The former isn't that bad in a lot of circumstances as you should have an external application monitoring and restarting a critical server that's no longer running (even for BEAM systems) but the latter can be very bad because it can be a silent issue. I have systems working at my job written in .net that have been running for over 6 months without a restart, even when Azure had failures and it could not talk to its command and control server for an hour and kept running perfectly fine with using a well thought out architecture and a well placed central error handler for generic errors without things cascading throughout the whole system. So resiliency has less to do with language or supervisor trees and more to do with making every step has failure scenarios thought about and planned for.
- candiodari 9y agolast I heard the idea of "tombstoning" (guaranteed cancellation) in Go was impossible and given up on.