Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

An idea for supporting cancellation in RandomAccess on Unix #96530

Open
alexrp opened this issue Jan 5, 2024 · 5 comments
Open

An idea for supporting cancellation in RandomAccess on Unix #96530

alexrp opened this issue Jan 5, 2024 · 5 comments
Labels
area-System.IO os-linux Linux OS (any supported distro)
Milestone

Comments

@alexrp
Copy link
Contributor

alexrp commented Jan 5, 2024

cc @adamsitnik @stephentoub @tmds (?)

I would very much like to switch to RandomAccess in my terminal driver code. One hard blocker (aside from #58381) for doing so is that it does not currently support read cancellation on Unix. I know that #51985 is one way that could be solved, but frankly, that seems far off at the moment.

In my project, I implemented a type backed by a pipe + poll to solve this. It's used like so when reading. (Obviously, it can be implemented much more efficiently, with fewer allocations and such, but as I only have two instances in an app, that wasn't a big concern for me.)

The idea is very simple:

  • Set up an anonymous pipe.
  • Set up a handler so that, when the CancellationToken is canceled, a byte is written into the anonymous pipe.
  • When we go to read from the file handle, first do a poll(pipe, file).
    • If the poll indicates data is available in the file, just read as normal.
    • If the poll indicates data is available in the pipe, read it and discard it, and throw OperationCanceledException.

I would assume the idea applies equally well to writes.

The strategy is not entirely dissimilar from the one .NET already uses for async-over-sync cancellation on Windows.

Has something like this already been considered? If not, would it make sense to adopt some variation of it?

@ghost ghost added the untriaged New issue has not been triaged by the area owner label Jan 5, 2024
@ghost
Copy link

ghost commented Jan 5, 2024

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

cc @adamsitnik @stephentoub (?)

I would very much like to switch to RandomAccess in my terminal driver code. One hard blocker for doing so is that it does not currently support read cancellation on Unix. I know that #51985 is one way that could be solved, but frankly, that seems far off at the moment.

In my project, I implemented a type backed by a pipe + poll to solve this. It's used like so when reading. (Obviously, it can be implemented much more efficiently, with fewer allocations and such, but as I only have two instances in an app, that wasn't a big concern for me.)

The idea is very simple:

  • Set up an anonymous pipe.
  • Set up a handler so that, when the CancellationToken is canceled, a byte is written into the anonymous pipe.
  • When we go to read from the file handle, first do a poll(pipe, file).
    • If the poll indicates data is available in the file, just read as normal.
    • If the poll indicates data is available in the pipe, read it and discard it, and throw OperationCanceledException.

I would assume the idea applies equally well to writes.

The strategy is not entirely dissimilar from the one .NET already uses for async-over-sync cancellation on Windows.

Has something like this already been considered? If not, would it make sense to adopt some variation of it?

Author: alexrp
Assignees: -
Labels:

area-System.IO, untriaged

Milestone: -

@adamsitnik
Copy link
Member

Set up an anonymous pipe.

This would be quite expensive, as it would create a new anonymous pipe for every file that we want to read from with cancellable tokens. Moreover, RandomAccess is thread safe and doing it right would be hard.

The strategy is not entirely dissimilar from the one .NET already uses for async-over-sync cancellation on Windows.

@tmds would it be possible to implement something similar on Unix? For example by using signals? From what I can see Linux offers io_cancel sys-call, but it works only for AIO.

@adamsitnik adamsitnik added os-linux Linux OS (any supported distro) and removed untriaged New issue has not been triaged by the area owner labels Jan 9, 2024
@adamsitnik adamsitnik added this to the Future milestone Jan 9, 2024
@tmds
Copy link
Member

tmds commented Jan 9, 2024

I would very much like to switch to RandomAccess in my terminal driver code.

I'm curious why you need RandomAccess in a terminal driver.

I implemented a type backed by a pipe + poll to solve this.

The Socket class isn't picky about the type of handle you give it. You can do something like this:

if (!Console.IsInputRedirected)
{
    Console.WriteLine("Input is a terminal.");

    const int STDIN_FILENO = 0;
    using var handle = new SafeSocketHandle(STDIN_FILENO, ownsHandle: false);
    using var socket = new Socket(handle);
    byte[] buffer = new byte[1024];
    while (true)
    {
        int bytesRead = await socket.ReceiveAsync(buffer);
        if (bytesRead == 0)
        {
            break;
        }
        Console.WriteLine($"Read {bytesRead} bytes.");
    }
}

would it be possible to implement something similar on Unix? For example by using signals?

You could track the thread that makes the blocking call, and send it a signal to get EINTR.
That needs quite some plumbing, and maybe add a non negligible overhead.

@alexrp
Copy link
Contributor Author

alexrp commented Jan 9, 2024

@adamsitnik

This would be quite expensive, as it would create a new anonymous pipe for every file that we want to read from with cancellable tokens. Moreover, RandomAccess is thread safe and doing it right would be hard.

I do not think we need to create one for every file.

For example, one strategy I can imagine here would be to create the pipe on demand when cancellationToken.CanBeCanceled. After using it, stash it away in some kind of thread-static state for reuse, with a reference to that state from a static variable. Then register a Gen2 GC callback that can close pipes that haven't been used in a while based on some kind of reasonable heuristic.

AFAIK anonymous pipes are not particularly expensive to keep around in terms of memory, and the buffer capacity can be adjusted down to the bare minimum with fcntl(F_SETPIPE_SZ, getpagesize()) from the default ~64k.

Seems not much harder than the Windows code I linked above, and the overhead should be minimal. I'd say it's well established that cancellation support (i.e. passing a cancellable token) always comes with some extra overhead to make the cancellation actually work.

@tmds

I'm curious why you need RandomAccess in a terminal driver.

It just so happens that RandomAccess aligns better with the abstraction I have in my library. But I could use FileStream too, it's just slightly less convenient. But since FileStream uses RandomAccess, I am still blocked by the lack of cancellation support in RandomAccess on Unix.

My hope is to one day unify all my Windows/Unix terminal driver code around FileStream/RandomAccess, so that the OS-specific bits only deal with signals and such.

The Socket class isn't picky about the type of handle you give it. You can do something like this:

I didn't know that; that is a bit neater. Do you think this would be cheaper than a pipe configured with fcntl(F_SETPIPE_SZ, getpagesize())?

@alexrp
Copy link
Contributor Author

alexrp commented Jun 1, 2024

Still curious about everyone's thoughts on the above; I would very much like to see Unix cancellation support here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-System.IO os-linux Linux OS (any supported distro)
Projects
None yet
Development

No branches or pull requests

3 participants