Skip to content

Add: report NEXT_LEVEL reservation stalls - #1613

Open
puddingfjz wants to merge 2 commits into
hw-native-sys:mainfrom
puddingfjz:feat/report-group-reservation-stalls
Open

Add: report NEXT_LEVEL reservation stalls#1613
puddingfjz wants to merge 2 commits into
hw-native-sys:mainfrom
puddingfjz:feat/report-group-reservation-stalls

Conversation

@puddingfjz

@puddingfjz puddingfjz commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • detect a blocked NEXT_LEVEL group reservation when an idle reserved target has queued single work
  • emit one native warning after the structural condition persists for five seconds
  • include the group slot, busy targets, idle-but-queued targets, and their single FIFO head slots

Behavior

This is diagnostic only. It does not classify the condition as a deadlock, release the reservation, or otherwise change scheduling policy. Reporting is edge-triggered per group/stall episode and uses a native no-throw sink rather than a Python logger callback.

Dependency

Depends on #1612.

This PR is intentionally stacked on the wake-generation fix because the warning deadline uses the scheduler timed-wait path introduced there. Merge #1612 first; the branch will then be rebased onto the updated main before this PR merges.

Review context

#1565 (comment)

Testing

  • test_scheduler: 28/28 passed
  • non-hardware C++ UT: 67/67 passed

@puddingfjz
puddingfjz force-pushed the feat/report-group-reservation-stalls branch from d688ef9 to 1a3a55c Compare July 31, 2026 09:41
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The scheduler now uses wake generations and completion events for waiting. NEXT_LEVEL dispatch returns reservation details, tracks structural stalls, and emits timed diagnostics. Queue inspection and tests cover blocked groups, queued singles, duplicate completions, and wake-up behavior.

Changes

Scheduler wake and reservation stall handling

Layer / File(s) Summary
Scheduler and queue contracts
src/common/hierarchical/scheduler.h, src/common/hierarchical/types.*
Scheduler configuration and dispatch results now include reservation-stall data. NextLevelReadyQueues supports worker-specific front inspection.
Wake-generation scheduler waiting
src/common/hierarchical/scheduler.cpp, src/common/hierarchical/scheduler.h, docs/scheduler.md
Scheduler notifications advance a wake generation. The scheduler waits on unconsumed generations, terminal completions, or reservation-stall deadlines.
Reservation-aware dispatch and diagnostics
src/common/hierarchical/scheduler.cpp, src/common/hierarchical/worker.cpp, docs/scheduler.md
Group dispatch reports busy targets and queued target heads. Stall episodes produce one diagnostic after the configured interval.
Scheduler behavior validation
tests/ut/cpp/hierarchical/test_scheduler.cpp
Tests cover stall diagnostics, blocked-group waiting, incremental dispatch, duplicate completion wake-ups, and invalid completion cleanup.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Scheduler
  participant ReadyQueues
  participant CompletionFIFO
  participant WorkerDiagnosticSink
  Scheduler->>ReadyQueues: inspect target queue heads
  ReadyQueues-->>Scheduler: reservation and queue state
  Scheduler->>Scheduler: track wake generation and stall deadline
  CompletionFIFO-->>Scheduler: terminal completion
  Scheduler->>WorkerDiagnosticSink: report reservation stall
Loading

Possibly related PRs

Poem

A rabbit watched the wake count grow,
While queued tasks waited in a row.
Stall heads spoke after five long beats,
Busy paws freed waiting seats.
The scheduler hopped, alert and bright.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely identifies the main change: reporting NEXT_LEVEL reservation stalls.
Description check ✅ Passed The description accurately explains the diagnostic behavior, warning conditions, included details, dependency, and test results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@puddingfjz puddingfjz changed the title Feat/report group reservation stalls Add: report NEXT_LEVEL reservation stalls Jul 31, 2026
Consume each READY or stop notification once instead of treating a
non-empty blocked queue as permanent work.

Group-member completions that do not enqueue a terminal task
completion also advance the wake generation. This keeps queued singles
and stop() moving after a member was terminalized early.

Tests cover blocked-head sleeping, partial group completion, and the
terminalized-member idle transition that previously hung CI.
Warn once when a blocked group continuously reserves an idle target that has queued single work. The native no-throw sink reports the group and target queue structure without releasing the reservation or classifying the state as a deadlock.
@puddingfjz
puddingfjz force-pushed the feat/report-group-reservation-stalls branch from 1a3a55c to 3b3ebb1 Compare July 31, 2026 15:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
tests/ut/cpp/hierarchical/test_scheduler.cpp (2)

1179-1186: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Assert the worker pointers before dereferencing them.

manager.get_worker_by_id returns nullptr for an unregistered id. Scheduler::dispatch_next_level_group in src/common/hierarchical/scheduler.cpp checks for that case explicitly. Worker ids 0 and 1 are registered in SetUp(), so the current test is safe. Add ASSERT_NE(..., nullptr) so a future registration change fails as an assertion instead of a segmentation fault inside a lock scope.

🛡️ Proposed change
         WorkerThread *manager_worker_a = manager.get_worker_by_id(WorkerType::NEXT_LEVEL, 0);
         WorkerThread *manager_worker_b = manager.get_worker_by_id(WorkerType::NEXT_LEVEL, 1);
+        ASSERT_NE(manager_worker_a, nullptr);
+        ASSERT_NE(manager_worker_b, nullptr);
         while ((!manager_worker_a->idle() || !manager_worker_b->idle()) &&
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/cpp/hierarchical/test_scheduler.cpp` around lines 1179 - 1186, Add
ASSERT_NE checks for manager_worker_a and manager_worker_b immediately after
their get_worker_by_id calls and before the idle() polling loop, so null worker
registrations fail the test safely before dereferencing either pointer.

786-796: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Gate the diagnostic field checks with an assertion.

If the report does not arrive within 200 ms, line 790 fails and the test continues. Lines 791-796 then compare default-initialized capture fields and emit five more failures. Use ASSERT_EQ for the report-count check so the test stops at the root cause.

The same pattern applies to the exact dispatched_count() checks at lines 797, 810-811, and 820-821. Those are less severe because a stale count fails only one expectation.

♻️ Proposed change
-    EXPECT_EQ(stall_capture.report_count.load(std::memory_order_acquire), 1);
+    ASSERT_EQ(stall_capture.report_count.load(std::memory_order_acquire), 1);
     EXPECT_EQ(stall_capture.group_slot, group.task_slot);
-    EXPECT_EQ(stall_capture.busy_target_count, 1u);
+    ASSERT_EQ(stall_capture.busy_target_count, 1u);
     EXPECT_EQ(stall_capture.busy_target_worker_ids[0], 1);
-    EXPECT_EQ(stall_capture.idle_queued_target_count, 1u);
+    ASSERT_EQ(stall_capture.idle_queued_target_count, 1u);
     EXPECT_EQ(stall_capture.idle_queued_target_worker_ids[0], 0);
     EXPECT_EQ(stall_capture.idle_queued_single_head_slots[0], single_a.task_slot);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/cpp/hierarchical/test_scheduler.cpp` around lines 786 - 796, Use
ASSERT_EQ for the stall_capture.report_count check before validating the
diagnostic fields, so the test returns immediately when no report arrives. Apply
the same assertion-strengthening to the exact dispatched_count() checks at the
indicated points, preserving their existing expected values and surrounding test
logic.
src/common/hierarchical/scheduler.h (2)

108-146: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document the single-writer-thread invariant on reservation_stall_episode_.

reservation_stall_deadline() reads reservation_stall_episode_ under completion_mu_ (in run()), and update_reservation_stall() writes it under loop_mu_ (in dispatch_ready()). This is safe only because both calls execute exclusively on the scheduler thread. Add a short comment near the member to record this invariant, so a future change that invokes either method from another thread does not introduce a data race.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/hierarchical/scheduler.h` around lines 108 - 146, Add a short
comment immediately above reservation_stall_episode_ documenting that it is
accessed only by the scheduler thread, including its reads in
reservation_stall_deadline() and writes in update_reservation_stall(). Do not
change the locking or surrounding dispatch logic.

59-69: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Document the pointer-lifetime contract on ReservationStallDiagnostic.

busy_target_worker_ids, idle_queued_target_worker_ids, and idle_queued_single_head_slots are raw pointers. In scheduler.cpp, update_reservation_stall builds this struct from dispatch_result.busy_target_worker_ids.data() and similar calls, where dispatch_result is a temporary owned by the caller of dispatch_ready(). The sink call is synchronous, so the pointers stay valid only for the duration of that call.

Add a comment on the struct that states this constraint. A future sink implementation that stores these pointers past the callback invocation reads freed memory.

📝 Proposed documentation addition
     struct ReservationStallDiagnostic {
+        // All pointer/count fields below reference memory owned by the caller
+        // of the sink and are valid only for the duration of the sink call.
+        // Do not store these pointers past the callback invocation.
         TaskSlot group_slot{INVALID_SLOT};
         const int32_t *busy_target_worker_ids{nullptr};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/hierarchical/scheduler.h` around lines 59 - 69, Add a
documentation comment to the ReservationStallDiagnostic struct stating that its
raw pointer fields are valid only during the synchronous ReservationStallSink
callback and must not be retained afterward, since they reference caller-owned
temporary storage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/common/hierarchical/worker.cpp`:
- Around line 37-54: Update report_reservation_stall so the synchronously
invoked diagnostic sink never performs potentially blocking stderr operations.
Replace flockfile and repeated std::fprintf calls with an existing bounded
non-blocking native sink, or add explicit EPIPE and backpressure handling that
prevents SIGPIPE and unbounded blocking before this sink is registered.

In `@tests/ut/cpp/hierarchical/test_scheduler.cpp`:
- Around line 1189-1208: Move the EXPECT_EQ assertions for worker_a and worker_b
dispatch counts until after the optional notify_ready retry completes. Keep the
initial wait and retry as best-effort cleanup protection, then assert the final
counts before conditionally calling complete() so a successful retry does not
leave a prior test failure.

---

Nitpick comments:
In `@src/common/hierarchical/scheduler.h`:
- Around line 108-146: Add a short comment immediately above
reservation_stall_episode_ documenting that it is accessed only by the scheduler
thread, including its reads in reservation_stall_deadline() and writes in
update_reservation_stall(). Do not change the locking or surrounding dispatch
logic.
- Around line 59-69: Add a documentation comment to the
ReservationStallDiagnostic struct stating that its raw pointer fields are valid
only during the synchronous ReservationStallSink callback and must not be
retained afterward, since they reference caller-owned temporary storage.

In `@tests/ut/cpp/hierarchical/test_scheduler.cpp`:
- Around line 1179-1186: Add ASSERT_NE checks for manager_worker_a and
manager_worker_b immediately after their get_worker_by_id calls and before the
idle() polling loop, so null worker registrations fail the test safely before
dereferencing either pointer.
- Around line 786-796: Use ASSERT_EQ for the stall_capture.report_count check
before validating the diagnostic fields, so the test returns immediately when no
report arrives. Apply the same assertion-strengthening to the exact
dispatched_count() checks at the indicated points, preserving their existing
expected values and surrounding test logic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c6f392a1-58b3-4829-a64a-9d5720437d0d

📥 Commits

Reviewing files that changed from the base of the PR and between f2a20ca and 3b3ebb1.

📒 Files selected for processing (7)
  • docs/scheduler.md
  • src/common/hierarchical/scheduler.cpp
  • src/common/hierarchical/scheduler.h
  • src/common/hierarchical/types.cpp
  • src/common/hierarchical/types.h
  • src/common/hierarchical/worker.cpp
  • tests/ut/cpp/hierarchical/test_scheduler.cpp

Comment on lines +37 to +54
void report_reservation_stall(void *, const Scheduler::ReservationStallDiagnostic &diagnostic) noexcept {
flockfile(stderr);
std::fprintf(
stderr, "[WARN] NEXT_LEVEL group reservation stalled: group_slot=%d busy_target_ids=[", diagnostic.group_slot
);
for (size_t i = 0; i < diagnostic.busy_target_count; ++i) {
std::fprintf(stderr, "%s%d", i == 0 ? "" : ",", diagnostic.busy_target_worker_ids[i]);
}
std::fputs("] idle_targets_with_queued_singles=[", stderr);
for (size_t i = 0; i < diagnostic.idle_queued_target_count; ++i) {
std::fprintf(
stderr, "%s%d:head_slot=%d", i == 0 ? "" : ",", diagnostic.idle_queued_target_worker_ids[i],
diagnostic.idle_queued_single_head_slots[i]
);
}
std::fputs("]\n", stderr);
funlockfile(stderr);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Keep the registered diagnostic sink off the blocking scheduler path.

Scheduler::update_reservation_stall invokes this sink synchronously. flockfile and the repeated std::fprintf calls can block when stderr is slow or its pipe is full. A closed pipe can raise SIGPIPE and terminate the process. Route the warning through a bounded non-blocking native sink, or define explicit EPIPE and backpressure handling before registering this direct stderr writer.

Also applies to: 151-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/common/hierarchical/worker.cpp` around lines 37 - 54, Update
report_reservation_stall so the synchronously invoked diagnostic sink never
performs potentially blocking stderr operations. Replace flockfile and repeated
std::fprintf calls with an existing bounded non-blocking native sink, or add
explicit EPIPE and backpressure handling that prevents SIGPIPE and unbounded
blocking before this sink is registered.

Comment on lines +1189 to +1208
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while ((worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
EXPECT_EQ(worker_a.dispatched_count(), 2);
EXPECT_EQ(worker_b.dispatched_count(), 2);

// Keep cleanup non-fatal so a missing wake reports a test failure instead
// of hanging the fixture in Scheduler::stop().
if (worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) {
sched.notify_ready();
deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while ((worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
if (worker_a.dispatched_count() >= 2) worker_a.complete();
if (worker_b.dispatched_count() >= 2) worker_b.complete();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The retry block cannot recover the test outcome.

Lines 1194-1195 record a failure with EXPECT_EQ when a dispatch count is below 2. The retry at lines 1199-1206 then runs for exactly that case. If the retry succeeds, the test still fails because the earlier EXPECT_EQ already failed. The stated intent in the comment at lines 1197-1198 is not achieved.

Pick one behavior. Either treat the first wait as best-effort and assert only after the retry, or remove the retry and keep the direct expectation.

🐛 Proposed fix: assert after the retry
     auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
     while ((worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) &&
            std::chrono::steady_clock::now() < deadline) {
         std::this_thread::sleep_for(std::chrono::milliseconds(1));
     }
-    EXPECT_EQ(worker_a.dispatched_count(), 2);
-    EXPECT_EQ(worker_b.dispatched_count(), 2);
 
-    // Keep cleanup non-fatal so a missing wake reports a test failure instead
-    // of hanging the fixture in Scheduler::stop().
+    // Retry with an explicit notification so a missing completion-driven wake
+    // is reported as a failure below instead of hanging Scheduler::stop().
     if (worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) {
         sched.notify_ready();
         deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
         while ((worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) &&
                std::chrono::steady_clock::now() < deadline) {
             std::this_thread::sleep_for(std::chrono::milliseconds(1));
         }
+        ADD_FAILURE() << "queued singles required an explicit notify_ready(); "
+                         "the completion callback did not wake the scheduler";
     }
+    EXPECT_EQ(worker_a.dispatched_count(), 2);
+    EXPECT_EQ(worker_b.dispatched_count(), 2);
     if (worker_a.dispatched_count() >= 2) worker_a.complete();
     if (worker_b.dispatched_count() >= 2) worker_b.complete();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while ((worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
EXPECT_EQ(worker_a.dispatched_count(), 2);
EXPECT_EQ(worker_b.dispatched_count(), 2);
// Keep cleanup non-fatal so a missing wake reports a test failure instead
// of hanging the fixture in Scheduler::stop().
if (worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) {
sched.notify_ready();
deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while ((worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
if (worker_a.dispatched_count() >= 2) worker_a.complete();
if (worker_b.dispatched_count() >= 2) worker_b.complete();
auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while ((worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
// Retry with an explicit notification so a missing completion-driven wake
// is reported as a failure below instead of hanging Scheduler::stop().
if (worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) {
sched.notify_ready();
deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
while ((worker_a.dispatched_count() < 2 || worker_b.dispatched_count() < 2) &&
std::chrono::steady_clock::now() < deadline) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
ADD_FAILURE() << "queued singles required an explicit notify_ready(); "
"the completion callback did not wake the scheduler";
}
EXPECT_EQ(worker_a.dispatched_count(), 2);
EXPECT_EQ(worker_b.dispatched_count(), 2);
if (worker_a.dispatched_count() >= 2) worker_a.complete();
if (worker_b.dispatched_count() >= 2) worker_b.complete();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/ut/cpp/hierarchical/test_scheduler.cpp` around lines 1189 - 1208, Move
the EXPECT_EQ assertions for worker_a and worker_b dispatch counts until after
the optional notify_ready retry completes. Keep the initial wait and retry as
best-effort cleanup protection, then assert the final counts before
conditionally calling complete() so a successful retry does not leave a prior
test failure.

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