Skip to content

fix(storage): address sidecar review follow-ups - #330

Merged
avallete merged 5 commits into
mainfrom
avallete/storage-xattr-sidecar-followups
Sep 30, 2026
Merged

avallete merged 5 commits into
mainfrom
avallete/storage-xattr-sidecar-followups

Conversation

@avallete

@avallete avallete commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

TL;DR

Follow-ups to the non-blocking review notes on #329 (merged). A native write now also clears the stale sidecar copy, the sweep resumes inside a large shard instead of restarting it, the sweep lock is only released by its owner, and two comments and REPORT.md statements are corrected.

Before

flowchart LR
  A["Sweep cut short<br/>inside shard ab"] --> B["Cursor = ab"] --> C["Next wake restarts ab<br/>from its first entry"]
Loading

After

flowchart LR
  A["Sweep cut short<br/>inside shard ab"] --> B["Cursor = ab/last swept entry"] --> C["Next wake resumes<br/>after that entry"]
Loading

Why

These address jgoux's review comments on #329:

  • A multipart part rewritten in place from an engine with xattrs kept its old sidecar ETag. Switching back to a no-xattr engine then failed completion with InvalidChecksum.
  • A pass repeatedly cut short inside one shard restarted that shard on every wake and never reached later shards.
  • Two processes taking over one stale lock could delete each other's lock in finally.
  • Comments and REPORT.md claimed every path is fresh, but the Iceberg S3 PutObject route is unversioned. They also misdescribed how a relative storage root behaves.

What changed

  • setAttributeSync: after a successful native set it drops the sidecar copy of that attribute, sharing one helper with removeAttributeSync.
  • Sweep cursor: it records <shard>/<last swept entry> at every batch pause and resumes after that entry, walking each shard in sorted order.
  • Sweep lock: it holds a random token, and a process only releases the lock if the token is still its own. A takeover race stays documented as best-effort; it only duplicates removals that are harmless to repeat.
  • Comments and REPORT.md: they now name the unversioned Iceberg path and its narrow race with the sweep, and state that a relative root is resolved against the working directory, so the original ENOTSUP remains.
  • Sweep lock refresh: the lock is refreshed after each pause too, so a slow batch can't let it go stale while the sweeper is live.
  • Unit tests: new cases for the native rewrite dropping the stale copy, resuming inside a shard, and writing the cursor at a pause while keeping another process's lock (20 tests).
  • REPORT.md: records the remaining limitation: a multipart part rewritten in place under Docker Desktop keeps its stale native etag on an engine with xattrs, which takes two engine switches within one upload.

Reviewer notes

  • Validation-only release runs passed on e668239 (same tree as the head before the test commit), including the delete-then-sweep smoke step on both Linux targets: v1.79.27, v1.79.21. The head (7d4a761) also passed a v1.79.27 run.
  • Cost: a native set now reads the sidecar file too. That's normally an immediate ENOENT, so only one extra syscall per attribute write where xattrs work.

Part of supabase/cli#6900

🤖 Generated with Claude Code

avallete and others added 4 commits September 30, 2026 14:40
Storage's file backend keeps object content type, cache control and
multipart part etags in extended attributes. Docker Desktop's macOS file
sharing rejects them with ENOTSUP, so every upload into a bind-mounted
uploads directory failed.

Replace the bundled fs-xattr package with a wrapper that delegates to the
real addon and, on ENOTSUP, stores values in a JSON sidecar under
<storage root>/.slim-xattr. Native misses also consult the sidecar so a
directory keeps its metadata when served by an engine with xattrs. The
build fails if fs-xattr changes its entry point or exports.

The image smoke forces the sidecar path, then re-serves the same volume
with native attributes and checks the stored headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deletes, new object versions and multipart parts never go through
fs-xattr, so their sidecars stayed forever. Docker Desktop dogfooding
saw 9,026 sidecars for 492 live objects, and small operations slowed
down as the single directory grew.

Shard sidecars into 256 subdirectories and sweep entries whose file is
gone. The sweep never competes with requests: it starts 30 s after the
process loads (each wake from idle sleep is a fresh container), repeats
hourly, uses async I/O one entry at a time, and pauses after every 50
entries, longer when the filesystem is slow. A cursor resumes a pass cut
short by idle sleep, and a short-lived lock keeps one sweeper per
directory.

The smoke deletes an object in sidecar mode and checks that a restarted
container sweeps its sidecar while live metadata survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- A successful native set drops the sidecar copy of that attribute, like
  remove already did. Multipart parts are rewritten in place, so a part
  uploaded without xattrs and re-uploaded with them no longer answers
  with its old ETag after switching back.
- The sweep cursor records the last swept entry at every batch pause, so
  a pass cut short inside a large shard resumes there instead of
  restarting the shard.
- The sweep lock holds a token, so a process only releases a lock it
  still owns.
- Comments and REPORT.md no longer claim every path is fresh (the
  Iceberg S3 PutObject route is unversioned), and describe how a
  relative storage root behaves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r-sidecar-followups

# Conflicts:
#	services/storage/REPORT.md
#	services/storage/fs-xattr-fallback.test.ts
#	services/storage/overlay/fs-xattr/index.js
Refresh the sweep lock after each pause as well as before it, so a slow
batch cannot let it go stale while the sweeper is still live. Record the
remaining multipart etag limitation after two engine switches in
REPORT.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@avallete
avallete merged commit 5ad31f4 into main Sep 30, 2026
10 checks passed
@avallete
avallete deleted the avallete/storage-xattr-sidecar-followups branch September 30, 2026 16:46
TFSebben pushed a commit to TFSebben/supabase_cli that referenced this pull request Sep 30, 2026
…upabase#6921)

## Summary

On Docker Desktop for macOS, every upload to a stack's Storage on the
Docker runtime failed with 500 `ENOTSUP`. Storage's file backend keeps
object metadata in extended attributes, and Docker Desktop's file
sharing drops them on bind mounts. This pins the stack's Storage to slim
`v1.79.28-r1`, whose `fs-xattr` wrapper falls back to sidecar metadata
files when xattrs are rejected. The bind mount at
`supabase/.temp/stack-uploads/<stack-id>` stays as it is. This replaces
the engine-volume workaround in supabase#6908.

### Before

```mermaid
flowchart LR
  A["Upload to stack Storage<br/>(Docker Desktop macOS)"] --> B["storage v1.73.0<br/>fs-xattr setAttributeSync"]
  B -->|ENOTSUP on bind mount| C["500 on every upload"]
```

### After

```mermaid
flowchart LR
  A["Upload to stack Storage<br/>(Docker Desktop macOS)"] --> B["storage v1.79.28-r1<br/>fs-xattr wrapper"]
  B -->|xattrs work| C["native attributes"]
  B -->|ENOTSUP| D["sidecar under<br/>stack-uploads/.slim-xattr"]
```

### What changed

- **Storage pin:** `packages/stack/src/Artifacts.ts` moves from
`v1.73.0` to `v1.79.28-r1`, pinned by digest. This is the first catalog
pin to a revisioned slim release, and the release tag, asset names,
native OCI tags and image tag all follow the `v1.79.28-r1` form the
catalog already derives.
- **Fix location:** the fix lives in the slim build, in
supabase/slim-services#329 and supabase/slim-services#330.
- Native xattrs stay authoritative wherever they work: Linux, OrbStack
and Windows Docker Desktop.
- On `ENOTSUP`, metadata goes to `.slim-xattr/<2 hex>/<sha256>.json`
under the uploads directory.
- A throttled background sweep removes sidecars for deleted objects
without blocking requests.
- **`supabase services`:** in stack mode it reports catalog versions
without the slim revision suffix. A linked project on the same upstream
release, for example `v1.79.28`, therefore doesn't show a version
mismatch. `SIDE_EFFECTS.md` is updated to match.

### Reviewer notes

- The Storage version jump from `v1.73.0` to `v1.79.28` was compared on
Docker Desktop against the pinned image: status codes and headers
matched across a broad set of Storage API scenarios, both on native
volumes and through the sidecar.
- Known limitations of the fallback are recorded in slim-services
`services/storage/REPORT.md`.
- An object uploaded where xattrs work, then read under Docker Desktop,
is served as `application/octet-stream`.
- A multipart part rewritten across two engine switches within one
upload keeps a stale etag.
- The legacy `supabase start` Dockerfile pins
(`supabase/storage-api:v1.77.0`) are unchanged. It mounts Storage from a
named volume, so it isn't affected.

## Linked issue

Closes supabase#6900

- [x] The linked issue is **open** and carries the
`open-for-contribution` label (or I'm a Supabase maintainer).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants