mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-25 01:15:58 +00:00
fix(actor): harden lifecycle, supervision, Redis mailbox, and add comprehensive tests
- Fix spawn() zombie cell: clean up registry on start() failure - Fix shutdown(): cancel + await tasks that exceed graceful timeout - Fix _shutdown(): await mailbox.close() to release backend resources - Fix escalate directive: stop failing child before propagating to grandparent - Fix RedisMailbox.put(): wrap Redis errors in try/except, return False on failure - Fix retry.py: replace assert with proper raise for last_exc - Add put_batch() to Mailbox abstraction for single-roundtrip bulk enqueue - Add RedisMailbox.put_batch() with atomic Lua script for bounded queues - Add MailboxFullError exception type for semantic backpressure handling - Add redis>=7.4.0 dependency with public PyPI sources in uv.lock Tests added (31 total, up from 27): - test_middleware_on_restart_hook: verifies middleware.on_restart() on supervision restart - test_ask_propagates_actor_exception: ask() re-raises original exception type - test_ask_propagates_exception_while_supervised: exception propagates; root actor survives - test_ask_timeout_late_reply_no_exception: late reply after timeout is silent no-op - test_actor_backpressure.py: MailboxFullError + dead letter on full mailbox - test_actor_retry.py: ask_with_retry with exponential backoff - test_mailbox_redis.py: RedisMailbox put/get/batch/close - bench_actor_redis.py: RedisMailbox throughput benchmarks
This commit is contained in:
@@ -107,12 +107,16 @@ class RedisMailbox(Mailbox):
|
||||
if self._closed:
|
||||
return False
|
||||
data = _serialize(msg)
|
||||
if self._maxlen > 0:
|
||||
# Atomic check+push via Lua script to avoid TOCTOU race
|
||||
result = await self._redis.evalsha_or_eval(self._LUA_BOUNDED_PUSH, 1, self._queue_name, data, self._maxlen)
|
||||
return bool(result)
|
||||
await self._redis.lpush(self._queue_name, data)
|
||||
return True
|
||||
try:
|
||||
if self._maxlen > 0:
|
||||
# Atomic check+push via Lua script to avoid TOCTOU race
|
||||
result = await self._redis.eval(self._LUA_BOUNDED_PUSH, 1, self._queue_name, data, self._maxlen)
|
||||
return bool(result)
|
||||
await self._redis.lpush(self._queue_name, data)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warning("RedisMailbox.put failed for %s: %s", self._queue_name, e)
|
||||
return False
|
||||
|
||||
def put_nowait(self, msg: Any) -> bool:
|
||||
"""Redis cannot do synchronous non-blocking enqueue reliably.
|
||||
@@ -122,6 +126,36 @@ class RedisMailbox(Mailbox):
|
||||
"""
|
||||
return False
|
||||
|
||||
async def put_batch(self, msgs: list[Any]) -> int:
|
||||
"""Push multiple messages in a single LPUSH command (one round-trip).
|
||||
|
||||
Unbounded queues: all messages sent atomically in one LPUSH.
|
||||
Bounded queues: sequential puts to respect maxlen (no batch Lua script needed).
|
||||
"""
|
||||
if self._closed or not msgs:
|
||||
return 0
|
||||
data_list = []
|
||||
for msg in msgs:
|
||||
try:
|
||||
data_list.append(_serialize(msg))
|
||||
except TypeError as e:
|
||||
logger.warning("Skipping non-serializable message in put_batch: %s", e)
|
||||
if not data_list:
|
||||
return 0
|
||||
if self._maxlen > 0:
|
||||
count = 0
|
||||
for data in data_list:
|
||||
# Reuse the Lua script for TOCTOU-safe bounded check (same as put())
|
||||
result = await self._redis.eval(self._LUA_BOUNDED_PUSH, 1, self._queue_name, data, self._maxlen)
|
||||
if result:
|
||||
count += 1
|
||||
else:
|
||||
break # queue full — stop early
|
||||
return count
|
||||
# Unbounded: single LPUSH with all values — one network round-trip
|
||||
await self._redis.lpush(self._queue_name, *data_list)
|
||||
return len(data_list)
|
||||
|
||||
async def get(self) -> Any:
|
||||
"""Blocking dequeue via BRPOP. Retries until a message arrives."""
|
||||
while not self._closed:
|
||||
|
||||
Reference in New Issue
Block a user