diff --git a/services/web-ui/public/jobs.html b/services/web-ui/public/jobs.html
index f87c205..d52592f 100644
--- a/services/web-ui/public/jobs.html
+++ b/services/web-ui/public/jobs.html
@@ -656,14 +656,25 @@ function renderRow(job) {
${dur} |
-
+
+
| `;
return tr;
}
+async function killJob(jobId, ev) {
+ ev.stopPropagation();
+ if (!confirm('Remove this job from the queue? If a worker is still processing it, the run is abandoned.')) return;
+ try {
+ const r = await fetch('/api/v1/jobs/' + encodeURIComponent(jobId), { method: 'DELETE', credentials: 'include' });
+ if (r.ok) { toast('Job removed', 'success'); fetchJobs(); }
+ else { const d = await r.json().catch(()=>({})); toast('Remove failed: ' + (d.error || r.statusText), 'error'); }
+ } catch (err) {
+ toast('Remove failed: ' + err.message, 'error');
+ }
+}
+
function statusBadge(status) {
const map = {
active: 'Active',
diff --git a/services/worker/src/index.js b/services/worker/src/index.js
index 5c238d7..53163cb 100644
--- a/services/worker/src/index.js
+++ b/services/worker/src/index.js
@@ -16,7 +16,15 @@ const parseRedisUrl = (url) => {
const redisOptions = parseRedisUrl(process.env.REDIS_URL || 'redis://localhost:6379');
const createWorker = (queueName, handler) => {
- const worker = new Worker(queueName, handler, { connection: redisOptions });
+ const worker = new Worker(queueName, handler, {
+ connection: redisOptions,
+ // Stall detection: if a worker dies mid-job, BullMQ moves it back to wait
+ // after stalledInterval. Without this a crashed run sits in active forever.
+ stalledInterval: 30000,
+ maxStalledCount: 1,
+ lockDuration: 60000,
+ lockRenewTime: 15000,
+ });
worker.on('completed', (job) => {
console.log(`[${queueName}] Job ${job.id} completed`);
@@ -26,7 +34,10 @@ const createWorker = (queueName, handler) => {
console.error(`[${queueName}] Job ${job.id} failed:`, err.message);
});
- // job.progress is a property (the value set by updateProgress), not a function
+ worker.on('stalled', (jobId) => {
+ console.warn(`[${queueName}] Job ${jobId} stalled — reclaimed`);
+ });
+
worker.on('progress', (job, progress) => {
console.log(`[${queueName}] Job ${job.id} progress:`, progress);
});