Close TD-15 task housekeeping
This commit is contained in:
11
TECHDEBTS.md
11
TECHDEBTS.md
@@ -590,7 +590,16 @@ polling in the rebuild wait path.
|
|||||||
unchanged (or interval ≥ 1s with backoff); rebuild sequencing has no
|
unchanged (or interval ≥ 1s with backoff); rebuild sequencing has no
|
||||||
`Process.sleep`; CLI-sync round-trip latency stays ≤ current behavior.
|
`Process.sleep`; CLI-sync round-trip latency stays ≤ current behavior.
|
||||||
|
|
||||||
### TD-15: `BDS.Tasks` housekeeping (queue type, eviction timers)
|
### TD-15: `BDS.Tasks` housekeeping (queue type, eviction timers) ✅ DONE (2026-06-12)
|
||||||
|
|
||||||
|
**Status: implemented.** `BDS.Tasks` now uses `:queue` for its pending work
|
||||||
|
queue, so enqueue/dequeue on the hot path are O(1) and the FIFO behavior is
|
||||||
|
unchanged. Finished-task cleanup now tracks a single live eviction timer ref
|
||||||
|
instead of scheduling a fresh `send_after/3` on every terminal task; the timer
|
||||||
|
fires, prunes expired finished tasks, and only reschedules itself if finished
|
||||||
|
tasks still remain. Coverage now includes a focused task-state test proving
|
||||||
|
multiple finished tasks share the same live eviction timer and a source guard
|
||||||
|
that forbids `queue ++` churn.
|
||||||
|
|
||||||
**Context.** Minor inefficiencies in `tasks.ex`: the pending queue is a list
|
**Context.** Minor inefficiencies in `tasks.ex`: the pending queue is a list
|
||||||
appended with `++` (O(n) per submit), and **every** finishing task schedules
|
appended with `++` (O(n) per submit), and **every** finishing task schedules
|
||||||
|
|||||||
@@ -69,9 +69,10 @@ defmodule BDS.Tasks do
|
|||||||
{:ok,
|
{:ok,
|
||||||
%{
|
%{
|
||||||
tasks: %{},
|
tasks: %{},
|
||||||
queue: [],
|
queue: :queue.new(),
|
||||||
running: %{},
|
running: %{},
|
||||||
ref_to_task: %{}
|
ref_to_task: %{},
|
||||||
|
finished_task_eviction_timer: nil
|
||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -83,8 +84,7 @@ defmodule BDS.Tasks do
|
|||||||
if map_size(next_state.running) < max_concurrent() do
|
if map_size(next_state.running) < max_concurrent() do
|
||||||
{:reply, {:ok, public_task(task)}, start_task(next_state, task.id, work)}
|
{:reply, {:ok, public_task(task)}, start_task(next_state, task.id, work)}
|
||||||
else
|
else
|
||||||
{:reply, {:ok, public_task(task)},
|
{:reply, {:ok, public_task(task)}, enqueue_task(next_state, task.id, work)}
|
||||||
%{next_state | queue: next_state.queue ++ [{task.id, work}]}}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -143,13 +143,11 @@ defmodule BDS.Tasks do
|
|||||||
|
|
||||||
{:reply, :ok, next_state}
|
{:reply, :ok, next_state}
|
||||||
|
|
||||||
Enum.any?(state.queue, fn {queued_id, _work} -> queued_id == task_id end) ->
|
queued_task?(state.queue, task_id) ->
|
||||||
next_state =
|
next_state =
|
||||||
state
|
state
|
||||||
|> update_task(task_id, %{status: :cancelled, finished_at: DateTime.utc_now()})
|
|> update_task(task_id, %{status: :cancelled, finished_at: DateTime.utc_now()})
|
||||||
|> Map.update!(:queue, fn queue ->
|
|> remove_queued_task(task_id)
|
||||||
Enum.reject(queue, fn {queued_id, _work} -> queued_id == task_id end)
|
|
||||||
end)
|
|
||||||
|> start_queued_tasks()
|
|> start_queued_tasks()
|
||||||
|> schedule_finished_task_eviction()
|
|> schedule_finished_task_eviction()
|
||||||
|
|
||||||
@@ -213,7 +211,19 @@ defmodule BDS.Tasks do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_info(:evict_finished_tasks, state) do
|
def handle_info(:evict_finished_tasks, state) do
|
||||||
{:noreply, prune_expired_finished_tasks(state)}
|
next_state =
|
||||||
|
state
|
||||||
|
|> Map.put(:finished_task_eviction_timer, nil)
|
||||||
|
|> prune_expired_finished_tasks()
|
||||||
|
|
||||||
|
next_state =
|
||||||
|
if any_finished_tasks?(next_state) do
|
||||||
|
schedule_finished_task_eviction(next_state)
|
||||||
|
else
|
||||||
|
next_state
|
||||||
|
end
|
||||||
|
|
||||||
|
{:noreply, next_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({ref, result}, state) do
|
def handle_info({ref, result}, state) do
|
||||||
@@ -315,11 +325,11 @@ defmodule BDS.Tasks do
|
|||||||
map_size(state.running) >= max_concurrent() ->
|
map_size(state.running) >= max_concurrent() ->
|
||||||
state
|
state
|
||||||
|
|
||||||
state.queue == [] ->
|
:queue.is_empty(state.queue) ->
|
||||||
state
|
state
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
[{task_id, work} | remaining] = state.queue
|
{{:value, {task_id, work}}, remaining} = :queue.out(state.queue)
|
||||||
|
|
||||||
state
|
state
|
||||||
|> Map.put(:queue, remaining)
|
|> Map.put(:queue, remaining)
|
||||||
@@ -390,8 +400,12 @@ defmodule BDS.Tasks do
|
|||||||
defp broadcast_terminal_task(_task), do: :ok
|
defp broadcast_terminal_task(_task), do: :ok
|
||||||
|
|
||||||
defp schedule_finished_task_eviction(state) do
|
defp schedule_finished_task_eviction(state) do
|
||||||
Process.send_after(self(), :evict_finished_tasks, finished_task_ttl_ms())
|
if state.finished_task_eviction_timer do
|
||||||
state
|
state
|
||||||
|
else
|
||||||
|
timer_ref = Process.send_after(self(), :evict_finished_tasks, finished_task_ttl_ms())
|
||||||
|
%{state | finished_task_eviction_timer: timer_ref}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp prune_expired_finished_tasks(state) do
|
defp prune_expired_finished_tasks(state) do
|
||||||
@@ -405,6 +419,32 @@ defmodule BDS.Tasks do
|
|||||||
%{state | tasks: tasks}
|
%{state | tasks: tasks}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp enqueue_task(state, task_id, work) do
|
||||||
|
%{state | queue: :queue.in({task_id, work}, state.queue)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp queued_task?(queue, task_id) do
|
||||||
|
queue
|
||||||
|
|> :queue.to_list()
|
||||||
|
|> Enum.any?(fn {queued_id, _work} -> queued_id == task_id end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp remove_queued_task(state, task_id) do
|
||||||
|
remaining_queue =
|
||||||
|
state.queue
|
||||||
|
|> :queue.to_list()
|
||||||
|
|> Enum.reject(fn {queued_id, _work} -> queued_id == task_id end)
|
||||||
|
|> :queue.from_list()
|
||||||
|
|
||||||
|
%{state | queue: remaining_queue}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp any_finished_tasks?(state) do
|
||||||
|
Enum.any?(state.tasks, fn {_task_id, task} ->
|
||||||
|
task.status in [:completed, :failed, :cancelled]
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp expired_finished_task?(%{status: status, finished_at: %DateTime{} = finished_at}, now)
|
defp expired_finished_task?(%{status: status, finished_at: %DateTime{} = finished_at}, now)
|
||||||
when status in [:completed, :failed, :cancelled] do
|
when status in [:completed, :failed, :cancelled] do
|
||||||
DateTime.diff(now, finished_at, :millisecond) >= finished_task_ttl_ms()
|
DateTime.diff(now, finished_at, :millisecond) >= finished_task_ttl_ms()
|
||||||
|
|||||||
@@ -266,6 +266,37 @@ defmodule BDS.TasksTest do
|
|||||||
assert running.id in task_ids
|
assert running.id in task_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "finished task eviction uses a single live timer" do
|
||||||
|
Application.put_env(:bds, :tasks,
|
||||||
|
max_concurrent: 3,
|
||||||
|
progress_throttle_ms: 250,
|
||||||
|
finished_task_ttl_ms: 50
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, first} = BDS.Tasks.register_external_task("first finished")
|
||||||
|
assert {:ok, second} = BDS.Tasks.register_external_task("second finished")
|
||||||
|
|
||||||
|
assert :ok = BDS.Tasks.complete_task(first.id)
|
||||||
|
first_timer = :sys.get_state(BDS.Tasks).finished_task_eviction_timer
|
||||||
|
assert is_reference(first_timer)
|
||||||
|
assert is_integer(Process.read_timer(first_timer))
|
||||||
|
|
||||||
|
assert :ok = BDS.Tasks.complete_task(second.id)
|
||||||
|
second_timer = :sys.get_state(BDS.Tasks).finished_task_eviction_timer
|
||||||
|
|
||||||
|
assert second_timer == first_timer
|
||||||
|
assert is_integer(Process.read_timer(second_timer))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "task queue implementation avoids list append churn" do
|
||||||
|
source = File.read!("lib/bds/tasks.ex")
|
||||||
|
|
||||||
|
assert String.contains?(source, ":queue"), "tasks queue should use :queue"
|
||||||
|
|
||||||
|
refute String.contains?(source, "queue ++"),
|
||||||
|
"tasks queue should not append with ++"
|
||||||
|
end
|
||||||
|
|
||||||
test "terminal task states are broadcast on PubSub" do
|
test "terminal task states are broadcast on PubSub" do
|
||||||
Phoenix.PubSub.subscribe(BDS.PubSub, BDS.Tasks.topic())
|
Phoenix.PubSub.subscribe(BDS.PubSub, BDS.Tasks.topic())
|
||||||
|
|
||||||
@@ -304,4 +335,5 @@ defmodule BDS.TasksTest do
|
|||||||
defp wait_for_task(_task_id, _predicate, 0) do
|
defp wait_for_task(_task_id, _predicate, 0) do
|
||||||
flunk("task did not reach expected state")
|
flunk("task did not reach expected state")
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user