[{"data":1,"prerenderedAt":17},["ShallowReactive",2],{"api:\u002Fapi\u002Fconfig:ru":3,"api:\u002Fapi\u002Fnotes\u002Flease-and-fencing:ru":8},{"email":4,"lang":5,"name":6,"role":7},"hello@ewsirdev.example","ru","Ewsirdev","Веб-разработчик: сайты, лендинги и сервисы на Go",{"slug":9,"lang":5,"title":10,"date":11,"summary":12,"project":13,"minutes":14,"body_md":15,"body_html":16},"lease-and-fencing","Lease и fencing: как очередь не теряет задачу и не даёт ей выполниться дважды «зомби»","2026-10-20","Воркер может упасть, зависнуть или проснуться, когда его задачу уже забрали. Разбираю, как lease и токен аренды в job-queue решают это в SQLite.","job-queue",2,"У фоновой очереди две неприятные ситуации. Воркер упал посреди задачи, и она «зависла» в статусе running. Или воркер просто надолго замер (пауза GC, сеть), задачу отдали другому, а потом он очнулся и пишет результат поверх чужого.\n\n## Lease\n\nЗахват задачи выдаёт **аренду**: срок `lease_until` (по умолчанию 30 секунд). Пока обработчик работает, воркер продлевает аренду heartbeat-ом каждую треть срока. Если процесс упал, heartbeat прекращается, аренда истекает, и «reaper» возвращает задачу в очередь с backoff или, если попытки кончились, в dead.\n\n## Захват одним оператором\n\nВыбор задачи и выдача аренды делает один SQL-оператор:\n\n```sql\nUPDATE jobs SET status='running', attempts=attempts+1,\n  lease_until=?, worker=?, token=?, started_at=?\nWHERE id=(SELECT id FROM jobs WHERE status='pending' AND run_at\u003C=?\n          ORDER BY priority DESC, run_at ASC, id ASC LIMIT 1)\nRETURNING ...\n```\n\nПоэтому два воркера, даже из разных процессов на одном файле базы, не получат одну задачу.\n\n## Fencing\n\nКаждый захват записывает уникальный токен. Продление, завершение и ошибка выполняются с условием `WHERE status='running' AND token=?`. Если задачу уже перехватили, условие не совпадёт, и «зомби»-воркер получит `ErrLeaseLost` вместо того, чтобы испортить результат.\n\n## Что это не даёт\n\nГарантия остаётся **at-least-once**: после истечения аренды обработчик может быть вызван повторно, поэтому он должен быть идемпотентным. Exactly-once очередь не обещает. Подробности и ограничения в README проекта.\n","\u003Cp>У фоновой очереди две неприятные ситуации. Воркер упал посреди задачи, и она «зависла» в статусе running. Или воркер просто надолго замер (пауза GC, сеть), задачу отдали другому, а потом он очнулся и пишет результат поверх чужого.\u003C\u002Fp>\n\u003Ch2>Lease\u003C\u002Fh2>\n\u003Cp>Захват задачи выдаёт \u003Cstrong>аренду\u003C\u002Fstrong>: срок \u003Ccode>lease_until\u003C\u002Fcode> (по умолчанию 30 секунд). Пока обработчик работает, воркер продлевает аренду heartbeat-ом каждую треть срока. Если процесс упал, heartbeat прекращается, аренда истекает, и «reaper» возвращает задачу в очередь с backoff или, если попытки кончились, в dead.\u003C\u002Fp>\n\u003Ch2>Захват одним оператором\u003C\u002Fh2>\n\u003Cp>Выбор задачи и выдача аренды делает один SQL-оператор:\u003C\u002Fp>\n\u003Cpre tabindex=\"0\">\u003Ccode class=\"lang-sql\">UPDATE jobs SET status=&#39;running&#39;, attempts=attempts+1,\n  lease_until=?, worker=?, token=?, started_at=?\nWHERE id=(SELECT id FROM jobs WHERE status=&#39;pending&#39; AND run_at&lt;=?\n          ORDER BY priority DESC, run_at ASC, id ASC LIMIT 1)\nRETURNING ...\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Поэтому два воркера, даже из разных процессов на одном файле базы, не получат одну задачу.\u003C\u002Fp>\n\u003Ch2>Fencing\u003C\u002Fh2>\n\u003Cp>Каждый захват записывает уникальный токен. Продление, завершение и ошибка выполняются с условием \u003Ccode>WHERE status=&#39;running&#39; AND token=?\u003C\u002Fcode>. Если задачу уже перехватили, условие не совпадёт, и «зомби»-воркер получит \u003Ccode>ErrLeaseLost\u003C\u002Fcode> вместо того, чтобы испортить результат.\u003C\u002Fp>\n\u003Ch2>Что это не даёт\u003C\u002Fh2>\n\u003Cp>Гарантия остаётся \u003Cstrong>at-least-once\u003C\u002Fstrong>: после истечения аренды обработчик может быть вызван повторно, поэтому он должен быть идемпотентным. Exactly-once очередь не обещает. Подробности и ограничения в README проекта.\u003C\u002Fp>\n",1792697892639]