One Slow Connection Froze Every Other One
Receive shutdown and response completion are different events
In one line
The other side having finished sending does not mean I cannot send a reply. You have to manage receive EOF and send queue exhaustion as different states.
Why this was needed
The line-based server from the previous lab marked closed and reclaimed the socket when it met EOF. That is an easy starting point within the range where a client exchanges requests and responses and then closes the connection. But to support a client that, after sending its whole request, closes only its own send direction and waits for the result, you have to extend this policy. It is not that multiplexing itself is broken; it is a limit of the model that expresses termination with a single boolean.
This time you build a toy "uppercase print shop." The client sends the manuscript bytes and closes its send direction. The print shop takes EOF as the end of the manuscript and sends back a reply with ASCII lowercase letters converted to uppercase. If you decide that a client who handed in a manuscript and fell silent has also closed its ears, you end up throwing the reply away. The lab's protocol is one request per connection, and it does not replace HTTP, TLS, or a persistent connection protocol.
How it works
shutdown(2) distinguishes which direction to shut down, reading or writing. A client's shutdown(socket.SHUT_WR) means it will send no more, and reading the response on the same socket remains possible. Its role differs from close, which reclaims the local file descriptor. An example that tries to recv again on a closed socket does not test half-close.
The return value for stream EOF in recv(2) is 0. In Python you observe it as a recv requested with a positive size returning b"". You must not generalize the same rule to a zero-length datagram in UDP or to recv(0). In particular, if you call recv(0) because the input buffer has reached its limit, you may decide the manuscript has ended without any evidence that it has. The lab leaves room to observe one byte more than the maximum allowed, and tells EOF apart from exceeding the limit.
| State | Interest events | Next action |
|---|---|---|
| Receiving the manuscript | READ | Read up to 4096 bytes once |
| EOF, reply remaining | WRITE | Send once and delete only the length actually sent |
| Reply has all left the queue | None | Unregister, then close the socket |
| Size exceeded, error, deadline | None | Preserve the reason and reclaim the buffers and the socket |
EOF is not an event to keep trying to read. If you keep READ registered because a reply remains, you may observe EOF repeatedly and the loop may spin idle. Conversely, if you remove WRITE at EOF too, the reply that has to be sent never makes progress. You compute the interest mask not from the single question "is the connection alive," but from the receive state and the remaining output.
A success from send(2) is not confirmation that the other side's application has read and processed the reply. In the lab, complete is limited to meaning that the local output queue has been emptied. A system where you must confirm the other side's processing too, like transaction completion, needs a separate response or an application-level acknowledgment message. That is why the grader does not look only at the fact that the reference solution left complete, but also compares the bytes the real client received.
A memory limit and a deadline are different defenses
Even if you set the manuscript limit to 4096 bytes, a client that sends one byte at a time, slowly, can occupy a connection for a long time. If there is only an idle timeout that extends on every activity, it may survive forever. Here, at the start you fix an absolute deadline with time.monotonic() + 수명 (the placeholder is the lifetime), and you do not extend it even when you receive bytes or see EOF. The same deadline applies when sending is blocked after EOF because the other side does not read the reply. In production you can set separate deadlines for receiving the request, processing, and sending the response, but this model closes out responsibility with a single total lifetime.
When the limit is exceeded, you close rather than return a huge error response. This is a learning policy for a small conversion server, and a production protocol has to design separately for error frames and for handling unreceived input. Even if each connection's input and output are finite, total memory is not finite if the number of connections is unlimited. So you cannot say this lab alone completes resource protection for the whole service.
Ownership ends in one place
Peer.finish only changes the termination reason and the buffer state, and does not close the socket directly. As the Python selectors documentation guides, the loop side does unregister and then close. If you close first, a descriptor that has already become invalid stays in the registration list, which makes exceptions or leak analysis hard. The final finally is also needed to reclaim the socket not only on normal completion but on an exception in the middle.
What it looks like in the field
The Cloudflare Spectrum job posting checked on 2026-09-13 asks for debugging Linux sockets, TCP connection states, and connection lifecycle problems, and for analyzing production incidents. This module practices, among those, termination direction, resource reclamation, and boundary condition validation. It is not a course that guarantees a company's official training or hiring preparation, and this one lab alone does not satisfy the Go, Rust, and large-scale distributed system requirements.
What you will do in the next lab
In 8 steps, you build the Peer state and the multiplexing loop. The final check opens four real TCP connections. The first client stays silent, the next client sends its manuscript in pieces and then calls only SHUT_WR, and the rest produce an empty request and a limit overflow. You check whether the normal client receives its whole reply without waiting for the silent client to terminate, and whether all the sockets are reclaimed. Partial send and EAGAIN are forced by separate deterministic tests. We do not say the partial-send branch has been verified by the observation that a small reply happened to be delivered fine.