Summary
gearman_client_st::task_list never unlinks a task when it transitions to GEARMAN_TASK_STATE_FINISHED. The only removal path is the auto-free branch at the bottom of _client_run_task():
// libgearman/run.cc:423-426 (current master)
if (task->client->options.free_tasks and task->type == GEARMAN_TASK_KIND_ADD_TASK)
{
gearman_task_free(task->shell());
}
This only fires when GEARMAN_CLIENT_FREE_TASKS is set and the task is of kind GEARMAN_TASK_KIND_ADD_TASK. For any client that doesn't set that option, or that uses other task kinds, finished tasks accumulate on task_list for the lifetime of the gearman_client_st — they're only ever removed by an explicit gearman_task_free() call or by freeing the whole client. Long-running clients that submit many one-off tasks can see task_list (and thus the per-packet scan cost and memory footprint) grow unbounded.
This is pre-existing behavior, not something introduced by #435 — that PR left run.cc untouched. It surfaced during review of #435 because switching the client task list from LIFO (prepend-to-head) to FIFO (append-to-tail) moved already-finished-but-unlinked tasks from the tail (scanned last) to the head (scanned first) of the packet-matching loop, which is what turned this latent issue into an actual hang (fixed in #435 via an is_active() guard in the scan loop). The guard fixes the symptom; this issue is about the underlying cause.
Why this needs care, not a one-line fix
The three packet-matching loops in _client_run_tasks() (libgearman/client.cc:1453, :1489, :1542) walk task_list with:
for (client->task= client->task_list; client->task;
client->task= client->task->impl()->next)
...calling _client_run_task() (which reaches the FINISHED transition at run.cc:418) from inside the loop body, before the increment reads ->next. Unlinking naively at that transition breaks these loops:
- If the unlink also clears the task's own
next/prev, finishing a task while it's the loop's current node makes the increment read the just-cleared next (NULL), silently ending the scan early for that pass.
- If it leaves
next/prev untouched, a single removal is fine, but two adjacent tasks finishing before either is freed via gearman_task_free() leaves stale pointers, so the second free can re-splice an already-removed node back into the list, corrupting it.
Proposed fix
Restructure the three scan loops in _client_run_tasks() to snapshot task->impl()->next before running the task, then perform the unlink (and task_list_tail update, once #435's FIFO change lands) at the GEARMAN_TASK_STATE_FINISHED transition in run.cc, mirroring what gearman_task_free() already does for head/tail bookkeeping. Alternatively/additionally, consider extending the free_tasks auto-free branch to cover all task kinds, not just GEARMAN_TASK_KIND_ADD_TASK.
Context
Raised during review of #435 (LIFO→FIFO client task list change) by @p-alik, agreed as a follow-up by @esabol:
#435 (comment) and follow-up discussion.
Summary
gearman_client_st::task_listnever unlinks a task when it transitions toGEARMAN_TASK_STATE_FINISHED. The only removal path is the auto-free branch at the bottom of_client_run_task():This only fires when
GEARMAN_CLIENT_FREE_TASKSis set and the task is of kindGEARMAN_TASK_KIND_ADD_TASK. For any client that doesn't set that option, or that uses other task kinds, finished tasks accumulate ontask_listfor the lifetime of thegearman_client_st— they're only ever removed by an explicitgearman_task_free()call or by freeing the whole client. Long-running clients that submit many one-off tasks can seetask_list(and thus the per-packet scan cost and memory footprint) grow unbounded.This is pre-existing behavior, not something introduced by #435 — that PR left
run.ccuntouched. It surfaced during review of #435 because switching the client task list from LIFO (prepend-to-head) to FIFO (append-to-tail) moved already-finished-but-unlinked tasks from the tail (scanned last) to the head (scanned first) of the packet-matching loop, which is what turned this latent issue into an actual hang (fixed in #435 via anis_active()guard in the scan loop). The guard fixes the symptom; this issue is about the underlying cause.Why this needs care, not a one-line fix
The three packet-matching loops in
_client_run_tasks()(libgearman/client.cc:1453,:1489,:1542) walktask_listwith:...calling
_client_run_task()(which reaches the FINISHED transition atrun.cc:418) from inside the loop body, before the increment reads->next. Unlinking naively at that transition breaks these loops:next/prev, finishing a task while it's the loop's current node makes the increment read the just-clearednext(NULL), silently ending the scan early for that pass.next/prevuntouched, a single removal is fine, but two adjacent tasks finishing before either is freed viagearman_task_free()leaves stale pointers, so the second free can re-splice an already-removed node back into the list, corrupting it.Proposed fix
Restructure the three scan loops in
_client_run_tasks()to snapshottask->impl()->nextbefore running the task, then perform the unlink (andtask_list_tailupdate, once #435's FIFO change lands) at theGEARMAN_TASK_STATE_FINISHEDtransition inrun.cc, mirroring whatgearman_task_free()already does for head/tail bookkeeping. Alternatively/additionally, consider extending thefree_tasksauto-free branch to cover all task kinds, not justGEARMAN_TASK_KIND_ADD_TASK.Context
Raised during review of #435 (LIFO→FIFO client task list change) by @p-alik, agreed as a follow-up by @esabol:
#435 (comment) and follow-up discussion.