Skip to content

Fix/posix block size - #467

Open
ftarasenko wants to merge 4 commits into
k0sproject:mainfrom
ftarasenko:fix/posix-block-size
Open

ftarasenko wants to merge 4 commits into
k0sproject:mainfrom
ftarasenko:fix/posix-block-size

Conversation

@ftarasenko

Copy link
Copy Markdown

Fixes #466

PosixFile.fsBlockSize asked stat for %s where %o was meant. %s is the
size of the probed directory's own data; %o is the optimal I/O block size,
which is what the BSD %k in the same fallback already asks for.

On ext4 a directory occupies whole blocks, so %s answers 4096 and happens to
agree with %o — the reason this survived. XFS keeps a small directory inline
in the inode, so /var/lib/k0s/images holding one 15-character filename
answers 29, and a k0sctl airgap bundle upload ran as dd ... bs=29. That is
~18 million syscall pairs for a 500 MB bundle, and turns a seconds-long upload
into an hours-long one. The issue has the byte-by-byte breakdown of the 29.

Three commits' worth of behaviour in two:

Block size probe. Ask for %o, and reject an answer that is not a
plausible power-of-two block size so a future misprobe degrades to the default
instead of to something unusable. That also removes a crash: when stat exited 0
but printed something unparseable, blockSize stayed 0 and ddParams panicked
on numBytes % 0.

CopyFrom. dd's bs there is only the size of the chunks it streams stdin
into the file with, unrelated to the remote filesystem, so it now uses a fixed
1 MiB rather than the probed value — 4096 was costing 128k syscall pairs per
gigabyte even in the good case. The same line passed f.pos to seek=, which
dd counts in output blocks and not bytes, so a resumed copy wrote at
f.pos * bs; the offset is 0 for an ordinary upload, which is why it went
unnoticed. The block size now shrinks when it does not divide the offset, which
is a real if unlikely regression in throughput for an unaligned resume — an
appending write would avoid it, but oflag=append is not portable to the BSD
and macOS dd this library targets, so I left it.

WinFS has no block-size probe, so there is nothing to mirror there. No
exported API changes.

Verified: both commits build, vet and test green independently; the two new
test cases fail against the unfixed code (one by assertion, one by the panic
above). I could not run make lint — the golangci-lint I have is built against
Go 1.25 and refuses this repo's 1.27 target — so the lint job is unverified.


Written by an AI coding agent working for @ftarasenko, who hit this on their
own XFS hosts, confirmed the stat -c "%s %o" output above and reviewed the
change before it was opened.

That agent's session has no API access to this repository, so it will not see
review comments or CI results here. Follow-up is @ftarasenko's — they will
relay review feedback and push changes.


Generated by Claude Code

fsBlockSize probes the parent directory to decide what block size to hand
dd, and asked for stat's %s. That is the size of the directory's own data,
not a block size. On ext4 a directory occupies whole blocks, so %s returns
4096 and matches what %o would have said, which is why this looked correct
everywhere it was tried. XFS stores a small directory inline in the inode:
/var/lib/k0s/images holding one 15-character filename reports 29 bytes, and
the upload then ran as

    dd of=/var/lib/k0s/images/bundle.tar bs=29 seek=0 conv=notrunc

Streaming a multi-hundred-megabyte airgap bundle 29 bytes at a time takes
hours instead of seconds. Ask for %o, the optimal I/O block size, which is
what the BSD %k in the same fallback already means.

Reject an implausible answer as well, so a future misprobe degrades to the
default rather than to a block size that cannot work. That also removes a
crash: when stat exited 0 but printed something unparseable, blockSize was
left at 0 and ddParams panicked with an integer divide by zero.

Signed-off-by: Fedor Tarasenko <ftarasenko@itkey.com>
CopyFrom pipes a whole file into dd's stdin, so dd's bs is only the size of
the chunks it reads and writes; it has nothing to do with the remote file
system's block size. Taking it from fsBlockSize meant a 4096 byte block even
in the good case, which is 128k syscall pairs per gigabyte for no reason.
Use a fixed 1 MiB instead.

The same line also passed f.pos to seek, which dd counts in output blocks
rather than in bytes, so a resumed copy started writing at f.pos * bs. It
went unnoticed because the offset is 0 for an ordinary upload. Divide the
offset by the block size, and shrink the block size when it does not divide
the offset evenly.

Signed-off-by: Fedor Tarasenko <ftarasenko@itkey.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The resumed-copy behavior and power-of-two validation need direct regression coverage.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Fixes POSIX transfer performance and resume offsets by correcting block-size detection and dd usage.

Changes:

  • Uses stat’s optimal I/O block size with validation and fallback.
  • Streams uploads in 1 MiB chunks and converts byte offsets to dd block offsets.
  • Adds block-size and transfer command tests.
File summaries
File Description
remotefs/posixfs.go Defines block-size bounds and streaming chunk size.
remotefs/posixfile.go Corrects probing and resumed dd writes.
remotefs/posixfile_test.go Updates mocks and adds validation coverage.
Review details

Suppressed comments (1)

remotefs/posixfile_test.go:94

  • This 29 case is below minBlockSize, so it passes even if the new power-of-two check is accidentally removed. Add an in-range non-power-of-two value to cover that distinct validation branch.
		{"implausible", "29"}, // an XFS directory holding a single short-named file
		{"unparseable", "?"},
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

require.Equal(t, int64(5), n)
require.Equal(t, "hello", string(got))
require.Equal(t, "dd of=/tmp/file bs=4096 seek=0 conv=notrunc", mr.LastCommand())
require.Equal(t, "dd of=/tmp/file bs=1048576 seek=0 conv=notrunc", mr.LastCommand())
The existing CopyFrom test only ever ran with f.pos == 0, where the byte
offset and the block offset are both zero, so neither dd's seek unit nor
the block size reduction that keeps the two consistent was exercised.

Add a table of resume points and assert that the bs and seek dd is handed
multiply back to the byte offset: whole blocks, an offset smaller than the
streaming block size, an offset that is not a power-of-two multiple, and an
odd one that forces bs down to 1. Against the previous byte-valued seek, a
3 MiB resume asks dd to write at byte 3298534883328.

Signed-off-by: Fedor Tarasenko <ftarasenko@itkey.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKNtKouZgoxKp5udM2DdVu
ddParams picked the block size from the alignment of the length alone and
then divided the offset by it, so an offset that was not a whole number of
blocks rounded down: seeking to 2048 and reading 8192 bytes on a 4096 byte
block size was handed skip=0 and returned the first 8 KiB of the file. It
is the same bytes-versus-blocks confusion as dd's seek, one argument over.

Reduce the block size until it divides the offset and the length both, and
share that reduction with CopyFrom, which grew its own copy of the loop.
Lengths that do not fit the block size no longer collapse straight to bs=1
either: a 100 byte read at offset 4096 now runs at bs=4. count can drop its
rounding up, since the block size divides the length by construction, and
blocksize*count stays exactly numBytes as Write requires.

Signed-off-by: Fedor Tarasenko <ftarasenko@itkey.com>

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TKNtKouZgoxKp5udM2DdVu
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PosixFile.fsBlockSize asks stat for %s, making dd transfers to XFS hosts crawl

3 participants