Skip to content

[coro_io] Allow multiple random_coro_file instances to share a single file descriptor #1217

Description

@sqrtpineapple

Search before asking

  • I searched the issues and found no similar issues.

What happened + What you expected to happen

In our searcher workload, we create multiple random_coro_file instances for the same physical file to perform concurrent random reads across different executors or I/O contexts.

Currently, every path-based random_coro_file opens the file independently and consumes one file descriptor. Therefore, FD usage grows approximately as:

number of files × random_coro_file instances per file

For example:

10 files × 32 random_coro_file instances per file = 320 file descriptors

A searcher pod may contain many files and concurrent readers. The resulting FD usage can exceed the pod's ulimit -n, causing open() to fail with EMFILE (Too many open files). This may also prevent the process from opening unrelated files or sockets.

I expect random_coro_file to support multiple instances sharing one existing file descriptor, especially for positioned operations such as async_read_at.

The desired behavior is:

  • Open each physical file only once.
  • Allow multiple random_coro_file instances to use the same fd concurrently.
  • Support non-owning fd semantics so destroying one wrapper does not close the shared fd.
  • Keep the fd valid until all wrappers and in-flight operations have completed.
  • Preserve the existing path-based API and behavior.

With this model, FD usage becomes proportional to the number of physical files instead of the number of random_coro_file instances.

Reproduction way

Create multiple random_coro_file instances for the same file:

std::vector<std::unique_ptr<coro_io::random_coro_file>> files;

for (std::size_t i = 0; i < concurrency; ++i) {
  files.emplace_back(std::make_unique<coro_io::random_coro_file>(
      "/path/to/data", std::ios::in, executor));
}

Keep the process running and inspect its descriptors:

ls -l /proc/<pid>/fd

When concurrency is 320, approximately 320 descriptors refer to the same file.

In a deployment with a sufficiently low FD limit, increasing the number of files or readers eventually causes:

open: Too many open files

Expected result after adding fd sharing:

10 physical files × 32 wrappers per file
Current:  320 file descriptors
Expected: 10 file descriptors

All wrappers should still be able to perform concurrent async_read_at operations correctly.

Anything else

A possible solution is to add constructors that accept an existing fd and explicitly define its ownership:

random_coro_file(
    int fd,
    asio::io_context::executor_type executor,
    std::string_view file_path = "",
    bool own_fd = true);

When own_fd is false:

  • The wrapper should use the supplied fd without calling open() or dup().
  • close() and the destructor should detach or release the native handle without closing it.
  • The caller is responsible for keeping the fd alive until all wrappers and in-flight I/O operations have completed.

The implementation should cover both the thread-pool pread/pwrite backend and the native asynchronous backend where supported.

I have implemented a prototype and added ownership, lifecycle, concurrency, and native-handle tests.

I also ran a multi-file benchmark with 10 files, 32 wrappers per file, 320 concurrent 4 KiB direct random-read coroutines, and the Linux io_uring backend.

Mode FDs per file Total file FDs Median IOPS (32 I/O threads)
Shared fd 1 10 3,653,516
Duplicated fd 33 330 3,669,355
Path-based open 32 320 3,686,683

The maximum throughput difference was about 0.9%, while shared-fd mode reduced the file descriptor count from 320 to 10. All benchmark runs completed with zero I/O errors.

Are you willing to submit a PR?

  • Yes I am willing to submit a PR!

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions