fix(pyamber): repair LinkedBlockingMultiQueue removal paths; add unit tests#6903
Conversation
… tests SubQueue.remove matched the predecessor's item but unlinked the following node, and unlink cleared the wrong item and never decremented SubQueue.count, so removing an item dropped its successor and made the next get() return None. add_sub_queue built PriorityGroup through class access, leaving owner bound to the class so remove_sub_queue raised AttributeError for any enabled sub-queue.
Automated Reviewer SuggestionsBased on the
|
There was a problem hiding this comment.
Pull request overview
This PR improves the correctness and test coverage of LinkedBlockingMultiQueue (PyAmber’s internal keyed multi-queue) by fixing two removal-path defects and adding a comprehensive unit-test suite that exercises previously uncovered behavior.
Changes:
- Fix
SubQueue.remove()/unlink()to correctly identify and unlink the target node, clear the removed node’s item, and keepSubQueue.countconsistent withowner.total_count. - Fix
add_sub_queue()to constructPriorityGroupvia instance access (self.PriorityGroup(...)) so the@innerdescriptor bindsownercorrectly, enablingremove_sub_queue()for enabled sub-queues. - Add ~30 new unit tests covering
peek()at all levels, removal/clear paths,remove_sub_queue(), and various guard behaviors; refactor the sharedqueuefixture to module scope.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| amber/src/main/python/core/util/customized_queue/linked_blocking_multi_queue.py | Fix removal logic/count bookkeeping and correct PriorityGroup construction for proper owner binding. |
| amber/src/test/python/core/util/customized_queue/test_linked_blocking_multi_queue.py | Expand unit test coverage to exercise peek/removal/clear and guard behaviors; share a module-level queue fixture. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6903 +/- ##
============================================
+ Coverage 78.39% 78.57% +0.18%
Complexity 3723 3723
============================================
Files 1161 1161
Lines 46066 46067 +1
Branches 5107 5107
============================================
+ Hits 36112 36199 +87
+ Misses 8343 8257 -86
Partials 1611 1611
*This pull request uses carry forward flags. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
| config | throughput | MB/s | latency | max Δ latest / 7d | |
|---|---|---|---|---|---|
| 🔴 | bs=10 sw=10 sl=64 | 363 | 0.221 | 26,792/38,040/38,040 us | 🔴 -15.3% / 🔴 +129.2% |
| 🔴 | bs=100 sw=10 sl=64 | 776 | 0.473 | 128,596/161,670/161,670 us | 🔴 +21.6% / 🔴 +48.2% |
| ⚪ | bs=1000 sw=10 sl=64 | 904 | 0.552 | 1,105,721/1,196,679/1,196,679 us | ⚪ within ±5% / 🔴 +14.4% |
Baseline details
Latest main 4af3a85 from same runner
| config | metric | PR | latest main | 7d avg | Δ latest | Δ 7d |
|---|---|---|---|---|---|---|
| bs=10 sw=10 sl=64 | throughput | 363 tuples/sec | 427 tuples/sec | 754.55 tuples/sec | -15.0% | -51.9% |
| bs=10 sw=10 sl=64 | MB/s | 0.221 MB/s | 0.261 MB/s | 0.461 MB/s | -15.3% | -52.0% |
| bs=10 sw=10 sl=64 | p50 | 26,792 us | 23,372 us | 12,816 us | +14.6% | +109.1% |
| bs=10 sw=10 sl=64 | p95 | 38,040 us | 33,420 us | 16,594 us | +13.8% | +129.2% |
| bs=10 sw=10 sl=64 | p99 | 38,040 us | 33,420 us | 19,806 us | +13.8% | +92.1% |
| bs=100 sw=10 sl=64 | throughput | 776 tuples/sec | 822 tuples/sec | 969.38 tuples/sec | -5.6% | -19.9% |
| bs=100 sw=10 sl=64 | MB/s | 0.473 MB/s | 0.502 MB/s | 0.592 MB/s | -5.8% | -20.1% |
| bs=100 sw=10 sl=64 | p50 | 128,596 us | 121,866 us | 103,584 us | +5.5% | +24.1% |
| bs=100 sw=10 sl=64 | p95 | 161,670 us | 133,000 us | 109,097 us | +21.6% | +48.2% |
| bs=100 sw=10 sl=64 | p99 | 161,670 us | 133,000 us | 117,304 us | +21.6% | +37.8% |
| bs=1000 sw=10 sl=64 | throughput | 904 tuples/sec | 915 tuples/sec | 1,004 tuples/sec | -1.2% | -9.9% |
| bs=1000 sw=10 sl=64 | MB/s | 0.552 MB/s | 0.559 MB/s | 0.613 MB/s | -1.3% | -9.9% |
| bs=1000 sw=10 sl=64 | p50 | 1,105,721 us | 1,092,994 us | 1,002,357 us | +1.2% | +10.3% |
| bs=1000 sw=10 sl=64 | p95 | 1,196,679 us | 1,169,952 us | 1,046,463 us | +2.3% | +14.4% |
| bs=1000 sw=10 sl=64 | p99 | 1,196,679 us | 1,169,952 us | 1,073,661 us | +2.3% | +11.5% |
Raw CSV
config_idx,batch_size,schema_width,string_len,num_batches,total_ms,total_tuples,total_bytes,tuples_per_sec,mb_per_sec,lat_p50_us,lat_p95_us,lat_p99_us
0,10,10,64,20,551.27,200,128000,363,0.221,26792.21,38039.97,38039.97
1,100,10,64,20,2578.96,2000,1280000,776,0.473,128595.50,161670.25,161670.25
2,1000,10,64,20,22112.32,20000,12800000,904,0.552,1105720.77,1196678.75,1196678.75|
@mengw15 I found the description very hard to read. Can you improve it? |
What changes were proposed in this PR?
LinkedBlockingMultiQueueis the message queue behind every Python UDF worker:InternalQueuewraps it to give control messages their own priority lane ahead of data batches, and to implement pause and backpressure by disabling the data sub-queues.It sat at ~70% line coverage. Writing tests for the uncovered parts surfaced two real defects, both on removal paths — which is exactly why those lines had never been exercised. This PR fixes both and adds the tests that pin the corrected behavior.
Neither defect is reachable from production code today.
internal_queue.pyis the only caller of this class and it usesput/get/size/enable/disable/is_empty/is_enabled/in_mem_size/add_sub_queue, so no existing caller changes behavior. They matter for the next caller:InternalQueue._queue_idsonly ever grows, and whoever adds per-channel teardown will reach forremove_sub_queuefirst.Defect 1 —
SubQueue.removecorrupts the chain and desynchronises the countheadis a sentinel, so the candidate node is alwaystrail.next, withtrailas its predecessor.removematchedtrail.item— the predecessor's own item — and then unlinkedtrail.next.unlinkin turn clearedtrail.itemrather than the removed node's, and never decrementedself.count.On
["a", "b", "c"],remove("a")returnsTrueand leaves:"a"is blanked in place instead of unlinked, and"b"is droppedSubQueue.size()stays at 3 while the queue total goes to 2get()returnsNone, a valueputexplicitly rejectsremoveon a single-element sub-queue returnsFalseFixed by matching
trail.next.item, clearingnext_.item, and decrementingself.countinunlink.Defect 2 —
remove_sub_queueraisesAttributeErrorfor any enabled sub-queueThe inner classes here carry an
ownerback-pointer to the outer queue, and@innerdecides what it points at from how the inner class was reached:self.PriorityGroup(...)—owneris the queue objectLinkedBlockingMultiQueue.PriorityGroup(...)—owneris the classadd_sub_queueused the second form.total_countis assigned in__init__, so it lives on the object, not on the class — andself.owner.total_count.get_and_dec(...)inremove_queuefailed with:The throw lands mid-mutation, leaving the queue neither removed nor not-removed:
total_countstill counts the removed sub-queue's items, and the now-emptyPriorityGroupis never dropped — so the nextget()raisesIndexErroronqueues[self.next_idx]inget_next_sub_queue. Disabled sub-queues skip the failing line, which is the only reason this path ever appeared to work.Fixed by constructing through
self.PriorityGroup(...), matching howSubQueueis already built.NodeandDefaultSubQueueSelectionalso use class access but never readself.owner, so they are left unchanged.Tests
30 new tests cover
peekat all three levels,SubQueue.clear, the corrected removal paths,PriorityGroup.remove_queue,remove_sub_queue, and the small guards. Thequeuefixture moved to module scope so the new classes can share it. Two current behaviors are pinned rather than changed:clear()does not resetin_mem_size, andSubQueue.__str__only supportsstritems.Any related issues, documentation, discussions?
Closes #6899.
How was this PR tested?
The target file passes with 42 tests (12 existing, 30 new), and the full
pytest -m "not integration"suite passes with no regressions.ruff checkandruff format --checkare clean.Both defects were reproduced before the fix and confirmed gone after. To check the failure path, each fix was reverted in turn: reverting the
removefix reddens 4 removal tests, and reverting the owner-binding fix brings back the originalAttributeError. The new tests genuinely guard both fixes.Was this PR authored or co-authored using generative AI tooling?
Generated-by: Claude Code (Fable 5)