Stream: general

Topic: Thread panics/assertions and program termination


Josh Triplett (Jan 28 2020 at 20:28, on Zulip):

In a test case, I need to spawn a thread (std::thread::spawn). How can I safely do assertions in that thread, as part of the test case? As far as I can tell, the thread panics but the test keeps running. If the thread fails, the main thread will run forever, so the test case keeps running forever.

Laurențiu Nicola (Jan 28 2020 at 20:33, on Zulip):

Can you join it?

Josh Triplett (Jan 28 2020 at 20:34, on Zulip):

The main thread is waiting on something else, and waiting on both the something else and the child thread would substantially complicate the logic.

Laurențiu Nicola (Jan 28 2020 at 20:35, on Zulip):

I suppose you'll need to synchronize them some way, depending on what the main thread is doing.

Josh Triplett (Jan 28 2020 at 20:35, on Zulip):

(The child thread exists to call wait on a child process, and then notify the main thread via something it's already watching.)

Josh Triplett (Jan 28 2020 at 20:36, on Zulip):

(I really wish that pidfd worked everywhere, but even with that it'd be substantially more complex than the simple test case I'm trying to write.)

Laurențiu Nicola (Jan 28 2020 at 20:38, on Zulip):

Do you expect the helper thread to panic (outside of the case when the child could not be launched)?

Josh Triplett (Jan 28 2020 at 20:39, on Zulip):

Even that case would be problematic, as I'd like to not have the test case turn into an infinite loop in that case. But yes, the helper thread could also assert!.

Laurențiu Nicola (Jan 28 2020 at 20:39, on Zulip):

Like.. if you do Command::new("foo").status(), it's probably not going to panic, even if I don't think it's really documented not to.

Josh Triplett (Jan 28 2020 at 20:40, on Zulip):

I actually call .spawn() in the main thread, and then pass the resulting Child to the helper thread.

Josh Triplett (Jan 28 2020 at 20:40, on Zulip):

So that case will panic in the main thread.

Josh Triplett (Jan 28 2020 at 20:40, on Zulip):

The helper thread just calls let status = child.wait().expect("..."); and assert!(status.success());.

Laurențiu Nicola (Jan 28 2020 at 20:41, on Zulip):

Why not send status over a channel to the main thread?

Josh Triplett (Jan 28 2020 at 20:42, on Zulip):

I very easily could (the helper thread exists to wait and then pass data to the main thread, and this is a test case for a channel-like construct). But it feels fragile that if the child thread panics for any reason the parent thread will hang.

Josh Triplett (Jan 28 2020 at 20:42, on Zulip):

I'd like something better than "carefully avoid panicking".

Laurențiu Nicola (Jan 28 2020 at 20:43, on Zulip):

There is catch_unwind, I suppose, so you could wrap your helper thread logic inside that

Laurențiu Nicola (Jan 28 2020 at 20:44, on Zulip):

I don't have other ideas, sorry

Josh Triplett (Jan 28 2020 at 20:44, on Zulip):

Yeah, I could certainly go there. But in the case of a test case, what can I call from the thread that'll cause the test to fail, and ideally still run destructors on the way out?

Laurențiu Nicola (Jan 28 2020 at 20:46, on Zulip):

That depends on what the main thread (test) is doing. If they use a channel to communicate, it can detect that the sender was closed. But there's no "test framework runtime" per se that could detect that a background thread has panicked.

Josh Triplett (Jan 28 2020 at 20:47, on Zulip):

Unfortunately, the nature of the channel I'm using is that you can't tell if the other end was closed.

Josh Triplett (Jan 28 2020 at 20:47, on Zulip):

Which is why the main thread would block forever: it's waiting on a message that'll never come.

Laurențiu Nicola (Jan 28 2020 at 20:48, on Zulip):

And I guess there's a reason why you can't use a standard channel to pass back the result, regardless of what the test is.. testing.

Josh Triplett (Jan 28 2020 at 20:49, on Zulip):

Right. Because the main thread can't simultaneously wait on a standard channel and the channel I'm testing, without a lot more complexity in the test.

Laurențiu Nicola (Jan 28 2020 at 20:50, on Zulip):

Then you could spawn a third thread to wait on the channel you're testing and have the main wait for both (using channels, if you need to)

Laurențiu Nicola (Jan 28 2020 at 20:51, on Zulip):

Yeah, that's a bit awkward

Josh Triplett (Jan 28 2020 at 20:51, on Zulip):

Heh. True, and true. :)

Josh Triplett (Jan 28 2020 at 20:52, on Zulip):

I think I'm more broadly trying to figure out how people write tests involving threads in a less painful way.

Laurențiu Nicola (Jan 28 2020 at 20:54, on Zulip):

With a lot of joining and channels, I'd wager :-). One thing I like about the Windows API is that you can wait on basically every type of handle.

Josh Triplett (Jan 28 2020 at 20:55, on Zulip):

The same is true on Linux with fds and select/poll/epoll, as long as you have an fd. signalfd and pidfd make it possible to wait on more things as fds.

Laurențiu Nicola (Jan 28 2020 at 20:55, on Zulip):

And timerfd :D

Josh Triplett (Jan 28 2020 at 20:56, on Zulip):

And timerfd, yes. :)

Laurențiu Nicola (Jan 28 2020 at 20:57, on Zulip):

Hmm, can you pass the thread a helper struct that wraps a channel Sender and checks thread::panicking in drop?

Josh Triplett (Jan 28 2020 at 20:57, on Zulip):

...devious. Thinking...

Laurențiu Nicola (Jan 28 2020 at 20:59, on Zulip):

Actually, the real problem might be how to wait for one thread out of a set

Josh Triplett (Jan 28 2020 at 20:59, on Zulip):

This can also be used in multithreaded applications, in order to send a message to other threads warning that a thread has panicked (e.g., for monitoring purposes).

Josh Triplett (Jan 28 2020 at 20:59, on Zulip):

Yeah, that's pretty much what I need...

Josh Triplett (Jan 28 2020 at 20:59, on Zulip):

Wouldn't necessarily even have to use a channel, could just print a message to stderr and abort. That sounds like a useful primitive for testing.

Laurențiu Nicola (Jan 28 2020 at 21:00, on Zulip):

Actually, the real problem might be how to wait for one thread out of a set

Because if one of them panics you know you can kill the rest

Laurențiu Nicola (Jan 28 2020 at 21:01, on Zulip):

If you abort, the other tests won't run, right?

Josh Triplett (Jan 28 2020 at 21:02, on Zulip):

Good point.

Josh Triplett (Jan 28 2020 at 21:02, on Zulip):

It would be nice to be able to abort the test without aborting the entire test framework.

Laurențiu Nicola (Jan 28 2020 at 21:03, on Zulip):

So how do you wait for one thread out of many? I can't find a way.

Josh Triplett (Jan 28 2020 at 21:04, on Zulip):

I'm not sure I know what you mean? There are several ways to wait for a thread, but how does that solve the problem of "kill the test (with a failure)"?

Laurențiu Nicola (Jan 28 2020 at 21:05, on Zulip):

As in "wait for one", not "wait for all" of them. My reasoning is that you can spawn your two helper threads, wait for either of them to finish, and if it panicked, kill the other thread and propagate the panic.

Josh Triplett (Jan 28 2020 at 21:06, on Zulip):

Ah, I see what you're getting at.

Laurențiu Nicola (Jan 28 2020 at 21:06, on Zulip):

The main issue seems to be that you can't cancel the read from the channel thingy you're testing

Josh Triplett (Jan 28 2020 at 21:07, on Zulip):

If I decided to go with the "two helper threads" approach, I could just wait on the helper thread then the main thread, and panic if either panics.

Josh Triplett (Jan 28 2020 at 21:07, on Zulip):

But that's still awkward.

Josh Triplett (Jan 28 2020 at 21:07, on Zulip):

@Laurențiu Nicola The thing I'm testing is roughly equivalent to a UDP socket.

Josh Triplett (Jan 28 2020 at 21:08, on Zulip):

You don't get a definitive EOF-like signal, because there's no connection.

Laurențiu Nicola (Jan 28 2020 at 21:08, on Zulip):

If I decided to go with the "two helper threads" approach, I could just wait on the helper thread then the main thread, and panic if either panics.

But if there's no way to wait for one of them, you'll need to use channels (probably from crossbeam, since they have select).

Josh Triplett (Jan 28 2020 at 21:08, on Zulip):

"wait on the helper thread then the main thread" could just use join.

Laurențiu Nicola (Jan 28 2020 at 21:09, on Zulip):

If it's a single one, yes, but in the general case you might want to wait for either of them to finish

Laurențiu Nicola (Jan 28 2020 at 21:10, on Zulip):

You don't get a definitive EOF-like signal, because there's no connection.

Got it. But if it was a socket, you could close it, which would result in a read error, so your thread would know it needs to shut down

Josh Triplett (Jan 28 2020 at 21:12, on Zulip):

If it's a single one, yes, but in the general case you might want to wait for either of them to finish

Sure. And if I can get Rust to add better support for threaded test cases, it might require something more sophisticated. :)

Josh Triplett (Jan 28 2020 at 21:12, on Zulip):

You don't get a definitive EOF-like signal, because there's no connection.

Got it. But if it was a socket, you could close it, which would result in a read error, so your thread would know it needs to shut down

Closing a sender to a UDP socket (or equivalent) doesn't result in any indication to the receiving socket.

Josh Triplett (Jan 28 2020 at 21:13, on Zulip):

(I would love a suggestion otherwise. The whole "helper thread" mechanism here exists to work around the lack of any EOF mechanism.)

Josh Triplett (Jan 28 2020 at 21:14, on Zulip):

I'm working with AF_UNIX SOCK_DGRAM sockets.

Josh Triplett (Jan 28 2020 at 21:14, on Zulip):

std::os::unix::net::UnixDatagram

Laurențiu Nicola (Jan 28 2020 at 21:19, on Zulip):

I meant closing the receiving end. I might have drifted too much from your original question, but I was thinking that you:

  1. are writing a test for reading from a socket
  2. can spawn a thread that tries to read and blocks
  3. spawn another thread that uses an external process to send data to your socket
  4. want to detect if either of the two panics, so you can fail the test, and preferably clean up
  5. with a "wait one thread" primitive you can wait for one of the two to finish, decide if the test failed or not, then signal the other thread to close (or kill it)
  6. you can signal the socket thread to shut down by closing the socket it's reading from
Laurențiu Nicola (Jan 28 2020 at 21:21, on Zulip):

Anyway, you already know these things ^

Josh Triplett (Jan 28 2020 at 21:22, on Zulip):

Closing the socket that thread is reading from is a potentially hazardous way of making a thread error, since a more complex test could potentially open another file that ends up with the same descriptor.

Laurențiu Nicola (Jan 28 2020 at 21:24, on Zulip):

Yeah, that's a good point. I don't think there's a race in the simple case, but it could turn into a mess

Josh Triplett (Jan 28 2020 at 21:25, on Zulip):

I'm also realizing that Rust doesn't seem to expose "kill this thread".

Josh Triplett (Jan 28 2020 at 21:26, on Zulip):

Given a JoinHandle, it should be possible to terminate a thread.

Laurențiu Nicola (Jan 28 2020 at 21:27, on Zulip):

I thought there was a deprecated method for that, but it might have been removed

Peter Rabbit (Apr 20 2020 at 01:43, on Zulip):

Terminating a thread is not a particularly safe operation. If it was in the process of writing to any shared data, that data can be left in a corrupt state and cause UB when other threads access it. Also if you use scoped threads, all the stack memory is deallocated so the scoped thread now might accidentally access invalid memory.

Last update: Jun 07 2020 at 09:50UTC