Skip to content

Client task_list never unlinks FINISHED tasks (libgearman/run.cc:418) #462

Description

@p-alik

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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions