1. 45
  1.  

  2. 21

    Why did I know the article will mention Nagle before opening it? It is interesting that we keep running into this little delicacy in every framework in every stack ever.

    1. 8
    2. 10

      I just read https://rachelbythebay.com/w/2020/10/14/lag/ the other day, and now this. Small world I suppose.

      1. 11

        Obviously, Rust doesn’t catch all the bugs, this one have slipped through the compiler 😈.

        I believe this myth that rust makes things less buggy is pretty counter-productive. Empirical evidence suggests that this is probably not true for CLI applications. After working for years in both, Rust will often be buggier than comparable Go because the author of the Rust program will have had to spend more time fighting the compiler, controlling for similar number of years of experience with either language. I’ve found async Rust libraries to be some of the buggiest software I’ve ever used. It’s not mysterious - async adds even more bug classes, things that become compilation errors, compilation latency, and often under-documented frameworks that would not have been an issue if the author chose to avoid async. There is simply far less time and motivation in the budget to actually address the complexity of the domain.

        “almost all (92%) of the catastrophic system failures are the result of incorrect handling of non-fatal errors explicitly signaled in software.” and yet it’s an extremely common pattern in the Rust community to just convert every error that pops up anywhere in the system into a single global ball-of-mud enum without handling it properly. Just like every other currently popular language, real reliability comes from things like property testing, fuzzing and simulation which is rare enough as it is but in Rust people have even less energy and motivation left over to apply them.

        Of course, Send, Sync and the memory safety (which is why I use Rust at all) really do help you in some situations, especially if you would have been writing in a memory-unsafe language, as you’re usually able to avoid much of that unsafety without significant performance impacts (and often performance improvements due to less defensive copying and better aliasing information in the compiler similar to Fortran). Low-level systems programming becomes a lot more fun for me. High-level service engineering becomes significantly less fun for me in many cases, though, especially if I have to rely on the buggy async ecosystem.

        1. 5

          I’m not here to evangelize Rust, but I take issue with your conclusion even though I agree with most of your points.

          For example, the “empirical evidence” paper you cite only passingly mentions Rust and Go. What they say is that when they tested some Rust ports of popular CLI utils, they found the reliability of the Rust version to be similar to the original C version. I do not find this to be compelling evidence that Rust would not lead to better CLI apps for the following reasons:

          • The original C versions of many of these utilities have existed for decades. That’s plenty of time to shake out bugs, even while adding features. Give me three decades or so and even I could write a C app that doesn’t crash much. So one could read the paper’s claim more charitably as “Some guy ported cat to Rust and it only took him a month to make it work just as well as the original which took N months + years of updates and bugfixes.”
          • If we’re talking about a port of an already-existing app, how do I know that this thing is written in “idiomatic Rust” vs. transliterated from C? Maybe it wasn’t actually a port and was a true re-implementation, but we can’t know that from the paper. Similarly, one might even imagine that a Rust port would choose to copy erroneous behavior of the original so as to be a true drop-in replacement bug-for-bug. I think that’s a silly thing to do, but I could see someone making that point.

          I agree that some of the async libraries are still buggy. I was bitten by a bug in the async database driver I used in a project that wasn’t freeing connections back to its own pool correctly. On the other hand, I’ve also been working with Kotlin a lot lately, and finding bugs and inconsistent behavior in some very popular Java libraries that don’t even have the excuse of “well, this feature has only existed for a year or two” like the Rust guys have. In fact, I just made a post in the Kotlin subreddit about how stupid the Map interface is when it comes to dealing with null.

          Mostly I’m just disappointed in how buggy everything I use seems to be. I’m a jack of all trades at my company and so I see crappy libraries for JavaScript, Kotlin/Java, Swift, PHP, and Rust. My Swift project is probably only the least offensive because I have so few dependencies.

          Anyway, back on topic. I LOVE the article from the sled guy(s) that you linked. I refer to that frequently. Rust has a genuine issue with error handling. Both in the language itself and in the “ecosystem” and “best practices”. That article points out the worst flaw.

          There was a period where I would purposely not write a From implementation for my error types so that I would have to intentionally write foo().mapErr(MyError::FooErr)? instead of just foo()?. It gives you just a tiny pause to think about whether you actually intend to just bubble the error up as-is. I actually think that From for errors is kind of a liability (see the sled errors article). It’s not generally true that every time your function sees a FooError that it should always map to a specific variant of your error type. Your errors should have more context than that. If you got a FooError while trying to write to a file, you need to return a MyError::FileError. If you got a FooError while trying to make a network call, then you need to return a MyError::NetworkIssue. It’s less helpful to the caller of your function to just get MyError::FooError. Mapping TypeA->TypeB without any thought at the call-site seems wrong.

          What Rust is also sorely missing is the ability to write an Error type per function without making us pull our hair out. People think defining errors in Rust is tedious already, and they mostly just define one monster error type like you’re describing. But, really, that big monster Error enum is not useful if each function can only fail in a small subset of the ways described by that enum. People go through all this work of writing strictly typed code only to use these giant catch-all error types. Bleh.

          In any case, anecdotally, I’d say that code I write in Rust is definitely less buggy and more stable than code I write in other languages. It also takes longer to get out the door.

          At the end of your comment you wrote: “Of course, Send, Sync and the memory safety (which is why I use Rust at all) really do help you in some situations”. I’d say that’s consistent with the claim that writing code in Rust leads to less buggy programs. Not just in “unsafe” languages, either. Java is memory-safe, of course. But writing concurrent code is still a bug-prone nightmare.

          1. 3

            In fact, I just made a post in the Kotlin subreddit about how stupid the Map interface is when it comes to dealing with null.

            I hold my breath waiting to see whether hey manage to discover an Option type…

            1. 2

              A couple of the suggestions in the comments did suggest basically using my own Option type.

              What’s funny is that most Kotlin users (on Reddit) tend to shit on Option when it’s brought up. But Option can be nested, so it wins, IMO.

              1. 2

                Yeah, saw it. :-)

                The sad thing is, that if Optional was implemented correctly in Java, none of the problems now would have existed in the first place: They could have extended Map with a method that returned Optional and be done with it.

            2. 3

              Anyway, back on topic. I LOVE the article from the sled guy(s) that you linked. I refer to that frequently. Rust has a genuine issue with error handling. Both in the language itself and in the “ecosystem” and “best practices”. That article points out the worst flaw.

              Just a small thing, but @spacejam is the sled guy(s) 🙂

              1. 1

                lol. Fair enough!

              2. 2

                Thanks for your response here. I wanted to respond in a similar way, but you definitely worded everything better than I could have!

                I think the fascinating thing about errors in Rust is that I have generally found Rust libraries to have good errors while applications tend to have bad errors (one catch-all, regardless of size). It is really unfortunate because I think the error handling in Rust is fantastic if you’re careful about things. But the usage of crates like failure and anyhow I think make it far too easy to be really sloppy. Sometimes this is useful when you want to move quickly, but otherwise it is detrimental to code quality. I think crates like those should only be used for a very thin “binary” layer on top of libraries with clean errors.

                At my current company we use anyhow, and basically everything returns an anyhow error (even our sub-libraries). This makes it really annoying to handle errors one function up, and usually everyone just has workarounds such that the Error in the Result is something that always bubbles up, and the Ok contains handle-able errors. In my opinion this is ridiculous.

                In any case, I think error handling in Rust can be good, but it does require more boiler-plate and being careful. I would argue that poor error handling in Rust is about the same as exceptions in other languages but with more boiler-plate, so its not like it is that much worse.

                1. 2

                  I agree with your conclusion that libraries like anyhow should only be used at the very top layer of an application. If your app has any complexity at all, it should have parts of it that act like a library and parts that are the “controller” (as in MVC). The “library” part should still have good errors just in case you need to build more on top later, or refactor, or whatever.

                  But shuttling handleable errors through the Ok… that’s a facepalm. It sounds to me like you guys would be better off just panicking and having a top-level panic catcher. Isn’t that basically what you’re doing now, just way more tediously?

                  1. 1

                    But shuttling handleable errors through the Ok… that’s a facepalm. It sounds to me like you guys would be better off just panicking and having a top-level panic catcher. Isn’t that basically what you’re doing now, just way more tediously?

                    Yes, its a disaster 🙃. Unfortunately I only recently joined, and its going to be tedious to undo (as well as tedious to convince everyone that this is bad and worth fixing). So we’ll see what happens, I pick my battles carefully these days and some things are just not worth it.

                    But, regardless of my current situation, I do still believe error handling can be good in Rust it just looks different from exceptions in other languages and requires a lot more care.

                    1. 2

                      Agreed. And it’s a shame that it does require so much more care. It’s interesting to compare and contrast Rust’s error handling with Java’s checked exceptions.

                      On the one hand, Java’s CEs don’t tell you exactly which line in a function can fail, and they can’t be generic in the function signature, so you can’t pass a lambda that throws FooExcpetion | BarException.

                      On the other hand, it’s MUCH cleaner in Java to have each function signature clearly indicate exactly what exceptions it can throw. And it takes ZERO boilerplate. You already had all the Exceptions you defined for your domain, now you just say that function foo throws FooException and function bar throw BarException and both of them might be a subtype of MyLibraryException.

                      1. 2

                        I haven’t worked with Java checked exceptions, but I do remember reading something about them being controversial. I think Rust having anonymous enums would potentially help with errors, and I believe be similar to Java CEs, but arguably they could also cause more pain than they are worth.

                        I think the reality is that no language has figured out a truly good way to work with errors. All of them have their flaws, which is really unfortunate because it seems like such an easy problem. I guess at least with Rust there is so much churn on error handling crates we might get that perfect solution one day.

                        1. 2

                          Java’s checked exceptions are controversial. Although, I think at this point, it’s less controversial and more that people have just decided they’re bad.

                          I don’t agree with that. I think they have strong redeeming qualities. I also think that they are similar in spirit to returning Result types.

                          Ad-hoc enums would be really cool for Rust, but runs into obvious problems very quickly. How do you write a From implementation for them? How do you implement non-trivial Error and Display traits for them?

                          Honestly, if std::error::Error didn’t require Display, it would take away a LARGE chunk of tedium in defining them. All you’d have to do is derive Debug and optionally implement Error::source if you had source error.

                          1. 3

                            Hm, uncanny timing that this new error handling crate [1] popped up on /r/rust! Might be interesting to play around with to get something like Java CEs.

                            [1] https://jam1.re/blog/anonymous-sum-types-for-rust-errors

              3. 3

                anywhere in the system into a single global ball-of-mud enum without handling it properly.

                Fwiw our company used to use Scala heavily, and we used a very similar pattern. We would create a global error type, wrap up all of our errors in this type, and return them in cats Either types (well EitherT). What would actually happen is we would never actually handle the error properly and just bubble everything up to 500s with minimal logging and no stack trace. As a result, we would have no idea why or where errors would occur. I see this same pattern happening with anyhow in Rust, which is why I’m puzzled over the enthusiasm everyone has for Rust’s error handling.

                I’m also not a fan of the advice that libraries should use error types and applications should use something like anyhow. I think upper level code, like top level code in an interactive application, or controllers for web applications, should use anyhow while sub crates and all other functions should return specific error types.

                1. 1

                  Yep. That’s exactly my experience as well. In JVM-land, though, you can get a stacktrace if your error types extend Exception, because Exceptions collect a stack trace on construction. So you could’ve at least logged that.

                  What I typically do is an error type per “top level” package. Then you at least have <10 variants to deal with for any given method call. There’s a much higher chance of actually looking through the <10 failures and saying “Ah. I can do something about that one.” Or “I need to convert this failure mode into a 404 instead of a 500”.

                  It’s still clumsy and tedious and leads to more 500s than it should (until you hunt them down and fix them).

                  I 100% agree with your advice to not “use anyhow for applications”. I read some advice a long time ago that suggested that ALL Rust projects should have a lib.rs, including applications. Most of your logic goes into lib.rs, and if you are writing an application, then your bin.rs has the top level interactive layer. I think this works well with your advice. You’d only use anyhow in the bin.rs part of your code.

                  1. 2

                    Yep. That’s exactly my experience as well. In JVM-land, though, you can get a stacktrace if your error types extend Exception, because Exceptions collect a stack trace on construction. So you could’ve at least logged that.

                    Yeah that awareness came a bit later for us, but this was one of the thousand papercuts that eventually soured us to operating Scala services.

                    Or “I need to convert this failure mode into a 404 instead of a 500”

                    Haha I remember this pain well.

                2. 1

                  After working for years in both, Rust will often be buggier than comparable Go because the author of the Rust program will have had to spend more time fighting the compiler, controlling for similar number of years of experience with either language.

                  Is that a fair comparison? It seems like C++ would be a bit more comparable in terms of language complexity. As a someone on the sidelines, it seems that there are far more levers and knobs to turn in Rust.

                  (Granted this might be a high bar that has been set by Rust’s own adherents.)

                  1. 1

                    Why do they have to be equally complex languages to be compared? He’s comparing two languages that one could use to complete a task. In this case, he’s comparing two languages that one might use to write a CLI app.

                    1. 1

                      If the thesis here:

                      There is simply far less time and motivation in the budget to actually address the complexity of the domain.

                      is accurate then the complexity of the language makes it difficult to compare with a language in a different complexity bucket.

                      1. 2

                        I’m not sure I see why.

                        That statement, if I read it correctly, was a criticism of Rust’s async ecosystem. As a user of the language and its ecosystem, you have to make a choice. From the POV of the app author you could say “I’m going to use the more complex language with broken libraries, or I’m going to use the more simple language with more robust libraries.” The author of the next cool CLI app doesn’t care if it’s “fair” to compare a simple language to a complex language- they just want to use the best tool for the job.

                        It’s like how some people complain when you point out that Java is much faster than Python. “It’s not fair! Python is interpreted and Java is compiled! Apples to oranges!” But it’s NOT apples to oranges. Those are two popular languages for web backends. If I’m going to write a web backend and I want it to be fast, I’ll pick Java. I’m not going to pick Python because it’s “unfair” to say Java is faster.

                3. 3

                  Please don’t turn milliseconds into megaseconds, it hurts my pedantic soul.

                  1. 2

                    Yikes, I missed that!

                    That was grabbed from the <title> of the page, I’ve updated it to reflect the actual post title.

                    I have heard of bugs taking more than a year to appear though (40 megasecond is 460 days)

                    1. 3

                      Yes, certainly some bugs take that long. Uptime wraparound bugs, for a start.

                      I imagine one might also confuse 1000000 with 1048576 and that as a result, some bug appears for 48576 seconds every 460 days, and then disappears again. That would be worth a story.

                      1. 2

                        Thanks.

                    2. 2

                      Via @varjag in IRC.

                      1. 2

                        If a router declares to be able to handle a gigabit connection, it actually means a gigabit if you use full-sized packets, but will get to few megabits if you split the data into tiny packets.

                        Not necessarily. Higher quality routers and switches will also list the packets per second (PPS) they can handle. Top notch hardware can handle line rate even with mostly small packets. Technical specifications for high quality hardware usually document small packet performance.

                        On the other hand, it can get worse than the author describes. Lots of networking equipment can’t even achieve its specified data rates without fully loaded jumbo frames.