Case study

Airflow tasks stuck in "queued" forever after a twelve-second database blip

A short network flap between two datacenters left a task in "queued" for two and a half hours, and its DAG with it. The cause is a lost executor event: SELECT … FOR UPDATE SKIP LOCKED skips exactly the row it needs. Why restarting the scheduler is the wrong fix.

3 min read · Markdown

TL;DR

The symptom

One afternoon a sensor task sat in "queued" for two and a half hours. Its DAG allows one active run, so the whole pipeline behind it stopped until the on-call engineer cleared the task by hand. Alerts of the form "state attribute is queued" had appeared on three earlier days in the summer and autumn, and each time nobody found a cause.

The setup

So every database connection from the scheduler and its tasks went from A to B and back to A. Any flap on the A–B link broke all of them at once.

What the logs showed

The scheduler's event log has a gap of about twelve seconds, during which the database answered nobody. Then came server closed the connection unexpectedly, and less than a minute later one more drop, at connect time. Patroni did not fail over: the timeline had not changed in months.

Two kinds of task failures came out of the blip:

The mechanism

  1. The task process starts and runs UPDATE task_instance SET state = 'running' …. The row is locked.
  2. The connection dies mid-transaction. The process exits; the LocalExecutor reports a failed event.
  3. The scheduler processes executor events and loads the task instances with SELECT … FOR UPDATE SKIP LOCKED, so that several schedulers can share the work.
  4. The row is still locked, most likely because the database has not yet noticed that the client is gone. SKIP LOCKED skips the row. The event is consumed and dropped.
  5. The task instance stays "queued". The LocalExecutor in 2.11 has no handling for task_queued_timeout, so nothing ever revisits it.

The fix, and the wrong fix

Clear the stuck task instance. In the UI: Browse → Task Instances, state = queued, and check queued_when.

Do not restart the scheduler. The service runs with KillMode=control-group and a 90-second stop timeout. Tasks run as child processes inside the scheduler's cgroup, so a restart kills everything that is running. Some of our long tasks can only be restarted together with their whole DAG.

What we changed and recommend

Takeaways


All writing · A similar problem? contact@ivanblinov.com