diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-24 11:00:33 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-24 11:00:33 +0300 |
| commit | 928f28d0c51ca47f9958fda4c4bb7c8b0c571e34 (patch) | |
| tree | 5d9d7a80aeb845598d1bdac928ed5a1ed2e9540f /src/lib/job-pool.source.sh | |
| parent | 6697431d3855d3030b35c2ce232c9ea0e9282ba7 (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.sh | 257 |
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 } |
