Skip to content

Task Lock

The Task Lock is a distributed lock for scheduled tasks. Unlike a regular Lock, it does not release immediately. It keeps the lock held for a configurable minimum duration to stop re-execution on other nodes.

As the gate of an interval task, the task extends the lease from the moment it holds the lock until the body ends, so lease_duration only bounds how long a crashed worker keeps it. Entered directly with async with, the lock relies on the TTL (lease_duration) set at acquire time. If the body runs longer than lease_duration, the lock expires and another node may acquire it.

  • min_hold_duration: minimum duration to hold the lock after the task completes. Stops another node from re-executing too soon.
  • lease_duration: maximum duration to hold the lock. Acts as a TTL for crash and deadlock protection.

Call extend() on a TaskLock to extend the lease while the task body is still running. It raises LockNotOwnedError when the lease was lost, and LockExtendError when the backend call fails:

fragment
async with task_lock:
    await long_operation_part1()
    await task_lock.extend()  # before lease_duration elapses
    await long_operation_part2()

Tip

For interval tasks, pass the lock as the gate of every(). A lock without a name takes the task name, and the task extends it while the body runs, so you never call extend() yourself. Most tasks need no TaskLock at all: gate="claim" builds one sized to the interval. Cron tasks never take a lock. They claim each fire against the schedule backend instead.

Leaving async with task_lock: releases the lock, or shortens its hold to min_hold_duration. That step finishes on the backend even when the task is cancelled, by a client disconnect or a shutdown. A backend that never answers is given up on once lease_duration has passed.

Warning

When the lock expires before the task completes (lease_duration exceeded), another node may acquire the lock and execute concurrently. A warning is logged in this case.