Repository navigation
[Bug] A recovered Batch job past its window with no execution metadata never finishes expiring #3074
Description
🐛 Describe the bug
An in-progress Batch job that is already past its completion window when the metadata service recovers it, and that has no execution metadata (status.execution is None), never finishes expiring. It stays in_progress with the expired condition set, and clients keep seeing in_progress.
On recovery, job_committed_handler (python/aibrix/aibrix/batch/batch_manager.py:554 at 99325e7) does not schedule overdue work and calls expire_job instead. For an in-progress job, expire_job (line 1184) persists the expired condition and leaves finalization to the live driver it then signals. A recovered job has no driver. The orphan cleanup path that would finish it runs only when job_expired.status.execution is not None (line 1258). Every later expiry pass from the scheduler then returns early, because the job is already IN_PROGRESS with EXPIRED (line 1215). Nothing else owns the job, so it never becomes terminal.
The same recovery with execution = {} (not None) does finish expired, and an overdue created job finalizes expired as expected (batch-api.rst:147 documents expiry after the window).
Steps to Reproduce
From python/aibrix at 99325e7, put this in tests/batch/ and run it with pytest:
test_overdue_recovery.py
import os
from datetime import datetime, timedelta, timezone
import pytest
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-testing")
from aibrix.batch.batch_manager import BatchManager
from aibrix.batch.job_entity import (
BatchJob,
BatchJobEndpoint,
BatchJobSpec,
BatchJobState,
BatchJobStatus,
CompletionWindow,
ConditionType,
ObjectMeta,
TypeMeta,
)
from aibrix.context import InfrastructureContext
from aibrix.metadata.api.v1.batch import _batch_job_to_openai_response
class _Scheduler:
def __init__(self):
self.appended = []
def append_job(self, job_id):
self.appended.append(job_id)
def _overdue_in_progress_job(job_id, execution):
created = datetime.now(timezone.utc) - timedelta(hours=25)
job = BatchJob(
typeMeta=TypeMeta(apiVersion="batch/v1", kind="Job"),
metadata=ObjectMeta(
name=f"job-{job_id}", namespace="default", uid=f"uid-{job_id}",
creationTimestamp=created, resourceVersion=None, deletionTimestamp=None,
),
spec=BatchJobSpec(
input_file_id=f"input-{job_id}",
endpoint=BatchJobEndpoint.CHAT_COMPLETIONS.value,
completion_window=CompletionWindow.TWENTY_FOUR_HOURS.expires_at(),
),
status=BatchJobStatus(
jobID=job_id, state=BatchJobState.IN_PROGRESS,
createdAt=created, inProgressAt=created,
),
)
job.status.request_counts.total = 1
job.status.execution = execution
return job
@pytest.mark.asyncio
@pytest.mark.parametrize("execution", [None, {}], ids=["execution-none", "execution-empty"])
async def test_recovered_overdue_in_progress_job_expires(execution):
manager = BatchManager(InfrastructureContext())
scheduler = _Scheduler()
manager.set_scheduler(scheduler)
job = _overdue_in_progress_job(f"overdue-{execution is None}", execution)
await manager.job_committed_handler(job) # recovery after a restart
await manager.expire_job(job.job_id) # the scheduler's next expiry pass
assert scheduler.appended == [] # overdue work is not scheduled
current = manager._done_jobs.get(job.job_id) or await manager.get_job(job.job_id)
assert current.status.state == BatchJobState.FINALIZED, (
f"state={current.status.state.value} condition={current.status.condition} "
f"client={_batch_job_to_openai_response(current).status}"
)
assert current.status.condition == ConditionType.EXPIRED
assert _batch_job_to_openai_response(current).status == "expired"execution-empty passes. execution-none fails with:
AssertionError: state=in_progress condition=expired client=in_progress
The test calls the recovery handler and one more expiry pass directly; it does not restart a process against durable storage.
Expected behavior
A recovered job past its window finishes expired (state finalized, condition expired, client status expired), the same as when it has an empty execution record or was still created.
Environment
- AIBrix version:
mainat99325e7 - Deployment environment: Batch API, metadata service recovery; reproduced in-process on Linux, Python 3.10.12
- LLM(s) being used: none
Area
Batch
I don't plan to send a fix for this soon, so anyone is welcome to take it. A fix probably belongs in expire_job: route a recovered job that has no live driver and no execution reference through the existing stopped-job finalization, without stamping FINALIZED directly and so bypassing output finalization.
This report was prepared with AI assistance (Claude Code); the test above was run against main.
Activity
github-actions commentedon Oct 14, 2026
Thanks for contributing to AIBrix! Please review the contribution guide and make sure this issue contains enough context for maintainers to reproduce or evaluate it.