mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-30 08:50:48 +01:00
a2496ad7ae4de6f62bccb4b265fcf7817115fd7e
10385
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
94e9b8a49a |
fix(volume): return conflict and explicit proof for read-only upload refusals (#12236)
* fix(volume): report read-only HTTP refusals in Rust * fix(volume): mirror HTTP write refusal proof in Go * test(volume): cover HTTP read-only refusal parity * test(volume): cover local refusal after remote append * fix(volume): honor decoded replica controls before retry proof * fix(operation): reassign proven HTTP conflict refusals * fix(operation): stop same-target retries on HTTP conflict * fix(volume): replace replica marker when forwarding requests * fix(java): gate conflict reassignment on refusal proof * fix(volume): preserve authentication query encoding in replication * fix(operation): stop unproven conflicts in forever retries |
||
|
|
9ee8604ff3 |
webdav: answer 204 to a PUT that replaces a file (#12233)
* webdav: answer 204 to a PUT that replaces a file * webdav: decide the 204 from what OpenFile actually replaced A Stat before the write left a window where a delete or create in between made the status wrong, and it cost a second filer lookup. The OpenFile create path already knows whether it removed an existing entry, so it now marks the request context and the writer maps 201 to 204 only on a real replace. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test/webdav: cover OpenFile's own put-outcome flag for both create paths Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * webdav: take the put outcome from the create's OldEntry A stat-then-create still races: a second writer landing between the check and the write makes the reported status describe a state the write never saw. The filer decides create-vs-replace under its per-path lock and reports the previous entry in the response's metadata event, so flag the PUT replace from OldEntry instead of a pre-write stat. The in-place update keeps the replaced entry's crtime and inode and reuses the filer's own chunk cleanup, so the separate removeAll goes away, and an exclusive create now defers to the filer's atomic O_EXCL instead of a racy local check. * webdav: send no body with the 204 a replacing PUT answers --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
cba409cae7 |
backup: carry index-only tombstones through incremental copies (#12231)
* volume server: add idx_content to VolumeIncrementalCopyResponse A remote-tier delete is an index-only tombstone row with no .dat record, so it cannot reach an incremental copy through the .dat stream. Carrying the .idx tail verbatim lets the backup reproduce those rows at their exact positions. * backup: carry index-only tombstones through incremental copies A remote-tier delete leaves an offset-0 tombstone in .idx with no .dat record. The copy only streamed .dat bytes and regenerated .idx from them, so the tombstone never reached the backup: the deleted needle stayed live and every later incremental diverged and rebuilt again. backupCursor and ResumeByLastEntry now accept an index-only tombstone as a boundary, matched by key and size since there is no record to read a cookie from. VolumeIncrementalCopy appends the .idx tail after the .dat tail, and IncrementalBackup writes it at the index boundary, truncates, and loads the new rows — so tombstones land at the exact positions the replica has them. * seaweed-volume: mirror index-only tombstone handling in incremental copy resume_by_last_entry accepts an offset-0 tombstone boundary matched on key and size, skips its missing cookie check, and resumes .dat streaming at the next entry that has a record — index-only rows ride the new idx_content stream instead of reporting divergence. The server captures the .idx bound before the .dat one (a row is published only after its bytes) and streams the tail after the data. * backup: keep incremental copy bounds and streams consistent Review fixes: * Bound capture moved after the resume reads and reads .dat/.idx sizes together (Go FileStat, Rust file_stat under one lock), so every row resume saw is inside the index bound and each row's record is always inside the data bound. * A compaction commit changes CompactionRevision: the server now rejects the copy when the revision moved between resume, file opens, or the end of the stream, so a backup never mixes revisions. The .idx is opened up front to pin the inode. * An index tail that ends before the captured bound is an error, not a silent success. * IncrementalBackup rolls the .idx tail and .dat bytes back to the pre-copy boundary when the stream fails, so a partial row cannot wedge backupCursor and unapplied rows cannot pass for current ones. * The index tail is replayed through a small indexTailReceiver interface, covering the leveldb needle maps too, not just the in-memory map. * backup: bound the resume scan to the captured index size The resume and the stream bounds are now captured in one order: revision, then the .dat/.idx pair, then the scan. ResumeByLastEntry and resume_by_last_entry take the index bound and scan no further, so a row appended while the copy is being set up can never enter the index stream without its .dat bytes. The index-tail replay stays on the in-memory map only: the leveldb map's DoOffsetLoading writes to the .cpldb compaction database, not the live one, so it cannot serve a live tail replay; other maps keep regenerating the index from .dat as before. * seaweed-volume: guard incremental copy total against a located offset past .dat A corrupt index can place the resume offset beyond the captured .dat size; has_data already refuses the reader, but total still evaluates dat_size - s and underflows. * backup: bound the incremental .dat tail by index-covered records A record staged but not yet indexed can sit inside the captured .dat bound while its row stays outside the index bound — the fsync gap between staging and publishing is not covered by one stat. Streaming those bytes leaves them unindexed on the backup, and the next run appends the record again. Bound the .dat tail by the last record the streamed rows cover on both producers, and cover the bound walk with tests. * backup: cap the .dat stream at the captured bound and roll back replay failures sendFileContent could read past stopOffset on its last chunk, so a needle appended after FileStat arrived without its index row and the next copy appended at the wrong position. Each read is now limited to the remaining captured bytes. A failed truncateIndex or DoOffsetLoading after a successful stream left the index tail on disk unapplied, which a later backupCursor could mistake for applied rows; both now roll the tail back to the pre-copy boundary like the stream-error path. * backup: bound the incremental .dat tail by the maximum covered offset A key-sorted .idx ends on the highest-key row, which need not be the record physically last in .dat, so the last-row bound could cut off live records. Take the end of the highest-offset row inside the bound instead, and cover the case with a sorted-idx test in Go and Rust. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * backup: restore .dat and the needle map when index replay fails A failed DoOffsetLoading left the received .dat bytes in place and could leave partial mappings for rows the index rollback just removed, so the next copy appended the same records again or read stale entries. Truncate .dat back to the copy boundary and restore the touched mappings and metrics from a pre-replay snapshot. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * backup: cover no index rows with the superblock bound When no row inside the index bound names a record — an empty index or a bound of index-only tombstones — the covered end is the superblock, not the captured .dat size, so staged bytes are never streamed without a covering row. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * seaweed-volume: apply the covered bound even when the index streams no rows The bound was gated on idx_end > idx_start, so an empty captured index skipped it entirely and a staged .dat tail still shipped. Gate on the stream-index predicate instead, matching Go's streamIndex check. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * backup: propagate index coverage read errors * backup: rebuild the needle map from the truncated index on replay failure * seaweed-volume: propagate index coverage read errors --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
0a4668fcfa |
webdav: overwrite on MOVE when the Overwrite header is missing (#12232)
* webdav: overwrite on MOVE when the Overwrite header is missing * webdav: defer a MOVE destination's delete into the rename x/net removes an existing destination before Rename, so a refused or failed move — a CanRename rejection, a vanished parent, a transient filer error — loses the target entirely. For an overwriting MOVE onto an existing destination, run the request against a filesystem that skips the destination RemoveAll and lets the filer's rename replace the target inside one transaction instead; a file vs. collection clash still takes the delete + rename path. Rename itself now tolerates an existing target, which the atomic filer rename already handles. * webdav: only default Overwrite when the header has no values An Overwrite header sent with an empty first value makes Header.Get return "", which the defaulting treated as absent and rewrote to T — overriding a client's explicit F in a later value. Check the value list instead so a present-but-empty header is left alone. * webdav: move a collection onto a non-empty one via delete + rename deferredDeleteFS skips the destination RemoveAll so the filer rename can replace the target in one transaction, but the filer only replaces an empty collection: moveSelfEntry deletes the target non-recursively and refuses ErrNonEmptyFolder. Detect a non-empty collection destination and take the depth-infinity delete + rename path moveFiles would have run. * webdav: park a clashing MOVE target aside until the rename lands A refused rename (cross-collection move, bucket target) after deleting the destination lost its contents; the target now renames to a hidden sibling first and is restored when the winning rename fails. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * webdav: restore a parked MOVE target under a detached context A client disconnect cancels the request context mid-move, which also canceled the restore and stranded the destination under its hidden sibling name; the restore and parked-entry removal now run under context.WithoutCancel with a bounded deadline. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * webdav: bound the parked MOVE target's restore after the move The detached restore deadline started before the source rename, so a long move could exhaust it; each detached context now starts where its work begins. A failed parked-entry removal retries briefly and its warning names the parked sibling for recovery. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * webdav: restore a parked MOVE target without replacing a new claimant A client can create the destination name while the old target is parked, so the restore rename must fail instead of replacing that new entry. * webdav: propagate RemoveAll's context to the filer delete The parked-target cleanup runs under a bounded detached context, but the delete RPC discarded it; passing ctx through makes the deadline effective. --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
2a1afadcd0 |
s3api: return NoSuchUpload when aborting an upload that was already completed (#12227)
s3: AbortMultipartUpload answers NoSuchUpload for an upload that no longer exists
Complete and abort both remove .uploads/<id>. abortMultipartUpload answered
204 whenever that record was missing, so aborting an upload that had already
completed reported success while the object stayed in place, and a client
could read it as the object being discarded. AWS returns 404 NoSuchUpload
("The specified multipart upload does not exist") for that case, and
ListParts here already does.
Answer ErrNoSuchUpload when the record is not found. checkUploadStillOpen
reuses abort for cleanup after the upload is gone; NoSuchUpload is not a
failed cleanup there.
The leftover-directory path from #11385 is unchanged: a .uploads/<id> that
outlived its completed object is still removed metadata-only and answered.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
a2f595b207 |
backup: don't Close a volume after Destroy (#12225)
Co-authored-by: user.email <chrislusf@users.noreply.github.com> |
||
|
|
349576b63e |
backup: resume incremental copies by index position instead of appendAtNs (#12228)
* volume server: add last-entry cursor fields to VolumeIncrementalCopy * backup: resume incremental copies by index position instead of appendAtNs appendAtNs is stamped by whichever replica received the write, so the same needle carries a different timestamp on every replica. A backup that resumes from a different replica than last time can append the last needle again or skip needles written inside the clock gap. Send the local copy's index entry count and last entry identity (key, size, cookie) with VolumeIncrementalCopy. The replica verifies the same record sits at that position and streams from the next index entry, so the resume no longer depends on any replica's clock. When the boundary record does not match, or the local copy is ahead, the replica returns FailedPrecondition and the backup rebuilds the local copy from scratch. Old servers ignore the new fields and keep the timestamp behavior. * rust volume server: mirror positional resume in VolumeIncrementalCopy * volume server: add entries_hash prefix checksum to VolumeIncrementalCopy * backup: verify the whole index prefix, not just the boundary entry * rust volume server: verify the index prefix hash in resume_by_last_entry * backup: keep the source's compaction revision on rebuilt copies * backup: diverge on a zero-offset boundary entry A remote-tier tombstone lives in the .idx with no .dat record, so readNeedleAt(0) parses the superblock and its error bypassed the ErrIncrementalCopyDiverged mapping; the backup got a generic failure instead of FailedPrecondition and never rebuilt. * rust volume server: mirror zero-offset boundary divergence * rust volume server: gate the empty-dat early return on entry_count With a positional resume the Go handler still reaches ResumeByLastEntry, so an empty replica reports divergence; the Rust early return answered a clean empty stream and weed backup kept a stale copy. * backup: hash the index entry's size field, not trailing offset bytes In a 5BytesOffset build the offset occupies bytes 8-12 and the size starts at byte 13, so the hash folded in one byte of the offset. Matching records at different offsets then hashed differently and a valid backup was reported diverged and rebuilt. * backup: read the index in whole-entry chunks for the prefix hash When entries are 17 bytes a 1 MiB read buffer does not end on an entry boundary: the partial tail entry was skipped by the loop but still counted as consumed, so every later chunk was hashed at a misaligned position. * rust volume server: hash the index entry's size field after the offset Mirrors the Go hashIndexEntries change: with the 5bytes feature the offset occupies bytes 8-12, so starting the size slice at byte 12 folded one offset byte into the hash and matching records at different offsets compared diverged. * rust volume server: read the index in whole-entry chunks for the prefix hash Mirrors the Go change. With 17-byte entries a 1 MiB buffer does not end on an entry boundary, so slicing the partial tail entry panicked once a backup held 61681 entries. * rust volume server: default the new incremental copy request fields in tests |
||
|
|
d0441bbdbb |
s3vectors: validate index ARNs and expand live integration coverage (#12222)
* test: align vector bucket policy coverage with vector actions * s3vectors: validate ARN service and resource shape before resolving indexes * test: exercise signed vector bucket policies and catalog lifecycle * ci: require the expanded vector integration matrix and all gateway tests * test: reject index ARNs in bucket-level API requests * s3vectors: scope ARN resolution to the gateway namespace * s3vectors: reject name fields that conflict with resolved ARNs bucketARNs and resolveIndexARNs already reject an indexArn that names a different bucket than vectorBucketArn, but a vectorBucketName or indexName disagreeing with the ARNs was silently dropped. Validate the plain names against the resolved ARN values the same way. * test: cover conflicting vector names and ARNs in live requests --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
cc5912b8b8 |
s3vectors: e2e conformance for vector bucket policy evaluation (#12221)
* s3vectors: reject empty boolean-operator lists and sub-objects in filters An empty $and/$or list or a nested empty object rendered '()'' — invalid SQL surfaced as an engine error instead of a clean ValidationException. * s3vectors: e2e conformance for vector bucket policy evaluation The handler-level test covers statements; this runs the whole stack a client sees — HTTP dispatch, authentication, policy evaluation, catalog, engine — and walks the full lifecycle: a non-owner is denied without a grant, an Allow on the bucket ARN grants every index under it, a Deny vetoes an Allow on the same action, and deleting the policy restores owner-only access. * s3vectors: prove deny precedence and grant removal in policy e2e * s3vectors: reject field operator objects with no operators * test: use an s3vectors action in the vector policy round-trip |
||
|
|
9d5929a73f |
s3vectors: reject empty boolean-operator lists and sub-objects in filters (#12220)
* s3vectors: reject empty boolean-operator lists and sub-objects in filters An empty $and/$or list or a nested empty object rendered '()'' — invalid SQL surfaced as an engine error instead of a clean ValidationException. * s3vectors: reject field operator objects with no operators |
||
|
|
3f954e34dc |
s3api: label bucket-level requests correctly in the access log (#12224)
* s3api: label bucket-level requests correctly in the access log getResourceType treated a request as bucket-level only when the object key was "/". NormalizeObjectKey strips the leading slash, so bucket routes arrive with an empty key and every bucket operation was logged as an object operation: ListObjectsV2 as REST.GET.OBJECT, DeleteObjects as REST.POST.OBJECT, and the lifecycle, ACL and policy calls likewise. Treat an empty object key as bucket-level too. The new test covers five bucket-level and two object-level request shapes; it fails without the fix. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * s3api: label only POST ?delete as a batch delete in the access log With bucket-level requests now reaching the bucket branch, any method on /bucket?delete was labelled BATCH.DELETE.OBJECT. Only the POST route is DeleteObjects; label other methods as the plain bucket operation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
fd493f2547 |
mount: refuse to attach opens to a draining file handle (#12219)
* mount: refuse to attach opens to a draining file handle With the writeback cache, releasing the last open of a file leaves the shared FileHandle in the map at counter zero while an async flush drains it; the worker removes the map entries once the flush commits. AcquireHandle waits for a pending flush and then attaches, but the two steps are not atomic: a release can slip in between, drop the counter to zero, and queue the drain while the opener goes on to mint a new file-handle id on the draining handle. When the drain completes, RemoveFileHandle deletes every fh id for the inode -- including the one just minted. The kernel still holds that id, so its next FUSE_READ gets ENOENT from GetHandle; the failed read leaves the folio not uptodate and the kernel surfaces EIO to userspace. This was the flaky TestWritebackCacheConcurrentMixedOps failure. AcquireFileHandle now refuses to attach when the shared handle has reached a zero counter and reports it as draining. AcquireHandle and the Create path retry the whole wait-resolve-attach sequence; the next pass blocks on the pending flush (flag registration and the counter drop share pendingAsyncFlushMu) and builds a fresh handle from the post-flush entry. The DLM-failure release in AcquireHandle also goes through ReleaseHandle so it cannot strand a counter-zero handle outside the drain bookkeeping. Removes the refreshEntryForRead retry from #12206, which worked around the symptom by refetching the entry after the fh id was already gone. Test: TestAcquireFileHandleRefusesDrainingHandle covers the refusal and fresh-handle path; TestWritebackCacheConcurrentMixedOps and an amplified concurrent-overwrite repro run clean. * mount: keep refresh-on-error retry for stale-chunk reads |
||
|
|
ac7b4f4cf6 |
s3vectors: bound ListVectorBuckets and ListIndexes maxResults (#12218)
* s3vectors: bound ListVectorBuckets and ListIndexes maxResults Both pass maxResults to the catalog unbounded; AWS caps these list pages at 100, the same way ListVectors is bounded and topK is bounded. * s3vectors: raise ListVectorBuckets/ListIndexes maxResults cap to 500 AWS documents a valid range of 1-500 for both list APIs; the shared limit of 100 rejected legitimate page sizes. * s3vectors: reject explicit maxResults of zero on list calls AWS requires maxResults >= 1, but a plain int field cannot tell an omitted field from an explicit zero. Making MaxResults a pointer keeps the catalog default for omitted fields while a supplied zero fails validation. * s3vectors: test max ListIndexes page against an existing bucket |
||
|
|
f122f313e1 |
s3tables: keep the admin bypass off an identified caller's empty action list (#12217)
Generated with [Devin](https://devin.ai) Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
229ad84e2d |
s3tables: reject s3vectors resources in every ARN partition (#12215)
* s3tables: reject s3vectors resources in bucket policies An arn:aws:s3vectors: Resource is as dead as an s3vectors action: the evaluator compares against the generated s3tables ARN, so the rule the writer believes enforced never matches. * s3tables: reject s3vectors resources in every ARN partition * s3tables: test wildcard-partition and mixed-list s3vectors resources |
||
|
|
8a2fec8610 |
filer.sync: skip superseded replays before fetching any chunks (#12212)
* filer.sync: skip superseded replays before fetching any chunks On a directory under heavy overwrite, most entries the sync replays are already superseded on the source. The sink only learned that after chunk reads failed — or worse, after fetching and uploading bytes a later event replaces — so the backlog drained slowly and the log filled with 404s. CreateEntry and UpdateEntry now check sourceMovedOn before touching a chunk: one entry lookup skips a stale replay entirely. A mid-replication supersession is still caught by the existing retry gates. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * replication: bound the source liveness probes with a timeout The supersession checks added a source entry lookup to the replication path, and every lookup ran on context.Background() with no deadline. A source that accepts the call but whose metadata store stalls would hold the sync forever — before a single chunk fetch, even when every chunk is still readable. Bound the probe at 30s; an unanswered lookup proves nothing, so replication proceeds and the existing retry gates decide. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * replication: sync the live version a superseded replay points at Skipping a stale replay assumed a later event always re-carries the current content. It does not when the superseding write was itself replicated in from another cluster: those events are dropped before they reach a filer sink, so the destination could be left missing the file. When the source reports a live entry newer than the replay, commit that version through the normal path instead — equivalent to applying the later event, with no dependence on its delivery. A gone source entry still skips, since the destination already matches the source. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * replication: reconcile moved entry content after a native rename AtomicRenameEntry relocates whatever version the destination held. If the preceding update replay was skipped because the source had already moved to the new name, the moved entry keeps the old bytes and — with no further writes — stays stale forever. MoveEntry now runs the same reconciliation as an in-place update against the rename's snapshot, so the moved entry picks up pending content changes. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * replication: repair from the live snapshot a superseded replay resolves to An incremental diff against the replayed oldEntry can resolve manifests the overwrite already vacuumed, which fails every retry of the event. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
98fe70a247 |
s3vectors: keep bucket policies in vector form and evaluate them in the gateway (#12214)
* s3vectors: translate vector bucket policies to the catalog form The document is stored verbatim on the shared table bucket and evaluated against s3tables actions and ARNs, so a policy written for the vector API was a dead rule. Actions now map to the catalog operation the request authorizes against, and an arn:aws:s3vectors: resource becomes the bucket/.../table/vector/... ARN the index's catalog entry answers to. * s3tables: expose PrincipalFor and PolicyContext for façade evaluation A façade authorizing against its own action namespace needs the same identity resolution and evaluation context the catalog uses internally. * s3vectors: keep bucket policies in vector form and evaluate them in the gateway The earlier approach translated the document to catalog actions and ARNs, which lost the caller's original policy for GetVectorBucketPolicy and could not cover data-plane calls that never touch the catalog. The document now stays in s3vectors form under a dedicated bucket attribute and every handler evaluates it through authorizeVector against the action and ARN the request names; a dedicated key also keeps a vector grant from ever authorizing a catalog call. * s3vectors: close policy-mutation races and keep vector grants visible - Authorize Put/DeleteVectorBucketPolicy inside the policy slot's compare-and-swap, so a grant revoked between reads cannot authorize the pending write. - Assert the policy attributes stay absent in the update precondition, so a concurrent policy or tag write trips the retry instead of being silently reverted. - Migrate a raw vector document out of the shared policy slot on replace or delete, keeping its catalog rules. - Pass the unresolved identity to PolicyContext so open-by-default deployments still work. - Filter ListVectorBuckets on the vector policy itself, so buckets a caller was granted under s3vectors actions stay listed. * s3vectors: reuse SnapshotExtended for the vector-policy CAS precondition * s3vectors: skip buckets that vanish mid-list instead of failing the page * s3vectors: reject non-vector bucket formats inside authorizeVector * s3vectors: check bucket format after authorization in authorizeVector An unauthorized caller probing an ICEBERG bucket got NoSuchBucket,\nleaking that the bucket exists. The format check now runs after the\npermission check so denied callers always see AccessDenied. * s3vectors: cover legacy shared-slot policy removal on delete * s3tables: resolve the forwarded identity name like getAccountID in PrincipalFor A synthesized manager request resolves its caller through the identity- name branch of getAccountID, which normalizes the name (ARNs collapse to their trailing segment, whitespace trims) before falling back to the handler's account ID. PrincipalFor returned the raw name, so ARN-shaped identities never matched their own grants. * s3tables: assert the vector policy key in update preconditions s3vectors-policy is stored on table bucket entries but was missing from s3tablesExtendedKeys, so a catalog update that read the entry before the first vector policy was saved could overwrite it without tripping the compare-and-swap. Asserting the key makes such a race fail instead of silently erasing the policy. * s3vectors: reject rules the evaluator cannot honor in vector bucket policies The shared Statement shape has no negated fields, the vector evaluation supplies no condition keys, and a document that fails the typed decode is discarded wholesale at evaluation — including denies the writer believed active. Reject NotPrincipal, Condition, foreign-service actions and resource ARNs, and require the typed PolicyDocument decode. Also cover that an admin-named identity still reaches its own buckets. * s3vectors: read vector policy fields the way the evaluator decodes them Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3tables: match ARN policy principals through caller normalization Caller principals are normalized ids, so a statement naming a full IAM ARN could never match: an ARN-scoped deny was silently dead and an ARN-scoped allow unreachable. Compare arn: specs against the caller under the same normalizePrincipalID the ownership path uses, while an ARN caller keeps verbatim wildcard matching. * s3tables: grant the admin bypass on the caller's actions, not the name A principal that only normalizes to the admin id skipped every policy deny. When the request carries an identity, the bypass now requires the identity's own admin action — the same gate getAccountID applies — while name-only transports with no action list keep it. PolicyContext takes the request so it can carry IdentityActions, and the vector context helpers route through the shared identity keys so the resolved name and actions agree. * s3tables: match ARN policy principals only for deny statements Normalization drops the ARN's account and path, so an allow matched on the trailing name could grant a same-named caller the statement never verified. Denies keep the normalized match so the rule cannot be ducked through the same collapse. * s3tables: scope service wildcards to their own service s3tables:* counted as blanket admin inside the shared permission check, so an identity holding only catalog grants administered s3vectors operations too. hasAdminActionFor now requires the wildcard to name the operation's service; the global admin markers and s3:* stay service-agnostic. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
4ef123d764 |
s3vectors: fix shared API behavior and test Go/Rust volume parity (#12216)
* s3vectors: reuse shared authenticated identity context * s3vectors: encode empty results as arrays * test: cover S3 Vectors with Go and Rust volume servers * ci: require both volume implementations in vector integration tests * s3vectors: keep identityOf as a shim over the shared identity context Concurrent branches add identityOf call sites; deleting the helper would break the build when they merge in either order. Route it through GetIdentityNameFromContext so every caller resolves the same identity. * test: wait for the vector engine to register before running The gateway resolves weed-vector lazily through the master cluster list, so a listening port does not mean the engine is reachable yet; the first vector write or query could race KeepConnected and fail. Restore the cluster.ps registration wait the previous harness had, and ignore the pytest cache directory the suite creates. * test: wait for master registration across OSS and enterprise Use the exact engine registration event after AddClusterNode completes. The current enterprise cluster.ps does not list vector nodes, so waiting on its output prevents the shared fixture from starting there. * s3vectors: keep contextWithIdentity as a shim alongside identityOf In-flight callers still write the caller name through it; route the write through SetIdentityNameInContext so both helpers agree on the shared key whichever lands first. |
||
|
|
88565df885 |
s3tables: reject s3vectors resources in bucket policies (#12213)
An arn:aws:s3vectors: Resource is as dead as an s3vectors action: the evaluator compares against the generated s3tables ARN, so the rule the writer believes enforced never matches. |
||
|
|
15066c8a9f |
vector: implement drop_index and have DeleteIndex drop the data (#12204)
* vector: implement drop_index in the engine The RPC was a stub; it now resolves the table's vended location and removes every object under it through the same credential path open uses. A table the catalog can no longer describe is already dropped, so the call stays idempotent. * s3vectors: drop index data after the catalog delete DeleteTable carries the delete authorization, so it runs first; the engine DropIndex then removes the data under the index's location. NotFound and Unimplemented are tolerated so an index whose data went away with the catalog entry, or an older engine, still answers 200. * s3vectors: let the catalog delete carry the index data removal A vector index is colocated: the catalog entry sits at the dataset root, so DeleteTable's recursive delete already removes entry and data. The engine DropIndex call afterwards could only ever describe a missing or recreated record — a no-op in the common case, and a delete of a replacement index's files when the name was reused in between. |
||
|
|
2dbf9c9108 |
s3: evaluate all conditional write headers under the object lock (#12211)
* filer_pb,filer: per-mutation preconditions on ObjectTransaction A transaction's request-level condition evaluates one entry before any mutation runs, so a commit guarding two paths — the object it publishes and the upload directory it assembles — could not be expressed. Give each ObjectMutation an optional condition (evaluated against condition_key, else the mutation's target, under the same lock just before the mutation applies) and report which mutation failed through failed_mutation on the response, so callers can tell a mutation guard's rejection apart from the request-level precondition's. * s3: evaluate multipart completion conditions under the object lock CompleteMultipartUpload checked conditional headers gateway-side before committing, and the routed commit transaction guarded only the upload directory — nothing made If-None-Match: * atomic with the publish, so concurrent conditional completions of the same absent key could all report success. The request's reducible write condition (If-None-Match/If-Match, star or single strong ETag) now rides the commit as the transaction's request condition on the owner filer: on the object path for unversioned buckets, falling back to the .versions directory for suspended and enabled versioning. The upload-exists guard moves onto the PUT mutation, keeping its check-before-publish ordering. Conditions the reducer cannot express stay on the distributed write lock, where check and commit already serialize. * filer,s3: resolve the latest version first for versioned conditional writes * filer: keep directory entries holding content out of the absent-slot rule * s3: keep a gateway-side upload-exists check for filers without mutation conditions * filer: honor null-version-is-latest resolving prefer-alt conditions * drop scratch multipart-race repro programs * s3: note the write lock does not cover routed writes on non-reducible conditions * filer: mirror the gateway's remote-etag precedence in condition evaluation storedEntryETag only knew the extended attribute and the chunk fallback, so a synced remote entry compared chunk-derived ETags while the gateway compares the remote ETag. Follow the same precedence: extended attribute, remote ETag while the entry still holds the remote bytes, Md5, then the chunk hash — and no ETag for an entry holding none. * s3: reduce all conditional write headers into owner-lock clauses Time-based headers, combined conditions, ETag lists, and weak ETags stayed on the distributed write lock because buildWriteCondition could not express them, leaving their gateway check racing owner-routed writes. Reduce every parseable header form to WriteCondition clauses — multiple clauses compose AND, ETag candidates carry the raw list for the filer's canonicalization, and RFC 7232 precedence keeps If-Unmodified-Since hidden by If-Match and If-Modified-Since hidden by If-None-Match — so the owner filer evaluates them under the same object-path lock as the commit. Only unparseable headers still fall back, where the lock path reports the same parse error. |
||
|
|
2737315e9d |
s3: evaluate multipart completion conditions under the object lock (#12209)
* filer_pb,filer: per-mutation preconditions on ObjectTransaction A transaction's request-level condition evaluates one entry before any mutation runs, so a commit guarding two paths — the object it publishes and the upload directory it assembles — could not be expressed. Give each ObjectMutation an optional condition (evaluated against condition_key, else the mutation's target, under the same lock just before the mutation applies) and report which mutation failed through failed_mutation on the response, so callers can tell a mutation guard's rejection apart from the request-level precondition's. * s3: evaluate multipart completion conditions under the object lock CompleteMultipartUpload checked conditional headers gateway-side before committing, and the routed commit transaction guarded only the upload directory — nothing made If-None-Match: * atomic with the publish, so concurrent conditional completions of the same absent key could all report success. The request's reducible write condition (If-None-Match/If-Match, star or single strong ETag) now rides the commit as the transaction's request condition on the owner filer: on the object path for unversioned buckets, falling back to the .versions directory for suspended and enabled versioning. The upload-exists guard moves onto the PUT mutation, keeping its check-before-publish ordering. Conditions the reducer cannot express stay on the distributed write lock, where check and commit already serialize. * filer,s3: resolve the latest version first for versioned conditional writes * filer: keep directory entries holding content out of the absent-slot rule * s3: keep a gateway-side upload-exists check for filers without mutation conditions * filer: honor null-version-is-latest resolving prefer-alt conditions * drop scratch multipart-race repro programs * s3: note the write lock does not cover routed writes on non-reducible conditions |
||
|
|
3955e7218f |
mount: keep the shared directory build out of the requester's context (#12206)
* mount: keep the shared directory build out of the requester's context The shared directory build behind EnsureVisited is a singleflight call: the first requester for a path owns the build, and every later requester for the same path waits on that same build. The build ran on the owner's request context, so a FUSE request that returned -- or that the kernel interrupted -- canceled the build for everybody waiting on it. A waiter whose own context was still alive then received somebody else's cancellation. weed/mount/weedfs_dir_read.go classified a canceled read as EINTR only when the local context was done, so such a waiter fell through to fuse.EIO: the kernel handed EIO to readdirent, kubelet's applyFSGroup failed, and the volume never mounted. Build the shared directory on a context that keeps the request's values but not its cancellation, and report a canceled read as EINTR even when the local context is still alive. Waiters still abandon on their own context, so a canceled request stops waiting immediately. This follows up #11717, which made the wait abandonable and mapped the local cancellation to EINTR, but left the shared build itself on the owner's context. The surrounding build lifecycle calls already use context.Background() for the same reason. Test: TestEnsureVisitedSharedBuildSurvivesOwnerRequestCancel reproduces the cancellation with a fake filer and fails without this change. * mount: classify gRPC-canceled directory listings as EINTR * mount: log only real directory read failures, not interruptions * mount: keep shared section refresh out of the requester's context * mount: let section-refresh waiters leave on their own context The shared refresh flight now outlives the requester that started it, but callers still blocked in singleflight.Do until it finished — an interrupted directory read could not stop waiting, and the caller's own deadline never applied to the wait. DoChan + select on the caller's context restores early abandon while the detached refresh still completes for the remaining waiters. * ci: re-run FUSE integration after a flaky writeback-cache failure * mount: refresh a stale entry once before reporting a read error A read resolves chunk views from the handle's entry. A concurrent overwrite commits a new entry whose replaced file ids the filer queues for deletion; the open handle only learns the new chunk list when the metadata invalidation lands. A fetch that lands between the volume delete and the invalidation names data that is already gone and the read reports EIO. On a chunk read error, wait out any in-flight async flush, re-read the entry from the filer, install it when it outranks the handle, and retry once. An unchanged version means the failure is genuine. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: fence the read-error entry refresh by file identity Two guards on refreshEntryForRead's install path: the unlocked dirtyMetadata read raced with Write under the handle lock — drop it, installAckedEntry already checks under that lock, and it now fences isDeleted handles the same way. And a saved path can name a different file after unlink/reuse, so refuse the refresh when the fetched inode disagrees with the one the handle holds. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
c7d77eb9a8 |
s3vectors: accept and return encryptionConfiguration on vector buckets (#12203)
* s3tables: carry encryptionConfiguration in table bucket metadata * s3vectors: accept and return encryptionConfiguration on vector buckets The configuration is validated (sseType AES256 or aws:kms) and stored on the bucket record, so the API round-trips what the caller declares. * s3vectors: reject caller-chosen encryption keys on vector buckets * s3tables: reject caller-chosen encryption keys on table buckets * s3tables: reject unknown sseType in bucket encryptionConfiguration The bucket check rejected aws:kms and kmsKeyArn but stored any other sseType — a made-up or mistyped value would round-trip through GetTableBucket as an encryption claim the storage does not keep. The validator now admits only the AES256 declaration, shared so the vector layer can drop its copy. * s3vectors: delegate encryption validation to the shared catalog check * s3tables: accept the sseAlgorithm name in bucket encryptionConfiguration S3 Tables declares the algorithm as sseAlgorithm while S3 Vectors uses sseType; the shared check now accepts either spelling, still admits only AES256, and still refuses any caller-chosen key. * s3vectors: call the shared encryption validator directly * s3vectors: require each API's own encryption field name The shared validator accepted sseAlgorithm on the vector path, so a CreateVectorBucket carrying the s3tables field stored a configuration GetVectorBucket could never echo back in the vectors shape (and vice versa). ValidateEncryptionConfiguration now takes the wire name its caller expects; the vectors create path normalizes to sseAlgorithm for the catalog and GetVectorBucket renames it back to sseType. |
||
|
|
fc9be77dbb |
s3vectors: bound QueryVectors topK at the AWS limit (#12202)
* s3vectors: bound QueryVectors topK at the AWS limit of 30 topK passed through unbounded; AWS caps a query at 30 results, so a larger request is a ValidationException before it reaches the engine. * s3vectors: raise QueryVectors topK bound to the AWS limit of 10,000 * s3vectors: validate QueryVectors topK before the catalog lookup * s3vectors: cover the QueryVectors topK bounds |
||
|
|
eecc461b39 |
s3vectors: put, get and delete vector bucket policies (#12201)
* util: do not bind outbound dials to a loopback -ip.bind `weed server -ip.bind=127.0.0.1` keeps every listener local, but the same address became the source of outbound connections, so the filer dialled its remote mount from 127.0.0.1 and failed with "connect: invalid argument". A loopback source cannot reach off the host; leave the source to the OS, as for a wildcard bind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * s3vectors: put, get and delete vector bucket policies The AWS S3 Vectors policy operations had no handlers; the policy document is stored on the shared table bucket, so the handlers delegate to the catalog's PutTableBucketPolicy, GetTableBucketPolicy and DeleteTableBucketPolicy and keep its guard. * s3vectors: treat ICEBERG buckets as absent in policy handlers GetVectorBucketPolicy/PutVectorBucketPolicy/DeleteVectorBucketPolicy delegated straight to the table bucket, so a bucket created for ICEBERG tables — invisible to every other vectors operation — could still have its policy read and written through the vectors API. Gate all three on lanceBucket, the same format check DeleteVectorBucket and CreateIndex already apply. * test/vectors: cover vector bucket policy put/get/delete Round-trips a policy document through the new ops and asserts a get on the deleted policy surfaces NotFoundException, so the boto3 conformance run exercises the added handlers end to end. * s3vectors: validate the bucket ARN and policy document Bucket-level operations now reject an index ARN and an ARN minted for another service, and PutVectorBucketPolicy refuses a document carrying s3vectors actions — the evaluator only honors s3tables actions, so such a rule would be a dead grant. * s3vectors: validate the bucket ARN on the remaining bucket-level operations * s3vectors: cover bucket ARN and policy document validation TestBucketARNs pins the accepted forms and the index/foreign-service rejections; TestValidatePolicyDocument covers a valid document, a malformed one, and the dead s3vectors action namespace. * s3vectors test: use an enforceable action in the bucket policy test The catalog refuses s3vectors actions in a stored policy, so the put call rejected the document before the get and delete assertions ran. --------- Co-authored-by: Ólafur Bjarki Bogason <olafur@genkiinstruments.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8e44178dc6 |
s3vectors: cap PutVectors at 500 items and check data against the index dimension (#12200)
* s3vectors: cap PutVectors at 500 items and check data against the index dimension A batch could carry any number of vectors and a vector of the wrong dimensionality reached the engine: AWS caps a PutVectors call at 500 items and requires each vector to match the index's declared dimension. * s3vectors: cover the PutVectors batch and dimension checks * s3vectors: reject query vectors that do not match the index dimension Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3vectors: cover the PutVectors batch size bounds Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3vectors: cover the catalog-backed PutVectors handler path Run PutVectors through the real handlers over the in-memory catalog: a matching vector reaches the engine, a mismatching one is rejected with ValidationException before PutVectors is called, and a full vectorsMaxPutBatch batch is accepted. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
4be28fef2d |
iceberg maintenance: cache a remote-only file before reading it (#12191)
* iceberg maintenance: an unreadable parquet file fails the job, not the process parquet.NewReader panics on any file it cannot open (wrong magic, a truncated footer, zero bytes), and parquet-go's row decoder panics on a corrupt page under a valid footer. Maintenance runs in the worker's goroutines with no recover, so one bad delete or data file in any table ended the whole process. - Every parquet open in compaction goes through newParquetReader, which opens with parquet.OpenFile and returns the error. - A panic while decoding a delete file or a data file's rows is recovered into an error naming the file (recoverRead/guardRead). - As a backstop, detection recovers per table and Execute per operation: the table is skipped or the operation fails, and the worker keeps running. A failed delete file still fails the table's compaction (no commit), as a read error did before; a failed data file still skips only its bin. * iceberg maintenance: cache a remote-only file before reading it On a bucket mounted with remote.mount, a file that is not cached locally has no chunks and no inline content, only its RemoteEntry. Maintenance read it as an empty file: a parquet reader failed on zero bytes (before the previous change, it panicked and ended the process) and a manifest failed to parse, so a table whose files had been uncached could not be maintained. lookupFileByIcebergPath, the lookup every read goes through, now calls CacheRemoteObjectToLocalCluster for a remote-only entry, as the S3 gateway does on a read, and reads the entry it returns; the call is tried twice, and an entry still remote-only after caching is an error. The lookups that only resolve a path (orphan removal) do not cache. readEntryContent reports a file with no content and no chunks as empty instead of returning zero bytes. The fake volume server in the tests now serves chunk reads, so the remote-only tests read the cached file through the chunk reader as a real one is read. * iceberg maintenance: bound the metadata location read before caching a remote file * iceberg maintenance: bound the metadata read on the entry the cache returns * iceberg maintenance: enforce the metadata size bound inside the remote cache call The worker checks the resolved entry before asking the filer to cache a remote-only object, but the filer resolves the entry again at cache time and downloads whatever its remote size then says. A replacement landing in between could push an oversized object through the download even though the post-cache check would refuse to read it. CacheRemoteObjectToLocalClusterRequest gains an optional max_size; the filer rejects an over-limit remote object before the download starts. loadMetadata passes metadataFileMaxBytes; data-file reads leave it unset. * iceberg maintenance: key the remote cache singleflight by the size limit Callers sharing one flight share its outcome, including a bounded caller's over-limit rejection: an unrestricted reader joining the same path would fail even though its own bound allows the download. The limit joins the key so only callers with the same bound share a flight. * iceberg: gofmt compact_order_test.go * filer: bound remote cache callers before the shared download Each caller's max_size is now enforced on the entry the filer resolves before that caller joins the singleflight, which is keyed by path again. A bounded caller's rejection is never shared with other callers, and callers with different bounds share one download instead of pulling the same remote object twice. * filer: bind the remote cache size bound to the downloaded entry The pre-flight check and the shared download resolve the entry separately, so a remote metadata refresh landing in between lets a larger object through: the download ran to the refreshed size and only the reader's post-check refused it. The bound is now re-checked inside the flight on the entry the download streams. The rejection is typed so a co-tenant sharing the flight does not inherit it: callers whose own bound permits the reported size retry on a fresh flight instead of failing for another caller's limit. --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> |
||
|
|
9df7d62b4b |
mount, s3: clarify allowOthers requirement and bucket name error (#12197)
* mount, s3: clarify allowOthers requirement and bucket name error
`weed mount -allowOthers` defaults to true. On Linux, a non-root
user's mount then needs `user_allow_other` in /etc/fuse.conf, but the
help text only said "allows other users to access the file system".
Say so in the help, and point at -allowOthers=false.
Also fix the grammar of the bucket name length error ("must between
[3, 63]" -> "must be between 3 and 63").
Help and error text only; no behaviour change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* s3: normalize the remaining VerifyS3BucketName error messages
The length message was clarified; the rest of the function still had
inconsistent phrasing ("name must start", "cannot be ip addresses",
"prefix xn-- ... in bucket prefix"). Align them on "bucket name ..."
wording matching the AWS bucket naming rules.
---------
Co-authored-by: pingqiu <pingqiu999@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: user.email <chrislusf@users.noreply.github.com>
|
||
|
|
d21a1eda54 |
util: do not bind outbound dials to a loopback -ip.bind (#12198)
`weed server -ip.bind=127.0.0.1` keeps every listener local, but the same address became the source of outbound connections, so the filer dialled its remote mount from 127.0.0.1 and failed with "connect: invalid argument". A loopback source cannot reach off the host; leave the source to the OS, as for a wildcard bind. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
43626431c3 |
vector: recheck master discovery quickly while no worker is cached (#12199)
The vectors gateway backs off discovery for 5s between resolutions, but the backoff also suppressed rediscovery after an empty lookup. A worker that landed its KeepConnected registration a moment after the first query stayed invisible for the whole window — longer than a client's retry budget — so the S3 Vectors e2e flaked: CreateIndex raced the registration, got InternalFailure, and every boto retry fell inside the same 5s fast-fail window. With no cached backend, recheck every 500ms instead; the 5s interval now only bounds membership refreshes against a live client, and an absent worker still cannot flood the master with lookups. The empty-membership error also becomes codes.Unavailable so the API answers 503 rather than InternalFailure. cluster.ps lists registered vector nodes like the other node types, and the e2e harness polls it for the engine's registration instead of only checking the gRPC port — a listening engine is not discoverable until the registration lands. |
||
|
|
2c510cc30e |
ec.rebuild: remount shards the rebuild replaced, keep regenerated copies (#12193)
* volume: remount ec shards the rebuild replaced under a mounted handle Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec.rebuild: keep copied shards the rebuild regenerated in place Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: reopen regenerated ec shards in place instead of remounting UnmountEcShards and MountEcShards send deltas through separate buffered channels, so a remount can reach the master as add-then-delete and drop the repaired shard from lookups until the next full EC heartbeat. ReopenEcShard swaps the mounted shard's descriptor under the volume lock instead: registration, disk type, and heartbeat state never flip, and readers holding the old shard degrade to a clean error the parity path already recovers from. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: reopen rebuilt ec shards in place, mirroring Go volume_ec_shards_rebuild recreates a shard file under a mounted handle whose descriptor still serves the detached inode (a mounted shard whose file was deleted under it). DiskLocation::reopen_ec_shard swaps the mounted descriptor for a fresh open, matching Go's ReopenEcShard — the registration and heartbeat view never flip. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * volume: fail ec rebuild when a regenerated shard cannot be reopened * rust volume: fail ec shards rebuild when a rebuilt shard cannot be reopened * volume: repoint mounted ec shards after rebuild across disks A mounted shard whose file was deleted under the mount is "missing" to the rebuild: the regenerated file lands on the rebuild location while the handle keeps serving the detached inode on another disk. Reopening the mounted disk then failed the whole RPC, so the caller never saw the rebuilt ids and its deferred cleanup deleted the regenerated copies. Repoint every mounted handle of the rebuilt volume instead of only this run's outputs: reopen in place when the rebuilt file is on the mounted disk, otherwise drop the stale handle and remount the shard from whichever local disk holds the file via a delta-free load split out of MountEcShards — a same-server move, so the master's registration never flips. A shard that can be neither reopened nor remounted is left unmounted rather than serving replaced bytes, and the repoint step no longer fails the RPC, so RebuiltShardIds always reaches the caller. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: repoint mounted ec shards after rebuild across disks Mirror the Go repoint loop: iterate every mounted shard of the rebuilt volume, reopen in place when the rebuilt file is on the mounted disk, and otherwise drop the stale handle and remount the shard from whichever local disk holds it. The repoint no longer fails the RPC, so rebuilt_shard_ids still reaches the caller and its copy cleanup keeps the regenerated shards. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: still rebuild ecx and repoint mounts when no shard is missing * master: keep a node in an ec shard's lookup while a disk still holds it A rebuild that relocates a mounted shard between a node's disks reports the move as a delete on the source disk plus an add on the destination. Registering first is a no-op because shard lookups key on the node, then the unregister dropped the node outright — and steady-state heartbeats never restore it, so readers and later rebuilds lose the repaired copy. Skip the delete while another disk on the node still holds the shard. UnregisterDataNode now clears the node's own ec records before dropping its registrations, so the retained-shard check cannot skip the teardown, and each unregister gets a ShardsInfo copy since DeleteEcShard drains the shared one. * rust volume: fold the journal on a mounted index instead of rewriting it The rebuild handler regenerated .ecx through File::create even while the volume was mounted, truncating the index under readers that search the open descriptor with its old size — a live lookup could hit EOF mid-scan. Go folds the deletion journal in place on a mounted volume; do the same through rebuild_ecx_from_journal and reserve the full regen for the unmounted path, where nothing holds the file open to interrupt. * volume, rust volume: report rebuilt shards on late failures, fix rust corrupt discovery Two review findings the repoint rework left open: - A RebuildEcxFile failure still returns a plain error after shards were regenerated, so the caller discards the response holding the rebuilt ids and the deferred cleanup deletes copies the rebuild regenerated in place. The Go handler now packs the ids into the gRPC status details and the shell pulls them back out on error; the Rust handler carries them in grpc-status-details-bin the same way. - The Rust rebuild still counted a sidecar-corrupt shard as present, so it fed bad bytes into reconstruction and never regenerated the shard. Discovery now loads the generation-0 .ecsum the way Go does, verifies every present shard, and reclassifies mismatches (and zero-size residue) as rebuild targets written to a .rebuilding temp file, verified, and atomically renamed over the original only after every output passes the sidecar check. Honors unsafe_ignore_sidecar, added to VolumeEcShardsRebuildRequest to match the Go proto. * volume: repoint sibling journals sharing a folded .ecj after rebuild RebuildEcxFile unlinks the deletion journal and the mounted path only reopens its own handle — a sibling mount journaling into the same file keeps appending to the detached inode, and those deletes never reach a path-based reader again. Reopen every mounted volume sharing the index base before leaving the rebuild. * rust volume: repoint sibling journals sharing a folded .ecj after rebuild rebuild_ecx_from_journal replaces the journal and reopens only its own handle; mounts on sibling disks sharing that .ecj keep the detached handle, so their next appends are lost at restart. Adopt the folded journal on every mounted runtime sharing the path before releasing the store lock, clearing their in-memory set — every journaled id is now an .ecx tombstone. * ec: swap the deletion journal handle only after the new file opens A failed reopen left ecjFile nil, so every later delete returned "ec volume closed". Open first, then close and swap — mirroring the Rust adopt_folded_journal ordering. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec.rebuild: fail the rebuild when a sibling journal cannot be repointed A warning left the sibling mount journaling to a detached inode while the RPC reported success. Propagate the error like the Rust handler's adopt_folded_journal map_err. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec: keep deletes loud when the journal reopen cannot open the replacement Reverts the swap-on-success ordering: a detached handle kept on failure lets deletes append and sync to the removed inode, then vanish at restart. Closing first also frees the descriptor before the open. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * rust volume: drop the folded journal handle before reopening it adopt_folded_journal and the post-fold recreate now clear ecj_file before opening the replacement, so a failed reopen leaves deletes erroring instead of appending to the detached inode. Mirrors Go's ReopenDeletionJournal. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * ec: drop the cached journal size when its reopen fails Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
5c92a54a12 |
remote: reuse one guarded remote storage client per configuration (#12189)
* remote: reuse one guarded remote storage client per configuration BuildGuardedRemoteStorageClient (the filer's lazy remote stat and list, remote deletes and renames, the volume server's cold reads, the S3 gateway's remote reads) built a new client, with its own http.Transport, on every call. Each transport kept its connection idle for IdleConnTimeout (60s) and was never reused or closed, so a steady stream of remote lookups held one open connection per request. An endpoint that caps connections per source then refuses every new connection. - remote_storage: GetRemoteStorage's per-name cache becomes a bounded LRU (WEED_REMOTE_CLIENT_CACHE_MAX, default 64) keyed by kind, name, a hash of the whole configuration and a credentials version, built outside the lock once per key (singleflight) from a copy of the configuration. A client leaving the cache has its idle connections closed. GetRemoteStorageWith exposes the cache to other builders. - BuildGuardedRemoteStorageClient caches the guarded client per configuration. The endpoint is checked against the deny-list when a client is built; every new connection still dials through the guarded dialer. A gcs credentials file, and for an S3-compatible remote using the default credential chain the shared credentials file and AWS_* variables, are part of the key by content. - The shared transport keeps 32 idle connections per host, caps connections per host (WEED_REMOTE_MAX_CONNS_PER_HOST, default 64) and has dial, TLS handshake and response header deadlines, so a hung remote frees its slots. - The filer drops the clients of removed remotes (and all but the newest client per name) on every /etc/remote reload, also when /etc/remote does not exist; reloads are serialized. * remote: drop clients whose build spans a reload or whose remote is gone * server: hash the shared AWS config file into the guarded credential version * server: bound a stalled body on the shared remote transport * remote: waive path-injection on the configured credentials file * remote: keep caching remotes the local filer does not name * remote: drop the inline codeql waiver the analyzer ignores * remote: version the plain gcs client's credentials file * remote: hash the environment's inline gcs credentials GOOGLE_APPLICATION_CREDENTIALS can hold the JSON itself, which the gcs client accepts; it is not in the configuration hash, so a changed value kept the old client. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * remote: version the well-known gcs credentials file With neither the configuration nor GOOGLE_APPLICATION_CREDENTIALS set, the SDK reads application_default_credentials.json; rotating it left the cache key unchanged and the old client behind. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * remote: share the gcs credentials version with the guarded client The guarded gcs client resolves the same environment and well-known credentials as the plain one; keying it only on a configured path left those rotations invisible to the cache. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * remote: version s3 keys resolved from the environment A remote whose access and secret keys both arrive through its type's environment fallback (aliyun, baidu, contabo, filebase, storj, tencent) never entered the default-chain branch of guardedCredentialsVersion, so an in-process environment change kept serving the stale cached client. The version now hashes the resolved keys for every S3-compatible remote. * volume: reuse one remote storage client per configuration (rust) fetch_and_write_needle built a fresh aws_sdk_s3::Client per request, so every cold read kept its connector's idle connections without ever reusing them — the same leak the Go side fixes. Cache clients keyed by the configuration's protobuf encoding in an LRU of WEED_REMOTE_CLIENT_CACHE_MAX entries (default 64), mirroring weed/remote_storage/client_cache.go. An evicted entry drops the client's last Arc and with it the connector's idle connections. The per-request endpoint validation is unchanged: Rust has no dial-time guard yet, so the up-front DNS check stays on every call. --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
9e41500a3a |
mount: rebuild dropped directory listings and stale sections on lookup (#12195)
* mount: rebuild dropped directory listings on lookup Invalidation, idle eviction, and a kernel Forget all clear a directory's cached-listing flag, but only an expired listing was rebuilt on lookup — a dropped one read through per entry until a ReadDir happened to run. Track directories that once held a listing in wasCachedDirs, which survives those drops, and let ShouldRebuildExpiredDir rebuild them on the next lookup. Never-cached and read-through directories keep reading through. * mount: re-validate a stale directory section on lookup A burst of remote changes invalidates only the directory section it lands in, but nothing re-validates that section until a ReadDir runs — so a lookup-heavy workload reads through per entry indefinitely. EnsureNameFresh re-lists just the section covering the looked-up name, after which lookups in it are served from the cache again. * mount: wait for pending flush before a stale-section refresh on lookup A section refresh lists the range from the filer; without the wait a pending async flush of the looked-up entry could land after the listing and leave the refreshed section holding pre-flush metadata — the same hazard the rebuild path already waits out. * mount: bound the dropped-listing rebuild history wasCachedDirs grew one full path per directory ever cached, removed only by the read-through mark, so deleted and forgotten directories accumulated forever. Remove the record when its name is deleted, move it on rename so rebuild eligibility travels with the directory, and cap the history with FIFO eviction — past the bound an evicted directory reads through per entry, the same end state as a never-cached one. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: drop oversized stale sections and back off failed refreshes A section that outgrew one refresh failed EnsureNameFresh with ErrRefreshRangeTooLarge on every lookup, repeating the multi-page listing before each per-entry read, and other refresh failures retried with no cooldown. Drop the cached listing on ErrRefreshRangeTooLarge so a later lookup rebuilds it re-tiled, as ReadDir does, and stamp the rebuild attempt on other failures so retries back off under the same cooldown the rebuild gate uses. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: version hard-link sibling syncs at the committed log position * mount: tag dropped-listing queue slots so stale ones cannot evict RemovePath, MovePath, and MarkDirectoryReadThrough delete the rebuild record but leave the FIFO slot behind, so a path removed and re-cached holds two queue entries. When the stale slot reaches the front it evicts the newer record, and the directory reads through per lookup until the next listing. Tag each record with an insertion generation and only evict on a match, keeping the newer record for its own slot's turn. * mount: refresh hard-link siblings from the filer, not the cached copy Stamping a cached sibling with the mutation's version claimed that position for content that may predate it — an in-flight event with newer chunks or attributes then hits the version gate and the sibling serves stale data until a refresh. Fetch each cached sibling's entry from the filer after the commit and record the higher of the fetched and committed positions, falling back to the in-place counter patch only when the fetch itself fails. * mount: back off failed section refreshes per section, not per directory A failed refresh of one stale section marked a directory-wide attempt, so every other stale section in that directory waited out the cooldown with per-entry filer reads. Key the attempt timestamp by the section's lower bound so unrelated ranges still refresh on their own schedule. * mount: fence hard-linked entries by the group's newest write position Every path of a hard-link group reads one shared content blob, but the version gate only tracks per-path records — a sibling absent from inodeToPath got no version from the sibling sync, so a stale section refresh or event could roll the shared blob back for all of them. Record a monotonic version floor per hard-link id wherever versioned content lands, and check it in the section-refresh and event gates. * mount: keep hard-link names landing when the group floor fences content The per-group version floor rejected a delayed event's newEntry while its oldPath still applied — a rename could drop the source without adding the destination, and a section refresh could skip then mark a live link fresh. Rebase the group's current cached content onto the new path so the name lands without rolling the shared blob back, only skip refresh entries already cached, and gate the sibling refresh write under the same lock so a racing event or delete wins. * mount: rebase hard-link entries from the shared blob when no sibling is cached Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: park group content on uncached hard-link names a stale refresh installs Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: stale the destination section when a fenced hard-link name cannot land Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: bound the sibling hard-link refresh under one request deadline Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * mount: replace a reused-name occupant the hard-link floor would skip * mount: claim no version for a sibling counter patched without a fetch * mount: vacate a reused-name occupant without decrementing its group A sibling write can fold the occupant's filer-side unlink into the shared blob counter while the stale entry is still cached, so replacing it through the plain insert may decrement a group that no longer counts the name and drop a blob live siblings still read. Vacate the path like a rename first; the counter then only ever drifts high, which leaks a blob rather than losing shared content. * mount: keep a fetch-failed sibling's version when patching its counter The fallback marked the entry unversioned, which fences nothing — a replayed delete older than the cached content could remove a live link. Claim the version the entry already holds instead: newer in-flight events still land and correct it, older replays stay fenced. --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
8f6db360da |
admin: warn on dashboard when cluster nodes run mixed versions (#12196)
* admin: warn on dashboard when cluster nodes run mixed versions
A partially-finished upgrade leaves servers on different releases, and
nothing surfaces that drift today. The dashboard now compares the
binary version reported by every node:
- volume servers are asked directly via VolumeServerStatus (the
heartbeat topology carries no version), fanned out with bounded
concurrency and a 60s cache since versions only change on restart
- filers, S3 servers, and message brokers already report their version
over KeepConnected; ListClusterNodes returns it, the dashboard just
never read the field
- masters are excluded: raft membership does not carry versions
When every node matches, the Cluster panel shows the shared version.
When they differ, a warning banner calls out the mixed cluster and the
panel lists each node type with its count grouped by version, e.g.
"31x 4.39, 24x 4.44". Version columns on the volume server, filer, and
S3 tables (and the dedicated volume servers page) identify which nodes
are stale. Versions are compared without the volume-size-limit prefix
("8000GB 4.44 abc123" -> "4.44 abc123") so capacity config does not
masquerade as version drift; nodes reporting nothing count as
"unknown".
* admin: run the volume version queries concurrently, not under the map lock
The per-server VolumeServerStatus call ran while holding the versions
mutex, so the semaphore-bounded fan-out was fully serialized: every
server's RPC waited for the previous one, up to the 3s timeout each.
* admin: export the volume server table's real columns
The new Version cell shifted every later column, so the CSV read the
version as Rack and dropped Max Volumes, EC Shards and Usage a slot
early. Map the cells the table actually renders and add Version to the
export.
* admin: warn on mixed versions only when nodes report versions
An empty or master-only cluster produced a non-uniform summary with an
empty node list, which rendered the "Mixed cluster versions detected"
banner and warning icon over nothing. Mixed() requires reported nodes,
and the version card shows a dash when none answered.
|
||
|
|
e9c280e095 |
iceberg maintenance: bound decimal, time, uuid, fixed and rescaled timestamp columns on compacted files (#12190)
* iceberg maintenance: bound decimal, time, uuid, fixed and rescaled timestamp columns on compacted files The column statistics written for a compacted data file give lower and upper bounds only for booleans, integers, floats, dates, strings, binaries and timestamps already stored in the Iceberg type's unit. Every other column is left unbounded, so a reader can never skip a compacted file on it: a DECIMAL(18,2) column written by DuckDB (stored as INT64) loses its bounds after compaction, as does a millisecond timestamp, a TIME or a UUID. boundLiteral now also converts: - DECIMAL stored as INT32 or INT64 at the column's scale. A decimal stored as a byte array still gets no bound: Parquet compares those statistics as bytes, which does not order negative two's-complement values; - TIME, TIMESTAMP, TIMESTAMPTZ and TIMESTAMP_NS from any Parquet time unit, rescaled to the Iceberg unit; a value rescaled to a coarser unit is rounded down for a lower bound and up for an upper bound so it stays a bound, and a value that would overflow gets none; - UUID and FIXED[n] from a FIXED_LEN_BYTE_ARRAY of the right length; - a DOUBLE column read from a FLOAT file. arrow-go becomes a direct dependency (decimal128, already in the module graph through iceberg-go). * iceberg maintenance: bound TIME(MILLIS) columns stored as INT32 * iceberg maintenance: bound timestamptz_ns columns too --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> |
||
|
|
edf2b0f5ee |
iceberg maintenance: an unreadable parquet file fails the job, not the process (#12188)
* iceberg maintenance: an unreadable parquet file fails the job, not the process parquet.NewReader panics on any file it cannot open (wrong magic, a truncated footer, zero bytes), and parquet-go's row decoder panics on a corrupt page under a valid footer. Maintenance runs in the worker's goroutines with no recover, so one bad delete or data file in any table ended the whole process. - Every parquet open in compaction goes through newParquetReader, which opens with parquet.OpenFile and returns the error. - A panic while decoding a delete file or a data file's rows is recovered into an error naming the file (recoverRead/guardRead). - As a backstop, detection recovers per table and Execute per operation: the table is skipped or the operation fails, and the worker keeps running. A failed delete file still fails the table's compaction (no commit), as a read error did before; a failed data file still skips only its bin. * iceberg maintenance: test a corrupt page inside a merge fails it naming the file * iceberg maintenance: assert a skipped bin's files stay in the snapshot --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> |
||
|
|
55f2234026 |
iceberg maintenance: apply equality deletes only to older data files in their partition (#12187)
* iceberg maintenance: apply equality deletes only to older data files in their partition
Compaction applied every equality-delete key to every data file of every
bin. The Iceberg spec ("Scan Planning") applies an equality delete file only
to data files with a strictly lower data sequence number, and only to data
files in the delete file's own partition unless its spec is unpartitioned.
An upsert writer (Flink, the Iceberg Kafka Connect sink, CDC tools) commits
an update as the row's new version plus an equality delete of its key in the
same commit, so the compaction deleted both the old and the new version of
every updated row.
- equalityDeleteGroup is keyed by equality field ids and scope (unpartitioned,
or spec id plus partition) and keeps, per key, the highest data sequence
number of the delete files carrying it. A delete file without a data
sequence number is an error rather than a guess.
- Partition values are compared typed through the spec's partition type, so
one partition decoded in different Go shapes (a uuid as uuid.UUID or
[16]byte, a long as int32 or int64, a null present or missing) is one
partition. A spec whose fields are all void is unpartitioned. A delete whose
scope cannot be decided (unknown spec, no partition values under a
partitioned spec, a value that does not convert) stops the compaction; a bin
whose own partition cannot be typed while a delete of its spec exists is
not compacted.
- Each bin is merged with only the groups that apply to its partition, and a
row is deleted only by a key whose sequence number is higher than its data
file's.
- A where filter no longer drops unpartitioned delete files (the clause's
field is not in their spec), which let deleted rows return in a filtered
compaction.
Tests: the table fixture now commits its delete files after the data (data
sequence number 1, deletes 2), as writers do; hand-built entries in the unit
tests get sequence numbers. New eqdelete_sequence_test.go covers rows written
with and after their delete, partition scope, unpartitioned and all-void
specs, typed partition matching, missing sequence numbers and partition
values, where filters, and group-key collisions.
* iceberg maintenance: split the undecidable-scope branch in deleteInPredicateScope
---------
Co-authored-by: user.email <chrislusf@users.noreply.github.com>
|
||
|
|
ec43f4ca00 |
s3: accept bare composite checksum on CompleteMultipartUpload (#12194)
* s3: accept bare composite checksum on CompleteMultipartUpload Clients such as minio-go send the composite object checksum without the "-<parts>" suffix. Compare against the base64 part of the computed composite value as well as the full form. * s3: test object checksum matching with and without part suffix |
||
|
|
4ed9595a58 |
admin: explain detection cooldowns and skipped scans (#12185)
* plugin: describe detection cooldowns and report skipped scans * admin: expose cooldown eligibility and preserve skipped detection outcomes * admin: show cooldown eligibility alongside maintenance lock waits * plugin: retain cooldown activity for older admin servers * admin: keep manual detection skips out of scheduler run state * plugin: refresh descriptors on reconnect without downgrading saved schemas * plugin: allow valid schema refreshes to repair corrupt descriptors * admin: store eligible_after activity detail as RFC3339 * worker: report admin_script interval skips as skipped detection * worker: report admin_script interval skips after the last completed job * admin: show the selected job's cooldown while scheduling is disabled * admin: report an unsigned interval skip instead of a completed scan * worker: populate new DetectionComplete fields in Rust senders Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
cb993ab83b |
filer.redis3: accept username and keyPrefix options (#12192)
* filer.redis3: prefix every key via getKey, honoring keyPrefix Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer.redis3: read username and keyPrefix options into client config Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer.redis3: reject a username configured without a password go-redis only sends AUTH when a password is set, so a configured username with an empty password silently connects as the default user instead of the intended ACL user. Fail loudly at initialize instead. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer.redis3: cover keyPrefix scoping in the store tests Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer.redis3: derive the test prefix and kv key from the per-run directory --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
e7c1713457 |
admin: show maintenance lock blockers in scheduler UI (#12184)
* admin: expose maintenance lock waiters and current owner * admin: display maintenance lock waits across scheduler pages * admin: preserve the active lock message while acquisitions wait * admin: synchronize lock fairness assertions with waiter registration * admin: display lock wait start times alongside elapsed duration |
||
|
|
6cd14f06c9 |
filer.remote.sync: upload a holed entry zero-filled instead of skipping it (#12178)
* filer.remote.sync: skip an event the filer has written past, not only rewritten
isSuperseded only looked for chunks the event named that the filer no
longer holds. A file copied in through a mount logs a create while the
copy is still running: the mount flushes metadata with chunk uploads in
flight, so that chunk list can have holes. ChunkStreamReader stops at
the first hole, the multipart upload sends a short part ("ContentLength=
8388608 with Body length 4194304"), and since every chunk the event
names is still live the event was never superseded. The daemon retried
it forever and the offset never moved past it.
Treat chunks the filer has added since the event the same way. The
event that added them follows in the log and uploads the finished file.
* filer.remote.sync: skip a holed snapshot even while the filer still holds it
A mid-write snapshot the writer never finished is neither deleted nor
written past: the filer holds it unchanged, so the superseded check never
fires and every retry reads short at the same gap, pinning the sync offset
forever. The upload can never succeed for that entry — skip it like a
superseded one. A finishing write logs its own event and uploads the full
content; a dead write has nothing readable to lose.
The check also covers a chunkless entry that is not a dropped local copy
(e.g. a truncated remote-only entry): its reader yields zero bytes against
a nonzero size, which can silently clobber the remote object with an empty
body where the backend does not verify the length.
* filer.remote.sync: upload a holed entry zero-filled instead of skipping it
Skipping the event left a completed sparse file absent or stale on the
remote, and the coverage check on the raw chunk list missed both gaps a
manifest's bounding extent hides and a FileSize smaller than the chunk
extent. ChunkReadAt already resolves manifests the way the upload reader
does and zero-fills every uncovered range up to the effective size, so
route the upload through it: a sparse file, a mid-write snapshot, and a
chunkless entry all send the bytes a read of the entry returns, and a
hole can neither wedge the event nor publish a truncated object.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* filer.remote.sync: skip a superseded entry on reader-construction error too
---------
Co-authored-by: Alex Russell <alex@sengine.cloud>
Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
|
||
|
|
6225e0a705 |
s3: return x-amz-copy-source-version-id from CopyObject and UploadPartCopy (#12181)
* s3: return x-amz-copy-source-version-id from CopyObject and UploadPartCopy * s3: cover x-amz-copy-source-version-id on latest copies and part copies The explicit-version case was already covered; add a .versions pointer fixture so the unversioned request resolves the latest version and asserts the header on CopyObject, plus the same assertion on UploadPartCopy. * s3: carry the resolved id on a version file that lacks it A .versions child written before the version-id attribute resolves to an empty source version even though the pointer or the rescan already knows it. Stamp the id on the returned entry so copy and get readers report it; the version copy tests now drop the attribute to cover that shape. * s3: report the null version in x-amz-copy-source-version-id A regular-path source in a versioning-configured bucket is the null version; echo it like every other version id, matching the GET path and AWS. Unversioned sources still report no header. * s3: report the version a self-copy retry actually read --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> |
||
|
|
48782e2b30 |
s3: allow PutBucketAcl when auth is off (#12180)
* s3: allow PutBucketAcl when auth is off * s3: resolve the PutBucketAcl owner against the recorded owner The unauthenticated path now resolves the account as anonymous, but XML ACLs were still checked against it while GetBucketAcl reported an empty owner for ownerless buckets, so a read-modify-write ACL update failed with AccessDenied. GetBucketAcl and the XML check now both use the recorded owner when the bucket has one and the resolved account otherwise. The canned-ACL grantee moves to the same owner, and a request carrying no account no longer erases a stored owner. * s3: cover PutBucketAcl without auth for XML bodies and stored owners The XML case posts the anonymous owner GetBucketAcl reports for an ownerless bucket; the canned case asserts a recorded owner survives an unauthenticated update and receives the full-control grant. Ambient AWS credentials are cleared so the memory store never turns auth on. * s3: keep the recorded owner through a PutBucketAcl update The grantee and the stored owner were resolved differently: grants named the recorded owner while config.Owner still took the caller's account, so an admin updating another account's bucket rewrote the owner and broke an XML retry naming the original one. An ownerless bucket adopts the ACL's owner; an owned bucket keeps it. * s3: fall back to the admin owner for an ownerless bucket in ACL handlers An unauthenticated PutBucketAcl resolved the owner as anonymous and stored it on the bucket, but the registry reports an ownerless bucket as admin-owned: BucketOwnerEnforced uploads then stamped anonymous instead of admin, and ownership checks after enabling auth compared against the wrong owner. Both ACL handlers now resolve an anonymous fallback to the admin account, so GetBucketAcl reports the same effective owner the registry does and the stored owner matches it. * s3: resolve the admin owner for bucket grants when the caller is anonymous or untrusted Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3: skip recording anonymous identity or account as bucket owner Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3: report stored bucket ACL grants verbatim Explicitly saved anonymous full-control grants must survive a GET-to-PUT round trip unchanged, so the read path no longer rewrites them to the reported owner. Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3: expect the stored anonymous grant verbatim in GetBucketAcl Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: user.email <chrislusf@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
fffdb7cc75 |
filer.remote.sync: skip an event the filer has written past, not only rewritten (#12176)
isSuperseded only looked for chunks the event named that the filer no
longer holds. A file copied in through a mount logs a create while the
copy is still running: the mount flushes metadata with chunk uploads in
flight, so that chunk list can have holes. ChunkStreamReader stops at
the first hole, the multipart upload sends a short part ("ContentLength=
8388608 with Body length 4194304"), and since every chunk the event
names is still live the event was never superseded. The daemon retried
it forever and the offset never moved past it.
Treat chunks the filer has added since the event the same way. The
event that added them follows in the log and uploads the finished file.
|
||
|
|
74f196ceab |
s3: route regular copy source reads to the write owner (#12173)
* s3: read regular copy sources from their write owner * filer: report mutation sentinel codes in ObjectTransaction responses A failed mutation only surfaced resp.Error text, so routed writers could not distinguish an existing-directory conflict from a generic failure and could not apply the prefix-object retry the plain CreateEntry path has. * s3api: retry a routed mkfile as a prefix object over a directory mkFile retries ErrExistingIsDirectory by marking the entry a prefix object; the routed path returned the conflict instead, so a routed write to a key that is also a prefix failed where the lock path succeeds. The filer now reports the code on the transaction response, making the retry recognizable. * s3api: commit copy destinations on the key's write owner An owner-routed source read still left the destination on the gateway's default filer, so a second copy resolved the first copy's destination on the owner, found nothing, and returned NoSuchKey. Write unversioned and suspended destinations through routedMkFile, and write the version file on the owner the finalize recomputes on. * s3api: cache remote objects on the filer that served the entry An owner-routed lookup can return a remote-only entry that has not replicated to the gateway's default filer; the cache RPC then cannot find it and the copy or read degrades to a 503. Ask the entry's write owner to cache it first, falling back to the default filer. * s3: read versioned object state from the key's write owner CopyObject destinations now commit version files and the .versions pointer on the owner's object-scope ring key, so versioned reads through the default filer could miss them until replication. Generalize the routed read to dir/name lookups and paginated lists scoped to the object's owner, and point the versioned read paths at it: getSpecificObjectVersion, doGetLatestObjectVersion, the .versions quick-checks in Get/Head/attributes, the self-heal rescans, tagging, delete conditionals, directory-marker history checks, rename destination, multipart completion checks, and the version-id format probe. Reads fall back to the default filer when no ring owner is resolvable, preserving single-filer deployments. * test(s3api): fake RECOMPUTE_LATEST/DELETE and cover versioned copy reads The ambiguous-PUT fake filer now creates parent directories on PUT (mirroring InsertEntry), applies DELETE mutations, and re-derives the .versions pointer for RECOMPUTE_LATEST the way applyRecomputeLatest does, so owner-vs-replica tests can exercise versioned writes. TestVersionedCopyDestinationReadsOnOwner copies into a versioned bucket whose writes commit on the owner while the gateway's default filer is a replica with no replication, then asserts reads by versionId, latest-version resolution, and a copy chained off the new version all resolve on the owner. * test(s3api): gofmt the versioned-copy coverage * s3: only swap in a cache fill of the resolved remote generation A cache response can come back from a filer holding an older generation of the key. Replacing the authorized source or swapping its chunks under a size and range computed for the resolved entry serves the wrong object or truncates the response. Match RemoteEntry identity and size before using a cached entry, in both the copy and streaming paths. * filer: keep directory attributes when an object lands as prefix object The object-onto-directory retry replaced the existing directory's mode, owner, and extended attributes with the object's. Preserve the directory identity; merge directory-held extended keys only when the old entry never held an object, so a prior generation's object metadata is not carried forward. * s3: keep marker lock checks on the filer that replaces the marker The object-lock precondition read the key's write owner while mkdir still replaces the marker on the default filer, so a locked marker not yet replicated to the owner could be overwritten. Read the entry where the replacement lands, in both PutObjectHandler and zip extraction. Also hash a trailing-slash marker key to the same route key as the object it names, so marker and object share one owner and one write lock. * s3: route tag writes to the owner tag reads resolve GetObjectTagging reads the key's write owner, but PutObjectTagging and DeleteObjectTagging still wrote through the default filer, so a successful change returned the owner's stale tags until replication. setTags, rmTags, and the versioned tag updates now write on the owner first, falling back to the default filer when no owner resolves. * s3: keep rename lookups on the filer that performs the move AtomicRenameEntry and the retry token it carries both execute on the default filer, but the source and destination reads went to the write owner — a completed rename's token was invisible to its own retry, and a source that only exists on the default filer looked absent. Read both ends on the default filer. * s3: send the .versions pointer repair with the object's route key The heal reads the .versions entry on the object's owner, but the conditional UpdateEntry is routed by the .versions path's own ring key, which can hash to a different filer — leaving the pointer the routed reads consult unchanged. Ship the CAS repair as a PATCH_EXTENDED ObjectTransaction on the object's route key instead. * filer: preserve only directory identity on a prefix-object write Merging the old directory's extended attributes could carry a marker's encryption, version, or etag fields onto the new plaintext object, since a directory's own keys cannot be told apart from the object it held. Keep only mode, uid, gid, user name and group names. * s3: also require matching size when a cache fill replaces the source The remote-generation check covers identity; size equality is the cheap guard that keeps the streamed or copied range consistent with what was resolved. * s3: lock the object path for a routed .versions pointer repair conditionalUpdateEntryRouted held the lock on the .versions path while version writes and deletes serialize on the object path, so a repair could pass IF_ENTRY_EQUAL and then apply over a pointer a writer had advanced in between. Lock the object path like the writers and keep the condition on the .versions entry via condition_key. * s3: do not replay an owner-side tag mutation on another filer withWriteOwnerFilerClient re-ran fn on the default filer after any owner error, so an authoritative not-found could update a stale replica entry and still report success, and a post-apply transport error could apply the mutation twice. The failover walk now ends on any error fn returns; only a channel that never came up moves to another filer. * filer: keep a directory's xattrs when a prefix object replaces it The prefix-object branch preserves the directory's identity but dropped its Extended map wholesale; FUSE xattrs live there under the xattr- prefix and belong to the directory, not to the object the marker held. * s3: delete null and specific versions on the filer that serves them On the default filer an absent entry deletes as success, so a version that only reached the write owner survived a reported success while owner-routed reads kept serving it. The suspended-versioning and lifecycle deletes now go through routedDeleteNullVersion and routedDeleteSpecificVersion when an owner resolves; an unsettled attempt keeps the lock path. * s3: route a lifecycle null-version delete to the object entry The "null" version is the regular object entry, not a .versions file, so it goes through routedDeleteNullVersion; only a real version id takes the .versions-file delete. * s3: repoint .versions only while it still names the deleted version routedDeleteSpecificVersion recomputed the latest pointer on every version delete, so removing a noncurrent version re-derived the pointer to the newest remaining file — resurrecting an older version over a cleared pointer (a suspended bucket's null write keeps it clear while the null object stays current) or over a pointer that moved on. The pointer is now read on the owner and repointed under an IF_ENTRY_EQUAL guard only while it still names the deleted version; a contested pointer is left for the self-heal path rather than overwritten by a stale scan. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3: let lifecycle expiry keep its metadata-only deletes on routed paths The routed lifecycle deletes hardcoded IsDeleteData, so expiring an entry with a volume TTL queued per-chunk deletions instead of leaving the data for the volume to reclaim — extra work on the owner for deletes recordMetadataOnlyIf still counts as metadata-only. Pass the caller's cleanup choice through routedDeleteNullVersion and routedDeleteSpecificVersion; the interactive DELETE path keeps deleting data. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * filer: let RECOMPUTE_LATEST skip a pointer that moved on A version delete repoints .versions only while the pointer still names the deleted version, but the gateway cannot carry the pointer's current state into the transaction's condition — the condition slot is already spent on the version's WORM guards. only_if_current_name moves the check under the transaction lock: the filer compares the pointer's name_to_key value before scanning and skips the recompute when it moved (or was cleared by a suspended bucket's null write), so a noncurrent delete cannot resurrect an older version. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * s3: repoint inside the version's locked delete repointLatestBeforeVersionDelete ran in its own transaction ahead of the delete, so a version delete denied by retention or legal hold still moved the latest pointer — GET and HEAD went on serving the older version it rolled to. The repoint and the delete now ride one transaction behind the version's WORM guards, atomically gated by the new only_if_current_name recompute guard instead of a stale-read IF_ENTRY_EQUAL. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |
||
|
|
511727cf3a |
vector: DeleteIndex format guard, sticky discovery, advertise normalize (#12171)
* vector: DeleteIndex format guard, sticky discovery, advertise normalize - handleDeleteIndex verifies the table carries the Lance format before DeleteTable — an Iceberg table under the same namespace is not an index and must not be removed by it. - discovery keeps the current worker while it is still registered — membership-order churn can no longer cancel in-flight calls, and the resolver closes each master conn inside a 5s deadline. - advertised_addr strips scheme and .grpcPort so the UDP probe reaches every supported master form. * vector: empty-follower retry, format check without read grant - discoverVectorWorkers keeps looking past an empty master reply — followers list nothing when workers registered with the leader. - the DeleteIndex format check uses the internal catalog read, so DeleteTable-only callers don't gain a GetTable requirement, and the format must be Lance exactly — format-less tables no longer pass. |
||
|
|
ae95b2c777 |
ec: rebuild a needle's suspect shard ranges when it fails to parse (#12170)
* ec: let shard-interval recovery exclude a set of suspect shards gatherEcShardIntervals and recoverOneRemoteEcShardInterval skipped only the shard being rebuilt. A needle-level rebuild wants every suspect shard kept out of the gather so a bad copy cannot poison the reconstruction it is being compared against. * ec: rebuild a needle's suspect shard ranges when it fails to parse A bit-rotted data shard serves a clean-looking but wrong interval, so the missing/unreadable-shard fallback never engages and the needle CRC check in ReadEcShardNeedle returns the corruption to the caller. Each interval's source shard is a suspect: rebuild the suspect ranges from the surviving shards — all of them at once while parity covers the suspect set, or one at a time when the needle spans more shards than that — and let a successful needle parse arbitrate which copy was corrupt. Shards whose range changed are dropped from the location cache so later reads go straight to reconstruction. * ec: mirror the corrupt-shard needle rebuild in the Rust volume server The snapshot now keeps every interval's shard placement so a parse failure can name its suspects. rebuild_ec_shard_needle rebuilds the suspect ranges through recover_one_remote_ec_shard_interval with the suspect set excluded from the gather, and the winning candidate's differing shards are dropped via forget_shard_location. * ec: recover suspect ranges under the requested needle id A failed ReadBytes re-reads the id out of the possibly corrupt header, so the rebuild passed whatever it decoded to peer recovery requests; carry the requested id through instead, and reject a candidate whose parsed id does not match it. * ec: retry a failed needle rebuild with each gathered input dropped The gather behind a suspect-range rebuild can itself serve a second corrupt shard: Reed-Solomon then reconstructs wrong bytes and the needle parse rejects the only candidate. Gather every surviving shard once, then re-decode with each of the first decode's inputs dropped in turn; the parse arbitrates the honest subset. * ec: mirror the dropped-input rebuild and id check in the Rust volume server Same two guards as the Go side: a candidate that parses under a different needle id is rejected, and a failed candidate whose gather fed it a corrupt input is retried by dropping one gathered shard at a time. The gather is shared through a want-bounded gather_ec_shard_intervals so the retry pays no extra fan-out. * ec: answer deleted when a rebuild's holder reports the needle deleted The rebuild discarded the deleted flag its interval recovery collected, so a needle deleted on its other holders but live in the local index came back as a corruption error. A deleted report is authoritative, so it now short-circuits the hypothesis search and answers ErrorDeleted. * ec: hydrate rebuild candidates into a scratch needle A failed ReadBytes leaves partially parsed fields on the needle, and optional sections do not reset when absent, so a corrupt first parse could leak stale name/mime/ttl into a successful candidate. Parse each candidate into a fresh needle and adopt it only on a matching id. * ec: corrupt parity+1 shards in the exceeds-parity read test The loop skipped the suspect's own shard when it fell inside the parity range, leaving only ParityShards corrupt shards — a case recovery can legitimately fix. Count the additional corruptions so the test always exercises the boundary it names. * ec: mirror the deleted-report short-circuit in the Rust volume server A holder's deleted answer during a rebuild propagates out as EcMiss::Deleted instead of surfacing as InvalidData, matching the Go path's authoritative-delete rule. |
||
|
|
eec2452e30 |
master: replicate the recovery-wait growth unfreeze through raft (#12165)
* master: replicate the recovery-wait growth unfreeze through raft state_loss_recovery_wait lifted the volume-growth freeze only in memory, while the freeze itself was replicated. A restart replayed the frozen flag and growth stayed refused until the wait expired again, on every restart, unless an operator ran volume.growth.resume. The lift now applies GrowthFrozen=false like ResumeVolumeGrowth, from a goroutine since IsGrowthFrozen is a read path that cannot block on raft. The apply runs under reportedTopologyIdsLock so a freeze landing after the lift keeps its command last in the log; the stale unfreeze skips itself when the flag is frozen again. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: cover raft replication of the recovery-wait unfreeze Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: retry and re-arm a growth unfreeze raft failed to record applyGrowthFreeze swallowed the raft error, so a rejected lift during a leader change left the flag clear while the log still held the freeze; nothing retried and a restart re-froze growth anyway. The apply now returns the error, replicateGrowthUnfreeze retries it, and on persistent failure re-arms the flag so the next IsGrowthFrozen attempts the lift again instead of diverging from the log. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: release the topology-id lock before the freeze raft write CheckReportedTopologyId and SetTopologyInitPending held reportedTopologyIdsLock across the blocking raft apply, so a slow apply stalled every later report — including the heartbeat that should trigger the next freeze. The lock now covers only the mismatch decision; the replicate step runs after it is released. * master: bind the recovery unfreeze to the freeze instant it lifts An unfreeze submitted while a newer freeze sat committed but not yet applied — e.g. after this node won an election — could land behind that freeze in the log and erase it. The command now carries the freeze instant cluster-wide (GrowthFrozenAt) and the lift is bound to the instant its submitter observed (GrowthUnfreezeFor): the state machine no-ops a lift while a strictly newer freeze exists, which also lets the unfreeze run without holding the topology-id lock at all. Concurrent freeze writes keep their own mutex so log order cannot regress the instant. * master: a superseded unfreeze retry leaves the resumed flag alone replicateGrowthUnfreeze broke out of its retry loop when a newer\nfreeze superseded the lift, but the break fell through to the\nre-arm: an operator resume in between was silently undone. Return\ninstead — the newer freeze generation owns the flag. * master: replayed freeze entries without an instant stay ageless Freeze commands written before the instant field existed carry zero.\nSubstituting the replay time made each restart stamp the freeze as\nnew, so a committed bound unfreeze always looked older and was\nrejected — the freeze came back on every restart. Keep the zero\ninstant: the recovery window reads as elapsed and bound lifts apply. * master: pack the growth freeze flag and generation into one atomic Separate flag and timestamp atomics left three holes: a resume's\nload-and-clear could lift a generation stamped in between, a bound\nlift compared stamps numerically although masters' clocks need not\nbe monotonic, and an ageless generation had no identity a bound lift\ncould match without colliding with the unconditional encoding.\n\nOne value now carries the state — 0 unfrozen, -1 an ageless freeze,\nelse the freeze instant — so every transition is a single atomic\nswap or compare-and-swap bound to an exact generation. * master: a repeat mismatch report keeps the armed freeze generation Re-stamping while already frozen never reaches the raft log, so the local generation diverges from the replicated one and every bound unfreeze becomes a no-op on replay — the freeze returns after restart or failover. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * master: snapshots read the freeze flag and generation in one load Save, Snapshot, and PersistMaxVolumeId read IsGrowthFrozen and the generation separately, so a resume landing between them persisted frozen=true with stamp 0 — restoring an ageless freeze a committed bound lift cannot clear. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> |