Skip to content

feat(cache): add byte-range support to Cache.Open#343

Closed
alecthomas wants to merge 2 commits into
mainfrom
aat/open-range
Closed

feat(cache): add byte-range support to Cache.Open#343
alecthomas wants to merge 2 commits into
mainfrom
aat/open-range

Conversation

@alecthomas

@alecthomas alecthomas commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator

Change the Cache.Open signature to Open(ctx, key, start, end) with
half-open [start, end) semantics; end == -1 reads to EOF, so a full
read is Open(ctx, key, 0, -1). Out-of-range starts return
ErrRangeNotSatisfiable and over-long ends are clamped, mirroring HTTP.

Backends: memory slices the buffer, disk uses io.NewSectionReader
(still returning the raw os.File for full reads to keep the
ServeContent sendfile path), and S3 issues a single ranged GET,
replacing the parallel range-get reader (its download-
tuning options
are now ignored and warn if set, pending a higher-level downloader).

Plumb ranges over HTTP: the client sends a Range header, Remote.Open
maps 206/416, and apiv1 serves 206 Partial Content via new
httputil.ParseByteRange/ServeCachePartial, evaluating preconditions
before the range per RFC 9110 §13.2.2.

Rework tiered backfill: a generation-keyed backfillManager dedupes
concurrent backfills per (namespace, key) and lets a Create cancel an
in-flight one. Full reads keep the tee-backfill but now commit only on
EOF (early close or mid-stream error discards the fragment); partial
reads trigger a background full backfill so a fragment is never stored
as the whole object.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

🤖 Generated with Claude Code

Change the Cache.Open signature to Open(ctx, key, start, end) with
half-open [start, end) semantics; end == -1 reads to EOF, so a full
read is Open(ctx, key, 0, -1). Out-of-range starts return
ErrRangeNotSatisfiable and over-long ends are clamped, mirroring HTTP.

Backends: memory slices the buffer, disk uses io.NewSectionReader
(still returning the raw *os.File for full reads to keep the
ServeContent sendfile path), and S3 issues a single ranged GET,
replacing the parallel range-get reader (its download-* tuning options
are now ignored and warn if set, pending a higher-level downloader).

Plumb ranges over HTTP: the client sends a Range header, Remote.Open
maps 206/416, and apiv1 serves 206 Partial Content via new
httputil.ParseByteRange/ServeCachePartial, evaluating preconditions
before the range per RFC 9110 §13.2.2.

Rework tiered backfill: a generation-keyed backfillManager dedupes
concurrent backfills per (namespace, key) and lets a Create cancel an
in-flight one. Full reads keep the tee-backfill but now commit only on
EOF (early close or mid-stream error discards the fragment); partial
reads trigger a background full backfill so a fragment is never stored
as the whole object.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alecthomas

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 481ea97889

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread internal/strategy/apiv1.go
Comment thread internal/cache/api.go Outdated
Address PR review feedback:

- apiv1 now evaluates If-Range: a stale validator serves the full
  current object (200) instead of a 206 of the new object, so a
  resuming client cannot append fresh bytes onto a stale prefix
  (RFC 9110 §13.1.5).
- resolveRange returns ErrRangeNotSatisfiable for any non-full range
  whose start is at or beyond the object size, including non-full
  ranges on empty objects, so local backends agree with the HTTP path
  (which returns 416 for bytes=0-0 on a zero-byte object).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alecthomas

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fc7fa1b216

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

return
}

if err := httputil.ServeCachePartial(w, r, headers, cr, start, end, size); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recompute Content-Range after opening

When a key is overwritten between Stat and Open, Open resolves/clamps the requested range against the new object but this response still advertises the start, end, and size computed from the old stat. For example, if an object shrinks from 100 to 50 bytes while handling Range: bytes=40-99, the body contains only bytes 40-49 while Content-Range says bytes 40-99/100, which breaks resumable clients that trust the advertised range. Retry/re-resolve when the opened representation differs from the stat result, or derive the advertised range from the opened object.

Useful? React with 👍 / 👎.

Comment thread internal/cache/tiered.go
// object. It is deduplicated and cancellable via the backfill manager. The
// background work uses a context detached from the request so it outlives it.
func (t Tiered) startBackgroundBackfill(ctx context.Context, key Key, src, dst Cache) {
bgCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Cancel background backfills before closing tiers

For partial range hits this detaches the full-object backfill from the request, but Tiered.Close only closes the underlying caches and never cancels or waits for these goroutines. If the cache is closed right after a range response while the backfill is still copying, the goroutine can continue writing into a closed backend (for example Memory.Close nils its map, and disk closes its metadata DB), causing panics/errors during shutdown. Track these background backfills and cancel/wait for them before closing the tiers.

Useful? React with 👍 / 👎.

@alecthomas alecthomas closed this Jun 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant