<commit_before>//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore
import NIOConcurrencyHelpers

/// `NonBlockingFileIO` is a helper that allows you to read files without blocking the calling thread.
///
/// It is worth noting that `kqueue`, `epoll` or `poll` returning claiming a file is readable does not mean that the
/// data is already available in the kernel's memory. In other words, a `read` from a file can still block even if
/// reported as readable. This behaviour is also documented behaviour:
///
///  - [`poll`](http://pubs.opengroup.org/onlinepubs/009695399/functions/poll.html): "Regular files shall always poll TRUE for reading and writing."
///  - [`epoll`](http://man7.org/linux/man-pages/man7/epoll.7.html): "epoll is simply a faster poll(2), and can be used wherever the latter is used since it shares the same semantics."
///  - [`kqueue`](https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2): "Returns when the file pointer is not at the end of file."
///
/// `NonBlockingFileIO` helps to work around this issue by maintaining its own thread pool that is used to read the data
/// from the files into memory. It will then hand the (in-memory) data back which makes it available without the possibility
/// of blocking.
public struct NonBlockingFileIO: NIOSendable {
    /// The default and recommended size for `NonBlockingFileIO`'s thread pool.
    public static let defaultThreadPoolSize = 2

    /// The default and recommended chunk size.
    public static let defaultChunkSize = 128*1024

    /// `NonBlockingFileIO` errors.
    public enum Error: Swift.Error {
        /// `NonBlockingFileIO` is meant to be used with file descriptors that are set to the default (blocking) mode.
        /// It doesn't make sense to use it with a file descriptor where `O_NONBLOCK` is set therefore this error is
        /// raised when that was requested.
        case descriptorSetToNonBlocking
    }

    private let threadPool: NIOThreadPool

    /// Initialize a `NonBlockingFileIO` which uses the `NIOThreadPool`.
    ///
    /// - parameters:
    ///   - threadPool: The `NIOThreadPool` that will be used for all the IO.
    public init(threadPool: NIOThreadPool) {
        self.threadPool = threadPool
    }

    /// Read a `FileRegion` in chunks of `chunkSize` bytes on `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `fileRegion.readableBytes` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ fileRegion.readableBytes / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `fileRegion.readableBytes` bytes can be read from the file,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
    /// same `FileRegion` in multiple threads.
    ///
    /// - parameters:
    ///   - fileRegion: The file region to read.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    public func readChunked(fileRegion: FileRegion,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop,
                            chunkHandler: @escaping (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        let readableBytes = fileRegion.readableBytes
        return self.readChunked(fileHandle: fileRegion.fileHandle,
                                fromOffset: Int64(fileRegion.readerIndex),
                                byteCount: readableBytes,
                                chunkSize: chunkSize,
                                allocator: allocator,
                                eventLoop: eventLoop,
                                chunkHandler: chunkHandler)
    }

    /// Read `byteCount` bytes in chunks of `chunkSize` bytes from `fileHandle` in `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// - note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses
    ///         `FileRegion` object instead of raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s,
    ///         please consider using `readChunked(fileHandle:fromOffset:chunkSize:allocator:eventLoop:chunkHandler:)`
    ///         because it doesn't use the file descriptor's seek pointer (which may be shared with other file
    ///         descriptors and even across processes.)
    ///
    /// - parameters:
    ///   - fileHandle: The `NIOFileHandle` to read from.
    ///   - byteCount: The number of bytes to read from `fileHandle`.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    public func readChunked(fileHandle: NIOFileHandle,
                            byteCount: Int,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop, chunkHandler: @escaping (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        return self.readChunked0(fileHandle: fileHandle,
                                fromOffset: nil,
                                byteCount: byteCount,
                                chunkSize: chunkSize,
                                allocator: allocator,
                                eventLoop: eventLoop,
                                chunkHandler: chunkHandler)
    }

    /// Read `byteCount` bytes from offset `fileOffset` in chunks of `chunkSize` bytes from `fileHandle` in `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
    /// same `NIOFileHandle` in multiple threads.
    ///
    /// - note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses
    ///         `FileRegion` object instead of raw `NIOFileHandle`s.
    ///
    /// - parameters:
    ///   - fileHandle: The `NIOFileHandle` to read from.
    ///   - byteCount: The number of bytes to read from `fileHandle`.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    public func readChunked(fileHandle: NIOFileHandle,
                            fromOffset fileOffset: Int64,
                            byteCount: Int,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop,
                            chunkHandler: @escaping (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        return self.readChunked0(fileHandle: fileHandle,
                                 fromOffset: fileOffset,
                                 byteCount: byteCount,
                                 chunkSize: chunkSize,
                                 allocator: allocator,
                                 eventLoop: eventLoop,
                                 chunkHandler: chunkHandler)
    }

    private func readChunked0(fileHandle: NIOFileHandle,
                              fromOffset: Int64?,
                              byteCount: Int,
                              chunkSize: Int,
                              allocator: ByteBufferAllocator,
                              eventLoop: EventLoop, chunkHandler: @escaping (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        precondition(chunkSize > 0, "chunkSize must be > 0 (is \(chunkSize))")
        let remainingReads = 1 + (byteCount / chunkSize)
        let lastReadSize = byteCount % chunkSize
      <commit_msg>Adopt `Sendable` in `NonBlockingFileIO` (#2212)<commit_after>//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2017-2021 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIOCore
import NIOConcurrencyHelpers

/// `NonBlockingFileIO` is a helper that allows you to read files without blocking the calling thread.
///
/// It is worth noting that `kqueue`, `epoll` or `poll` returning claiming a file is readable does not mean that the
/// data is already available in the kernel's memory. In other words, a `read` from a file can still block even if
/// reported as readable. This behaviour is also documented behaviour:
///
///  - [`poll`](http://pubs.opengroup.org/onlinepubs/009695399/functions/poll.html): "Regular files shall always poll TRUE for reading and writing."
///  - [`epoll`](http://man7.org/linux/man-pages/man7/epoll.7.html): "epoll is simply a faster poll(2), and can be used wherever the latter is used since it shares the same semantics."
///  - [`kqueue`](https://www.freebsd.org/cgi/man.cgi?query=kqueue&sektion=2): "Returns when the file pointer is not at the end of file."
///
/// `NonBlockingFileIO` helps to work around this issue by maintaining its own thread pool that is used to read the data
/// from the files into memory. It will then hand the (in-memory) data back which makes it available without the possibility
/// of blocking.
public struct NonBlockingFileIO: NIOSendable {
    /// The default and recommended size for `NonBlockingFileIO`'s thread pool.
    public static let defaultThreadPoolSize = 2

    /// The default and recommended chunk size.
    public static let defaultChunkSize = 128*1024

    /// `NonBlockingFileIO` errors.
    public enum Error: Swift.Error {
        /// `NonBlockingFileIO` is meant to be used with file descriptors that are set to the default (blocking) mode.
        /// It doesn't make sense to use it with a file descriptor where `O_NONBLOCK` is set therefore this error is
        /// raised when that was requested.
        case descriptorSetToNonBlocking
    }

    private let threadPool: NIOThreadPool

    /// Initialize a `NonBlockingFileIO` which uses the `NIOThreadPool`.
    ///
    /// - parameters:
    ///   - threadPool: The `NIOThreadPool` that will be used for all the IO.
    public init(threadPool: NIOThreadPool) {
        self.threadPool = threadPool
    }
    
    #if swift(>=5.7)
    /// Read a `FileRegion` in chunks of `chunkSize` bytes on `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `fileRegion.readableBytes` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ fileRegion.readableBytes / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `fileRegion.readableBytes` bytes can be read from the file,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
    /// same `FileRegion` in multiple threads.
    ///
    /// - parameters:
    ///   - fileRegion: The file region to read.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    @preconcurrency
    public func readChunked(fileRegion: FileRegion,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop,
                            chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        let readableBytes = fileRegion.readableBytes
        return self.readChunked(fileHandle: fileRegion.fileHandle,
                                fromOffset: Int64(fileRegion.readerIndex),
                                byteCount: readableBytes,
                                chunkSize: chunkSize,
                                allocator: allocator,
                                eventLoop: eventLoop,
                                chunkHandler: chunkHandler)
    }
    #else
    /// Read a `FileRegion` in chunks of `chunkSize` bytes on `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `fileRegion.readableBytes` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ fileRegion.readableBytes / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `fileRegion.readableBytes` bytes can be read from the file,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
    /// same `FileRegion` in multiple threads.
    ///
    /// - parameters:
    ///   - fileRegion: The file region to read.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    public func readChunked(fileRegion: FileRegion,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop,
                            chunkHandler: @escaping (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        let readableBytes = fileRegion.readableBytes
        return self.readChunked(fileHandle: fileRegion.fileHandle,
                                fromOffset: Int64(fileRegion.readerIndex),
                                byteCount: readableBytes,
                                chunkSize: chunkSize,
                                allocator: allocator,
                                eventLoop: eventLoop,
                                chunkHandler: chunkHandler)
    }
    #endif

    #if swift(>=5.7)
    /// Read `byteCount` bytes in chunks of `chunkSize` bytes from `fileHandle` in `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// - note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses
    ///         `FileRegion` object instead of raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s,
    ///         please consider using `readChunked(fileHandle:fromOffset:chunkSize:allocator:eventLoop:chunkHandler:)`
    ///         because it doesn't use the file descriptor's seek pointer (which may be shared with other file
    ///         descriptors and even across processes.)
    ///
    /// - parameters:
    ///   - fileHandle: The `NIOFileHandle` to read from.
    ///   - byteCount: The number of bytes to read from `fileHandle`.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    @preconcurrency
    public func readChunked(fileHandle: NIOFileHandle,
                            byteCount: Int,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop, chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        return self.readChunked0(fileHandle: fileHandle,
                                fromOffset: nil,
                                byteCount: byteCount,
                                chunkSize: chunkSize,
                                allocator: allocator,
                                eventLoop: eventLoop,
                                chunkHandler: chunkHandler)
    }
    #else
    /// Read `byteCount` bytes in chunks of `chunkSize` bytes from `fileHandle` in `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// - note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses
    ///         `FileRegion` object instead of raw `NIOFileHandle`s. In case you do want to use raw `NIOFileHandle`s,
    ///         please consider using `readChunked(fileHandle:fromOffset:chunkSize:allocator:eventLoop:chunkHandler:)`
    ///         because it doesn't use the file descriptor's seek pointer (which may be shared with other file
    ///         descriptors and even across processes.)
    ///
    /// - parameters:
    ///   - fileHandle: The `NIOFileHandle` to read from.
    ///   - byteCount: The number of bytes to read from `fileHandle`.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    public func readChunked(fileHandle: NIOFileHandle,
                            byteCount: Int,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop, chunkHandler: @escaping (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        return self.readChunked0(fileHandle: fileHandle,
                                fromOffset: nil,
                                byteCount: byteCount,
                                chunkSize: chunkSize,
                                allocator: allocator,
                                eventLoop: eventLoop,
                                chunkHandler: chunkHandler)
    }
    #endif

    #if swift(>=5.7)
    /// Read `byteCount` bytes from offset `fileOffset` in chunks of `chunkSize` bytes from `fileHandle` in `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
    /// same `NIOFileHandle` in multiple threads.
    ///
    /// - note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses
    ///         `FileRegion` object instead of raw `NIOFileHandle`s.
    ///
    /// - parameters:
    ///   - fileHandle: The `NIOFileHandle` to read from.
    ///   - byteCount: The number of bytes to read from `fileHandle`.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    @preconcurrency
    public func readChunked(fileHandle: NIOFileHandle,
                            fromOffset fileOffset: Int64,
                            byteCount: Int,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop,
                            chunkHandler: @escaping @Sendable (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        return self.readChunked0(fileHandle: fileHandle,
                                 fromOffset: fileOffset,
                                 byteCount: byteCount,
                                 chunkSize: chunkSize,
                                 allocator: allocator,
                                 eventLoop: eventLoop,
                                 chunkHandler: chunkHandler)
    }
    #else
    /// Read `byteCount` bytes from offset `fileOffset` in chunks of `chunkSize` bytes from `fileHandle` in `NonBlockingFileIO`'s private thread
    /// pool which is separate from any `EventLoop` thread.
    ///
    /// `chunkHandler` will be called on `eventLoop` for every chunk that was read. Assuming `byteCount` is greater than
    /// zero and there are enough bytes available `chunkHandler` will be called `1 + |_ byteCount / chunkSize _|`
    /// times, delivering `chunkSize` bytes each time. If less than `byteCount` bytes can be read from `descriptor`,
    /// `chunkHandler` will be called less often with the last invocation possibly being of less than `chunkSize` bytes.
    ///
    /// The allocation and reading of a subsequent chunk will only be attempted when `chunkHandler` succeeds.
    ///
    /// This method will not use the file descriptor's seek pointer which means there is no danger of reading from the
    /// same `NIOFileHandle` in multiple threads.
    ///
    /// - note: `readChunked(fileRegion:chunkSize:allocator:eventLoop:chunkHandler:)` should be preferred as it uses
    ///         `FileRegion` object instead of raw `NIOFileHandle`s.
    ///
    /// - parameters:
    ///   - fileHandle: The `NIOFileHandle` to read from.
    ///   - byteCount: The number of bytes to read from `fileHandle`.
    ///   - chunkSize: The size of the individual chunks to deliver.
    ///   - allocator: A `ByteBufferAllocator` used to allocate space for the chunks.
    ///   - eventLoop: The `EventLoop` to call `chunkHandler` on.
    ///   - chunkHandler: Called for every chunk read. The next chunk will be read upon successful completion of the returned `EventLoopFuture`. If the returned `EventLoopFuture` fails, the overall operation is aborted.
    /// - returns: An `EventLoopFuture` which is the result of the overall operation. If either the reading of `fileHandle` or `chunkHandler` fails, the `EventLoopFuture` will fail too. If the reading of `fileHandle` as well as `chunkHandler` always succeeded, the `EventLoopFuture` will succeed too.
    public func readChunked(fileHandle: NIOFileHandle,
                            fromOffset fileOffset: Int64,
                            byteCount: Int,
                            chunkSize: Int = NonBlockingFileIO.defaultChunkSize,
                            allocator: ByteBufferAllocator,
                            eventLoop: EventLoop,
                            chunkHandler: @escaping (ByteBuffer) -> EventLoopFuture<Void>) -> EventLoopFuture<Void> {
        return self.readChunked0(fileHandle: fileHandle,
                                 fromOffset: fileOffset,
                                 byteCount: byteCount,
                                 chunkSize: chunkSize,
                                 allocator: allocator,
                                 eventLoop: eventLoop,
                                 chunkHandler: chunkHandler)
    }
    #endif
    
    #if swift(>=5.7)
    private typealias ReadChunkHandler = @Sendable (ByteBuffer) -> EventLoopFuture<Void>
    #else
    private typealias ReadChunkHandler = (ByteBuffer) -> EventLoopFuture<Void>
    #endif

    private func readChunked0(fileHandle: NIOFileHandle,
                              fromOffset: Int64?,
                              byteCount: Int,
                              chunkSize: Int,
                              allocator: ByteBufferAllocator,
                              eventLoop: EventLoop, chunkHandler: @escaping ReadChunkHandler) -> EventLoopFuture<Void> {
        precondition(chunkSize > 0, "chunkSize must be > 0 (is \(chunkSize))")
        let remainingReads = 1 + (byteCount / chunkSize)
        let lastReadSize = byteCount % chunkSize
      
{'fraction_non_alphanumeric': 0.09344252276477419, 'fraction_numerical': 0.0036362525209313696, 'mean_word_length': 2.2557699960206925, 'pattern_counts': {'":': 0, '<': 26, '<?xml version=': 0, '>': 57, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 18, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3386255', 'n_tokens_mistral': 8177, 'n_tokens_neox': 7973, 'n_words': 3261}