Commit Graph
25 Commits
Author SHA1 Message Date
Chris Lu a653a7f72a fix(shell): honor explicit fs.mergeVolumes from/to direction (#10159)
* fix(shell): honor explicit fs.mergeVolumes from/to direction

mergeVolumes only ever merged a smaller volume into a larger one. When the
user named both -fromVolumeId and -toVolumeId with the source larger than the
target, the planner produced an empty plan and the command printed just
"max volume size: N MB" and moved nothing.

Build the requested pair directly when both ids are given, instead of routing
through the size-descending heuristic. Read-only, empty, and wrong-collection
endpoints are rejected with a clear error rather than a silent no-op.

* fix(shell): allow fs.mergeVolumes into an empty target volume

Merging chunks into an empty volume is valid, e.g. consolidating data into a
freshly created or recently vacuumed volume. Only reject an empty source, which
has nothing to move.

* fix(shell): reject self-map in directed mergeVolumes planner

createMergePlan with from == to returned a {vid: vid} self-merge when called
directly. Guard it in the planner so it is correct independent of the Do
entrypoint.
2026-06-30 13:28:53 -07:00
Chris Lu da2e90aefd fix(mount): sanitize non-UTF-8 filenames; keep marshal errors per-request (#9207)
* fix(mount): sanitize non-UTF-8 filenames; keep marshal errors per-request (#9139)

A single file with invalid-UTF-8 bytes in its name (e.g. a GNOME Trash
"partial" like \x10\x98=\\\x8a\x7f.trashinfo.9a51454f.partial) made every
FUSE-initiated filer RPC fail with:

  rpc error: code = Internal desc = grpc: error while marshaling:
  string field contains invalid UTF-8

and then produced an avalanche of "connection is closing" errors on
unrelated LookupEntry / ReadDirAll / UpdateEntry calls, causing the
volume-server QPS dips reported in #9139.

Root cause is twofold:

1. Proto3 `string` fields require valid UTF-8, but the FUSE kernel passes
   raw name bytes. Create/Mknod/Mkdir/Unlink/Rmdir/Rename/Lookup/Link/
   Symlink all forwarded those bytes directly into CreateEntryRequest.Name,
   DeleteEntryRequest.Name, StreamRenameEntryRequest.{Old,New}Name and
   Entry.Name. saveDataAsChunk also copied the FullPath into
   AssignVolumeRequest.Path unchecked.

2. When the marshal failed, shouldInvalidateConnection treated the
   resulting codes.Internal as a connection problem and dropped the
   shared cached ClientConn — canceling every other in-flight RPC on it.

Fix:

- Add sanitizeFuseName (strings.ToValidUTF8 with '?' replacement, matching
  util.FullPath.DirAndName) and make checkName return the sanitized name.
  Apply at every FUSE entry point that passes a name to the filer RPC,
  including Unlink/Rmdir (which did not previously call checkName) and
  both oldName/newName in Rename. Add a backstop scrub for
  AssignVolumeRequest.Path so async flush paths cannot reintroduce
  invalid bytes from a pre-sanitization cached FullPath.

- In weed/pb.shouldInvalidateConnection, detect client-side marshal
  errors via the gRPC library's "error while marshaling" prefix and
  return false: the connection is healthy, only the request is bad.

Refs: https://github.com/seaweedfs/seaweedfs/issues/9139#issuecomment-4301184231

* fix(mount,util): use '_' for invalid-UTF-8 replacement (URL-safe)

Sanitized filenames flow downstream into HTTP URLs (volume-server uploads,
filer HTTP API, S3/WebDAV gateways). '?' is the URL query-string
delimiter and would split the path the first time the name lands in one,
so swap every invalid-UTF-8 replacement to '_'. This covers the two
pre-existing sites in weed/util/fullpath.go as well, keeping all paths
sanitized the same way.

* refactor(pb): detect client-side marshal errors via errors.As, not substring

Replace the raw `strings.Contains(err.Error(), ...)` check with a
type-based carve-out: use errors.As against the `GRPCStatus() *Status`
interface to pull the original Status out of any fmt.Errorf("...: %w")
wrapping, then match the library-owned "grpc:" prefix on that Status's
Message.

Why not errors.Is against a proto-level sentinel: gRPC's encode()
collapses the inner proto error with "%v" (stringification) before
wrapping it in a Status, so the original error type does not survive
into the caller. The Status itself is the structural signal that does
survive.

Why not status.FromError: when the caller wraps the Status error with
fmt.Errorf("...: %w", ...), status.FromError rewrites Status.Message
with the full err.Error() of the outermost wrapper, which defeats a
prefix check on the library-owned message. errors.As gives us the
original Status whose Message is still verbatim from the gRPC library.

A new test asserts that a plain errors.New("grpc: error while marshaling: …")
— i.e. the same text attached to something that is NOT a gRPC status —
does not short-circuit invalidation, so we never silently keep a cached
connection alive based on a coincidental substring match.

* refactor(util): centralize UTF-8 sanitization; add FullPath.Sanitized

Addresses review feedback on PR #9207.

Nitpick: every invalid-UTF-8 replacement across the codebase (DirAndName,
Name, mount.sanitizeFuseName, the weedfs_write.go backstop) now goes
through a single util.SanitizeUTF8Name helper, so the replacement char
('_' — URL-safe) is chosen in one place.

Outside-diff: three proto fields took raw FullPath strings that could
break marshaling if an entry ever carried invalid UTF-8
(CreateEntryRequest.Directory in Mkdir, DeleteEntryRequest.Directory in
Unlink, AssignVolumeRequest.Path in command_fs_merge_volumes). The
reviewer's suggested fix — using DirAndName() — would have silently
changed Directory from parent to grandparent, because DirAndName
sanitizes only the trailing component. Added FullPath.Sanitized(), which
scrubs every component, and applied it at the three sites. Exposure is
narrow in practice (FUSE-boundary sanitization and the gRPC-side
isClientSideMarshalError carve-out already cover the #9139 cascade),
but the defense-in-depth is cheap and consistent with the existing
AssignVolume backstop.

New tests in weed/util/fullpath_test.go document:
- SanitizeUTF8Name: valid UTF-8 passes through unchanged; invalid bytes
  become '_' (not '?', which is URL-special).
- FullPath.Sanitized: scrubs bytes in any component, not just the last.
- FullPath.DirAndName: dir remains raw on purpose — callers needing a
  clean full path must use Sanitized(). The test pins this behavior so
  it is not accidentally "fixed" in a way that changes the (dir, name)
  semantics callers depend on.
2026-04-23 19:17:35 -07:00
Chris Lu 49f62df2cf fix(shell): mergeVolumes hard-link safety and cleaner cleanup logging (#9163)
* fix(shell): skip hard-linked entries in fs.mergeVolumes

Hard-linked entries share a chunk list with their siblings, but the
filer's UpdateEntry only rewrites one entry at a time. Moving a chunk
here leaves every other hard-linked sibling pointing at a fid that
either gets deleted by the filer's own garbage step after UpdateEntry
or by deleteMovedSourceNeedles (#9160) — either way, the siblings end
up with dangling references.

Skip the entry with a visible log line so operators know the file was
bypassed and can handle it explicitly (copy-then-unlink, or dedup
before merge). Detected via entry.HardLinkId being non-empty, which is
the same signal the filer itself uses (weed/pb/filer_pb/filer.pb.go:451).

Flagged by coderabbit on #9160 post-merge.

* fix(shell): mergeVolumes suppresses 404 alongside 304 in source cleanup

BatchDelete also returns StatusNotFound (404) for an already-deleted
needle when ReadVolumeNeedle can't find it in the cookie-check path,
not only StatusNotModified (304) from DeleteVolumeNeedle returning
size 0. Both are benign races against a concurrent fsck purge or a
replica that already reconciled, so don't clutter the output with
"delete ... not found" warnings for them.

Flagged by coderabbit on #9160 post-merge.

* fix(shell): mergeVolumes merges hard-linked files via dedup on HardLinkId

Hard-linked siblings share one chunk list through a KV blob keyed by
HardLinkId (see weed/filer/filerstore_hardlink.go). UpdateEntry's
setHardLink rewrites that blob and maybeReadHardLink overrides per-entry
chunks with the blob's on every read, so a single UpdateEntry propagates
new fids to every sibling automatically — the previous skip-hardlinks
bailout was overly conservative and left hard-linked files stuck on
merge-source volumes forever.

Process each HardLinkId exactly once per run with a sync.Map so BFS
workers in different directories synchronize without a global lock.
First sibling carries the chunk move + UpdateEntry; later siblings find
the id in the map and return — preventing the real race, which is two
siblings trying to re-download an already-moved source needle or
double-queue the same fid for deletion.

Also address the log-spam review on deleteMovedSourceNeedles: an
unreachable volume server returns one error per needle, so collapse
multiple failures into a single per-server line with the first error as
an example.
2026-04-20 17:55:20 -07:00
Chris Lu 08a7502b2c fix(shell): error on missing volume id in fsck, mergeVolumes, vacuum (#9158)
* fix(shell): error on missing volume id in fsck, mergeVolumes, vacuum

Three shell commands silently report success when -volumeId /
-fromVolumeId / -toVolumeId names a volume the master doesn't know
about: typos, already-deleted volumes, and stale scripts all look
identical to a clean no-op, which is what made the confusion in #9116
take as long as it did to diagnose.

- volume.fsck: filter at the per-datanode loop drops unknown ids and
  findExtraChunksInVolumeServers ends with totalOrphanChunkCount==0,
  printing "no orphan data".
- fs.mergeVolumes: createMergePlan iterates only known volumes, so an
  unknown -fromVolumeId produces an empty plan and we print just the
  "max volume size: N MB" header (indistinguishable from "nothing to
  merge").
- volume.vacuum: the master's VacuumVolume RPC silently iterates
  matching volumes; a missing id returns success having done nothing.

Validate the requested ids against the current topology up front and
return an explicit "volume(s) not found on master: [X Y]" error. Also
drop a stale duplicate `if err != nil` in volume.fsck.Do left over from
a prior refactor.

Surfaces #9116 follow-up from madalee-com.

* address review: propagate reloadVolumesInfo error; dedupe vacuum missing ids

- fs.mergeVolumes: c.reloadVolumesInfo's return was ignored. If the
  master is unreachable or VolumeList fails, c.volumes stays empty and
  the new validation block reports "fromVolumeId X not found on master"
  — masking the real connection/RPC failure. Return the wrapped error
  instead.

- volume.vacuum: "volume.vacuum -volumeId 5,5,5" on a missing volume 5
  listed [5 5 5] in the error. Collect missing ids in a set so each
  missing id appears once.

* address review: reject fromVolumeId/toVolumeId values that overflow uint32

flag.Uint produces a uint (64-bit on amd64), and the existing cast to
needle.VolumeId silently truncates to uint32. A typo like
`-fromVolumeId=4294967297` would wrap to volume 1 and slip past every
other validation, so the merge would run against a completely
different volume than the operator intended.

Bail out with an explicit error when the raw flag value exceeds the
uint32 range, before the cast.
2026-04-20 15:32:31 -07:00
Chris Lu 6bdd775963 feat(shell): fs.mergeVolumes deletes source needles after filer update (#9160)
* feat(shell): fs.mergeVolumes deletes source needles after filer update

Before this change, mergeVolumes only copied chunks to the destination
volume and updated the filer — the source needle sat untouched on its
original volume as a silent orphan. Operators had to run a separate
volume.fsck + volume.vacuum pass to actually reclaim the space, and
#9116 (comment 4282692876) showed how that pipeline can look exactly
like "mergeVolumes did nothing": the source volume keeps reporting its
original size even though every chunk has been logically moved out.

Clean up the source inline. For each entry, track the pre-move fids as
they're captured, and after the UpdateEntry RPC commits, issue
BatchDelete on every replica of each source volume. Key invariants:

- Source fids are only deleted AFTER UpdateEntry succeeds; if the
  filer write fails we skip the cleanup for that entry so we never
  delete data the filer still references.
- rewriteManifestChunk grew a fourth return value so nested manifest
  and sub-chunk moves propagate their moved-source list back to the
  top-level callsite. The outer manifest itself is recorded at the
  callsite, since only the callsite sees the pre-rewrite fid.
- deleteMovedSourceNeedles logs errors but never returns them.
  Propagating would abort TraverseBfs mid-merge, stranding remaining
  entries; logging leaves the fallback path (fsck reconciles later)
  intact.
- StatusNotModified from the volume server is expected whenever a
  concurrent fsck purge beat us to the delete or a replica already
  reconciled — don't warn on it.

Readonly source volumes are already rejected up front by
createMergePlan, so by the time we reach the delete the source is
writable. If a replica's readonly bit has flipped since then the
delete will fail and get logged; the user can re-run once they've
fixed the replica (same failure mode as today's fsck purge).

Fixes the space-not-reclaimed half of #9116.

Related design discussion: #8589.

* address review: cast r.Status to int in StatusNotModified compare

http.StatusNotModified is an untyped constant so the compare works as
written, but the int32/int mixed-type signal trips static analyzers
and PR tooling. Cast explicitly and note why.
2026-04-20 14:37:20 -07:00
Chris Lu d57fc67022 fix(shell): fs.mergeVolumes now rewrites manifest chunks for large files (#9127)
* fix(shell): fs.mergeVolumes now rewrites manifest chunks for large files

Previously fs.mergeVolumes skipped any chunk whose IsChunkManifest flag was
true, printing "Change volume id for large file is not implemented yet" and
continuing. Because the BFS traversal only looks at top-level
entry.Chunks, sub-chunks referenced inside a manifest were never
considered either. For any file stored as a chunk manifest (large files
go this path), chunks in the source volume stayed put, leaving behind a
few MB of live data that vacuum and volume.deleteEmpty couldn't clean
up.

This change resolves each manifest chunk recursively, moves any
sub-chunk whose volume id is in the merge plan via the existing
moveChunk path, and re-serializes the manifest. If the manifest chunk
itself lives in a source volume, or any sub-chunk moved, the new
manifest blob is uploaded to a freshly assigned file id (the old
needle becomes orphaned and is reclaimed by vacuum like any other
moved chunk).

Fixes #9116.

* address review: batch UpdateEntry, fix dry-run, defer restore, avoid source volumes

- Call UpdateEntry once per entry after the chunk loop instead of once per
  moved chunk (gemini nit).
- In dry-run mode, mark anySubChanged when a sub-chunk in the plan is
  encountered and return changed=true after printing "rewrite manifest",
  so nested manifests also surface their would-rewrites (gemini nit).
- Defer filer_pb.AfterEntryDeserialization so the manifest chunk list is
  restored even when proto.Marshal fails (coderabbit nit).
- Reject AssignVolume results whose file id lands on a volume that is a
  source in the merge plan, and retry — otherwise the replacement
  manifest could be written to the volume being emptied (coderabbit).
2026-04-17 21:17:51 -07:00
Copilot 013362d2d3 fix(shell): show planned size in fs.mergeVolumes log to clarify size limit check (#8553)
The log message was comparing against the planned size of the destination
volume (including volumes already planned to merge into it) but only
displaying the raw volume size, making the output confusing when the
displayed sizes clearly didn't add up to exceed the limit.
2026-03-11 13:56:13 -07:00
Jaehoon KimandChris Lu f2e7af257d Fix volume.fsck -forcePurging -reallyDeleteFromVolume to fail fast on filer traversal errors (#8015)
* Add TraverseBfsWithContext and fix race conditions in error handling

- Add TraverseBfsWithContext function to support context cancellation
- Fix race condition in doTraverseBfsAndSaving using atomic.Bool and sync.Once
- Improve error handling with fail-fast behavior and proper error propagation
- Update command_volume_fsck to use error-returning saveFn callback
- Enhance error messages in readFilerFileIdFile with detailed context

* refactoring

* fix error format

* atomic

* filer_pb: make enqueue return void

* shell: simplify fs.meta.save error handling

* filer_pb: handle enqueue return value

* Revert "atomic"

This reverts commit 712648bc35.

* shell: refine fs.meta.save logic

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
2026-01-14 21:37:50 -08:00
Aleksey Kosov 283d9e0079 Add context with request (#6824) 2025-05-28 11:34:02 -07:00
Aleksey Kosovandakosov 165af32d6b added context to filer_client method calls (#6808)
Co-authored-by: akosov <a.kosov@kryptonite.ru>
2025-05-22 09:46:49 -07:00
Changrui Chen be74548cb5 fix: error info size bug in command_fs_merge_volumes.go (#6567) 2025-02-23 06:36:47 -08:00
Hadi Zamani b2f56d9add Add JWT authentication to fs.mergeVolumes command (#6461)
Add jwt authentication to fs.mergeVolumes command
2025-01-20 18:16:46 -08:00
Brad Murray bc3640ee64 Update command_fs_merge_volumes.go (#6406) 2025-01-02 08:57:26 -08:00
chrislu ec155022e7 "golang.org/x/exp/slices" => "slices" and go fmt 2024-12-19 19:25:06 -08:00
chrislu e28f55eb08 typo 2024-11-19 08:32:31 -08:00
chrislu 98f03862aa rename 2024-11-19 08:31:54 -08:00
chrislu bc7650bd61 adds more info on skipped volumes 2024-11-18 18:25:44 -08:00
chrislu ec30a504ba refactor 2024-09-29 10:38:22 -07:00
chrislu 701abbb9df add IsResourceHeavy() to command interface 2024-09-28 20:23:01 -07:00
vadimartynov 86d92a42b4 Added tls for http clients (#5766)
* Added global http client

* Added Do func for global http client

* Changed the code to use the global http client

* Fix http client in volume uploader

* Fixed pkg name

* Fixed http util funcs

* Fixed http client for bench_filer_upload

* Fixed http client for stress_filer_upload

* Fixed http client for filer_server_handlers_proxy

* Fixed http client for command_fs_merge_volumes

* Fixed http client for command_fs_merge_volumes and command_volume_fsck

* Fixed http client for s3api_server

* Added init global client for main funcs

* Rename global_client to client

* Changed:
- fixed NewHttpClient;
- added CheckIsHttpsClientEnabled func
- updated security.toml in scaffold

* Reduce the visibility of some functions in the util/http/client pkg

* Added the loadSecurityConfig function

* Use util.LoadSecurityConfiguration() in NewHttpClient func
2024-07-16 23:14:09 -07:00
Konstantin Lebedev 5ffacbb6ea refactor all methods strings to const (#5726) 2024-07-01 01:00:39 -07:00
Amir H. Yeganemehr 6f559fe838 fs.mergeVolumes: change dir to a named parameter 2023-11-13 08:25:44 -08:00
Amir H. Yeganemehr d3556479be Fix typo (dist => dest) 2023-11-13 06:41:24 -08:00
Amir H. Yeganemehr bb274ea8a3 fs.mergeVolumes: Make a plan based on volumes size 2023-11-13 06:41:24 -08:00
Amir H. Yeganemehr 3e8604a4c5 add fs.mergeVolumes command into shell (#4907)
* add fs.compact command into shell

* rename fs.compact to fs.mergeVolumes
2023-10-10 12:47:18 -07:00