Skip to content

[Bug] Polling fallback leaves sync job pending after undefined job error #830

Description

@GautamSharma99

Describe the Bug

The scheduler's polling fallback creates and commits a pending generation job, but then crashes while trying to launch it because the return
value from upsert_generation_job() is not assigned.

In packages/server/src/repowise/server/scheduler.py:137-166:

await crud.upsert_generation_job(
    session,
    repository_id=repo.id,
    status="pending",
    config={
        "mode": "sync",
        "trigger": "polling_fallback",
        "before": stored_commit or "",
        "after": current_head,
    },
)
await session.commit()

task = asyncio.create_task(
    execute_job(job.id, app_state),
    name=f"poll-job-{job.id}",
)

job is undefined when execute_job(job.id, ...) is evaluated. This raises:

NameError: name 'job' is not defined

The exception is caught by the broad except Exception at the end of polling_fallback(), so the scheduler logs polling_fallback_failed, but the
job row has already been committed with status pending.

This is also an F821 undefined-name violation under the repository's Ruff configuration.

## Steps to Reproduce

1. Initialize and index a local repository.
2. Make another commit so the current Git HEAD differs from .repowise/state.json's last_sync_commit.
3. Start the server with repowise serve.
4. Wait for the polling fallback interval, which defaults to 15 minutes.
5. Inspect the server logs and generation jobs.

The same behavior can be reproduced faster in a test by obtaining the polling_fallback APScheduler job from setup_scheduler() and directly
awaiting its callback.

## Expected Behavior

When the stored commit differs from the repository's current HEAD, the polling fallback should:

1. Create a pending sync job.
2. Launch execute_job() using that job's ID.
3. Allow the job to transition from pending to running, then to a terminal state.
4. Update the repository's sync baseline after successful completion.

## Actual Behavior

The scheduler:

1. Creates and commits the pending job.
2. Logs polling_sync_enqueued.
3. Raises NameError because job is undefined.
4. Catches and logs the exception as polling_fallback_failed.
5. Leaves the job permanently in pending.

The pending row then causes the active-job guard to skip future polling attempts. Manual sync requests for the same repository can also
return:

409 A sync job is already in progress for this repository

The job remains stuck until the server restarts and startup cleanup marks interrupted pending/running jobs as failed.

## Environment

- Repowise version: current main / 0.31.0
- Python version: any supported version
- Installation method: source checkout
- Platform: platform-independent

## Proposed Fix

At minimum, retain the created job:

job = await crud.upsert_generation_job(...)
await session.commit()

task = asyncio.create_task(
    execute_job(job.id, app_state),
    name=f"poll-job-{job.id}",
)

Ideally, scheduler jobs should also use the same managed background-job launch path as REST-triggered jobs so that:

- the task is registered in app.state.job_tasks;
- cancellation works consistently;
- task-launch failures mark the database job as failed;
- unhandled task failures cannot leave the row pending or running.

## Suggested Regression Tests

Add scheduler tests covering:

- A diverged repository creates exactly one sync job.
- execute_job() receives the persisted job ID.
- The callback does not raise NameError.
- A task-launch failure moves the job to failed instead of leaving it pending.
- A second polling pass does not create a duplicate while a real job is active.

## Additional Context

The scheduler currently has no direct behavioral tests. Existing job-executor tests verify pipeline configuration, but they do not execute the
APScheduler polling callback.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions