Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- Several transfers of the same content at once no longer fail. The receiver
names a transfer's `.partial` after the content hash so a retry can resume
it; two peers sending the same file (or one sender on two connections)
therefore wrote the same file, and all but the first failed at the final
rename. The first transfer keeps the resumable `.partial`; a concurrent one
gets a private file that is removed if it does not finish. A sender that
stalls and retries still resumes: once the transfer holding the `.partial`
has written nothing for two minutes (twice the sender's default wait for an
ACK), the retry takes the file over, and the stalled transfer is refused
any further write, its DONE, and the file itself if it starts over. A retry
sooner than that starts over in a private file. A transfer refused at the
start never takes a file over.
- A file that cannot fit on the receiver's disk is refused before any byte is
sent. The byte quota is a fixed number and can be larger than the disk, so a
transfer ran until the disk was full, failed, and left its bytes in
`.partial` — where they kept the disk full and blocked incoming messages.
The receiver now checks free space at the start (keeping 16 MiB in reserve),
and if a write still hits a full disk or disk quota it deletes that
`.partial`.
- A streamed transfer declaring a size of 2^63 bytes or more is refused by the
receiver's byte quota. The size was converted to a negative number and
passed the check.
- A sender whose transfer is refused at the start (quota, disk full, too many
transfers) reports the refusal. It was mistaken for a receiver too old to
support streamed transfers, so the caller retried with the single-frame
path and pushed the whole file at a peer that had just declined it.
- `WriteFrame` sends a frame of up to 64 KiB as one write. Header and payload
as two writes made the payload wait on Nagle and the peer's delayed ACK.
- The inbox byte cap no longer deletes the whole inbox. With the default
config (`InboxMaxBytes == 0`, meaning 256 MiB) the evictor compared the
inbox size against the raw value 0, so the first time the inbox passed
Expand Down
16 changes: 16 additions & 0 deletions dataexchange.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,29 @@ func WriteFrame(w io.Writer, f *Frame) error {
var hdr [8]byte
binary.BigEndian.PutUint32(hdr[0:4], ftype)
binary.BigEndian.PutUint32(hdr[4:8], uint32(len(payload)))
// A small frame goes out as one write. Written as header-then-payload,
// the payload is a second small write on a stream that still has the
// header unacknowledged: Nagle holds it until the peer's delayed ACK
// fires, ~40ms per frame on daemons that delay ACKs for short segments.
if len(payload) <= singleWriteMax {
buf := make([]byte, len(hdr)+len(payload))
copy(buf, hdr[:])
copy(buf[len(hdr):], payload)
_, err = w.Write(buf)
return err
}
if _, err := w.Write(hdr[:]); err != nil {
return err
}
_, err = w.Write(payload)
return err
}

// singleWriteMax is the largest payload WriteFrame copies next to its header
// to send both in one write. Above it the copy costs more than it saves: the
// payload spans many segments and is not held back by Nagle.
const singleWriteMax = 64 * 1024

// ReadFrame reads a frame from a reader.
func ReadFrame(r io.Reader) (*Frame, error) {
var hdr [8]byte
Expand Down
Loading
Loading