Skip to content

Et fixes - #14

Merged
g2px1 merged 4 commits into
mainfrom
et_fixes
Apr 25, 2026
Merged

Et fixes#14
g2px1 merged 4 commits into
mainfrom
et_fixes

Conversation

@g2px1

@g2px1 g2px1 commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Fix EPOLLET event loss and coroutine frame leak at shutdown

This PR fixes two long-standing bugs in the event loop that surface
under load with bursty I/O and consumers that don't drain the kernel
buffer fast enough.

1. EPOLLET event loss when no awaiter is registered

Symptom

Long-running sockets eventually hang. pg_stat_activity shows the
backend in ClientRead while ss -tnpi shows several MB sitting in
the socket's receive queue. The client never reads, the server never
gets NACKed, and the connection idles forever.

Root cause

EPoller registers fds with EPOLLIN | EPOLLOUT | EPOLLET. With
edge-triggered semantics, the kernel only re-fires EPOLLIN when the
buffer transitions from empty to non-empty. If EPOLLIN arrives at a
moment when no coroutine is parked in sock->first (for example,
between two awaits where the consumer is briefly busy), the wakeup is
silently dropped. The next co_await wait_readable() parks itself and
waits forever, because the kernel will not re-fire until the buffer
drains, which it never does.

Fix

Two new bits per socket header — READ_PENDING_MASK and
WRITE_PENDING_MASK — packed into the existing state word
(bits 57-58, alongside the unused bits above TIMEOUT_EPOCH_MASK).

  • EPoller: if EPOLLIN/EPOLLOUT arrives and sock->first/second
    is null, set the corresponding pending bit instead of discarding
    the event.
  • AwaiterRead/AwaiterWrite/AwaiterAccept::await_ready:
    consume the pending bit and proceed without suspending if it was set.
  • AwaiterRead/AwaiterWrite/AwaiterAccept::await_suspend:
    after registering into the slot and clearing busy, re-check pending.
    If a wakeup landed in the small window between await_ready and the
    store into first/second, self-resume by enqueueing into the
    ready queue. Closes the race without requiring a full lock.

Files:

  • include/uvent/utils/sync/RefCountedSession.h
  • include/uvent/net/SocketMetadata.h
  • src/poll/EPoller.cpp
  • src/net/AwaiterOperations.cpp

2. Coroutine frame leak at shutdown (~350 MB on a small workload)

Symptom

ASAN under -fsanitize=address reports leaks of Awaitable<...> frames
allocated by pump_input, wait_readable, and similar — over a million
allocations on a workload that only opens 16 connections. Even small
test programs leak hundreds of MB by exit time.

Root cause

AwaitableFrame::final_suspend returns suspend_always. After a
coroutine returns, its frame pushes itself into thread-local q_c,
the destruction queue. q_c is drained inside the main worker loop
(while (!token.stop_requested())). When Uvent::stop() is called,
the loop exits before processing the last batch of pending destructions.
The frames sitting in q_c at exit time are leaked.

Fix

Thread::threadFunction, after the main while exits and before the
QSBR detach, drains q_c (and q_sh under UVENT_ENABLE_REUSEADDR)
to empty in a tight loop. Each destroyed handle is overwritten with
nullptr in tmp_coroutines_ to keep the buffer consistent.

Drains live in threadFunction rather than ~Thread() because
thread-local q_c is only reachable from inside the worker thread.

Files:

  • src/system/Thread.cpp

Testing

  • Built with -fsanitize=address,undefined and run a workload that
    opens 16 connections, executes ~1M coroutine resumes across 8 large
    exports, and exits via Uvent::stop().
  • Before this PR: 354 MB leaked across ~1.08M allocations, plus
    intermittent heap-use-after-free at shutdown.
  • After this PR: 0 leaks, no UAF.
  • Pre-existing UBSan diagnostics at Thread.cpp:66 (null EPoller
    vptr in early init under REUSEADDR) and Thread.cpp:80 (null
    coroutine_handle write through the bulk-dequeue buffer) are
    unrelated to this PR and left for a separate change.

@g2px1
g2px1 merged commit 5736625 into main Apr 25, 2026
4 checks passed
@g2px1
g2px1 deleted the et_fixes branch May 4, 2026 21:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant