Fix recording artifact readiness race

This commit is contained in:
2026-06-16 12:06:34 +00:00
parent 12f35ccdc3
commit 97099095bd
2 changed files with 98 additions and 10 deletions
+74 -10
View File
@@ -167,6 +167,70 @@ async def wait_for_file_ready(file_path: str, attempts: int = 20, delay: float =
return False
async def wait_for_recording_artifacts_ready(file_path: str, attempts: int = 20, delay: float = 0.25) -> str | None:
"""Wait until either speaker tracks appear or the mixed WAV is finalized.
The voice receive library fires the post-recording callback before sink
cleanup writes ``tracks/tracks.json``. Polling for both artifact types keeps
us from failing on the corrupt mixed archive while finalized speaker tracks
are about to appear.
"""
last_size = -1
stable_count = 0
for attempt in range(1, attempts + 1):
tracks = _load_speaker_tracks(file_path)
if tracks:
logger.info(
"Speaker tracks ready: path=%s attempt=%s/%s speakers=%s",
file_path,
attempt,
attempts,
len(tracks),
)
return "tracks"
if os.path.exists(file_path):
size = os.path.getsize(file_path)
logger.info(
"Checking recording artifacts readiness: path=%s attempt=%s/%s size_bytes=%s",
file_path,
attempt,
attempts,
size,
)
if size > 0 and size == last_size:
stable_count += 1
if stable_count >= 2:
try:
with wave.open(file_path, "rb") as wav_file:
wav_file.getparams()
except (wave.Error, EOFError, OSError) as exc:
logger.info(
"Mixed recording is size-stable but WAV header is not finalized yet: path=%s error=%s",
file_path,
exc,
)
else:
logger.info("Mixed recording finalized: path=%s size_bytes=%s", file_path, size)
return "mixed"
else:
stable_count = 0
last_size = size
else:
logger.info(
"Recording file not present yet while waiting for artifacts: path=%s attempt=%s/%s",
file_path,
attempt,
attempts,
)
await asyncio.sleep(delay)
logger.warning("Recording artifacts were not ready before timeout: path=%s", file_path)
return None
def build_retry_notice(file_path: str, reason: str) -> str:
file_name = Path(file_path).name
return (
@@ -287,16 +351,11 @@ async def process_recording(
)
return
# Speaker tracks are written and finalized independently in MeetingSink
# cleanup — they always have valid WAV headers if they exist. Check
# them first so we can skip the mixed-file "ready" wait (which is prone
# to timing races with the SilenceGenerator shutdown path).
if _load_speaker_tracks(file_path):
logger.info(
"Speaker tracks present for guild_id=%s; skipping mixed-file readiness check",
guild_id,
)
elif not await wait_for_file_ready(file_path):
# Speaker tracks are written during MeetingSink cleanup, but the voice
# library fires this callback before cleanup runs. Wait for either the
# manifest to appear or the mixed archive to become a valid WAV.
ready_source = await wait_for_recording_artifacts_ready(file_path)
if not ready_source:
reason = "recording file was not finalized in time"
await _save_retry_state(guild_id, file_path, fallback_channel_id, reason)
await safe_send_chunked(
@@ -306,6 +365,11 @@ async def process_recording(
purpose="file-finalization warning",
)
return
logger.info(
"Recording artifacts ready for guild_id=%s via %s",
guild_id,
ready_source,
)
if os.path.exists(file_path):
logger.info(