Skip to content

fix(scheduler): unstick rate-limited tasks holding filled concurrency slots - #4804

Merged
grutt merged 3 commits into
mainfrom
fix/rate-limited-filled-concurrency-slots
Sep 21, 2026
Merged

grutt merged 3 commits into
mainfrom
fix/rate-limited-filled-concurrency-slots

Conversation

@grutt

@grutt grutt commented Aug 26, 2026

Copy link
Copy Markdown
Member

Description

Tasks that hit a rate limit while holding a filled GROUP_ROUND_ROBIN concurrency slot could deadlock their queue permanently: parked queue items were only requeued when the queuer had already observed rate limits in-process, so after a scheduler restart they never came back, and the schedule-timeout pass only deleted unfilled slots, so the filled slots never released. New runs then waited on the full group until they hit their scheduling timeout, indefinitely. Observed on a production tenant.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

What's Changed

  • Queuer always calls RequeueRateLimitedItems each loop (removed the in-memory hasRateLimits gate), so items parked in v1_rate_limited_queue_items are requeued even by a fresh queuer after a restart or lease change
  • RunGroupRoundRobin / RunChildGroupRoundRobin schedule-timeout now cancels any slot past schedule_timeout_at with no v1_task_runtime row (not actually running), instead of only unfilled slots
  • Add queuer unit tests and an integration test reproducing the filled-slot deadlock (all fail without the fix)

Checklist

Changes have been:

  • Tested (unit, integration, or manually with steps specified)
  • Linted and formatted

Testing

  • go test ./pkg/scheduling/v1/ -run TestQueuerRequeuesRateLimitedItems
  • go test -tags integration ./pkg/scheduling/v1/ (full suite, all 22 tests pass; the new test and unit tests fail on main)

🤖 AI Disclosure
  • I acknowledge that an LLM was used in the creation of this Pull Request, in accordance with Hatchet's AI_POLICY.md.

  • Details: Root-cause analysis, fix, and tests written with Cursor (Claude), reviewed by a human.

… slots

Rate-limited tasks keep their GROUP_ROUND_ROBIN slot filled while parked in
v1_rate_limited_queue_items. Two gaps let those slots deadlock a queue
permanently: the queuer only requeued parked items when it had already seen
rate limits in-process, so a scheduler restart never requeued them; and the
schedule-timeout pass only deleted unfilled slots, so the filled zombies
never released. Always requeue parked items, and time out any slot past
schedule_timeout_at that has no v1_task_runtime row (i.e. is not actually
running).
@vercel

vercel Bot commented Aug 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hatchet-docs Ready Ready Preview Sep 21, 2026 7:21pm UTC

Request Review

@greptile-apps

greptile-apps Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR prevents rate-limited tasks from permanently occupying group round-robin concurrency slots after scheduler restarts.

  • Requeues parked rate-limited items on every queuer loop.
  • Times out filled concurrency slots when no matching task runtime exists.
  • Adds unit and integration coverage for restart recovery and filled-slot cleanup.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Reviews (2): Last reviewed commit: "test(scheduler): drop duplicate queuer r..." | Re-trigger Greptile

Comment thread pkg/scheduling/v1/queuer_test.go Outdated
@grutt
grutt requested a review from juliusgeo August 26, 2026 17:02
@github-actions github-actions Bot added the engine Related to the core Hatchet engine label Aug 26, 2026
After removing the hasRateLimits gate, the "already observed" test
exercised the same path as the fresh-process case.
checkpoint := start
var err error

if q.hasRateLimits {

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.

why are we removing this conditional?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

hasRateLimits starts false and only flips after the queuer sees rate limits on items already in the live queue. Parked tasks are not in that queue, so after a restart the flag stays false and they are never requeued. They keep their filled slots and the queue stays stuck. Requeuing every tick is what picks those rows back up once requeue_after has passed.

strategy_id = @strategyId::bigint AND
schedule_timeout_at < NOW() AND
is_filled = FALSE
-- Filled slots are only running if they have a v1_task_runtime row.

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.

slightly confused by the second line of this comment--the fix here is making it so that is_filled is predicated on whether a v1_task_runtime row exists, otherwise the slot isn't filled? Isn't this logic an inversion of that, where it will only be set to is_filled if there isnt a runtime row?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This doesnt set is_filled. It only picks which expired slots get deleted as SCHEDULING_TIMED_OUT. Filling still happens in updated_slots, and that still requires is_filled = FALSE.

The old check treated filled as running, so a parked rate-limited task (is_filled, no v1_task_runtime row) never timed out. Now any expired slot that is not actually running times out. A real run has a runtime row, so it is left alone.

@github-actions

github-actions Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Benchmark results

goos: linux
goarch: amd64
pkg: github.com/hatchet-dev/hatchet/pkg/scheduling/v1
cpu: AMD Ryzen 9 7950X3D 16-Core Processor          
                                                                    │ /tmp/old.txt │             /tmp/new.txt             │
                                                                    │    sec/op    │    sec/op      vs base               │
BatchSchedulerWorstCaseMemory-8                                        1.802 ±  5%     1.613 ± 11%  -10.48% (p=0.015 n=6)
RateLimiter-8                                                         72.11µ ± 44%    48.56µ ±  7%  -32.66% (p=0.002 n=6)
Scheduler_ConcurrentAssignReplenishSnapshot-8                         350.5µ ± 18%    236.5µ ± 10%  -32.53% (p=0.002 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8          5.589m ± 13%    4.963m ±  4%  -11.19% (p=0.002 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8          5.925m ± 18%    5.616m ±  5%        ~ (p=0.065 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8          13.96m ±  4%    12.73m ±  8%   -8.84% (p=0.004 n=6)
Scheduler_InventoryShape_ReplenishMust/small_dense-8                  64.23µ ± 14%    55.65µ ±  8%  -13.36% (p=0.026 n=6)
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8     8.913m ±  8%   10.062m ± 19%        ~ (p=0.132 n=6)
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8     1.631m ±  6%    1.301m ± 17%  -20.20% (p=0.002 n=6)
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8            505.7µ ±  8%    403.6µ ±  6%  -20.18% (p=0.002 n=6)
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8               2.667m ± 11%    2.276m ± 17%  -14.66% (p=0.009 n=6)
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                12.49µ ± 15%    11.37µ ±  8%   -8.92% (p=0.009 n=6)
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8   4.476m ±  8%    4.084m ±  5%   -8.75% (p=0.009 n=6)
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8   61.29µ ± 10%    68.27µ ± 15%  +11.39% (p=0.041 n=6)
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8          73.21µ ± 18%    78.83µ ± 19%        ~ (p=0.394 n=6)
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8             1.020m ±  8%    1.248m ± 19%  +22.34% (p=0.002 n=6)
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                 48.22µ ±  8%    61.65µ ± 19%  +27.83% (p=0.009 n=6)
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8    69.82µ ± 20%    69.14µ ± 14%        ~ (p=0.937 n=6)
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8    62.22µ ±  7%    66.09µ ± 23%   +6.21% (p=0.026 n=6)
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8           22.30µ ±  3%    28.77µ ± 15%  +29.03% (p=0.002 n=6)
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8              33.99µ ±  2%    38.78µ ± 18%  +14.07% (p=0.002 n=6)
Scheduler_Replenish_DenseSharedActions-8                              4.317m ±  2%    5.487m ± 12%  +27.09% (p=0.002 n=6)
geomean                                                               572.3µ          555.0µ         -3.02%

                                │ /tmp/old.txt │         /tmp/new.txt          │
                                │  bytes/item  │ bytes/item  vs base           │
BatchSchedulerWorstCaseMemory-8     70.17 ± 0%   70.17 ± 0%  ~ (p=1.000 n=6) ¹
¹ all samples are equal

                                │ /tmp/old.txt  │           /tmp/new.txt           │
                                │ worst_case_MB │ worst_case_MB  vs base           │
BatchSchedulerWorstCaseMemory-8     1.403k ± 0%     1.403k ± 0%  ~ (p=1.000 n=6) ¹
¹ all samples are equal

                                │   /tmp/old.txt   │           /tmp/new.txt            │
                                │ worst_case_bytes │ worst_case_bytes  vs base         │
BatchSchedulerWorstCaseMemory-8        1.403G ± 0%        1.403G ± 0%  ~ (p=0.732 n=6)

                                                                    │ /tmp/old.txt │             /tmp/new.txt             │
                                                                    │     B/op     │     B/op      vs base                │
BatchSchedulerWorstCaseMemory-8                                       6.400Gi ± 0%   6.400Gi ± 0%       ~ (p=0.372 n=6)
RateLimiter-8                                                         137.7Ki ± 0%   137.7Ki ± 0%       ~ (p=0.281 n=6)
Scheduler_ConcurrentAssignReplenishSnapshot-8                         259.2Ki ± 0%   259.7Ki ± 1%       ~ (p=0.589 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8          2.263Mi ± 0%   2.263Mi ± 0%       ~ (p=0.071 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8          2.663Mi ± 0%   2.663Mi ± 0%       ~ (p=0.987 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8          4.426Mi ± 0%   4.426Mi ± 0%       ~ (p=0.459 n=6)
Scheduler_InventoryShape_ReplenishMust/small_dense-8                  28.03Ki ± 0%   28.03Ki ± 0%       ~ (p=0.426 n=6)
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8     3.730Mi ± 0%   3.729Mi ± 0%       ~ (p=0.485 n=6)
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8     3.132Mi ± 0%   3.132Mi ± 0%       ~ (p=0.126 n=6)
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8            665.1Ki ± 0%   665.1Ki ± 0%  +0.00% (p=0.045 n=6)
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8               1.872Mi ± 0%   1.872Mi ± 0%       ~ (p=0.123 n=6)
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                  168.0 ± 0%     168.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8     168.0 ± 0%     168.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8     168.0 ± 0%     168.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8            168.0 ± 0%     168.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8               168.0 ± 0%     168.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                 39.31Ki ± 0%   39.31Ki ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8    151.6Ki ± 0%   151.6Ki ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8    119.6Ki ± 0%   119.6Ki ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8           13.72Ki ± 0%   13.72Ki ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8              40.61Ki ± 0%   40.61Ki ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_Replenish_DenseSharedActions-8                              2.166Mi ± 0%   2.166Mi ± 0%  +0.00% (p=0.002 n=6)
geomean                                                               105.3Ki        105.4Ki       +0.01%
¹ all samples are equal

                                                                    │ /tmp/old.txt │            /tmp/new.txt             │
                                                                    │  allocs/op   │  allocs/op   vs base                │
BatchSchedulerWorstCaseMemory-8                                        3.720k ± 0%   3.720k ± 1%       ~ (p=0.974 n=6)
RateLimiter-8                                                          1.022k ± 0%   1.022k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_ConcurrentAssignReplenishSnapshot-8                          1.073k ± 1%   1.074k ± 1%       ~ (p=0.983 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8           6.683k ± 0%   6.683k ± 0%       ~ (p=1.000 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8           13.58k ± 0%   13.58k ± 0%       ~ (p=0.455 n=6)
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8           41.70k ± 0%   41.70k ± 0%       ~ (p=0.455 n=6)
Scheduler_InventoryShape_ReplenishMust/small_dense-8                    333.0 ± 0%    333.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8      13.51k ± 0%   13.51k ± 0%       ~ (p=0.636 n=6)
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8      56.57k ± 0%   56.57k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8             6.196k ± 0%   6.196k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8                15.21k ± 0%   15.21k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                  4.000 ± 0%    4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8     4.000 ± 0%    4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8     4.000 ± 0%    4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8            4.000 ± 0%    4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8               4.000 ± 0%    4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                   382.0 ± 0%    382.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8      430.0 ± 0%    430.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8      430.0 ± 0%    430.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8             277.0 ± 0%    277.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8                430.0 ± 0%    430.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_Replenish_DenseSharedActions-8                               4.890k ± 0%   4.891k ± 0%  +0.02% (p=0.002 n=6)
geomean                                                                 604.7         604.7       +0.00%
¹ all samples are equal

                                                                    │ /tmp/old.txt │            /tmp/new.txt             │
                                                                    │ action_rows  │ action_rows  vs base                │
Scheduler_ConcurrentAssignReplenishSnapshot-8                          10.00k ± 0%   10.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8           50.00k ± 0%   50.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8           50.00k ± 0%   50.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8           50.00k ± 0%   50.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/small_dense-8                    200.0 ± 0%    200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8      86.20k ± 0%   86.20k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8      1.200k ± 0%   1.200k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8             1.000k ± 0%   1.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8                20.00k ± 0%   20.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                  200.0 ± 0%    200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8    86.20k ± 0%   86.20k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8    1.200k ± 0%   1.200k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8           1.000k ± 0%   1.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8              20.00k ± 0%   20.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                   200.0 ± 0%    200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8     86.20k ± 0%   86.20k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8     1.200k ± 0%   1.200k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8            1.000k ± 0%   1.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8               20.00k ± 0%   20.00k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                                5.421k        5.421k       +0.00%
¹ all samples are equal

                                                                    │ /tmp/old.txt │            /tmp/new.txt             │
                                                                    │   actions    │   actions    vs base                │
Scheduler_ConcurrentAssignReplenishSnapshot-8                           200.0 ± 0%    200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8            500.0 ± 0%    500.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8            500.0 ± 0%    500.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8            500.0 ± 0%    500.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/small_dense-8                    20.00 ± 0%    20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8      1.724k ± 0%   1.724k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8       30.00 ± 0%    30.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8             2.000k ± 0%   2.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8                2.000k ± 0%   2.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                  20.00 ± 0%    20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8    1.724k ± 0%   1.724k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8     30.00 ± 0%    30.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8           2.000k ± 0%   2.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8              2.000k ± 0%   2.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                   20.00 ± 0%    20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8     1.724k ± 0%   1.724k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8      30.00 ± 0%    30.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8            2.000k ± 0%   2.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8               2.000k ± 0%   2.000k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                                 346.2         346.2       +0.00%
¹ all samples are equal

                                                                   │ /tmp/old.txt │            /tmp/new.txt            │
                                                                   │  batch_size  │ batch_size  vs base                │
Scheduler_ConcurrentAssignReplenishSnapshot-8                          64.00 ± 0%   64.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                  64.00 ± 0%   64.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8     64.00 ± 0%   64.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8     64.00 ± 0%   64.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8            64.00 ± 0%   64.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8               64.00 ± 0%   64.00 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                                64.00        64.00       +0.00%
¹ all samples are equal

                                                                    │ /tmp/old.txt │            /tmp/new.txt            │
                                                                    │  slot_types  │ slot_types  vs base                │
Scheduler_ConcurrentAssignReplenishSnapshot-8                           1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8            1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8            4.000 ± 0%   4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8            16.00 ± 0%   16.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/small_dense-8                    1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8       1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8       1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8              1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8                 1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                  1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8     1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8     1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8            1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8               1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                   1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8      1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8      1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8             1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8                1.000 ± 0%   1.000 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                                 1.245        1.245       +0.00%
¹ all samples are equal

                                                                    │   /tmp/old.txt   │               /tmp/new.txt               │
                                                                    │ slots_per_worker │ slots_per_worker  vs base                │
Scheduler_ConcurrentAssignReplenishSnapshot-8                               20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8                20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8                20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8                20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/small_dense-8                        4.000 ± 0%         4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8           20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8          1.400k ± 0%        1.400k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8                  20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8                     20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                      4.000 ± 0%         4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8         20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8        1.400k ± 0%        1.400k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8                20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8                   20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                       4.000 ± 0%         4.000 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8          20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8         1.400k ± 0%        1.400k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8                 20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8                    20.00 ± 0%         20.00 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                                     30.34              30.34       +0.00%
¹ all samples are equal

                                                                    │ /tmp/old.txt │             /tmp/new.txt             │
                                                                    │ unique_slots │ unique_slots  vs base                │
Scheduler_ConcurrentAssignReplenishSnapshot-8                          1.000k ± 0%    1.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8           2.000k ± 0%    2.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8           8.000k ± 0%    8.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8           32.00k ± 0%    32.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/small_dense-8                    40.00 ± 0%     40.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8      1.000k ± 0%    1.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8      56.00k ± 0%    56.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8             4.000k ± 0%    4.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8                4.000k ± 0%    4.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                  40.00 ± 0%     40.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8    1.000k ± 0%    1.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8    56.00k ± 0%    56.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8           4.000k ± 0%    4.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8              4.000k ± 0%    4.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                   40.00 ± 0%     40.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8     1.000k ± 0%    1.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8     56.00k ± 0%    56.00k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8            4.000k ± 0%    4.000k ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8               4.000k ± 0%    4.000k ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                                2.444k         2.444k       +0.00%
¹ all samples are equal

                                                                    │ /tmp/old.txt │            /tmp/new.txt            │
                                                                    │   workers    │  workers    vs base                │
Scheduler_ConcurrentAssignReplenishSnapshot-8                           50.00 ± 0%   50.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_01-8            100.0 ± 0%   100.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_04-8            100.0 ± 0%   100.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_SlotTypeCardinality/slot_types_16-8            100.0 ± 0%   100.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/small_dense-8                    10.00 ± 0%   10.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_action_fanout-8       50.00 ± 0%   50.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/dense_high_slot_capacity-8       40.00 ± 0%   40.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/sparse_low_fanout-8              200.0 ± 0%   200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ReplenishMust/partitioned_20-8                 200.0 ± 0%   200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/small_dense-8                  10.00 ± 0%   10.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_action_fanout-8     50.00 ± 0%   50.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/dense_high_slot_capacity-8     40.00 ± 0%   40.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/sparse_low_fanout-8            200.0 ± 0%   200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_ActiveCountScan/partitioned_20-8               200.0 ± 0%   200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/small_dense-8                   10.00 ± 0%   10.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_action_fanout-8      50.00 ± 0%   50.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/dense_high_slot_capacity-8      40.00 ± 0%   40.00 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/sparse_low_fanout-8             200.0 ± 0%   200.0 ± 0%       ~ (p=1.000 n=6) ¹
Scheduler_InventoryShape_TryAssignBatch/partitioned_20-8                200.0 ± 0%   200.0 ± 0%       ~ (p=1.000 n=6) ¹
geomean                                                                 64.71        64.71       +0.00%
¹ all samples are equal

Compared against main (4d80b95)

@Scalahansolo

Copy link
Copy Markdown

We put together a standalone reproduction for both defects covered by this PR: hatchet-rate-limit-repro.

The same tests run against the pinned PR base and this PR’s proposed fix:

Scenario PR base PR fix
Fresh scheduler recovers an eligible parked task Bug reproduced 10/10 Passed 10/10
Expired filled concurrency slot releases the waiting follow-up Bug reproduced 10/10 Passed 10/10

It uses the real scheduler and PostgreSQL repositories. To make the trigger deterministic, it seeds the parked-task state directly and constructs a fresh scheduling pool; it does not submit through an SDK or kill a full engine process. The recovery test checks that the queuer completed assignment passes, then confirms a direct repository call can recover the stuck row. The timeout test checks cancellation of the expired holder and queuing of the follow-up.

Run python3 reproduce.py after cloning; Docker, Git, and Python are the only local prerequisites. The repo includes recorded test evidence, and the comparison also passed on a clean GitHub Actions runner.

This gives us a repeatable check for both halves of the fix. Hope it helps with review and verification.

@grutt
grutt merged commit 8ccba17 into main Sep 21, 2026
58 checks passed
@grutt
grutt deleted the fix/rate-limited-filled-concurrency-slots branch September 21, 2026 19:46
igor-kupczynski added a commit that referenced this pull request Oct 6, 2026
RequeueRateLimitedItems runs on every queue-loop tick since #4804 and
refreshed last_active unconditionally, so no polled queue could ever age
out of ListQueues. Only refresh when rows were moved, and have the
cleanup reactivate inactive queues whose only work is rate limited.
igor-kupczynski added a commit that referenced this pull request Oct 6, 2026
RequeueRateLimitedItems runs on every queue-loop tick since #4804 and
refreshed last_active unconditionally, so no polled queue could ever age
out of ListQueues. Only refresh when rows were moved, and have the
cleanup reactivate inactive queues whose only work is rate limited.

This branch was successfully deployed

1 active deployment
Preview — c8be1b79 Deployed Sep 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

engine Related to the core Hatchet engine

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants