Problem with tokio::fs


#1

I found a strange behaviour whilst using tokio::fs with fifos. Here a simple test code:

extern crate tokio;

use std::time::{Duration, Instant};

use tokio::{fs::File, prelude::*, runtime::Runtime};

fn main() {
    let mut runtime = Runtime::new().unwrap();

    let fifo = File::open("/tmp/test.fifo");

    let future = fifo.deadline(Instant::now() + Duration::from_millis(5000))
        .then(|result| match result {
            Ok(_) => {
                println!("Fifo opened");
                future::ok::<(), ()>(())
            }
            Err(_) => {
                println!("Deadline reached");
                future::ok(())
            }
        });

    runtime.block_on(future).unwrap();
}

Obviously the fifo file must be created to use the test. The fact is, the open will block until the fifo file is opened elsewhere, and using deadline I expected to get an error after the specified duration.

Unfortunately, this is not what really happens, and I was wandering if I am misusing the APIs (in case, how and why), if this is a known issue related to the blocking behaviour of the tokio::fs APIs (the same reason we currently cannot use tokio::runtime::current_thread) or something unexpected and new.

If you know what is happening, help is appreciated. If it is a real issue, I can post it on Github. And if someone knows a possible workaround… :heart:


#2

This is an expected (if a bit confusing) behavior due to the way blocking works.

One possible workaround is to make a spawn off the File::open and send the file through a oneshot when it finishes. That way you can run the deadline on the oneshot receiver future which will work properly.


#3

Thank you! It is a nice workaround for now.

Unfortunately, I get another (sort of unexpected) issue in this way: when the deadline is reached, the future is not completely resolved, due to the blocking behaviour of the File inside Oneshot.

Do you think that there is a workaround as well or, for the time being, this is the best I can get?

P.s.: my real case, even if conceptually simple, relies on combining a Loop future, a File and a Deadline, which led me to this problem. Even if this is the best that I can obtain from tokio for now, it is probably muuuuch better than a headache-because-I-don’t-know-how-to-handle-everything implementation in C++ :grin:


#4

This seems pretty bad. I suspect many people will fall into this trap, but may not notice it if the I/O resolves quickly, as usual.

Perhaps a better workaround is to farm the open to a normal threadpool that returns a future, ie what one would do before tokio-fs came around.


#5

@sfackler can you explain why this is expected behavior? I think I understand how the blocking annotation works, but this makes me think I’m missing some important detail, perhaps related to worker vs. backup threads. Possibly this would be helpful to the thread but also helpful to my tokio documentation efforts!


#6

Deadline works by polling the main future, and if it’s not ready, polling a Delay. If the main future uses blocking, the first poll isn’t going to return NotReady, it’s just going to block until the operation is complete. There’s no opportunity to check the Delay.


#7

Couldn’t this conceivably be fixed by deadline setting some thread local or task/worker flag that says “I can’t be blocked” and then the blocking call returns NotReady in that case? If not that, then maybe there is some other way to avoid the issue? For example maybe some marker trait BlockingAllowed or BlockingNotAllowed that could avoid this combination at compile time?

My sense is this would be worth a tokio issue, @dodomorandi.


#8

That’s just going to change the behavior so it always times out rather than never timing out.


#9

OK, see possible fix #2 (marker trait)?


#10

A marker trait forces the entire ecosystem to care about this issue - it’s a very large hammer.


#11

Just to have an idea: what is the main reason the current implementation uses the blocking API? I mean, looking the problem from a single OS – Linux – it should not be so hard to create an Evented File struct. Are there any specific cross OS/cross arch issues? Or it is harder to implement than it looks like? Or it is more something like “we did not have a tokio fs, a blocking API is a fast implementation that allows as to have a working lib just now”?

I am asking because I would be happy to contribute, but at the moment I can easily work only on Linux, and if there are some major cross-platform issues that I cannot see, my efforts could be worthless.


#12

There’s no good async file I/O story for posix. And on linux itself, aio quality of implementation varies greatly across different filesystems (xfs generally being considered the best, from what I’ve heard).


#13

After your reply I searched around and I understood: epoll cannot be used with files because of its behaviour, and I was not aware of it :confused:.

So, ignore my dumb question made before, I start realizing it is a quite complex problematic…