1313 cast ,
1414)
1515
16- from redis .asyncio import BlockingConnectionPool , Connection , Redis , ResponseError
16+ from redis .asyncio import (
17+ BlockingConnectionPool ,
18+ Connection ,
19+ Redis ,
20+ RedisError ,
21+ ResponseError ,
22+ )
1723from taskiq import AckableMessage
1824from taskiq .abc .broker import AsyncBroker
1925from taskiq .abc .result_backend import AsyncResultBackend
@@ -327,16 +333,15 @@ def _ack_generator(
327333 async def _ack () -> None :
328334 nonlocal acked
329335 async with Redis (connection_pool = self .connection_pool ) as redis_conn :
330- try :
331- await redis_conn .xack (
332- queue_name ,
333- self .consumer_group_name ,
334- id ,
335- )
336- finally :
337- if not acked and on_ack is not None :
338- acked = True
339- on_ack ()
336+ await redis_conn .xack (
337+ queue_name ,
338+ self .consumer_group_name ,
339+ id ,
340+ )
341+ if not acked :
342+ acked = True
343+ if on_ack is not None :
344+ on_ack ()
340345
341346 return _ack
342347
@@ -380,45 +385,39 @@ async def _claim_timed_out_messages(
380385 stream : str ,
381386 count : int ,
382387 protected_message_ids : set [tuple [str , str ]],
383- ) -> list [tuple [str , dict [bytes , bytes ]]]:
388+ pending_start : str ,
389+ ) -> tuple [list [tuple [str , dict [bytes , bytes ]]], str ]:
384390 """Claim pending messages that exceeded their reclaim deadline.
385391
386392 protected_message_ids contains entries delivered by this listener
387393 instance and not acked yet. We skip those to avoid re-delivering our own
388394 in-flight work, while still allowing a restarted worker with the same
389395 Redis consumer name to recover messages left by its predecessor.
390396 """
391- try :
392- pending = await redis_conn .xpending_range (
393- stream ,
394- self .consumer_group_name ,
395- min = "-" ,
396- max = "+" ,
397- count = self .unacknowledged_batch_size ,
398- idle = 0 ,
399- )
400- except ResponseError as exc :
401- if "NOGROUP" not in str (exc ):
402- raise
403- logger .info ("Consumer group missing for %s, recreating" , stream )
404- await self ._declare_consumer_group ()
405- return []
397+ pending = await redis_conn .xpending_range (
398+ stream ,
399+ self .consumer_group_name ,
400+ min = pending_start ,
401+ max = "+" ,
402+ count = self .unacknowledged_batch_size ,
403+ idle = 0 ,
404+ )
406405
407406 claimed : list [tuple [str , dict [bytes , bytes ]]] = []
407+ last_checked_id : str | None = None
408408 for pending_message in pending :
409409 if len (claimed ) >= count :
410410 break
411411
412- message_id = pending_message ["message_id" ]
413- if isinstance (message_id , bytes ):
414- message_id = message_id .decode ()
412+ message_id = self ._to_str (pending_message ["message_id" ])
413+ last_checked_id = message_id
415414 if self ._message_key (stream , message_id ) in protected_message_ids :
416415 continue
417416
418417 message = await self ._get_pending_message (
419418 redis_conn ,
420419 stream ,
421- cast ( str , message_id ) ,
420+ message_id ,
422421 )
423422 if message is None :
424423 await redis_conn .xclaim (
@@ -448,26 +447,37 @@ async def _claim_timed_out_messages(
448447 message_ids = [message_id ],
449448 )
450449 claimed .extend (cast ("list[tuple[str, dict[bytes, bytes]]]" , result ))
451- return claimed
450+ next_pending_start = "-"
451+ if (
452+ len (pending ) == self .unacknowledged_batch_size
453+ and last_checked_id is not None
454+ ):
455+ next_pending_start = f"({ last_checked_id } "
456+ return claimed , next_pending_start
452457
453458 async def _claim_available_timed_out_messages (
454459 self ,
455460 redis_conn : Redis ,
456461 count : int ,
457462 protected_message_ids : set [tuple [str , str ]],
463+ pending_starts : dict [str , str ],
458464 ) -> list [tuple [str , dict [bytes , bytes ], str ]]:
459465 """Claim overdue messages across all configured streams."""
460466 claimed : list [tuple [str , dict [bytes , bytes ], str ]] = []
461467 for stream in [self .queue_name , * self .additional_streams .keys ()]:
462468 remaining_count = count - len (claimed )
463469 if remaining_count <= 0 :
464470 break
465- for msg_id , msg in await self ._claim_timed_out_messages (
466- redis_conn ,
467- stream ,
468- remaining_count ,
469- protected_message_ids ,
470- ):
471+ claimed_messages , pending_starts [stream ] = (
472+ await self ._claim_timed_out_messages (
473+ redis_conn ,
474+ stream ,
475+ remaining_count ,
476+ protected_message_ids ,
477+ pending_starts .get (stream , "-" ),
478+ )
479+ )
480+ for msg_id , msg in claimed_messages :
471481 claimed .append ((msg_id , msg , stream ))
472482 return claimed
473483
@@ -476,25 +486,18 @@ async def _read_new_messages(
476486 redis_conn : Redis ,
477487 count : int | None ,
478488 ) -> Any :
479- """Read newly added stream messages, recreating a missing group once."""
480- try :
481- return await redis_conn .xreadgroup (
482- self .consumer_group_name ,
483- self .consumer_name ,
484- {
485- self .queue_name : ">" ,
486- ** self .additional_streams , # type: ignore[dict-item]
487- },
488- block = self .block ,
489- noack = False ,
490- count = count ,
491- )
492- except ResponseError as exc :
493- if "NOGROUP" not in str (exc ):
494- raise
495- logger .info ("Consumer group missing, recreating" )
496- await self ._declare_consumer_group ()
497- return []
489+ """Read newly added stream messages."""
490+ return await redis_conn .xreadgroup (
491+ self .consumer_group_name ,
492+ self .consumer_name ,
493+ {
494+ self .queue_name : ">" ,
495+ ** self .additional_streams , # type: ignore[dict-item]
496+ },
497+ block = self .block ,
498+ noack = False ,
499+ count = count ,
500+ )
498501
499502 async def _abandon_buffered_messages (
500503 self ,
@@ -517,13 +520,12 @@ async def _abandon_buffered_messages(
517520 idle = ABANDONED_IDLE_MS ,
518521 justid = True ,
519522 )
520- except ResponseError as exc :
521- if "NOGROUP" not in str (exc ):
522- logger .warning (
523- "Failed to abandon messages in stream %s" ,
524- stream ,
525- exc_info = True ,
526- )
523+ except RedisError :
524+ logger .warning (
525+ "Failed to abandon messages in stream %s" ,
526+ stream ,
527+ exc_info = True ,
528+ )
527529
528530 def _build_ackable_message (
529531 self ,
@@ -578,12 +580,14 @@ async def _build_reclaimed_ackable_messages(
578580 redis_conn : Redis ,
579581 count : int ,
580582 delivered : set [tuple [str , str ]],
583+ pending_starts : dict [str , str ],
581584 ) -> list [tuple [str , dict [bytes , bytes ], str ]]:
582585 """Claim overdue messages for taskiq's receiver."""
583586 return await self ._claim_available_timed_out_messages (
584587 redis_conn ,
585588 count or self .unacknowledged_batch_size ,
586589 delivered ,
590+ pending_starts ,
587591 )
588592
589593 async def _build_due_reclaimed_messages (
@@ -592,6 +596,7 @@ async def _build_due_reclaimed_messages(
592596 count : int ,
593597 delivered : set [tuple [str , str ]],
594598 last_reclaim : float ,
599+ pending_starts : dict [str , str ],
595600 ) -> tuple [float , list [tuple [str , dict [bytes , bytes ], str ]]]:
596601 """Return overdue pending messages only when the reclaim interval elapsed."""
597602 if not self ._should_reclaim (last_reclaim ):
@@ -600,6 +605,7 @@ async def _build_due_reclaimed_messages(
600605 redis_conn ,
601606 count ,
602607 delivered ,
608+ pending_starts ,
603609 )
604610
605611 async def _build_new_ackable_messages (
@@ -637,6 +643,7 @@ async def listen(self) -> AsyncGenerator[AckableMessage, None]:
637643 buffered : list [tuple [str , dict [bytes , bytes ], str ]] = []
638644 slot_freed = asyncio .Event ()
639645 last_reclaim = 0.0
646+ pending_starts : dict [str , str ] = {}
640647
641648 def on_ack (message_key : tuple [str , str ]) -> None :
642649 nonlocal unacked
@@ -665,6 +672,7 @@ def _on_ack() -> None:
665672 count or self .unacknowledged_batch_size ,
666673 delivered ,
667674 last_reclaim ,
675+ pending_starts ,
668676 )
669677 unacked += len (buffered )
670678 async for message in self ._yield_buffered_messages (
0 commit comments