Skip to content

fix(flowcontrol): call up() after down() in maxMinHeap.Remove#1689

Closed
RishabhSaini wants to merge 1 commit into
llm-d:mainfrom
RishabhSaini:heapCorrupt
Closed

fix(flowcontrol): call up() after down() in maxMinHeap.Remove#1689
RishabhSaini wants to merge 1 commit into
llm-d:mainfrom
RishabhSaini:heapCorrupt

Conversation

@RishabhSaini

@RishabhSaini RishabhSaini commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • maxMinHeap.Remove only calls down() after swapping the removed element
    with the last array element. When the replacement comes from a different
    subtree, ancestor-level violations go unfixed. Add the missing up() call.
  • Add a white-box test with a 6-element heap that proves the corruption.

Details

In a max-min heap, levels alternate between max and min. The subtree property
(each node is the max or min of its entire subtree) is NOT transitive across
alternating levels, so down() alone cannot fix violations with ancestors.

Current production impact: none. The only Remove caller (dispatchItem)
always removes the root (index 0) via PeekHead, where down() alone is
correct regardless of heap layout. However, Remove accepts an arbitrary
handle and its contract promises O(log n) arbitrary removal. Any future caller
(request cancellation, queue eviction) would hit silent heap corruption.

Test plan

  • New test TestMaxMinHeap_Remove_CrossSubtreeSwap fails without the fix,
    passes with it
  • All existing queue conformance and property tests pass

@RishabhSaini
RishabhSaini requested review from a team, LukeAVanDrie and shmuelk as code owners June 18, 2026 16:24
@RishabhSaini
RishabhSaini requested review from ahg-g and liu-cong June 18, 2026 16:24
@github-actions github-actions Bot added the size/M Denotes a PR that changes 30-99 lines, ignoring generated files. label Jun 18, 2026
Remove swaps position i with the last element then calls down() but
not up(). When the last element comes from a different subtree, the
ancestor relationship can break:

  Before:              After Remove(index=3, time=1):

        0                    0
       / \                  / \
      5   50               5   50
     / \    \             / \
    1   4   40          40   4

  Last element (index 5, time=40) swaps into index 3.
  down(3): no children, no-op.
  Node 1 (min-level, time=5) must be lowest-priority in {5, 40, 4}.
  time=40 has lower priority than time=5 -- heap property violated.

Add the missing up(i) call after down(i). If down moved the element,
up is a no-op at the new position (and vice versa).

Signed-off-by: RishabhSaini <rishabhsaini01@gmail.com>
@LukeAVanDrie

Copy link
Copy Markdown
Contributor

@RishabhSaini and I discussed this and are going ahead with #1690 which resolves the issue and moves the code to a more maintainable state.

@shmuelk

shmuelk commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

@RishabhSaini and I discussed this and are going ahead with #1690 which resolves the issue and moves the code to a more maintainable state.

Can this PR be closed?

@RishabhSaini

Copy link
Copy Markdown
Contributor Author

in favor of #1690

@RishabhSaini
RishabhSaini deleted the heapCorrupt branch June 22, 2026 16:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/M Denotes a PR that changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants