summaryrefslogtreecommitdiff
path: root/src/lib/job-pool.source.sh
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-24 11:00:33 +0300
committerPaul Buetow <paul@buetow.org>2026-06-24 11:00:33 +0300
commit928f28d0c51ca47f9958fda4c4bb7c8b0c571e34 (patch)
tree5d9d7a80aeb845598d1bdac928ed5a1ed2e9540f /src/lib/job-pool.source.sh
parent6697431d3855d3030b35c2ce232c9ea0e9282ba7 (diff)
Encapsulate job pool behind a single handle (job_pool_*)
Replace the four-parallel-nameref job-pool API (pids/statuses/labels/failed, each needing its own `shellcheck disable=SC2034` at every call site) with a single pool handle: a name prefix whose four backing variables (${pool}_pids/_statuses/_labels/_failed) are derived on demand by the helpers. Bash can't nest indexed arrays in an associative array, so a prefixed-handle with declare -g backing vars is the simplest pure-nameref encoding (no eval). New public API: job_pool_init <pool> job_pool_submit <pool> <label> <cmd...> job_pool_wait <pool> # returns 1 if any job failed Migrate all callers (scalephotos, create_all_photo_derivatives, render_album_pages, render_view_redirects, render_filter_pages) to the handle. queue_preview_page_render_job / queue_album_view_render_job / _album_record_view_photo / _stats_enqueue_filter_album now take one pool arg instead of four names. Drop the dead wrappers and unused parameterization: wait_for_image_job_slot, wait_for_template_render_job_slot and their _jobs variants only ever passed IMAGE_JOBS, so the max_jobs parameter is gone and throttling is fixed at IMAGE_JOBS inside the pool. Also removes wait_for_album_view_render_jobs (now just job_pool_wait). Throttling (max IMAGE_JOBS concurrent), failure detection and failed-job propagation are unchanged; the parallel-throttling and failure-logging tests pass unmodified. SC2034 disable-comments across the touched files drop 29->7. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat (limited to 'src/lib/job-pool.source.sh')
-rw-r--r--src/lib/job-pool.source.sh257
1 files changed, 123 insertions, 134 deletions
diff --git a/src/lib/job-pool.source.sh b/src/lib/job-pool.source.sh
index a50a6e8..d3a3a9c 100644
--- a/src/lib/job-pool.source.sh
+++ b/src/lib/job-pool.source.sh
@@ -1,15 +1,61 @@
-wait_for_parallel_job_pid() {
+# Background-job pool, throttled to a fixed number of concurrent children.
+#
+# A "pool" is encapsulated behind a single handle: a name prefix string. The
+# helpers derive the pool's four backing variables from that prefix on demand:
+#
+# ${pool}_pids indexed array - pids of jobs still being tracked
+# ${pool}_statuses assoc array - pid -> exit status, for jobs already
+# reaped by `wait -n` but not yet finished
+# ${pool}_labels assoc array - pid -> human label, for failure messages
+# ${pool}_failed integer - 1 once any job has exited non-zero
+#
+# Encapsulating the four parallel arrays behind one prefix means callers declare
+# and pass a single handle instead of four namerefs (each previously needing its
+# own `shellcheck disable=SC2034`). Bash cannot nest indexed arrays inside an
+# associative array, so a literal single-variable struct is impossible; a name
+# prefix with `declare -g` backing variables is the simplest encoding that keeps
+# all four pieces together under one name while staying pure-nameref (no eval).
+#
+# Public API:
+# job_pool_init <pool> - create/reset the four backing variables
+# job_pool_submit <pool> <label> <cmd...>
+# - block until a slot is free (< IMAGE_JOBS
+# running), then background <cmd...> and
+# track it under <label>
+# job_pool_wait <pool> - wait for all remaining jobs; return 1 if
+# any job (now or earlier) failed
+#
+# Throttling is fixed at IMAGE_JOBS (the configured max concurrent jobs); the
+# previous per-call max_jobs parameterization was never varied, so it is gone.
+
+# Create or reset a pool's backing variables. `declare -g` makes them globals so
+# the derived namerefs in the helpers can see them regardless of the calling
+# function's scope; re-running it clears any stale state from a prior pool of the
+# same name (e.g. a re-generate).
+job_pool_init() {
+ local -r pool="$1"; shift
+
+ declare -ga "${pool}_pids=()"
+ declare -gA "${pool}_statuses=()"
+ declare -gA "${pool}_labels=()"
+ declare -gi "${pool}_failed=0"
+}
+
+# Reap a single finished pid: record its exit status. If `wait -n` already
+# observed this pid (its status is cached in ${pool}_statuses), reuse that;
+# otherwise `wait` on it directly. The status is returned through status_ref.
+_job_pool_reap_pid() {
+ local -r pool="$1"; shift
local -r pid="$1"; shift
- local -r job_statuses_name="$1"; shift
local -n status_ref="$1"; shift
# shellcheck disable=SC2178
- local -n job_statuses_ref="$job_statuses_name"
+ local -n statuses_ref="${pool}_statuses"
local -i wait_status=0
- if [ -n "${job_statuses_ref[$pid]+x}" ]; then
+ if [ -n "${statuses_ref[$pid]+x}" ]; then
# shellcheck disable=SC2034
- status_ref="${job_statuses_ref[$pid]}"
- unset "job_statuses_ref[$pid]"
+ status_ref="${statuses_ref[$pid]}"
+ unset "statuses_ref[$pid]"
return
fi
@@ -22,7 +68,8 @@ wait_for_parallel_job_pid() {
status_ref="$wait_status"
}
-parallel_job_pid_is_running() {
+# True if pid is among the still-running pids passed as the remaining args.
+_job_pool_pid_is_running() {
local -r pid="$1"; shift
local running_pid
@@ -35,42 +82,32 @@ parallel_job_pid_is_running() {
return 1
}
-log_parallel_job_failure() {
+# Finalise one reaped pid: on non-zero status flip the pool's failed flag and
+# log the job's label, then drop the label entry.
+_job_pool_finish_pid() {
+ local -r pool="$1"; shift
local -r pid="$1"; shift
local -r status="$1"; shift
- local -r job_labels_name="$1"; shift
# shellcheck disable=SC2178
- local -n job_labels_ref="$job_labels_name"
+ local -n labels_ref="${pool}_labels"
+ local -n failed_ref="${pool}_failed"
local label
- label="${job_labels_ref[$pid]:-pid $pid}"
- printf 'ERROR: parallel job failed (%s): %s\n' "$status" "$label" >&2
-}
-
-finish_parallel_job() {
- local -r pid="$1"; shift
- local -r status="$1"; shift
- local -r job_labels_name="$1"; shift
- local -n failed_target_ref="$1"; shift
- # shellcheck disable=SC2178
- local -n job_labels_ref="$job_labels_name"
-
if (( status != 0 )); then
- # shellcheck disable=SC2034
- failed_target_ref=1
- log_parallel_job_failure "$pid" "$status" "$job_labels_name"
+ failed_ref=1
+ label="${labels_ref[$pid]:-pid $pid}"
+ printf 'ERROR: parallel job failed (%s): %s\n' "$status" "$label" >&2
fi
- unset "job_labels_ref[$pid]"
+ unset "labels_ref[$pid]"
}
-reap_finished_parallel_jobs() {
- local -r job_pids_name="$1"; shift
- local -r job_statuses_name="$1"; shift
- local -r job_labels_name="$1"; shift
+# Sweep the pool's tracked pids: any that are no longer running get reaped and
+# finished; still-running ones are kept for the next sweep.
+_job_pool_reap_finished() {
+ local -r pool="$1"; shift
# shellcheck disable=SC2178
- local -n job_pids_ref="$job_pids_name"
- local -n failed_ref="$1"; shift
+ local -n pids_ref="${pool}_pids"
local -i status=0
local -a remaining_pids=()
local -a running_pids=()
@@ -78,138 +115,90 @@ reap_finished_parallel_jobs() {
mapfile -t running_pids < <(jobs -rp)
- for pid in "${job_pids_ref[@]}"; do
- if parallel_job_pid_is_running "$pid" "${running_pids[@]}"; then
+ for pid in "${pids_ref[@]}"; do
+ if _job_pool_pid_is_running "$pid" "${running_pids[@]}"; then
remaining_pids+=("$pid")
continue
fi
- wait_for_parallel_job_pid "$pid" "$job_statuses_name" status
- finish_parallel_job "$pid" "$status" "$job_labels_name" failed_ref
+ _job_pool_reap_pid "$pool" "$pid" status
+ _job_pool_finish_pid "$pool" "$pid" "$status"
done
- job_pids_ref=("${remaining_pids[@]}")
+ pids_ref=("${remaining_pids[@]}")
}
-wait_for_next_parallel_job() {
- local -r job_pids_name="$1"; shift
- local -r job_statuses_name="$1"; shift
- local -r job_labels_name="$1"; shift
- local -r failed_name="$1"; shift
+# Block until at least one tracked job finishes, then reap every job that is now
+# done. `wait -n` returns the first child to exit; its status is cached so the
+# subsequent sweep can finish it with the rest.
+_job_pool_wait_for_next() {
+ local -r pool="$1"; shift
# shellcheck disable=SC2178
- local -n job_pids_ref="$job_pids_name"
+ local -n pids_ref="${pool}_pids"
# shellcheck disable=SC2178
- local -n job_statuses_ref="$job_statuses_name"
+ local -n statuses_ref="${pool}_statuses"
local completed_pid=''
local -i status=0
set +e
- wait -n -p completed_pid "${job_pids_ref[@]}"
+ wait -n -p completed_pid "${pids_ref[@]}"
status=$?
set -e
if [ -n "$completed_pid" ]; then
- job_statuses_ref["$completed_pid"]="$status"
+ statuses_ref["$completed_pid"]="$status"
fi
- reap_finished_parallel_jobs \
- "$job_pids_name" \
- "$job_statuses_name" \
- "$job_labels_name" \
- "$failed_name"
+ _job_pool_reap_finished "$pool"
}
-wait_for_parallel_job_slot() {
- local -r job_pids_name="$1"; shift
- local -r job_statuses_name="$1"; shift
- local -r job_labels_name="$1"; shift
- local -r failed_name="$1"; shift
- local -r max_jobs="$1"; shift
+# Block until the pool has a free slot (fewer than IMAGE_JOBS jobs tracked),
+# reaping finished jobs while it waits. Throttles concurrency to IMAGE_JOBS.
+_job_pool_wait_for_slot() {
+ local -r pool="$1"; shift
# shellcheck disable=SC2178
- local -n job_pids_ref="$job_pids_name"
-
- while (( ${#job_pids_ref[@]} >= max_jobs )); do
- wait_for_next_parallel_job \
- "$job_pids_name" \
- "$job_statuses_name" \
- "$job_labels_name" \
- "$failed_name"
+ local -n pids_ref="${pool}_pids"
+
+ while (( ${#pids_ref[@]} >= IMAGE_JOBS )); do
+ _job_pool_wait_for_next "$pool"
done
}
-wait_for_parallel_jobs() {
+# Wait for a free slot, then background <cmd...> and track it under <label> so a
+# later failure can be reported against a meaningful name. The whole command
+# (and its arguments) runs in the child; the caller is free to reuse its own
+# variables for the next submission once this returns.
+job_pool_submit() {
+ local -r pool="$1"; shift
+ local -r label="$1"; shift
# shellcheck disable=SC2178
- local -n job_pids_ref="$1"; shift
- local -r job_statuses_name="$1"; shift
- local -r job_labels_name="$1"; shift
- local -n failed_ref="$1"; shift
- local -i status=0
-
- : "$failed_ref"
- while (( ${#job_pids_ref[@]} > 0 )); do
- wait_for_parallel_job_pid \
- "${job_pids_ref[0]}" \
- "$job_statuses_name" \
- status
- finish_parallel_job \
- "${job_pids_ref[0]}" \
- "$status" \
- "$job_labels_name" \
- failed_ref
- job_pids_ref=("${job_pids_ref[@]:1}")
- done
-}
+ local -n pids_ref="${pool}_pids"
+ # shellcheck disable=SC2178
+ local -n labels_ref="${pool}_labels"
-wait_for_image_job_slot() {
- local -r image_job_pids_name="$1"; shift
- local -r image_job_statuses_name="$1"; shift
- local -r image_job_labels_name="$1"; shift
- local -r failed_name="$1"; shift
-
- wait_for_parallel_job_slot \
- "$image_job_pids_name" \
- "$image_job_statuses_name" \
- "$image_job_labels_name" \
- "$failed_name" \
- "$IMAGE_JOBS"
+ _job_pool_wait_for_slot "$pool"
+ "$@" &
+ pids_ref+=("$!")
+ labels_ref["$!"]="$label"
}
-wait_for_image_jobs() {
- local -r image_job_pids_name="$1"; shift
- local -r image_job_statuses_name="$1"; shift
- local -r image_job_labels_name="$1"; shift
- local -r failed_name="$1"; shift
-
- wait_for_parallel_jobs \
- "$image_job_pids_name" \
- "$image_job_statuses_name" \
- "$image_job_labels_name" \
- "$failed_name"
-}
+# Wait for all remaining jobs to finish, finishing each in turn so a failed
+# child flips the pool's failed flag. Returns 1 if any job in the pool's
+# lifetime failed, 0 otherwise.
+job_pool_wait() {
+ local -r pool="$1"; shift
+ # shellcheck disable=SC2178
+ local -n pids_ref="${pool}_pids"
+ local -n failed_ref="${pool}_failed"
+ local -i status=0
-wait_for_template_render_job_slot() {
- local -r render_job_pids_name="$1"; shift
- local -r render_job_statuses_name="$1"; shift
- local -r render_job_labels_name="$1"; shift
- local -r failed_name="$1"; shift
-
- wait_for_parallel_job_slot \
- "$render_job_pids_name" \
- "$render_job_statuses_name" \
- "$render_job_labels_name" \
- "$failed_name" \
- "$IMAGE_JOBS"
-}
+ while (( ${#pids_ref[@]} > 0 )); do
+ _job_pool_reap_pid "$pool" "${pids_ref[0]}" status
+ _job_pool_finish_pid "$pool" "${pids_ref[0]}" "$status"
+ pids_ref=("${pids_ref[@]:1}")
+ done
-wait_for_template_render_jobs() {
- local -r render_job_pids_name="$1"; shift
- local -r render_job_statuses_name="$1"; shift
- local -r render_job_labels_name="$1"; shift
- local -r failed_name="$1"; shift
-
- wait_for_parallel_jobs \
- "$render_job_pids_name" \
- "$render_job_statuses_name" \
- "$render_job_labels_name" \
- "$failed_name"
+ if (( failed_ref != 0 )); then
+ return 1
+ fi
}