Fix recording artifact readiness race
This commit is contained in:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user