Skip to content

Commit b3fb689

Browse files
authored
Merge 8caccbf into 290c65b
2 parents 290c65b + 8caccbf commit b3fb689

4 files changed

Lines changed: 50 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Latest
44

5+
### Bug fixes
6+
7+
* 🐛 **Consumer group loss after `FLUSHALL`**: `FLUSHALL` at runtime deletes all stream consumer groups. The Coordinator recreates streams via `XADD`, but `xreadgroup` kept throwing `NOGROUP` on every iteration. Catch `NOGROUP` in `_try_read_stream` and call `setup_consumer_groups()` to recover; the Reconciler republishes any tasks queued before the group was restored.
8+
9+
* 🐛 **False "Task failed" after flush**: When a task finishes while the consumer group is gone, `xack` throws `NOGROUP`. The generic handler in `_execute_and_ack` was catching this, logging "Task failed", and applying backoff on a task that actually succeeded. Catch `NOGROUP` in `_ack_message` and log a warning instead — the message is already gone from the PEL.
10+
511
## 1.1.0 - 2026-04-15
612

713
### Features

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "fastapi-task-manager"
3-
version = "1.1.0"
3+
version = "1.1.1a1"
44
description = "A task manager for FastAPI. Robust Scheduling, Distributed Safety"
55
readme = "README.md"
66
authors = [

src/fastapi_task_manager/stream_consumer.py

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -288,13 +288,27 @@ async def _try_read_stream(
288288
Returns:
289289
Tuple of (message_id, data) if a message was read, None otherwise.
290290
"""
291-
messages = await self._redis.xreadgroup(
292-
groupname=group_name,
293-
consumername=self._worker.redis_safe_id,
294-
streams={stream_key: ">"},
295-
count=1, # Read one message at a time
296-
block=block_ms,
297-
)
291+
try:
292+
messages = await self._redis.xreadgroup(
293+
groupname=group_name,
294+
consumername=self._worker.redis_safe_id,
295+
streams={stream_key: ">"},
296+
count=1, # Read one message at a time
297+
block=block_ms,
298+
)
299+
except ResponseError as e:
300+
if "NOGROUP" in str(e):
301+
# Consumer group was deleted (e.g. Redis FLUSHALL). Recreate it so
302+
# the consume loop can resume; the Reconciler will republish any
303+
# tasks that were published to the stream before the group existed.
304+
logger.warning(
305+
"Consumer group '%s' not found on stream '%s' (Redis flush?), recreating groups",
306+
group_name,
307+
stream_key,
308+
)
309+
await self.setup_consumer_groups()
310+
return None
311+
raise
298312

299313
if not messages:
300314
return None
@@ -546,14 +560,28 @@ async def _ack_message(self, message_id: str, is_high_priority: bool) -> None:
546560
stream_keys = self._keys.get_stream_keys()
547561
stream_key = stream_keys.task_stream_high if is_high_priority else stream_keys.task_stream_low
548562

549-
await self._redis.xack(
550-
stream_key,
551-
stream_keys.consumer_group,
552-
message_id,
553-
)
563+
try:
564+
await self._redis.xack(
565+
stream_key,
566+
stream_keys.consumer_group,
567+
message_id,
568+
)
554569

555-
# Remove the message from the stream to keep memory usage low
556-
await self._redis.xdel(stream_key, message_id)
570+
# Remove the message from the stream to keep memory usage low
571+
await self._redis.xdel(stream_key, message_id)
572+
except ResponseError as e:
573+
if "NOGROUP" in str(e):
574+
# Consumer group no longer exists (e.g. Redis FLUSHALL); the
575+
# message is already gone from the PEL so there is nothing to
576+
# acknowledge. Swallow the error to avoid masking a successful
577+
# task execution as a failure.
578+
logger.warning(
579+
"Could not ACK message %s: consumer group '%s' not found (Redis flush?)",
580+
message_id,
581+
stream_keys.consumer_group,
582+
)
583+
else:
584+
raise
557585

558586
def _find_task(self, group_name: str, task_name: str) -> Task | None:
559587
"""Find a task by group and task name.

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)