Loading src/connection.h +9 −0 Original line number Diff line number Diff line Loading @@ -199,6 +199,15 @@ namespace netplus { // to the owning poll instance. int ownerPollFd = -1; // Opaque per-backend bookkeeping pointer, the IOCP-backend analogue of // ownerPollFd above -- IOCP has no per-connection "poll instance fd" (every // connection for one event instance shares a single completion port), but // requestWritablePoll()/detachConnection()/reattachConnection() still need to // find that instance's connection registry from just a con&. Set once at // accept time (event/iocp.cpp's accept_loop) to that translation unit's // EventState*; unused (stays null) on backends that use ownerPollFd instead. void* backendState = nullptr; // Proactively write out whatever is currently queued in SendData, // instead of waiting for the event loop's post-RequestEvent drain. // Safe to call from within a RequestEvent handler on the same Loading src/event/iocp.cpp +82 −15 Original line number Diff line number Diff line Loading @@ -262,13 +262,27 @@ namespace netplus { } catch (NetException& e) { c.slots[0].WritePending.store(false); if (e.getErrorType() != NetException::Note) throw; // EWOULDBLOCK: data remains in SendData but write is not pending. // Post a zero-byte completion to wake the worker and retry later. if (st && c.SendData.pos < c.SendData.size()) { SOCKET cs = (SOCKET)c.slots[0].csock->fd(); PostQueuedCompletionStatus(st->iocp, 0, (ULONG_PTR)c.slots[0].csock.get(), &c.slots[0].csock->_ReadBuffer->overlapped); } // tcp::sendData()'s IOCP-associated branch only ever throws Note for one // reason: another overlapped write is already in flight on this socket // (_pendingIocpWrite already true when we tried to post this one). That // write's own OP_WRITE completion -- guaranteed to arrive, whether it // finishes synchronously or not (Windows still queues a completion packet // either way) -- already re-invokes try_post_send() for whatever is left in // SendData once it lands (see the OP_WRITE handler below), so there is // nothing left to wake here. // // This used to PostQueuedCompletionStatus() a synthetic zero-byte // completion via c.slots[0].csock->_ReadBuffer->overlapped to force a // retry. That buffer's `operation` field is always OP_READ (post_recv() / // prime_read() are the only things that set it), so the synthetic // completion was misdelivered to the OP_READ branch above, where // bytes==0 && ok==true is indistinguishable from a real zero-byte read // completion -- i.e. the peer gracefully closing the connection. That // silently tore down the connection (PeerClosed -> mark_closing -> close()) // while its real write was still in flight, cutting the response short // for any multi-chunk (>BLOCKSIZE) send that raced a second try_post_send() // call (e.g. via con::flushSendData(), which -- unlike this function -- // doesn't check hasPendingWrite() between chunks) against this one. return false; } } Loading Loading @@ -396,6 +410,12 @@ namespace netplus { c->slots[0].csock->setIocpAssociated(true); register_con(st, accepted, c); // See connection.h's doc comment on backendState: lets // requestWritablePoll()/detachConnection()/reattachConnection() find this // instance's EventState (and hence its iocp handle + sockToCon registry) from // just a con&, mirroring how epoll.cpp/kqueue.cpp use ownerPollFd for the same // purpose. c->backendState = st; ev->ConnectEvent(*c, 0, 0); Loading Loading @@ -1026,15 +1046,62 @@ namespace netplus { // down — not a crash, not silent corruption, just the wakeup this // feature relies on not being available on IOCP yet. std::shared_ptr<con> lookupConnection(int /*fd*/) { return nullptr; } void requestWritablePoll(con &) {} // Not yet implemented for this backend (see connection.h's doc comment) -- // documented no-op, matching requestWritablePoll() above. std::unique_ptr<socket> detachConnection(con &) { return nullptr; } // Unlike epoll/kqueue, IOCP has no "arm for writable" step to redo -- a socket is // always eligible to have I/O posted on it, the only thing that ever blocked a send // was another overlapped write already in flight, and that write's own completion // already re-invokes try_post_send() for whatever's left in SendData (see the // OP_WRITE handler in EventWorker::operator() above). So the IOCP equivalent of "please // re-check this connection's writability" is simply: try the send right now. // // Caller must hold c.event_mutex (see connection.h's doc comment) -- same precondition // epoll.cpp's requestWritablePoll() has, since try_post_send() itself takes no lock. void requestWritablePoll(con &c) { EventState* st = reinterpret_cast<EventState*>(c.backendState); if (!st || c.slots.empty() || !c.slots[0].csock) return; try_post_send(nullptr, st, c, 0); } // Caller must hold c.event_mutex (see connection.h's doc comment). Removing the // sockToCon entry is enough to make this fd invisible to every worker thread from here // on: find_con() in EventWorker::operator() is the only thing that ever resolves a // completion's SOCKET back to a con, and it already drops anything it can't find // (`if (!conPtr) continue;`) -- there is no separate kernel-side interest to // deregister the way epoll_ctl(EPOLL_CTL_DEL) is needed on that backend, since IOCP // was never "polling" this fd in the first place, just waiting on whatever I/O had // actually been posted. std::unique_ptr<socket> detachConnection(con &c) { if (c.slots.empty() || !c.slots[0].csock) return nullptr; EventState* st = reinterpret_cast<EventState*>(c.backendState); if (!st) return nullptr; remove_con(st, (SOCKET)c.slots[0].csock->fd()); return std::move(c.slots[0].csock); } // Unlike detachConnection()/requestWritablePoll(), the CALLER MUST NOT hold // c->event_mutex (see connection.h's doc comment) -- this takes it itself. bool reattachConnection(std::shared_ptr<con> c) { if (!c || c->slots.empty() || !c->slots[0].csock) return false; EventState* st = reinterpret_cast<EventState*>(c->backendState); if (!st) return false; std::lock_guard<std::recursive_mutex> evlk(c->event_mutex); // The socket's CreateIoCompletionPort() association from accept_loop() is // permanent for the handle's lifetime (unlike epoll's fd interest, IOCP gives no // way to dissociate it) -- so all that's needed is making it a tracked connection // again and priming the next read, mirroring accept_loop()'s own post-registration // sequence. register_con(st, (SOCKET)c->slots[0].csock->fd(), c); try { post_recv(st, *c); } catch (...) { return false; } // Anything queued into SendData while detached gets its first send attempt kicked // off here too, same as requestWritablePoll(). if (c->SendData.pos < c->SendData.size()) { try { try_post_send(nullptr, st, *c, 0); } catch (...) {} } // detachConnection() never hands out a real socket on this backend (always nullptr // above), so no caller can ever have one to reattach -- documented no-op for the same // reason. bool reattachConnection(std::shared_ptr<con>) { return false; } return true; } } // namespace netplus Loading
src/connection.h +9 −0 Original line number Diff line number Diff line Loading @@ -199,6 +199,15 @@ namespace netplus { // to the owning poll instance. int ownerPollFd = -1; // Opaque per-backend bookkeeping pointer, the IOCP-backend analogue of // ownerPollFd above -- IOCP has no per-connection "poll instance fd" (every // connection for one event instance shares a single completion port), but // requestWritablePoll()/detachConnection()/reattachConnection() still need to // find that instance's connection registry from just a con&. Set once at // accept time (event/iocp.cpp's accept_loop) to that translation unit's // EventState*; unused (stays null) on backends that use ownerPollFd instead. void* backendState = nullptr; // Proactively write out whatever is currently queued in SendData, // instead of waiting for the event loop's post-RequestEvent drain. // Safe to call from within a RequestEvent handler on the same Loading
src/event/iocp.cpp +82 −15 Original line number Diff line number Diff line Loading @@ -262,13 +262,27 @@ namespace netplus { } catch (NetException& e) { c.slots[0].WritePending.store(false); if (e.getErrorType() != NetException::Note) throw; // EWOULDBLOCK: data remains in SendData but write is not pending. // Post a zero-byte completion to wake the worker and retry later. if (st && c.SendData.pos < c.SendData.size()) { SOCKET cs = (SOCKET)c.slots[0].csock->fd(); PostQueuedCompletionStatus(st->iocp, 0, (ULONG_PTR)c.slots[0].csock.get(), &c.slots[0].csock->_ReadBuffer->overlapped); } // tcp::sendData()'s IOCP-associated branch only ever throws Note for one // reason: another overlapped write is already in flight on this socket // (_pendingIocpWrite already true when we tried to post this one). That // write's own OP_WRITE completion -- guaranteed to arrive, whether it // finishes synchronously or not (Windows still queues a completion packet // either way) -- already re-invokes try_post_send() for whatever is left in // SendData once it lands (see the OP_WRITE handler below), so there is // nothing left to wake here. // // This used to PostQueuedCompletionStatus() a synthetic zero-byte // completion via c.slots[0].csock->_ReadBuffer->overlapped to force a // retry. That buffer's `operation` field is always OP_READ (post_recv() / // prime_read() are the only things that set it), so the synthetic // completion was misdelivered to the OP_READ branch above, where // bytes==0 && ok==true is indistinguishable from a real zero-byte read // completion -- i.e. the peer gracefully closing the connection. That // silently tore down the connection (PeerClosed -> mark_closing -> close()) // while its real write was still in flight, cutting the response short // for any multi-chunk (>BLOCKSIZE) send that raced a second try_post_send() // call (e.g. via con::flushSendData(), which -- unlike this function -- // doesn't check hasPendingWrite() between chunks) against this one. return false; } } Loading Loading @@ -396,6 +410,12 @@ namespace netplus { c->slots[0].csock->setIocpAssociated(true); register_con(st, accepted, c); // See connection.h's doc comment on backendState: lets // requestWritablePoll()/detachConnection()/reattachConnection() find this // instance's EventState (and hence its iocp handle + sockToCon registry) from // just a con&, mirroring how epoll.cpp/kqueue.cpp use ownerPollFd for the same // purpose. c->backendState = st; ev->ConnectEvent(*c, 0, 0); Loading Loading @@ -1026,15 +1046,62 @@ namespace netplus { // down — not a crash, not silent corruption, just the wakeup this // feature relies on not being available on IOCP yet. std::shared_ptr<con> lookupConnection(int /*fd*/) { return nullptr; } void requestWritablePoll(con &) {} // Not yet implemented for this backend (see connection.h's doc comment) -- // documented no-op, matching requestWritablePoll() above. std::unique_ptr<socket> detachConnection(con &) { return nullptr; } // Unlike epoll/kqueue, IOCP has no "arm for writable" step to redo -- a socket is // always eligible to have I/O posted on it, the only thing that ever blocked a send // was another overlapped write already in flight, and that write's own completion // already re-invokes try_post_send() for whatever's left in SendData (see the // OP_WRITE handler in EventWorker::operator() above). So the IOCP equivalent of "please // re-check this connection's writability" is simply: try the send right now. // // Caller must hold c.event_mutex (see connection.h's doc comment) -- same precondition // epoll.cpp's requestWritablePoll() has, since try_post_send() itself takes no lock. void requestWritablePoll(con &c) { EventState* st = reinterpret_cast<EventState*>(c.backendState); if (!st || c.slots.empty() || !c.slots[0].csock) return; try_post_send(nullptr, st, c, 0); } // Caller must hold c.event_mutex (see connection.h's doc comment). Removing the // sockToCon entry is enough to make this fd invisible to every worker thread from here // on: find_con() in EventWorker::operator() is the only thing that ever resolves a // completion's SOCKET back to a con, and it already drops anything it can't find // (`if (!conPtr) continue;`) -- there is no separate kernel-side interest to // deregister the way epoll_ctl(EPOLL_CTL_DEL) is needed on that backend, since IOCP // was never "polling" this fd in the first place, just waiting on whatever I/O had // actually been posted. std::unique_ptr<socket> detachConnection(con &c) { if (c.slots.empty() || !c.slots[0].csock) return nullptr; EventState* st = reinterpret_cast<EventState*>(c.backendState); if (!st) return nullptr; remove_con(st, (SOCKET)c.slots[0].csock->fd()); return std::move(c.slots[0].csock); } // Unlike detachConnection()/requestWritablePoll(), the CALLER MUST NOT hold // c->event_mutex (see connection.h's doc comment) -- this takes it itself. bool reattachConnection(std::shared_ptr<con> c) { if (!c || c->slots.empty() || !c->slots[0].csock) return false; EventState* st = reinterpret_cast<EventState*>(c->backendState); if (!st) return false; std::lock_guard<std::recursive_mutex> evlk(c->event_mutex); // The socket's CreateIoCompletionPort() association from accept_loop() is // permanent for the handle's lifetime (unlike epoll's fd interest, IOCP gives no // way to dissociate it) -- so all that's needed is making it a tracked connection // again and priming the next read, mirroring accept_loop()'s own post-registration // sequence. register_con(st, (SOCKET)c->slots[0].csock->fd(), c); try { post_recv(st, *c); } catch (...) { return false; } // Anything queued into SendData while detached gets its first send attempt kicked // off here too, same as requestWritablePoll(). if (c->SendData.pos < c->SendData.size()) { try { try_post_send(nullptr, st, *c, 0); } catch (...) {} } // detachConnection() never hands out a real socket on this backend (always nullptr // above), so no caller can ever have one to reattach -- documented no-op for the same // reason. bool reattachConnection(std::shared_ptr<con>) { return false; } return true; } } // namespace netplus