News for hackers. Stuff that matters.
Re:Using unwrap() in Rust is okay by alerighi on Monday August 08, 2022 @02:24PM (#32389099)
> When invariant violations or mistakes by programmers (aka bugs) are detected, the program should halt as it is in an inconsistent state and continuing could be very dangerous (think privacy/security/data corruption). Otherwise, don't halt (handle it or have the caller handle it).

Well it's not always the case. There are situations in which if you detect errors you want the program to continue running, and have only that particular functionality to fail.

I tend to write resilient code, since I work in embedded systems and what you never want is the system to crash. Halting a CPU on an invariant violation (i.e. and assert failing) is something useful for debugging (you trigger the debugger and you then analyze why it happened), but something you generally don't want in production.

Bette to have a ton of checks more and in case of an invariant violation (that maybe is resulting from a programmer mistake, but there is always the possibility of hardware memory corruption errors) to return an error and handle it in some ways (for example restart the task that returned the error, trying to go back to the last working state).

Re:Using unwrap() in Rust is okay by burntsushi on Monday August 08, 2022 @02:35PM (#32389229)
> There are situations in which if you detect errors you want the program to continue running, and have only that particular functionality to fail.

Yes, like a web server. If a request handler fails by panicking, in a Rust program, you catch the panic, respond with a 500 error and log the panic somewhere. But you continue serving other requests.

I talked about this in the blog post.

The problem with your strategy is that it requires you to be aware of your own mistakes. That doesn't sound like a robust strategy, unless you're investing huge resources into sophisticated tooling and have drastically restricted the expressivity of your programming environment. That exists and is fine, and I even addressed that in the blog post too.

Re:Using unwrap() in Rust is okay by marshray on Monday August 08, 2022 @03:44PM (#32390121)
Great article BTW, loved it! Will certainly become a classic.

The web server example scares me.

Something happened during execution that the programmer didn't expect. There's a bug in the program. What if the panic is due to memory corruption (less likely in Rust) or internal data structure corruption?

Without knowledge to the contrary, swallowing a panic and YOLO'ing execution is driving full speed down the road of very poorly defined behavior.

If the programmer had sufficient knowledge to conclude it was safe, they could have just used a Result<> to report the error.

So ... don't make panic part of your API, and don't recover from panics?

Re:Using unwrap() in Rust is okay by burntsushi on Monday August 08, 2022 @04:54PM (#32390881)
Doesn't scare me in a memory safe language. Request handlers tend to be very well isolated. It's standard practice for web servers written in Go too.

> So ... don't make panic part of your API, and don't recover from panics?

That's the wrong lesson. As my blog says, don't use panicking for error handling. Making panicking part of the API is standard practice. Otherwise `slice[i]` wouldn't exist. (Its API is to panic when `i` is out of bounds.)

Lest you think you should just never panic and convert everything into errors, that's addressed in the blog too. :)

Re:Using unwrap() in Rust is okay by marshray on Monday August 08, 2022 @06:28PM (#32391916)
As the sibling post said:

> I mean, if all bets are off, all bets are truly off.

We have to expect that panic (e.g. from slice index operator) exercised an execution path that the library developer had never even considered, much less validated.

So then, post-panic, are we left with any guarantees about the validity of postconditions?

Re:Using unwrap() in Rust is okay by burntsushi on Monday August 08, 2022 @07:44PM (#32392481)
> So then, post-panic, are we left with any guarantees about the validity of postconditions?

Yes, because in order to catch a panic, the routine you're running has to be unwind-safe: https://doc.rust-lang.org/std/panic/trait.UnwindSafe.html

If you want to go on crashing your process and restarting it, then that's fine. It's a fine strategy. But you can't ignore that request handlers tend to be very well isolated, and catching a panic from one of them while continuing to service requests actually works well in practice. Oodles of folks are doing this in production. I've been doing it production for years. I have literally, not once, not ever, seen any problems arise because of this.

Consider what happens if there's a 'slice[i]' that panics in a regex library. The stack unwinds and any objects associated with that specific search that panicking get dropped. And since 'Regex' implements 'UnwindSafe', it says, "If a panic occurs it's all good to continue using me if you catch the panic."

You keep wanting to argue the theory. Rust has UnwindSafe for that. But please, consider looking at the practice too. You don't seem to have acknowledged that at all.

Re:Using unwrap() in Rust is okay by toast0 on Monday August 08, 2022 @07:04PM (#32392216)
What you're looking for is isolation.

In a classic forking webserver, you get isolation from the OS; if your request panics, let that process die, and the main process will respawn it; further requests are isolated from the failure (although, if the request paniced after writing broken shared state outside the isolated process, who knows if further requests will work). If the part that paniced isn't isolated, you really should propagate the panic until the panicing part is isolated. But maybe that's the OTP/Erlang talking ;)

Re:Using unwrap() in Rust is okay by preseinger on Monday August 08, 2022 @09:04PM (#32393030)
OS processes are far too heavyweight to serve individual requests. OS _threads_ are far too heavyweight, for that matter.
Re:Using unwrap() in Rust is okay by lmm on Monday August 08, 2022 @10:36PM (#32393681)
That's actually kind of a myth nowadays. For example the honest answer to the classic C10K question (how do you write a webserver that can serve 10000 requests concurrently) is that you spawn 10k threads and let the OS scheduler handle it and it'll be fine.
Re:Using unwrap() in Rust is okay by preseinger on Tuesday August 09, 2022 @03:17PM (#32402701)
Even a modern Linux kernel on modern hardware will struggle with 10k processes if those processes are doing nontrivial (i.e. syscall-effecting) work. But that benchmark is what, 10 or 15 years old now? Contemporary benchmarks target 100k connections per server (with some constraints).
Re:Using unwrap() in Rust is okay by int_19h on Friday August 12, 2022 @12:24AM (#32434694)
The question, rather, is whether it'll struggle with 10k threads.
Re:Using unwrap() in Rust is okay by preseinger on Friday August 12, 2022 @10:06PM (#32446279)
Aye, the cost of context switching between OS threads is generally too high for thread-per-request workloads.
Re:Using unwrap() in Rust is okay by toast0 on Tuesday August 09, 2022 @12:52AM (#32394285)
You don't have to kill/respawn the process after every request, only the ones that ended poorly. Maybe you can provide enough isolation through other means, but OS process per request is probably the most isolation you can get while being sensible and easy to use.
Re:Using unwrap() in Rust is okay by preseinger on Tuesday August 09, 2022 @03:34PM (#32403016)
If you don't bind request lifetime to process lifetime, it's not process-per-request, it's a worker pool implemented with processes. That means you need a dispatcher, which tends to becomes the bottleneck. OS processes get you super excellent isolation, I agree, and sometimes that's the most important thing to have and it makes sense to design things that way. But request-per-process is just really hard to make fast. OS scheduling overhead is way less than it used to be but it's still a lot.
Re:Using unwrap() in Rust is okay by toast0 on Tuesday August 09, 2022 @07:08PM (#32405801)
Blocking accept(2) makes a decent to good dispatcher, depending on your OS, and if you can stomach one request per socket (if you can't, you would need to pass the sockets back to a dispatcher between requests to wait for the socket to become ready. In the good old days, you could use accept filters and not see incoming connections until they were ready, but that doesn't really work for TLS or modern http with persistent connections.) You could make that pretty fast if you run one dispatcher per core, and align them with the NIC queues; each dispatcher with its own pool of workers.

If your work is mostly compute, then you usually don't really want to run more concurrency than one, maybe a few workers per core, and then OS scheduling is easy. If your work is more of waiting for i/o, large concurrency makes more sense, but the OS scheduling is not going to be too hard there, because it takes almost nothing for the OS to leave a process blocked on i/o; but you do need to have good timer scalability if you have a lot of processes, since they're all going to want to set and clear a timeout on most of the syscalls. io_uring etc with a small number of os processes/threads might be less work for the kernel, but certainly at the cost of isolation.

Re:Using unwrap() in Rust is okay by preseinger on Saturday August 13, 2022 @11:05PM (#32455978)
My experience is that basically all request-servicing work is I/O-bound. And AFAIK there is no request-servicing system in normal production use which does process-per-request. Even request-per-socket is basically outmoded; modern protocols multiplex logical requests over physical connections one way or another, e.g. HTTP/2.
Re:Using unwrap() in Rust is okay by lmm on Monday August 08, 2022 @10:26PM (#32393614)
> So ... don't make panic part of your API, and don't recover from panics?

Don't recover from panic except at high level - retrying or failing that "scope" is ok, but don't try to do business logic around panicing or not (if you absolutely must, catch the panic at the lowest possible level and turn it into a result).

Re:Using unwrap() in Rust is okay by ninkendo on Monday August 08, 2022 @05:57PM (#32391630)
> Without knowledge to the contrary, swallowing a panic and YOLO'ing execution is driving full speed down the road of very poorly defined behavior.

I mean, if you’re worried about code “corrupting” things such that it may not be safe to even serve the next request, you probably need to worry about successful requests as well. I mean, if all bets are off, all bets are truly off.

At some point, if you decide to colocate code from multiple “apps” in one server, you gotta have some level of trust in said apps. You’d obviously never do this for untrusted code. And you’d never do this for code that (hand-waving as you have) “corrupts” things in such a way that it’s not safe to handle the next request, panic or no.

Re:Using unwrap() in Rust is okay by marshray on Monday August 08, 2022 @06:34PM (#32391972)
> You’d obviously never do this for untrusted code.

Doesn't panic imply that the program entered a state from which it was never explicitly designed to recover?

Since that state may persist across invocations of a function, doesn't that make the code effectively 'untrusted' for subsequent execution?

Re:Using unwrap() in Rust is okay by ninkendo on Monday August 08, 2022 @09:59PM (#32393444)
> Doesn't panic imply that the program entered a state from which it was never explicitly designed to recover?

No? I think you’re making a lot of assumptions about the “state” of a program. Panicking doesn’t mean “I maintain state in between requests and that state is now poison and I can’t be called again”. In fact it doesn’t mean anything really. It means someone wrote a code path that panics, no more, no less.

Sure it’s possible that code can be written where a panic indicates that there is some poison state and now nothing can be trusted etc. But… the same is possible for code that doesn’t panic, so I don’t really know where we’re going with this discussion.

Re:Using unwrap() in Rust is okay by lmm on Monday August 08, 2022 @10:37PM (#32393689)
Well, shouldn't there be a standard way to convey that, that is understood (as a matter of culture) to mean that? And if it's not panic then what is it?
Re:Using unwrap() in Rust is okay by ninkendo on Monday August 08, 2022 @10:47PM (#32393761)
You want there to be a language-standard way of saying “I’m a backend for a web server and I don’t want to cause an error 500 but instead I want to crash the whole process”? Seems pretty specific to me.

Panic means “something’s wrong, this error isn’t recoverable”. In most contexts, the best thing to do is crash in this case. In some specific cases, if you keep things well-isolated enough (making HTTP handlers stateless, etc) you can design a system where panic means “something’s wrong, this error isn’t recoverable, and inform the remote end as such by rendering a 500 error”, and everything works just fine.

You seem to want some sort of truly fatal way of conveying errors. Panic is not that. If it was, Rust would not have designed panic to be handleable. It would be called “crash”, not panic. Go is similar… go’s panic is not meant for typical error handling, but it’s not synonymous with “crash”, either. It’s meant for situations where you want the moral equivalent of a crash, but taking the form of whatever makes sense for your use case. A multiplexing web server that renders proper 500 errors on a panic is basically the premiere use case doe this.

Re:Using unwrap() in Rust is okay by int_19h on Friday August 12, 2022 @12:26AM (#32434708)
Panic means "something wrong and this system didn't anticipate it, so now any random invariant might be broken".

Or at least that's what it was back when people first started making the distinction vs handle-able errors. I guess it got skewed over time, just like everything else.

Re:Using unwrap() in Rust is okay by Tainnor on Tuesday August 09, 2022 @04:16PM (#32403644)
> The problem with your strategy is that it requires you to be aware of your own mistakes. That doesn't sound like a robust strategy, unless you're investing huge resources into sophisticated tooling and have drastically restricted the expressivity of your programming environment. That exists and is fine, and I even addressed that in the blog post too.

Those "huge resources" and "sophisticated tooling" could just mean something like Erlang, where fault isolation is a core design principle.

But fault isolation works in simpler systems, too. Here's a recent example: I wrote a function that automatically generates a table of contents for an HTML document. While extensively testing it, I ran across a number of edge cases that would lead to faults (mostly due to messy underlying libraries, but also due to programmer error). I fixed all that I found, but I still didn't feel comfortable not adding a generic exception handler to the function.

In case that an exception is ever thrown in that code, the function will just return the input HTML unchanged (and log an error, of course). That's graceful degradation, and a good example of recovering from a potentially unknown fault.

Similar situations can often be found in many applications. Not all functions are essential, and we're not always 100% confident about all parts of the code working correctly.

(Note that I'm not talking about Rust here, I lack sufficient knowledge about it. It's possible that this strategy wouldn't work in Rust for reasons of memory-safety or anything else.)

Re:Using unwrap() in Rust is okay by mook on Monday August 08, 2022 @02:52PM (#32389427)
> I tend to write resilient code, since I work in embedded systems and what you never want is the system to crash.

That might be okay too depending on what your system is. I've had a cell phone (the monochrome dumb kind) display an assertion error at me, that was kind of cool. A screw was coming loose, so there were hardware issues. Worst case, I'd have to bring it to the store and get it repaired or replaced; there was no threat of injury or large monetary damage.