Merge pull request 'fix(jobs): cancel running + delete failed jobs so the queue can be unstuck' (#28) from fix/jobs-cancel-stuck into main
This commit is contained in:
commit
2b85fb49df
1 changed files with 24 additions and 6 deletions
|
|
@ -82,11 +82,19 @@ function Jobs({ navigate }) {
|
||||||
.catch(e => alert('Retry failed: ' + e.message));
|
.catch(e => alert('Retry failed: ' + e.message));
|
||||||
}, [refresh]);
|
}, [refresh]);
|
||||||
|
|
||||||
const handleDelete = React.useCallback((job) => {
|
// One handler covers cancel (running) AND delete (queued / done / failed).
|
||||||
if (!window.confirm('Remove this job from the queue?')) return;
|
// 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' })
|
window.ZAMPP_API.fetch('/jobs/' + job.id, { method: 'DELETE' })
|
||||||
.then(() => setJobs(prev => prev.filter(j => j.id !== job.id)))
|
.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
|
// Retry every failed job at once. Useful after a transient infra issue
|
||||||
|
|
@ -221,12 +229,22 @@ function JobRow({ job, onRetry, onDelete }) {
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
<div><span className={'badge ' + (job.priority === 'high' ? 'warning' : 'outline')}>{job.priority}</span></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' && (
|
{job.status === 'failed' && (
|
||||||
<button className="btn ghost sm" onClick={() => onRetry(job)}><Icon name="refresh" />Retry</button>
|
<button className="btn ghost sm" onClick={() => onRetry(job)}><Icon name="refresh" />Retry</button>
|
||||||
)}
|
)}
|
||||||
{(job.status === 'queued' || job.status === 'done') && (
|
{job.status === 'running' && (
|
||||||
<button className="icon-btn" title="Remove job" onClick={() => onDelete(job)}><Icon name="x" /></button>
|
/* 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>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue