* s3: stop warming every bucket's config at startup
Listing all buckets in BucketRegistry.init() made S3 gateway startup
O(buckets) and pinned every bucket's metadata and config resident, which
does not scale past a few hundred thousand buckets. Both caches already
have lazy miss paths, so load on first access instead and let the
metadata subscription refresh only entries already resident; cold
buckets cost one filer round-trip on their first request.
* s3: bound the per-bucket caches with LRU eviction
The bucket config cache, bucket registry, and their negative caches were
plain maps that only ever grew: the config cache TTL made Get miss but
never evicted the entry, and the not-found sets grew on every probe of a
nonexistent bucket name. Cap all four at 65536 entries with LRU eviction
so a gateway keeps its hot working set and evicted buckets reload from
the filer on next access.
* s3: cache parsed bucket config instead of the full filer entry
Each cached BucketConfig retained the whole bucket entry (extended
attribute map plus raw content bytes) alongside the fields parsed from
it, roughly doubling per-bucket cache cost and keeping data the read
path never looks at. Parse everything up front in
newBucketConfigFromEntry - now also the creator identity, tags,
encryption config, and stored lifecycle XML - and drop the entry.
updateBucketConfig now reads the entry fresh from the filer and diffs
the mapped extended attributes against it, so the patch is computed
against current state instead of a cached copy; the config clone
helpers that existed for that path go away.
* s3: dedup cold bucket-registry loads per bucket
The registry's notFound lock doubled as the load serializer, holding one
global mutex across the filer round-trip so first-touch requests for
different buckets queued behind each other; the cache fill also happened
after the lock was released, so two concurrent misses for the same
bucket could both reach the filer. Replace it with a singleflight per
bucket that fills the cache inside the flight: different buckets load
concurrently, the same bucket loads once.
PutBucketLifecycleConfiguration used to install /buckets/<bucket>/<prefix>
day-TTL entries in filer.conf so the volume server's RocksDB compaction
filter would expire matching writes. With 9377 the s3api server now stamps
volume TTL per-write via LifecycleTTLResolver off the stored XML, which
covers the same prefix-only Expiration.Days subset and additionally
handles size filters and AWS overlapping-rule precedence. Maintaining
both paths means a rule change has to mutate two stores in lockstep, and
the filer.conf path can't represent everything the resolver does.
Drop the add path. Keep a one-way cleanup loop so an upgrade still wipes
day-TTL entries written by older builds — otherwise a stale entry would
silently double-stamp writes (volume server expires under the old rule)
or contradict the new XML after a rule change.
Also removes resolveLifecycleDefaultsFromFilerConf (no longer needed) and
the versioning-fast-path guard (the resolver itself returns nil for
versioned/object-lock buckets, covered by
TestNewLifecycleTTLResolver_NilOnVersionedBucket). Tests covering the
deleted helpers are deleted with them; the GET fallback that synthesizes
lifecycle rules from existing filer.conf TTLs is unchanged so users who
historically configured TTL via filer.conf directly still see a rule.
* s3api: skip TTL fast-path for versioned buckets (#8757)
PutBucketLifecycleConfiguration was translating Expiration.Days into
filer.conf TTL entries for all buckets. For versioned buckets this is
wrong:
1. TTL volumes expire as a unit, destroying all data — including
noncurrent versions that should be preserved.
2. Filer-backend TTL (RocksDB compaction filter, Redis key expiry)
removes entries without triggering chunk deletion, leaving orphaned
volume data with 0 deleted bytes.
3. On AWS S3, Expiration.Days on a versioned bucket creates a delete
marker — it does not hard-delete data. TTL has no such nuance.
Fix: skip the TTL fast-path when the bucket has versioning enabled or
suspended. All lifecycle rules are evaluated at scan time by the
lifecycle worker instead.
Also fix the lifecycle worker to evaluate Expiration rules against the
latest version in .versions/ directories, which was previously skipped
entirely — only NoncurrentVersionExpiration was handled.
* lifecycle worker: handle SeaweedList error in versions dir cleanup
Do not assume the directory is empty when the list call fails — log
the error and skip the directory to avoid incorrect deletion.
* address review feedback
- Fetch version file for tag-based rules instead of reading tags from
the .versions directory entry where they are not cached.
- Handle getBucketVersioningStatus error by failing closed (treat as
versioned) to avoid creating TTL entries on transient failures.
- Capture and assert deleteExpiredObjects return values in test.
- Improve test documentation.