fix(jobs): cancel running + delete failed jobs so the queue can be unstuck #28

Merged
zgaetano merged 1 commit from fix/jobs-cancel-stuck into main 2026-05-23 16:54:50 -04:00

View file

@ -82,11 +82,19 @@ function Jobs({ navigate }) {
.catch(e => alert('Retry failed: ' + e.message));
}, [refresh]);
const handleDelete = React.useCallback((job) => {
if (!window.confirm('Remove this job from the queue?')) return;
// One handler covers cancel (running) AND delete (queued / done / failed).
// BullMQ's job.remove() what the API calls works on any state, so a
// stalled-active job (worker died mid-process, holding a concurrency slot)
// gets yanked and the next queued job runs. mode just changes the prompt
// copy so the operator knows what they're doing.
const handleDelete = React.useCallback((job, mode) => {
const msg = mode === 'cancel'
? 'Cancel this running ' + job.kind + ' job?\n\nThe worker may run a few seconds longer in the background, but its result will be discarded and the queue slot frees up immediately.'
: 'Remove this ' + job.status + ' ' + job.kind + ' job from the queue?';
if (!window.confirm(msg)) return;
window.ZAMPP_API.fetch('/jobs/' + job.id, { method: 'DELETE' })
.then(() => setJobs(prev => prev.filter(j => j.id !== job.id)))
.catch(e => alert('Delete failed: ' + e.message));
.catch(e => alert((mode === 'cancel' ? 'Cancel' : 'Delete') + ' failed: ' + e.message));
}, []);
// Retry every failed job at once. Useful after a transient infra issue
@ -221,12 +229,22 @@ function JobRow({ job, onRetry, onDelete }) {
})()}
</div>
<div><span className={'badge ' + (job.priority === 'high' ? 'warning' : 'outline')}>{job.priority}</span></div>
<div style={{ display: 'flex', gap: 4 }}>
<div style={{ display: 'flex', gap: 4, justifyContent: 'flex-end' }}>
{job.status === 'failed' && (
<button className="btn ghost sm" onClick={() => onRetry(job)}><Icon name="refresh" />Retry</button>
)}
{(job.status === 'queued' || job.status === 'done') && (
<button className="icon-btn" title="Remove job" onClick={() => onDelete(job)}><Icon name="x" /></button>
{job.status === 'running' && (
/* Cancel a stalled-active job frees the BullMQ concurrency slot
so anything queued behind it can run. The worker may finish in
the background but its result is discarded. */
<button className="btn ghost sm" onClick={() => onDelete(job, 'cancel')}
style={{ color: 'var(--danger)' }} title="Cancel this running job and free its queue slot">
<Icon name="x" />Cancel
</button>
)}
{(job.status === 'queued' || job.status === 'done' || job.status === 'failed') && (
<button className="icon-btn" title="Remove job from the queue"
onClick={() => onDelete(job, 'delete')}><Icon name="x" /></button>
)}
</div>
</div>