Lifecycle
This is the heart of Taskhoster. Distributed work is messy (workers crash, stall and finish out of order), and the lifecycle is designed around that so you don't have to be. An item moves through three states, and a handful of timers make sure it never gets stuck and that effort is never needlessly wasted.
The three states
An item's state is where its file lives on the server: one directory per state. A claim is an
atomic move from pending to processing; finishing moves it to
done. The producer-visible status is pending,
processing, finished or failed; the last two both live in
done and are terminal.
post
│
▼
┌──────────┐ claim ┌────────────┐ result ┌───────────────────┐
┌────────▶│ pending │─────────────▶│ processing │────────────▶│ done · finished │
│ └──────────┘ └────────────┘ └───────────────────┘
│ │ │
│ │ │ lease lapses / worker socket drops
│ re-queue │ │
└──────────────┼──────────────────────────┘
│
│ age > queueSeconds (from either state)
▼
┌───────────────────┐
│ done · failed │ error: "timeout"
└───────────────────┘
done items are deleted after retainSeconds (from finished, or from first fetch)
Every transition
| Event | From → to | What happens |
|---|---|---|
| post | none → pending | A validated item is written to
pending with attempts: 0. |
| claim | pending → processing | The oldest matching item is leased
to the worker for leaseSeconds; attempts increments. The move is atomic,
so exactly one worker wins a race. |
| progress | processing → processing | Updates the item's last progress record. Ignored once the item is terminal. |
| result | processing (or re-queued pending) → finished | Accepted
whenever the item is not already terminal, even a late result from a re-queued worker. Sets
status: "finished" and stores the result. |
| lease timeout | processing → pending | A periodic sweep re-queues a processing item whose lease has lapsed. The original worker may still post a result later. |
| socket drop | processing → pending | A worker that claimed over
the /work socket has its in-flight items re-queued immediately when the socket
disconnects. |
| queue timeout | pending/processing → failed | Once an item is
older than queueSeconds it terminally fails with error: "timeout". A late
result is then refused. |
| retention | done → deleted | A done item is removed once
retainSeconds have passed since it finished (or since its result was first fetched). |
Leases and re-queue
A claim leases the item to the worker for leaseSeconds. If the worker finishes in time, it
posts a result and the item is done. If it doesn't (the process is slow, or it crashed), one of two
things re-queues the item:
- Lease timeout. A periodic sweep finds any processing item whose lease has lapsed
and moves it back to
pending, where another worker can claim it. This is the general safety net; it catches a worker that hung, lost power, or fell off the network. - Socket drop. A worker holding items over the
/worksocket has them re-queued the instant the socket closes, no waiting for the lease. This is the fast path for the common case of a worker process exiting.
Each re-queue increments attempts the next time the item is claimed, so you can see how
many times an item has been through a worker.
Late but useful results
Re-queuing raises an obvious worry: what if the "stalled" worker wasn't dead, just slow, and it hands in its result after the item was already given to someone else? Taskhoster keeps that work.
A result is accepted whenever the item is not already terminal. So the original worker
can still finish an item that was re-queued; its late result is used, as long as no other worker
finished it first and the queue didn't time it out. The first result to arrive wins; the loser is told
accepted: false and its result is discarded. Nobody's effort is wasted when everyone is
slow, and the item is never finished twice.
Queue timeout: the terminal backstop
Some tasks simply cannot be finished: no worker can handle them, or every worker keeps failing. Left
alone they would be re-queued forever. queueSeconds bounds the whole life of an item:
measured from when it was posted, once it is older than that it is moved to done with
status: "failed" and error: "timeout", from either pending or
processing.
After that the producer has been told the task timed out, so a late result is refused
(accepted: false); the verdict is final. This is the one failure the server itself
declares; application failures ride back in-band in the result
instead.
Retention
A finished or failed item (and its result) is kept for retainSeconds, then deleted by
the sweep. When that clock starts depends on retainFrom:
retainFrom | Clock starts | Use it when |
|---|---|---|
"finished" (default) | the moment the result was posted | you just want a bounded backlog of recent results and don't mind if some go uncollected. |
"fetched" | the first time a producer reads the terminal result (via
GET …/items/<id> or a monitor subscription) | you want a result guaranteed to survive until it has actually been collected; a not-yet-fetched result is never swept. |
Under "fetched", an item whose result nobody has read yet is held indefinitely; the
countdown only begins once you have seen it. Under "finished", the countdown begins at
completion regardless.
All the timers at a glance
| Setting | Governs | Effect on the item |
|---|---|---|
leaseSeconds | How long one claim holds an item | On expiry, the
item is re-queued to pending (the sweep). Its previous worker can still finish it. |
queueSeconds | Total lifetime from post | On expiry, the item fails
terminally with error: "timeout"; later results are refused. |
retainSeconds | How long a done item is kept | On expiry, the item and its result are deleted. |
retainFrom | When the retain clock starts | "finished"
at completion, or "fetched" at first read. |
A single periodic sweep runs across every live queue applying all three: re-queue lapsed leases, fail aged items, delete retained ones. Configure the timings per queue in its settings.