Skip to content

feat(libkernel): support EV_CLEAR on a kqueue watched by another kqueue - #9575

Open
DavidSamir wants to merge 4 commits into
boykopovar:mainfrom
DavidSamir:feat/libkernel-kqueue-evclear
Open

DavidSamir wants to merge 4 commits into
boykopovar:mainfrom
DavidSamir:feat/libkernel-kqueue-evclear

Conversation

@DavidSamir

Copy link
Copy Markdown
Contributor

What

kevent threw for EVFILT_READ with EV_CLEAR on a kqueue descriptor, that is, one kqueue watching another. In FreeBSD 11:

  • The filter: knote_enqueue calls kqueue_wakeup, which runs KNOTE on the watched kqueue's kq_sel.si_note. So filt_kqueue (sys/kern/kern_event.c:364, kn_data = kq_count) is evaluated again every time the watched kqueue queues a note.
  • With EV_CLEAR: kqueue_scan clears the watching note when it is retrieved, so it fires again only after the next such activation.
  • At registration: kqueue_register evaluates the filter once, so a watched kqueue that already holds notes fires immediately.

How:

  • Equeue.hpp / Equeue.cpp:
    • KernelEqueuePrivate counts activations, meaning an event becoming enabled and triggered. They come from a poll result, a timer expiring, TriggerEvent, AddEvent of a triggered event, a pending event re-queued after retrieval, and EditEvent (the path NOTE_TRIGGER and EV_ENABLE take).
    • Activations() reads the count.
    • EditEvent counts through a scope guard, so this change leaves its NotifyOne line alone.
  • Kqueue.cpp:
    • EV_CLEAR on a kqueue no longer throws.
    • The note keeps the activation count it last saw, and whether it is active, in filter.data/owner, so the shared event structure is unchanged.
    • It fires when the watched queue's count has moved while notes are queued, with data set to the queued count. The reset after retrieval clears it.
    • The cycle check is unchanged.
  • The kevent on a kqueue debt line: the "EV_CLEAR throws" sentence now describes this. Activations are seen when the watching queue polls, so one that is undone before that poll is missed.

Tested

Windows 11 Pro 26200, WinLibs GCC 15.2.0 r7, Release. This is CPU-side, so no GPU is involved.

guest_kqueue (core/libs/tests/GuestKqueue.cpp) changes:

  • The old throw check: the line that expected the EV_CLEAR throw now adds and deletes the note successfully.
  • New ClearedKqueue():
    • nothing is reported at first;
    • triggering a user note in the watched kqueue fires the watching note once, with data 1 and EV_CLEAR;
    • a repeat poll reports nothing, even while the watched kqueue still holds its note;
    • draining the watched kqueue reports nothing;
    • a second trigger fires again with data 2;
    • deleting the watching note and registering it again on the non-empty kqueue fires it once.

Results:

Checklist

  • Based on current main; no other open PR implements the same functions
  • One topic per PR; follow-ups go in a new PR
  • No comments in code except technical debt
  • Unimplemented paths throw (NotImplemented_nid_no_patch); silent stubs are listed under Silent stubs in their library's technical debt file
  • No notes, investigation .md files or images added to the repository (attach them to this PR)
  • New third-party code is a submodule built from source
  • Generic behaviour, not specific to one title
  • Depends on: none
  • Fixes: none
  • Unblocks: none
  • GPUs tested: n/a
  • AI-assisted: yes

FreeBSD re-evaluates a kqueue's EVFILT_READ note from kqueue_wakeup,
which runs whenever the watched kqueue queues a note, so an EV_CLEAR
note fires again only after such an activation. The equeue now counts
activations, and the EV_CLEAR note fires when the count moves while
notes are queued.

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First run on Linux (the PR is tested on Windows only). Ubuntu 26.04, x86_64, Intel i7-8700, GCC 15.2.0, Release, BUILD_TESTING=ON, head bd82db6 merged --no-commit onto current origin/main (6ef5a1c).

  • Build: ok.
  • ctest -R guest_kqueue with the PR: passes.
  • Negative control (revert Equeue.cpp, Equeue.hpp and Kqueue.cpp to origin/main, keep the new test): the test aborts with the real assert
terminate called after throwing an instance of 'std::runtime_error'
  what():  kevent: EV_CLEAR on a kqueue descriptor not implemented
  • Restoring the PR implementation: passes again.

So the EV_CLEAR path and the activation counter are exercised by ClearedKqueue() on Linux too, and the test genuinely fails without them.

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 4563 core/libs/prx/libkernel/CMakeLists.txt
PR 4630 docs/dev/debt/libkernel.md
PR 5021 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 6149 docs/dev/debt/libkernel.md
PR 6740 docs/dev/debt/libkernel.md
PR 6830 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 6936 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7088 core/libs/prx/libkernel/Equeue/Equeue.hpp
core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
docs/dev/debt/libkernel.md
PR 7090 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7105 docs/dev/debt/libkernel.md
PR 7121 core/libs/prx/libkernel/Equeue/Equeue.hpp
core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
docs/dev/debt/libkernel.md
PR 7135 docs/dev/debt/libkernel.md
PR 7149 docs/dev/debt/libkernel.md
PR 7156 core/libs/prx/libkernel/CMakeLists.txt
PR 7192 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7201 core/libs/prx/libkernel/CMakeLists.txt
PR 7447 docs/dev/debt/libkernel.md
PR 7464 docs/dev/debt/libkernel.md
PR 7492 core/libs/prx/libkernel/Equeue/Equeue.cpp
core/libs/tests/GuestKqueue.cpp
PR 7509 core/libs/prx/libkernel/Equeue/Equeue.cpp
core/libs/prx/libkernel/Equeue/Equeue.hpp
docs/dev/debt/libkernel.md
PR 7530 docs/dev/debt/libkernel.md
PR 7532 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 7565 docs/dev/debt/libkernel.md
PR 7804 docs/dev/debt/libkernel.md
PR 8010 docs/dev/debt/libkernel.md
PR 8041 core/libs/prx/libkernel/CMakeLists.txt
PR 8463 core/libs/prx/libkernel/CMakeLists.txt
PR 8477 core/libs/prx/libkernel/CMakeLists.txt
PR 8497 docs/dev/debt/libkernel.md
PR 8520 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 8605 docs/dev/debt/libkernel.md
PR 8725 docs/dev/debt/libkernel.md
PR 8726 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 8916 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9095 docs/dev/debt/libkernel.md
PR 9187 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9199 docs/dev/debt/libkernel.md
PR 9263 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9326 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9389 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9442 docs/dev/debt/libkernel.md
PR 9491 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9509 docs/dev/debt/libkernel.md
PR 9515 docs/dev/debt/libkernel.md
PR 9578 docs/dev/debt/libkernel.md
PR 9632 docs/dev/debt/libkernel.md
PR 9669 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9684 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9693 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9732 docs/dev/debt/libkernel.md
PR 9823 core/libs/prx/libkernel/Equeue/Equeue.cpp
core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
PR 9843 core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
docs/dev/debt/libkernel.md
PR 9882 docs/dev/debt/libkernel.md
PR 9938 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9947 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 9962 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10014 core/libs/tests/GuestKqueue.cpp
PR 10016 core/libs/prx/libkernel/Equeue/Equeue.hpp
core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
PR 10025 docs/dev/debt/libkernel.md
PR 10061 docs/dev/debt/libkernel.md
PR 10089 docs/dev/debt/libkernel.md
PR 10105 docs/dev/debt/libkernel.md
PR 10121 docs/dev/debt/libkernel.md
PR 10130 docs/dev/debt/libkernel.md
PR 10132 docs/dev/debt/libkernel.md
PR 10133 docs/dev/debt/libkernel.md
PR 10137 docs/dev/debt/libkernel.md
PR 10162 docs/dev/debt/libkernel.md
PR 10177 docs/dev/debt/libkernel.md
PR 10179 docs/dev/debt/libkernel.md
PR 10192 docs/dev/debt/libkernel.md
PR 10198 docs/dev/debt/libkernel.md
PR 10305 docs/dev/debt/libkernel.md
PR 10310 docs/dev/debt/libkernel.md
PR 10319 docs/dev/debt/libkernel.md
PR 10385 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10442 docs/dev/debt/libkernel.md
PR 10499 docs/dev/debt/libkernel.md
PR 10543 docs/dev/debt/libkernel.md
PR 10552 core/libs/prx/libkernel/CMakeLists.txt
PR 10558 docs/dev/debt/libkernel.md
PR 10621 docs/dev/debt/libkernel.md
PR 10645 core/libs/prx/libkernel/Equeue/Equeue.cpp
docs/dev/debt/libkernel.md
PR 10660 core/libs/prx/libkernel/CMakeLists.txt
PR 10666 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10671 docs/dev/debt/libkernel.md
PR 10725 docs/dev/debt/libkernel.md
PR 10749 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10755 core/libs/prx/libkernel/CMakeLists.txt
PR 10767 core/libs/prx/libkernel/Equeue/Equeue.cpp
core/libs/prx/libkernel/Equeue/Equeue.hpp
core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
docs/dev/debt/libkernel.md
PR 10789 docs/dev/debt/libkernel.md
PR 10806 core/libs/prx/libkernel/CMakeLists.txt
PR 10809 docs/dev/debt/libkernel.md
PR 10810 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10816 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10817 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10826 docs/dev/debt/libkernel.md
PR 10830 docs/dev/debt/libkernel.md
PR 10848 docs/dev/debt/libkernel.md
PR 10855 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10856 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 10885 core/libs/prx/libkernel/CMakeLists.txt
PR 10898 docs/dev/debt/libkernel.md
PR 10901 docs/dev/debt/libkernel.md
PR 10953 core/libs/prx/libkernel/CMakeLists.txt
PR 10975 core/libs/prx/libkernel/CMakeLists.txt
PR 10979 core/libs/prx/libkernel/CMakeLists.txt
PR 11002 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 11005 docs/dev/debt/libkernel.md
PR 11006 docs/dev/debt/libkernel.md
PR 11025 docs/dev/debt/libkernel.md
PR 11029 docs/dev/debt/libkernel.md
PR 11039 docs/dev/debt/libkernel.md
PR 11040 docs/dev/debt/libkernel.md
PR 11042 docs/dev/debt/libkernel.md
PR 11047 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 11048 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 11078 docs/dev/debt/libkernel.md
PR 11085 docs/dev/debt/libkernel.md
PR 11108 docs/dev/debt/libkernel.md
PR 11116 docs/dev/debt/libkernel.md
PR 11125 core/libs/prx/libkernel/Equeue/Equeue.cpp
core/libs/prx/libkernel/Equeue/Equeue.hpp
PR 11130 core/libs/prx/libkernel/CMakeLists.txt
PR 11135 docs/dev/debt/libkernel.md
PR 11141 docs/dev/debt/libkernel.md
PR 11170 docs/dev/debt/libkernel.md
PR 11173 docs/dev/debt/libkernel.md
PR 11182 docs/dev/debt/libkernel.md
PR 11190 core/libs/prx/libkernel/CMakeLists.txt
docs/dev/debt/libkernel.md
PR 11194 docs/dev/debt/libkernel.md
PR 11195 docs/dev/debt/libkernel.md
PR 11201 docs/dev/debt/libkernel.md
PR 11215 docs/dev/debt/libkernel.md
PR 11219 docs/dev/debt/libkernel.md
PR 11223 core/libs/prx/libkernel/Equeue/Equeue.cpp
core/libs/prx/libkernel/Equeue/Equeue.hpp
core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
docs/dev/debt/libkernel.md
PR 11224 docs/dev/debt/libkernel.md
PR 11230 docs/dev/debt/libkernel.md
PR 11236 core/libs/prx/libkernel/Equeue/src/Kqueue.cpp
core/libs/tests/GuestKqueue.cpp
docs/dev/debt/libkernel.md
PR 11247 docs/dev/debt/libkernel.md
PR 11251 docs/dev/debt/libkernel.md
PR 11253 docs/dev/debt/libkernel.md
PR 11257 docs/dev/debt/libkernel.md
PR 11263 docs/dev/debt/libkernel.md
PR 11264 docs/dev/debt/libkernel.md
PR 11267 docs/dev/debt/libkernel.md
PR 11341 docs/dev/debt/libkernel.md

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The activation snapshot is not atomic, which can permanently lose a kqueue wakeup. PollClearedKqueue first calls CountTriggeredEvents() and then Activations(), and each call acquires/releases the watched queue's mutex independently. If another thread triggers an event after the count returns 0 but before the activation count is read, this poll stores the new generation in note.seen while leaving note.active false. The next poll sees the same generation and never reports the queued event. AddKqueueKnote has the same split-read window while initializing pending and seen.

Please expose one locked snapshot operation that returns both the triggered count and activation generation, and use it in both places (with timer/filter polling performed under the same lock). Add a concurrency regression test that forces or reliably exercises activation between snapshot components; the current sequential ClearedKqueue() test cannot cover this race.

Please also resolve the unverified overlap warnings before approval: #7653 shares Equeue.cpp, Kqueue.cpp, GuestKqueue.cpp and the debt entry, and #7088 shares Equeue.hpp, Kqueue.cpp, GuestKqueue.cpp and the debt entry. The body discusses #7653/#7088 as conflicting with main but does not record author coordination or a required ordering. #6044 also has an uncomparable GuestKqueue.cpp path because its broad test move exceeds the diff limit; preserve this new test at the moved path if that reorganization lands first. If any of these require sequencing, agree the order with the authors and declare the prerequisite in Depends on.

The hosted eligibility, intake, labels, report and conventions checks pass. The Linux, Windows and relinker product jobs are skipped, so I did not treat them as executed. The author-reported Windows test matrix and the independent Linux run in the discussion are useful evidence, but neither exercises the interleaving above.

@harisibrar1 harisibrar1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked against main 4a17c71 and FreeBSD releng/11.4 sys/kern/kern_event.c:

  • kqueue_scan calls filt_kqueue again before it reports a note (kern_event.c:1782-1792, 1798). PollClearedKqueue (Kqueue.cpp:204-215) only refreshes pending when the activation count has moved, so a note that was polled but not collected keeps active and its old data. Example: outer has an EV_CLEAR user note U, added before the EV_CLEAR watch on inner. Trigger inner note 7 (EV_CLEAR) and U, run kevent(outer, n=1) (returns U), then drain inner. The next kevent(outer) reports the watch with data 1, where FreeBSD reports nothing because kq_count is 0. With two inner notes and one drained, it reports data 2 where FreeBSD reports 1. One possible fix: set active = true when the count moves, then on every poll clear active if pending == 0, else set data = pending.
  • kqueue_register re-evaluates the filter on EV_ADD or EV_ENABLE of an existing enabled knote and queues it if ready (kern_event.c:1435-1438, 1468-1478). ModifyKnote re-arms PollEdge watches for this (Kqueue.cpp:389) but not PollClearedKqueue notes. So after the first report, a re-EV_ADD (or EV_DISABLE + EV_ENABLE) of the watch while inner still holds note 7 returns 0 here and 1 on FreeBSD. Setting active = true there for an enabled PollClearedKqueue note, together with the check above, should match FreeBSD.
  • Could both cases go into ClearedKqueue(), or into the "kevent on a kqueue" debt line if you would rather leave them for later?
  • Small point: inner notes 7 and 8 are added without EV_CLEAR, so Poll(inner) does not empty inner. The test is still valid, but the description's "draining the watched kqueue" step does not really drain it.

Reviewed with AI assistance (Claude Code); the checks above were run locally on my machine.

…e snapshot

A cleared kqueue watch read the watched queue's triggered count and its
activation counter under two separate locks, so an activation between the
reads was marked seen without making the watch active, and the wakeup was
lost. SnapshotTriggered now returns both under one lock. Every poll of an
active watch also rechecks the count, as kqueue_scan reruns filt_kqueue:
it drops when the watched queue empties and reports the current count.
EV_ADD and EV_ENABLE of an existing watch re-arm it. A separate
guest_kqueue_cleared_race test triggers the watched queue from another
thread while the watching queue waits.
@DavidSamir

Copy link
Copy Markdown
Contributor Author

Thank you both, these were real. New commits, no force-push:

6deae6e09b:

  • @MarcZX10's lost wakeup: KernelEqueuePrivate::SnapshotTriggered() replaces the separate CountTriggeredEvents() + Activations() reads. It returns the triggered count and the activation generation under one lock, after the same timer and filter polling. PollClearedKqueue and the registration in AddKqueueKnote both use it.
  • @harisibrar1's FreeBSD semantics:
    • Every poll now rechecks the watched count, as kqueue_scan reruns filt_kqueue. An activation makes the watch active; an active watch drops when the count is 0 and otherwise reports the current count.
    • EV_ADD of an existing watch, and EV_ENABLE, re-arm it in ModifyKnote, as kqueue_register re-evaluates.
  • Tests:
    • ClearedKqueueRechecksTheWatchedCount(): your examples.
      • A watch polled but not collected (outer n=1 takes an earlier EV_CLEAR user note) reports nothing once the watched queue is drained.
      • With two watched notes and one drained, it reports data == 1.
      • A re-EV_ADD, and EV_DISABLE + EV_ENABLE, while the watched queue still holds a note report it again.
      • The watched notes here use EV_CLEAR, so the drain really drains. You're right that the original ClearedKqueue() notes don't; that check is unchanged and its description was loose.
    • ClearedKqueueKeepsConcurrentActivations(), a separate CTest entry guest_kqueue_cleared_race (timeout 60):
      • 5000 rounds, each triggering the watched queue from another thread while the outer queue waits with a 1 s timeout.
      • The watched queue holds 20000 extra registered notes. That makes each count long enough for the other thread's trigger to land between two separate reads.
      • A lost wakeup shows up as that wait timing out.

Negative controls, all on Windows:

  • The PR's original Kqueue.cpp/Equeue.* fail ClearedKqueueRechecksTheWatchedCount.
  • The new semantics with the two reads split again (count, then generation) pass guest_kqueue but fail guest_kqueue_cleared_race 8/8 (between 4 and 18 s).
  • With the single snapshot, guest_kqueue_cleared_race passes in about 11.4 s.

c842a3762e merges current main.

Re-run on the merge (Windows 11, WinLibs GCC 15.2): guest_kqueue, guest_kqueue_cleared_race and the other equeue, kevent and event-flag tests pass, and check_conventions.py reports 0 errors (its one warning is a debt entry already over the limit on main).

One note for anyone reproducing: StandardInput() (on main) assumes nothing is readable on fd 0, so run guest_kqueue with stdin from /dev/null or a terminal. A stdin pipe that has data makes it abort early, which is unrelated to this change.

On the overlaps (#7653, #7088, and #6044's move of GuestKqueue.cpp), I'm asking those authors for an order and will record it in the checklist.

AI-assisted: yes

@jotitadev jotitadev left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-ran on Linux at c842a37 (follow-up to my run at bd82db6). Ubuntu 26.04, x86_64 i7-8700 (6 cores / 12 threads), GCC 15.2.0, Release, head merged --no-commit onto origin/main 7e2ba73.

  • With the PR: guest_kqueue and guest_kqueue_cleared_race pass; the race test 10/10 more runs (about 20 s each).
  • Control, Equeue.cpp, Equeue.hpp and Kqueue.cpp from bd82db6 (before 6deae6e), tests from the head:
    • guest_kqueue aborts (the new ClearedKqueueRechecksTheWatchedCount).
    • guest_kqueue_cleared_race passes 5/5 as registered, and 3/3 more with taskset -c 0,1 / taskset -c 0-3. It only catches the lost wakeup when pinned to one CPU: taskset -c 0 guest_kqueue_tests cleared-race aborts 3/3 with the old files and passes 5/5 with the head's.

So on a multi-core Linux machine the race test doesn't distinguish the fix as registered. If you want it to, pinning the test to one CPU (or making the trigger thread run between the count and the activation read) would do it; I didn't try the latter.

Written with AI assistance (Claude Code). I ran the builds and tests above on my machine.

On a multi-core Linux machine the race test passed with the split reads it
is meant to catch, and caught them only when the process ran on one CPU.
It now pins itself to its first allowed CPU on Linux. Windows already
catches the split reads unpinned, and pinning there hides them, so it
stays unpinned.
@DavidSamir

Copy link
Copy Markdown
Contributor Author

Thank you @jotitadev, that's exactly the gap. 7ef0ff5420 makes guest_kqueue_cleared_race pin itself to its first allowed CPU on Linux (sched_setaffinity, before the trigger threads start, so they inherit it), as in your taskset -c 0 run.

I left Windows unpinned on purpose. There, the split reads fail the race test 8/8 unpinned, but pinned to one CPU they passed 6/6, so pinning would hide the race. On Windows the fix passes in about 11.4 s, and guest_kqueue still passes.

If you have a moment, could you rerun your control on Linux at this head (Equeue.* and Kqueue.cpp from bd82db6, tests from the head, no taskset)? I'd expect guest_kqueue_cleared_race to abort now.

AI-assisted: yes

@MarcZX10 MarcZX10 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed head 7ef0ff542027fba77853362820b763b452641c02. The two earlier functional findings appear addressed: count and activation generation are read in one lock-held snapshot, and the cleared watch is polled against the current watched count on each pass, including re-add/enable. The new Linux race test pins the test and its trigger thread to one allowed CPU, matching the independent reviewer’s negative-control setup. I found no remaining code defect in this pass. The exact guest_kqueue_tests target builds locally. CTest starts both guest_kqueue and guest_kqueue_cleared_race, but each aborts in HostCpu before assertions because this Xeon E5-1620 v2 lacks LZCNT, BMI1/2, MOVBE, AVX2 and FMA. The Windows runs and split-snapshot negative control described in the comments are author-reported; the independent Linux reviewer has tested earlier heads and demonstrated that the split-read control only fails when pinned. Current eligibility, conventions, labels and report pass; product/relinker jobs are skipped. Please resolve the remaining overlap coordination before merge. #7653 edits the same KernelEqueuePrivate::EditEvent function (timer expiry accounting versus this PR's activation snapshot accounting); the changes look composable but need an agreed carry-forward/order. #7088 adds regular-file handling in Kqueue.cpp and tests in GuestKqueue.cpp; the compared functions are separate, but the overlap bot cannot compare these files against its conflicted base and the debt line is shared. #6044 relocates GuestKqueue.cpp and its registration. I found no author agreement in the latest public threads. Please coordinate with these authors, retain the new tests and guest_kqueue_cleared_race registration at the relocated path if #6044 lands first, and declare a dependency only if the agreed sequence requires one. The PR is 4 ahead / 11 behind current main (clean merge). Please refresh it to satisfy the current-main branch rule. One documentation/conventions correction: the current-base convention run reports a debt-length warning at docs/dev/debt/libkernel.md:203 (1,095 characters). The same entry is 960 characters on current main; this PR expanded it past the 1,000-character threshold. Please shorten the entry and correct the Tested text that describes the warning as pre-existing on main.

I’ve noticed that holding this PR because of #5239 isn’t necessary. It’s fine for PRs to keep coming in; they’ll be handled in due course. Please disregard my earlier request to hold or defer it.

@harisibrar1 harisibrar1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rechecked at 7ef0ff5:

  • Both points are fixed. PollClearedKqueue now rechecks the watched count on every poll and drops the watch at 0 (Kqueue.cpp:204-217), and ModifyKnote re-arms it on EV_ADD/EV_ENABLE (Kqueue.cpp:392-394). Traced against kqueue_scan and kqueue_register: the drained case reports nothing, the one-drained case reports data 1, and re-add/enable report 1 (0 once the watched queue is empty), all as on FreeBSD.
  • Reading the count and the activations under one SnapshotTriggered lock (Equeue.cpp:297-306) also closes the window @MarcZX10 found, and ClearedKqueueRechecksTheWatchedCount covers our examples with notes that really drain.
  • Non-blocking, +1 to @MarcZX10: the "kevent on a kqueue" debt entry goes from 960 characters on main to 1095 here, so this PR is what introduces that debt-length warning.

Reviewed with AI assistance (Claude Code); the checks above were run locally on my machine.

@MarcZX10 MarcZX10 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed exact head 7ef0ff542027fba77853362820b763b452641c02. The snapshot/generation pair is now read under one lock, the cleared watch rechecks the current watched count, and the re-arm paths cover EV_ADD/EV_ENABLE; the independent FreeBSD-semantics review confirms these changes address the earlier functional findings. I found no new implementation defect in those paths.

There are still repository-rule and integration blockers:

  1. core/libs/prx/libkernel/CMakeLists.txt registers guest_kqueue_cleared_race with TIMEOUT 60. CONTRIBUTING.md requires tests to finish within 30 seconds and says to shrink/split a slow test instead of raising its timeout. The reported Windows run is about 11.4 seconds, but the current test does 5,000 thread creations and 20,000 registered notes, and the independent Linux report measured about 20 seconds per run on its machine. Please reduce or restructure the race test so it retains a useful negative control with adequate CI headroom, and keep its timeout at or below 30 seconds. Please provide an exact-head Linux run for the newly pinned one-CPU path as well; earlier independent Linux runs were on the preceding head.
  2. The new kevent on a kqueue debt entry is 1,095 characters on this head; the corresponding current-main entry is 960. tools/check_conventions.py --base upstream/main --head HEAD reports a new debt-length warning at line 203. Please shorten the entry below the 1,000-character limit and retain the gap/console-verification facts.
  3. The branch is based on d6eafa54d0464258574fd2f71889754d05a5bc93, 54 commits behind current upstream/main 8b3e39832af85f404c6195418d11b3e1ebd1924d. Please update it and rerun the focused matrix.
  4. #7653 and #7088 remain open, and I still see no agreed resolution with their authors for the shared equeue/kqueue changes. #6044 also moves GuestKqueue.cpp and its registration. Please record how both behavior changes and guest_kqueue_cleared_race will be preserved through the chosen order.

Checks I ran on this exact head: git diff --check passes; the GuestKqueue.cpp source passes GCC 12.2 C++20 -Wall -Wextra -Werror -fsyntax-only; tools/check_test_registration.py --scripts reports 171 registered scripts. Conventions reports 0 errors and the new 1,095-character debt warning. The prior exact-head local test target built, but CTest aborted during HostCpu initialization before assertions because this host lacks LZCNT/BMI/MOVBE/AVX2/FMA. The author reports Windows suites/negative controls; independent Linux runs cover earlier heads and show why the one-CPU control matters. Hosted product jobs are skipped.

I withdraw my earlier generic intake/queue objection. Given your contribution-count eligibility, Unblocks: none is not an intake blocker here.

@MarcZX10

Copy link
Copy Markdown
Contributor

I’ve noticed that my earlier request to hold this PR based on #5239 was unnecessary. It’s fine to keep submitting PRs; maintainers will handle them in due course. Please disregard that request.

@github-actions github-actions Bot added the needs rebase Conflicts with main, rebase needed label Oct 23, 2026
@adriadam10 adriadam10 self-assigned this Oct 25, 2026

@adriadam10 adriadam10 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation looks right to me: the lock-held SnapshotTriggered(), the per-poll recheck of the watched count and the re-arm on EV_ADD/EV_ENABLE match kqueue_scan/filt_kqueue, and the new tests fail without the change. What blocks the merge:

  • core/libs/prx/libkernel/CMakeLists.txt:1518: guest_kqueue_cleared_race has TIMEOUT 60, and CONTRIBUTING asks for tests that finish within 30 seconds (shrink or split a slow test instead of raising the timeout). It takes about 20 s on an i7-8700 (5000 thread creations, 20000 registered notes, GuestKqueue.cpp:590-598). Please reduce it while keeping a negative control that still fails with the split reads, and set the timeout to 30 or less.
  • docs/dev/debt/libkernel.md:203: the kevent on a kqueue entry grows from 960 to 1095 characters and triggers the debt-length warning (limit 1000). Please shorten it, keeping the EV_CLEAR and "not measured on a console" facts. The Tested section calls that warning pre-existing on main; it is new with this entry, so please correct it.
  • The branch conflicts with main in core/libs/tests/GuestKqueue.cpp and docs/dev/debt/libkernel.md. Please rebase and rerun guest_kqueue and guest_kqueue_cleared_race. #7653 is already merged, so its EditEvent change is on main now. #7088 is still open and touches the same Kqueue.cpp/GuestKqueue.cpp hunks and the debt entry, so check the merge once it lands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build docs libkernel needs rebase Conflicts with main, rebase needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants