62561c21fe70490a2a51fe822422d727d75171ad
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
62561c21fe |
kvm2: stop handing go-libvirt a reader that can error (H2)
uploadImage streamed the HTTP body straight into StorageVolUpload. When that body failed part way through, go-libvirt deadlocked: requestStream starts its stream sender on two unbuffered channels and, on the error path, returns without ever reading outErr, so SendStream emits a StatusError packet and parks on `outErr <-` forever while the main goroutine parks on `abort <- true` forever. StorageVolUpload never returned. The 20-minute transfer budget rescued the caller only, and the socket kill did nothing, because both ends are blocked on channel sends rather than on I/O -- which is what the comment at backend.go:152 got wrong. Each failed upload leaked kvm2's call goroutine, go-libvirt's sender goroutine and a 4 MiB buffer, permanently, against a 128Mi pod limit and a reconciler that retries on backoff. The defect is upstream and stays there: no fork, no replace. Instead eofReader, at the go-libvirt seam so it covers the seed and marker uploads too, converts any source error to io.EOF -- the one outcome that ends a stream the normal way -- and keeps the real error for the caller. countingReader sits underneath untouched, so the existing length and digest checks report exactly what they did before. The download also gets its own deadline a minute inside the transfer budget. fetchImage already bounded the whole fetch at 20 minutes, so the body's death and the budget's socket kill were racing and the socket usually won; with the body a minute ahead, a stalled mirror ends through its reader and unwinds cleanly instead of leaking. discardVolume moves from uploadImage into fetchImage's upload-error branch, so the dedicated connection is closed and libvirtd has let go of the stream's fd before the file is unlinked. Moving it rather than adding a second Close keeps image_test's closeCount() == 1 true. That discard is still unfenced against a second provider entry sharing the volume name; H4 closes that by making the name per-entry. What remains: a failure originating in libvirtd mid-upload still leaks, since SendStream has already returned by then. Confirmed unfixable from here. Known gaps quotes the two-character upstream fix (make(chan bool, 1) / make(chan error, 1)) and the replace escape hatch. Unverified at ~85%: that libvirtd answers a short-but-clean stream finish with OK rather than an error. virFDStreamWrite enforces length only on writes past it and nothing compares offset to length on the close path, which is also why virsh vol-upload of a small file into a large volume works. If it errors we are back to a deadlock by a different door, which is exactly today's behaviour, so this cannot be worse. The execution file carries a probe recipe to settle it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
0239fb7e69 |
kvm2: do not let a stalled write delay the connection teardown (H1)
sshSocket.Close was errors.Join(s.Conn.Close(), s.client.Close()). Go evaluates those left to right, so the blocking close ran before the unblocking one. s.Conn is the forwarded libvirt socket, and closing an SSH channel writes a close message under ch.writeMu — which go-libvirt already holds while streaming a base image through that same channel. Against a host that vanished without RST or FIN, that write blocks until the kernel stops retransmitting (tcp_retries2, ~15 min), and s.client.Close(), a bare net.Conn.Close() that takes no lock and unblocks the stalled writer through netpoll, was never reached. Close the client only; nothing wants the polite channel-close message, and reordering instead would leave an error to filter in a memoised closeErr. link.markDead compounded it by running kill() before close(l.closed), both inside a sync.Once. Reordering alone was not enough: sync.Once.Do blocks concurrent callers until the first body returns, so every other markDead — including the one in call is own ctx-cancel branch — queued behind the stalled kill, and cancelling the reconcile did not help. The once field is gone; the mutex the struct already had guards the close-exactly-once invariant, reason is set before the signal, and kill runs outside the lock. Blast radius was the whole operator, not one host: MaxConcurrentReconciles is 3 and ensureImage holds a per-image lock whose acquire is ctx-aware but reconcile contexts carry no deadline, so three Proxies on one host with an uncached image parked every worker. The same edits close a second route to the same pin: go-libvirt is Disconnect closes the net.Conn it was handed (libvirt.go:331 -> socket.go:164), and that conn is our sshSocket, so libvirtBackend.Close is Disconnect goroutine entered closeOnce and hung; the 5 s closeBudget released Close, which called markDead -> kill -> sshSocket.Close and blocked in the other goroutine is sync.Once. Verified against the pinned x/crypto v0.54.0 rather than assumed: the flow-control park (remoteWin.reserve, channel.go:268) is outside writeMu and only writePacket holds it across the TCP write, so a writer parked in SSH flow control never blocks the close and a writer inside the TCP write always does. To reproduce by hand, shrink SO_SNDBUF well under the 2 MiB channel window (channelWindowSize = 64 * channelMaxPacket) so the socket buffer fills before the window does. The kvm provider does not have this bug: it closes client only and never touches the forwarded channel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |