Commit 21e655fe authored by jan.koester's avatar jan.koester
Browse files

test

parent befe46c2
Loading
Loading
Loading
Loading
+23 −28
Original line number Diff line number Diff line
@@ -230,20 +230,12 @@ namespace netplus {
                    ccon->slots[0].csock = std::make_unique<ssl>(*srv->_tls.cert_map, -1);
                } else if (_ServerSocket->_Type == sockettype::QUIC) {
                    // QUIC: accept() drains up to 512 queued datagrams in
                    // one call (see quic::accept). The listener fd is
                    // registered EPOLLONESHOT (see runEventloop) so we're
                    // the only thread servicing it right now — rearm on
                    // scope exit (even if accept() throws) so the next
                    // datagram wakes a thread again.
                    int qfd = _ServerSocket->fd();
                    struct RearmOnExit {
                        poll* p; int fd;
                        ~RearmOnExit() {
                            try { p->setpollEventsFd(fd, EPOLLIN | EPOLLONESHOT); }
                            catch (...) {}
                        }
                    } rearm{this, qfd};

                    // one call (see quic::accept). The listener fd is level
                    // triggered without EPOLLONESHOT (see runEventloop), so
                    // no rearm is needed here — and no rearm-with-ONESHOT
                    // should happen here either, since that would silently
                    // put the fd back into single-thread-at-a-time mode
                    // after the first connection event.
                    std::unique_ptr<socket> quic_csock;
                    _ServerSocket->accept(quic_csock, true);
                    return;
@@ -808,20 +800,23 @@ namespace netplus {
            epoll_event setevent{};
            setevent.events = EPOLLIN;
            // QUIC multiplexes every client (and all of their ongoing
            // traffic) over this one UDP socket for the connection's
            // whole lifetime — unlike TCP/SSL, where only accept() shares
            // the listener and established connections get their own
            // EPOLLONESHOT'd fd. Without ONESHOT here, every incoming
            // datagram wakes all `threads` workers (level-triggered,
            // shared epoll fd) — and since quic_mtx() is one lock per
            // listener (see socket.h), only one of them can ever do
            // anything useful; the rest just contend on that mutex for
            // nothing. ONESHOT + explicit rearm in ConnectEventHandler
            // limits servicing to one thread at a time, same as TCP/SSL
            // already do per-connection.
            if (ss->_Type == sockettype::QUIC) {
                setevent.events |= EPOLLONESHOT;
            }
            // traffic) over this one UDP socket for the connection's whole
            // lifetime — unlike TCP/SSL, where only accept() shares the
            // listener and established connections get their own
            // EPOLLONESHOT'd fd. This used to also be EPOLLONESHOT'd for
            // QUIC, because quic_mtx() was one lock shared by every
            // connection on the listener (see socket.h's history) — level
            // triggered + shared epoll fd meant every datagram woke all
            // `threads` workers, and only one could ever do anything
            // useful while the rest contended on that single mutex for
            // nothing. Each connection now has its own quic_mtx(), and
            // _child_connections lookups/mutations go through their own
            // short-lived _registry_mutex (see quic::accept()), so
            // multiple threads calling accept() concurrently on this fd
            // now actually process different connections in parallel
            // instead of serializing on one lock — recvmmsg on the same
            // fd from multiple threads is safe (the kernel hands out
            // disjoint datagrams, never duplicates).
            setevent.data.fd = ss->fd();

            if (epoll_ctl(_pollFD, EPOLL_CTL_ADD, ss->fd(), &setevent) < 0) {
+281 −151

File changed.

Preview size limit exceeded, changes collapsed.

+39 −19
Original line number Diff line number Diff line
@@ -31,6 +31,7 @@
#include <iostream>
#include <atomic>
#include <mutex>
#include <shared_mutex>
#include <string>
#include <memory>
#include <deque>
@@ -996,20 +997,34 @@ namespace netplus {
		std::map<std::string, ssl::CertificateBundle> _cert_map;
		ssl::CertificateBundle* _selected_cert = nullptr;
		
		// Mutex for thread safety (recursive: musl returns EDEADLK on
		// double-lock by same thread, which happens when processFrame
		// callbacks re-enter locking functions like sendStreamData).
		// Declared before _child_connections/_child_owned_connections
		// deliberately: members are destroyed in reverse declaration
		// order, and ~quic() (run for every child when
		// _child_owned_connections is torn down) locks quic_mtx() —
		// which for a child resolves to _parent->_quic_mutex. If this
		// were declared after the child containers (as it used to be),
		// the parent's _quic_mutex would already be destroyed by the
		// time its children's destructors ran, locking an already-dead
		// mutex.
		// Mutex for this connection's own state (recursive: musl returns
		// EDEADLK on double-lock by same thread, which happens when
		// processFrame callbacks re-enter locking functions like
		// sendStreamData). Each quic object — listener or child — has and
		// uses its own; unlike the old design, a child's quic_mtx() no
		// longer delegates to the parent's, so different connections on
		// the same listener can be processed by different threads at the
		// same time (see _registry_mutex below for what still needs a
		// shared lock, and epoll.cpp for the EPOLLONESHOT removal this
		// enables). Declared before _child_connections deliberately:
		// members are destroyed in reverse declaration order, and
		// ~quic() (run for every child when _child_connections' last
		// shared_ptr reference to it drops) locks quic_mtx() on itself —
		// harmless now that it's not shared with the parent, but keeping
		// the ordering avoids relying on that distinction.
		mutable std::recursive_mutex _quic_mutex;

		// Guards _child_connections only (registry membership), never a
		// connection's own state — that's quic_mtx()'s job. shared_mutex
		// because lookups (every packet routed to an existing connection)
		// vastly outnumber inserts/erases (new/closed connections): many
		// threads can look up different — or the same — connections
		// concurrently via a shared_lock, and only take the exclusive
		// unique_lock for the rare registry mutation. Declared before
		// _child_connections for the same destruction-order reason as
		// _quic_mutex above.
		mutable std::shared_mutex _registry_mutex;

		// Connection tracking for server mode (maps DCID to child quic*).
		// Keyed by the raw CID bytes as a std::string (see cidKey() in
		// quic.cpp) rather than std::vector<uint8_t>: same correctness
@@ -1018,10 +1033,15 @@ namespace netplus {
		// byte-wise tree comparisons of the old std::map, and a CID that
		// fits in std::string's small-string buffer needs no heap
		// allocation for the lookup, unlike a std::vector key.
		// _child_connections maps DCID to raw pointer for fast lookup
		// _child_owned_connections vector owns the lifetime via unique_ptr
		std::unordered_map<std::string, quic*> _child_connections;
		std::vector<std::unique_ptr<quic>> _child_owned_connections;  // Owns child connection lifetimes
		// Holds shared_ptr rather than a raw pointer + separate owning
		// vector: each connection is registered under two keys (original
		// DCID and our local CID) that share ownership of the same
		// object, and a caller that finds an entry can copy the
		// shared_ptr out (a "pin") before releasing _registry_mutex,
		// keeping the connection alive across the handoff to its own
		// quic_mtx() even if another thread concurrently erases both
		// registry entries for it.
		std::unordered_map<std::string, std::shared_ptr<quic>> _child_connections;
		quic* _parent = nullptr;  // For child connections, pointer to parent listening socket

		// Stream data callback for application layer (quic-specific internal type)
@@ -1119,10 +1139,10 @@ namespace netplus {
		};
		std::vector<BatchEntry> _send_batch;

		// Children delegate to parent's mutex so there is only ONE lock
		// per QUIC listener — eliminates lock-ordering complexity.
		// Each connection's own lock — see _quic_mutex's comment for why
		// this no longer delegates to the parent.
		std::recursive_mutex& quic_mtx() const {
			return _parent ? _parent->_quic_mutex : _quic_mutex;
			return _quic_mutex;
		}

		// Friend classes for event handling