fix: #3621033 Release a task whose holder can no longer act on it

A task claimed by an account that was then blocked or deleted stayed claimed to it forever. The holder cannot act, because they cannot sign in; nobody else can, because the task is held. Nothing detected it, nothing logged it, and no sweep recovered it. The only trace was getHolderLabel() rendering "(unknown)" in a list, for whoever happened to be looking.

The door was already guarded from the other side: reassign() refuses a blocked or deleted target, and the users audience filters inactive accounts out when it resolves. The same account going bad after it took the work was the unhandled half.

Two answers, chosen by whether there is a pool

OrchestraUserHooks reacts to user_update (an active account crossing to blocked) and user_predelete, for the tasks that account holds and that are not yet terminal.

Where the audience has other members, the task goes back to open and anyone it admits can claim it.

Where the audience named that one person, there is no pool to return it to, so releasing would leave a task no account can see, which is the state #3621013 exists to prevent. That task is canceled and its step raised as an incident, the same answer a step that could never be offered gets, reaching the same operator resolutions. The token is halted first, guarded on PARKED: if something else already moved the branch on, the task is not ours to cancel. Canceling rather than leaving the task behind is what makes retry correct, the node runs again and mints one task, not a second beside the first.

Why not just release()

release() only accepts CLAIMED, and deliberately so: the claim timeout must not take work off somebody mid-way, which testInProgressTaskIsNotReleased pins. A holder who cannot act at all is the opposite case, and being part-way through is no reason to leave the task with them. releaseFromAbsentHolder() accepts both held states; both share one private locked body, so there is one release path, not two. It records no actor: nobody let this task go, it stopped being anybody's.

Cost where it does not apply

user_update runs on every user save on the site. The guard is one property comparison against the original, an account that was not active, or still is, returns before any query. Only the crossing pays for the one indexed query on assignee + state.

Not this issue

Two shapes are still out, and both want the same missing thing.

A task whose role: pool loses its last member is frozen and invisible, including when that member is the holder being blocked here. The hook sees a role token, not a headcount, so it releases the task to a pool nobody is in. Only a user: audience can be recognized as having no one left, because only a user: token names a person; candidates are otherwise opaque by design, and asking "does this dimension still admit anybody" needs a new question put to each audience plugin.

And a pool that empties with nothing happening to any account fires no hook at all, so it needs a periodic sweep to notice.

Both belong to the sweep #3621013 named. This issue covers the holder going away, which is the half that has a user event to hang off.

Follows #3621013.

Edited by Frank Mably

Merge request reports

Loading
Loading