hyperfile 0.11.3

A file-like interface abstraction layer provides random read and write capabilities for S3
Documentation
# Reading outside the lock

`read` needs `&mut self`, so a caller sharing a handle behind a lock has one
object request in flight per handle at a time. `plan_reads` splits the read in
two: deciding where every byte comes from, which needs the file, and fetching,
which does not.

```rust
let plan = {
    let mut h = file.lock().await;
    h.fs_plan_reads(&[(off_a, len_a), (off_b, len_b)], &policy).await?
};                                        // the lock is released here
let mut out = vec![0u8; plan.len()];
plan.execute(&mut out, &permits).await?;  // &self: any number at once
```

The ranges are laid out in `out` one after another, in the order given, each at
its requested length. Bytes past the end of the file are zero; `plan.lens()`
says how many of each range are inside it, which is what `read` would have
returned.

On the generic `HyperFile` it is `plan_reads`; `Hyper` has `fs_plan_reads`.

## What fetching outside the lock relies on

**An object, once written, is never changed.** This is a guarantee, not a
property of the current code: every segment write is create-only
(`If-None-Match: *`, single put or multipart), a block that changes is written
to a new object, and the library deletes objects only in `unlink`. It holds
across the WAL, partial segments and `relocate`:

- a segment a WAL-protected flush is still uploading is not planned as a request
  at all — its bytes are taken from memory, and from storage only once it has
  landed;
- a partial segment is uploaded before any pointer to it exists;
- `relocate` writes the blocks it moves to a new object and leaves the old one;
- a failed or rolled-back flush leaves its objects unreferenced, not rewritten.

So a plan fetches exactly what its blocks held when it was made. Two things it
does not promise:

- **Writes after planning are not seen.** A write to the same handle between
  `plan_reads` and `execute` is not in the plan's bytes, as a read racing a
  write need not see it.
- **Objects removed from outside are not there.** A plan run after `unlink`, or
  after a cleaner pruned an object the plan names, fails with `NotFound`. Pruning
  only objects no inode reaches, and not while readers hold old plans, is the
  cleaner's side of this.

## What a plan holds

Planning walks the ranges block by block with the read path's own test of where
a block is — the same function `read` and `read_plan` use — and:

| block | in the plan |
|---|---|
| in the data cache | its bytes, copied now |
| a hole, a zero block, past the end | zeros |
| written by reference | read from the block source when the plan runs |
| in a segment still being uploaded (WAL) | read from that segment's memory when the plan runs, or from storage if it has landed and been released |
| on storage | a piece of a request |

Before that, the block map is searched for every block of every range at once,
a tree level at a time: each level's index nodes not yet in memory are fetched
together, at most `runtime.meta_read_max_concurrency` requests in flight
(default 32). On a cold handle whose map was written by many flushes, its nodes
are in as many objects; searched block by block, that was one request after
another. `read_plan_many` searches the same way.

The pieces on storage, from all the ranges together, are grouped by object and
sorted by offset there, then joined into requests by the policy. So blocks far
apart in the file and close in an object — a batch of random blocks that landed
in a few segments — are few requests, where planning each range alone would
make one each.

## The policy

```rust
pub struct ReadPolicy {
    pub max_gap: usize,             // default 0
    pub max_get: Option<usize>,     // default: read_get_max_bytes
    pub max_overread: Option<f64>,  // default: no limit
}
```

- **`max_gap`** joins two requests to one object across a gap of at most this
  many bytes, fetching the gap and dropping it. A gap of `g` bytes costs `g / B`
  at one connection's throughput `B`; a request costs its time to first byte
  and its charge. On a store with tens of milliseconds to first byte and tens of
  megabytes a second per connection, that puts the break-even near a mebibyte;
  on S3 Express much lower. It is the caller's to say because it depends on
  where the caller runs.
- **`max_get`** caps one request, gap included. It is also how to split for
  concurrency: a caller with more permits than requests passes a smaller value,
  computed from what it is about to read and what it can have in flight.
- **`max_overread`** caps the gap bytes of the whole plan as a share of the
  bytes it needs (1.0: as many again). Gaps are joined smallest first while
  this, `max_gap` and `max_get` allow, so the plan is the same every time.

The default is the read path's rule — requests touch or they are separate — with
one difference that only grouping makes possible: requests to one object that
touch are one, even when the file does not have them next to each other.

`plan.gets()` lists the requests and where each one's bytes go;
`needed_bytes()`, `overread_bytes()` and `local_bytes()` say what the plan
costs.

## Executing

`execute(&self, out, permits)` makes the requests with only a shared reference.
Each request holds a permit from `permits` while it is in flight, so one
semaphore bounds every plan that shares it — the caller decides how many are in
flight across its whole process. Requests count in the file's read timing as a
read's do. The first failure is the error, and `out` is then partly filled.

Bytes fetched across a gap are dropped, not cached. Putting them in the data
cache would take reading the segment's summary to name them, a map lookup per
block to check each still points there, and the file again to insert them.
`read_ahead` exists for warming the cache.

## Using it on the reactor

`plan_reads` is on the direct API. A handler-surface form is not there yet.