@@ -60,19 +60,34 @@ func (q *Queue) key(suffix string) string {
6060 return fmt .Sprintf ("mara:run:%d:%s" , q .runID , suffix )
6161}
6262
63+ // enqueueChunk bounds how many RPUSH commands go in one pipeline, so publishing
64+ // a large lease set doesn't build one huge Redis transaction.
65+ const enqueueChunk = 1000
66+
6367// Enqueue publishes all leases for the run and seals it (no more will be added).
6468// Sealing lets workers distinguish "queue momentarily empty" from "run done".
69+ // Leases are pushed in chunks to keep each Redis round-trip bounded.
6570func (q * Queue ) Enqueue (ctx context.Context , leases []Lease ) error {
66- pipe := q .rdb .TxPipeline ()
71+ for start := 0 ; start < len (leases ); start += enqueueChunk {
72+ end := start + enqueueChunk
73+ if end > len (leases ) {
74+ end = len (leases )
75+ }
6776
68- for _ , l := range leases {
69- pipe .RPush (ctx , q .key ("pending" ), l .encode ())
70- }
77+ pipe := q .rdb .Pipeline ()
78+ for _ , l := range leases [start :end ] {
79+ pipe .RPush (ctx , q .key ("pending" ), l .encode ())
80+ }
7181
72- pipe .Set (ctx , q .key ("sealed" ), "1" , 0 )
82+ if _ , err := pipe .Exec (ctx ); err != nil {
83+ return fmt .Errorf ("enqueue leases: %w" , err )
84+ }
85+ }
7386
74- if _ , err := pipe .Exec (ctx ); err != nil {
75- return fmt .Errorf ("enqueue leases: %w" , err )
87+ // Seal only after every lease is enqueued, so a worker never sees an empty
88+ // pending list as "drained" mid-publish.
89+ if err := q .rdb .Set (ctx , q .key ("sealed" ), "1" , 0 ).Err (); err != nil {
90+ return fmt .Errorf ("seal queue: %w" , err )
7691 }
7792
7893 return nil
@@ -119,6 +134,34 @@ func (q *Queue) Complete(ctx context.Context, l Lease) error {
119134 return nil
120135}
121136
137+ // Requeue returns a lease to pending after the owning worker failed to process
138+ // it (e.g. target unreachable), and bumps the failure counter. Another worker
139+ // will retry it; the monitor uses the failure counter to detect a run that is
140+ // failing with no forward progress.
141+ func (q * Queue ) Requeue (ctx context.Context , l Lease ) error {
142+ pipe := q .rdb .TxPipeline ()
143+ pipe .LRem (ctx , q .key ("processing" ), 1 , l .encode ())
144+ pipe .RPush (ctx , q .key ("pending" ), l .encode ())
145+ pipe .ZRem (ctx , q .key ("hb" ), l .encode ())
146+ pipe .Incr (ctx , q .key ("fail" ))
147+
148+ if _ , err := pipe .Exec (ctx ); err != nil {
149+ return fmt .Errorf ("requeue failed lease: %w" , err )
150+ }
151+
152+ return nil
153+ }
154+
155+ // Fails returns the cumulative lease-failure count for the run.
156+ func (q * Queue ) Fails (ctx context.Context ) (int64 , error ) {
157+ n , err := q .rdb .Get (ctx , q .key ("fail" )).Int64 ()
158+ if err == redis .Nil {
159+ return 0 , nil
160+ }
161+
162+ return n , err
163+ }
164+
122165// Reap requeues leases whose heartbeat is older than the cutoff — the
123166// signature move of dead-worker recovery. Returns how many were requeued.
124167func (q * Queue ) Reap (ctx context.Context , cutoffMillis int64 ) (int , error ) {
@@ -176,25 +219,50 @@ func (q *Queue) Drained(ctx context.Context) (bool, error) {
176219func (q * Queue ) Cleanup (ctx context.Context ) error {
177220 return q .rdb .Del (ctx ,
178221 q .key ("pending" ), q .key ("processing" ), q .key ("hb" ),
179- q .key ("sealed" ), q .key ("done" ),
222+ q .key ("sealed" ), q .key ("done" ), q . key ( "fail" ),
180223 ).Err ()
181224}
182225
183- // PlanLeases divides [minID, maxID] into leases of the given stride.
226+ // MaxLeases caps how many leases a single run is split into, regardless of how
227+ // large or sparse the key space is. This bounds memory and the Redis enqueue
228+ // fan-out; each lease just covers a wider id range (workers sub-batch within).
229+ const MaxLeases = 100_000
230+
231+ // PlanLeases divides [minID, maxID] into at most MaxLeases leases. It widens the
232+ // stride when the key space is huge or sparse (e.g. snowflake ids up to 10^18)
233+ // and is overflow-safe near math.MaxInt64, so it can never build billions of
234+ // leases or spin forever.
184235func PlanLeases (minID , maxID , stride int64 ) []Lease {
236+ if maxID < minID {
237+ return nil
238+ }
239+
185240 if stride <= 0 {
186241 stride = 1
187242 }
188243
189- var leases []Lease
244+ // Widen the stride so the lease count can't exceed MaxLeases. float64 avoids
245+ // int64 overflow on very wide spans; the hard loop cap below is the backstop.
246+ spanF := float64 (maxID ) - float64 (minID ) + 1
247+ if spanF / float64 (stride ) > float64 (MaxLeases ) {
248+ stride = int64 (spanF / float64 (MaxLeases )) + 1
249+ }
250+
251+ leases := make ([]Lease , 0 , MaxLeases + 1 )
190252
191- for lo := minID ; lo <= maxID ; lo += stride {
253+ for lo := minID ; len ( leases ) <= MaxLeases ; {
192254 hi := lo + stride - 1
193- if hi > maxID {
255+ if hi < lo || hi > maxID { // overflow (wrapped) or past the end
194256 hi = maxID
195257 }
196258
197259 leases = append (leases , Lease {Lo : lo , Hi : hi })
260+
261+ if hi >= maxID {
262+ break // full coverage reached
263+ }
264+
265+ lo = hi + 1
198266 }
199267
200268 return leases
0 commit comments