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

EventFrom → toWhat happens
postnone → pendingA validated item is written to pending with attempts: 0.
claimpending → processingThe oldest matching item is leased to the worker for leaseSeconds; attempts increments. The move is atomic, so exactly one worker wins a race.
progressprocessing → processingUpdates the item's last progress record. Ignored once the item is terminal.
resultprocessing (or re-queued pending) → finishedAccepted whenever the item is not already terminal, even a late result from a re-queued worker. Sets status: "finished" and stores the result.
lease timeoutprocessing → pendingA periodic sweep re-queues a processing item whose lease has lapsed. The original worker may still post a result later.
socket dropprocessing → pendingA worker that claimed over the /work socket has its in-flight items re-queued immediately when the socket disconnects.
queue timeoutpending/processing → failedOnce an item is older than queueSeconds it terminally fails with error: "timeout". A late result is then refused.
retentiondone → deletedA 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:

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.

This is why the re-queue is safe to be aggressive: a re-queued item may end up claimed by two workers at once, but only the first result counts, and either worker's result is welcome.

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:

retainFromClock startsUse it when
"finished" (default)the moment the result was postedyou 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

SettingGovernsEffect on the item
leaseSecondsHow long one claim holds an itemOn expiry, the item is re-queued to pending (the sweep). Its previous worker can still finish it.
queueSecondsTotal lifetime from postOn expiry, the item fails terminally with error: "timeout"; later results are refused.
retainSecondsHow long a done item is keptOn expiry, the item and its result are deleted.
retainFromWhen 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.