| /*****************************************************************************\ | |
| * workq.c - work queue | |
| ***************************************************************************** | |
| * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| * | |
| * This file is part of Slurm, a resource management program. | |
| * For details, see <https://slurm.schedmd.com/>. | |
| * Please also read the included file: DISCLAIMER. | |
| * | |
| * Slurm is free software; you can redistribute it and/or modify it under | |
| * the terms of the GNU General Public License as published by the Free | |
| * Software Foundation; either version 2 of the License, or (at your option) | |
| * any later version. | |
| * | |
| * In addition, as a special exception, the copyright holders give permission | |
| * to link the code of portions of this program with the OpenSSL library under | |
| * certain conditions as described in each individual source file, and | |
| * distribute linked combinations including the two. You must obey the GNU | |
| * General Public License in all respects for all of the code used other than | |
| * OpenSSL. If you modify file(s) with this exception, you may extend this | |
| * exception to your version of the file(s), but you are not obligated to do | |
| * so. If you do not wish to do so, delete this exception statement from your | |
| * version. If you delete this exception statement from all source files in | |
| * the program, then also delete it here. | |
| * | |
| * Slurm is distributed in the hope that it will be useful, but WITHOUT ANY | |
| * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS | |
| * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more | |
| * details. | |
| * | |
| * You should have received a copy of the GNU General Public License along | |
| * with Slurm; if not, write to the Free Software Foundation, Inc., | |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| \*****************************************************************************/ | |
| #include "config.h" | |
| #include <stdint.h> | |
| #include "src/common/events.h" | |
| #include "src/common/log.h" | |
| #include "src/common/macros.h" | |
| #include "src/common/probes.h" | |
| #include "src/common/read_config.h" | |
| #include "src/common/slurm_time.h" | |
| #include "src/common/timers.h" | |
| #include "src/common/workq.h" | |
| #include "src/common/xassert.h" | |
| #include "src/common/xmalloc.h" | |
| #define WORKQ_MAX_ALLOCATORS 10 | |
| #define WORK_MAGIC 0xA241444F | |
| typedef struct workq_work_s workq_work_t; | |
| typedef struct workq_work_s { | |
| int magic; /* WORK_MAGIC */ | |
| enum { | |
| WORKQ_WORK_STATE_INVALID = 0, | |
| WORKQ_WORK_STATE_FREE, | |
| WORKQ_WORK_STATE_ASSIGNED, | |
| WORKQ_WORK_STATE_RUNNING, | |
| WORKQ_WORK_STATE_INVALID_MAX | |
| } state; | |
| work_func_t func; | |
| void *arg; | |
| const char *func_name; | |
| timespec_t ts_submitted; | |
| /* Owning allocator or NULL (if directly xmalloc()ed) */ | |
| workq_allocator_t *alloc; | |
| workq_work_t *next; | |
| } workq_work_t; | |
| typedef struct { | |
| int count; | |
| workq_work_t *begin; | |
| workq_work_t *end; | |
| } workq_queue_t; | |
| #define ALLOC_MAGIC 0xCC414DDF | |
| typedef struct workq_allocator_s { | |
| int magic; /* ALLOC_MAGIC */ | |
| pthread_mutex_t mutex; | |
| const char *name; | |
| /* Requested count of work to preallocate */ | |
| int count; | |
| /* Queue of unassigned work */ | |
| workq_queue_t queue; | |
| struct { | |
| size_t hit; | |
| size_t miss; | |
| } stats; | |
| } workq_allocator_t; | |
| #define WORKQ_MAGIC 0xB2414440 | |
| typedef struct workq_s { | |
| int magic; /* WORKQ_MAGIC */ | |
| pthread_mutex_t mutex; | |
| /* True if workq is shutting down */ | |
| bool shutdown; | |
| /* True to xfree workq pointer */ | |
| bool release; | |
| /* Probe name or NULL */ | |
| const char *probe_name; | |
| /* Assigned allocators */ | |
| workq_allocator_t *allocs[WORKQ_MAX_ALLOCATORS]; | |
| /* Priority queues */ | |
| workq_queue_t queues[WORKQ_PRIORITY_INVALID_MAX - 1]; | |
| /* Number of active threads running work */ | |
| int workers; | |
| /* Latency from submission to starting func() */ | |
| latency_histogram_t start_delay_histogram; | |
| /* Latency for func() run */ | |
| latency_histogram_t runtime_histogram; | |
| event_signal_t run_sleep; | |
| event_signal_t run_complete; | |
| } workq_t; | |
| #define T(x) [x] = { x, #x } | |
| static const struct { | |
| const workq_priority_t priority; | |
| const char *str; | |
| } priority_names[] = { | |
| T(WORKQ_PRIORITY_IDLE), | |
| T(WORKQ_PRIORITY_NORMAL), | |
| }; | |
| #undef T | |
| static workq_queue_t *_priority_to_queue(workq_t *workq, | |
| const workq_priority_t priority) | |
| { | |
| const int i = (priority - WORKQ_PRIORITY_IDLE); | |
| xassert(i >= 0); | |
| xassert(i < ARRAY_SIZE(workq->queues)); | |
| return &workq->queues[i]; | |
| } | |
| static const char *_priority_str(const workq_priority_t priority) | |
| { | |
| xassert(priority > WORKQ_PRIORITY_INVALID); | |
| xassert(priority < WORKQ_PRIORITY_INVALID_MAX); | |
| return priority_names[priority].str; | |
| } | |
| extern const size_t workq_bytes(void) | |
| { | |
| return sizeof(workq_t); | |
| } | |
| static int _workq_count(const workq_t *workq) | |
| { | |
| int count = 0; | |
| for (int i = 0; i < ARRAY_SIZE(workq->queues); i++) { | |
| xassert(workq->queues[i].count >= 0); | |
| count += workq->queues[i].count; | |
| } | |
| xassert(count >= 0); | |
| return count; | |
| } | |
| static workq_work_t *_queue_pop(workq_queue_t *queue) | |
| { | |
| workq_work_t *work = queue->begin; | |
| if (!work) { | |
| xassert(!queue->end); | |
| xassert(!queue->count); | |
| return NULL; | |
| } | |
| xassert(work->magic == WORK_MAGIC); | |
| xassert(work->state > WORKQ_WORK_STATE_INVALID); | |
| xassert(work->state < WORKQ_WORK_STATE_INVALID_MAX); | |
| if (work->next) { | |
| xassert(work->next->magic == WORK_MAGIC); | |
| xassert(queue->end != work); | |
| queue->begin = work->next; | |
| work->next = NULL; | |
| } else { | |
| /* List will be empty */ | |
| xassert(queue->end == work); | |
| queue->begin = NULL; | |
| queue->end = NULL; | |
| } | |
| queue->count--; | |
| xassert(queue->count >= 0); | |
| return work; | |
| } | |
| static void _queue_push(workq_queue_t *queue, workq_work_t *work) | |
| { | |
| xassert(work->magic == WORK_MAGIC); | |
| xassert(work->state > WORKQ_WORK_STATE_INVALID); | |
| xassert(work->state < WORKQ_WORK_STATE_INVALID_MAX); | |
| xassert(!work->next); | |
| if (!queue->end) { | |
| xassert(!queue->begin); | |
| xassert(!queue->count); | |
| queue->begin = work; | |
| queue->end = work; | |
| } else { | |
| workq_work_t *last = queue->end; | |
| xassert(last->magic == WORK_MAGIC); | |
| xassert(last->state > WORKQ_WORK_STATE_INVALID); | |
| xassert(last->state < WORKQ_WORK_STATE_INVALID_MAX); | |
| xassert(!last->next); | |
| last->next = work; | |
| queue->end = work; | |
| } | |
| queue->count++; | |
| xassert(queue->count > 0); | |
| } | |
| static timespec_t _work_run(workq_t *workq, workq_work_t *work, const int id, | |
| const bool shutdown) | |
| { | |
| timespec_t ts_diff = { 0, 0 }; | |
| timespec_t ts_started = timespec_now(); | |
| xassert(workq->magic == WORKQ_MAGIC); | |
| xassert(work->magic == WORK_MAGIC); | |
| xassert(work->state == WORKQ_WORK_STATE_ASSIGNED); | |
| work->state = WORKQ_WORK_STATE_RUNNING; | |
| if (slurm_conf.debug_flags & DEBUG_FLAG_THREAD) { | |
| char str[TIMESPEC_CTIME_STR_LEN] = "INVALID"; | |
| (void) timespec_ctime(timespec_diff_ns(ts_started, | |
| work->ts_submitted) | |
| .diff, | |
| false, str, sizeof(str)); | |
| log_flag(THREAD, "%s: [workq@%p+%d] BEGIN: %s %s()@%p after %s", | |
| __func__, workq, id, | |
| (shutdown ? "shutting down" : "running"), | |
| work->func_name, work, str); | |
| } | |
| work->func(shutdown, work->arg); | |
| ts_diff = timespec_diff_ns(timespec_now(), ts_started).diff; | |
| if (slurm_conf.debug_flags & DEBUG_FLAG_THREAD) { | |
| char str[TIMESPEC_CTIME_STR_LEN] = "INVALID"; | |
| (void) timespec_ctime(ts_diff, false, str, sizeof(str)); | |
| log_flag(THREAD, "%s: [workq@%p+%d] END: %s %s()@%p after %s", | |
| __func__, workq, id, | |
| (shutdown ? "shutting down" : "running"), | |
| work->func_name, work, str); | |
| } | |
| xassert(workq->magic == WORKQ_MAGIC); | |
| xassert(work->magic == WORK_MAGIC); | |
| xassert(work->state == WORKQ_WORK_STATE_RUNNING); | |
| work->state = WORKQ_WORK_STATE_FREE; | |
| if (work->alloc) { | |
| slurm_mutex_lock(&work->alloc->mutex); | |
| xassert(work->alloc->magic == ALLOC_MAGIC); | |
| _queue_push(&work->alloc->queue, work); | |
| slurm_mutex_unlock(&work->alloc->mutex); | |
| } else { | |
| work->magic = ~WORK_MAGIC; | |
| xfree(work); | |
| } | |
| return ts_diff; | |
| } | |
| static bool _worker_run_work(workq_t *workq, const int id) | |
| { | |
| workq_work_t *work = NULL; | |
| timespec_t ts_diff = { 0, 0 }; | |
| const char shutdown = workq->shutdown; | |
| for (int i = (ARRAY_SIZE(workq->queues) - 1); !work && (i >= 0); i--) | |
| work = _queue_pop(&workq->queues[i]); | |
| if (!work) | |
| return false; | |
| HISTOGRAM_ADD_DURATION(&workq->start_delay_histogram, | |
| work->ts_submitted); | |
| slurm_mutex_unlock(&workq->mutex); | |
| ts_diff = _work_run(workq, work, id, shutdown); | |
| slurm_mutex_lock(&workq->mutex); | |
| latency_metric_add_histogram_value(&workq->runtime_histogram, ts_diff); | |
| return true; | |
| } | |
| static bool _worker_run(workq_t *workq, const bool blocking, const int id) | |
| { | |
| xassert(workq->magic == WORKQ_MAGIC); | |
| log_flag(THREAD, "%s: [workq@%p+%d] queued:%d", | |
| __func__, workq, id, _workq_count(workq)); | |
| while (_worker_run_work(workq, id)) | |
| ; /* do nothing */ | |
| if (workq->shutdown || !blocking) | |
| return false; | |
| log_flag(THREAD, "%s: [workq@%p+%d] waiting for work", | |
| __func__, workq, id); | |
| EVENT_WAIT(&workq->run_sleep, &workq->mutex); | |
| return true; | |
| } | |
| extern void workq_run(workq_t *workq, const bool blocking) | |
| { | |
| int id = 0; | |
| slurm_mutex_lock(&workq->mutex); | |
| xassert(workq->magic == WORKQ_MAGIC); | |
| id = ++workq->workers; | |
| xassert(workq->workers > 0); | |
| while (_worker_run(workq, blocking, id)) | |
| ; /* do nothing */ | |
| xassert(workq->workers > 0); | |
| log_flag(THREAD, "%s: [workq@%p+%d] stopped", __func__, workq, id); | |
| workq->workers--; | |
| xassert(workq->workers >= 0); | |
| EVENT_BROADCAST(&workq->run_complete); | |
| slurm_mutex_unlock(&workq->mutex); | |
| } | |
| static void _probe_verbose(probe_log_t *log, workq_t *workq) | |
| { | |
| char histogram[LATENCY_METRIC_HISTOGRAM_STR_LEN] = { 0 }; | |
| probe_log(log, "state: workers:%d shutdown:%c queued=%d", | |
| workq->workers, BOOL_CHARIFY(workq->shutdown), | |
| _workq_count(workq)); | |
| (void) latency_histogram_print_labels(histogram, sizeof(histogram)); | |
| probe_log(log, "histogram: %s", histogram); | |
| (void) latency_histogram_print(&workq->start_delay_histogram, histogram, | |
| sizeof(histogram)); | |
| probe_log(log, "start delay histogram: %s", histogram); | |
| (void) latency_histogram_print(&workq->runtime_histogram, histogram, | |
| sizeof(histogram)); | |
| probe_log(log, "runtime histogram: %s", histogram); | |
| for (int i = 0; i < ARRAY_SIZE(workq->queues); i++) { | |
| workq_queue_t *queue = &workq->queues[i]; | |
| workq_priority_t priority = (i + WORKQ_PRIORITY_IDLE); | |
| int count = 0; | |
| probe_log(log, "%s queue: queued=%d", | |
| _priority_str(priority), queue->count); | |
| for (workq_work_t *work = workq->queues[i].begin; work; | |
| work = work->next) { | |
| xassert(work->magic == WORK_MAGIC); | |
| xassert(work->next || (workq->queues[i].end == work)); | |
| count++; | |
| probe_log(log, "queue[%s]+%d: %s() allocator:%s", | |
| _priority_str(priority), count, | |
| work->func_name, | |
| (work->alloc ? work->alloc->name : "N/A")); | |
| } | |
| } | |
| } | |
| static probe_status_t _probe(probe_log_t *log, void *arg) | |
| { | |
| probe_status_t status = PROBE_RC_UNKNOWN; | |
| workq_t *workq = arg; | |
| slurm_mutex_lock(&workq->mutex); | |
| xassert(workq->magic == WORKQ_MAGIC); | |
| if (log) | |
| _probe_verbose(log, workq); | |
| if (workq->shutdown) | |
| status = PROBE_RC_ONLINE; | |
| else | |
| status = PROBE_RC_READY; | |
| slurm_mutex_unlock(&workq->mutex); | |
| return status; | |
| } | |
| extern workq_t *workq_init(workq_t *workq, const char *name) | |
| { | |
| bool release = false; | |
| if (!workq) { | |
| workq = xmalloc(workq_bytes()); | |
| release = true; | |
| } | |
| xassert(workq->magic != WORKQ_MAGIC); | |
| *workq = (workq_t) { | |
| .magic = WORKQ_MAGIC, | |
| .shutdown = false, | |
| .release = release, | |
| .probe_name = name, | |
| .mutex = PTHREAD_MUTEX_INITIALIZER, | |
| .run_sleep = EVENT_INITIALIZER("WORKQ_RUN_SLEEP"), | |
| .run_complete = EVENT_INITIALIZER("WORKQ_RUN_COMPLETE"), | |
| .start_delay_histogram = LATENCY_HISTOGRAM_INITIALIZER, | |
| .runtime_histogram = LATENCY_HISTOGRAM_INITIALIZER, | |
| }; | |
| if (name) | |
| probe_register(name, _probe, workq); | |
| return workq; | |
| } | |
| extern void workq_fini(workq_t *workq) | |
| { | |
| DEF_TIMERS; | |
| bool release = false; | |
| xassert(workq); | |
| START_TIMER; | |
| slurm_mutex_lock(&workq->mutex); | |
| xassert(workq->magic == WORKQ_MAGIC); | |
| workq->shutdown = true; | |
| while (workq->workers) { | |
| xassert(workq->workers > 0); | |
| /* Wake up any sleeping workers to cleanup */ | |
| EVENT_BROADCAST(&workq->run_sleep); | |
| log_flag(THREAD, "%s: [workq@%p] waiting on queued=%d workers=%d", | |
| __func__, workq, _workq_count(workq), | |
| workq->workers); | |
| EVENT_WAIT(&workq->run_complete, &workq->mutex); | |
| } | |
| xassert(workq->magic == WORKQ_MAGIC); | |
| xassert(workq->shutdown); | |
| xassert(!workq->workers); | |
| for (int i = 0; i < ARRAY_SIZE(workq->queues); i++) { | |
| xassert(!workq->queues[i].count); | |
| xassert(!workq->queues[i].begin); | |
| xassert(!workq->queues[i].end); | |
| } | |
| /* | |
| * Free the allocators and their preallocated work. The probe only | |
| * walks workq->queues[] (drained above), never workq->allocs[], so | |
| * this is safe while the workq stays probe-registered. | |
| */ | |
| for (int i = 0; i < ARRAY_SIZE(workq->allocs); i++) { | |
| if (!workq->allocs[i]) | |
| continue; | |
| xassert(workq->allocs[i]->magic == ALLOC_MAGIC); | |
| workq->allocs[i]->magic = ~ALLOC_MAGIC; | |
| xfree(workq->allocs[i]); | |
| } | |
| /* workq with a probe must never be xfree()ed */ | |
| if (!workq->probe_name) { | |
| release = workq->release; | |
| EVENT_FREE_MEMBERS(&workq->run_sleep); | |
| EVENT_FREE_MEMBERS(&workq->run_complete); | |
| workq->magic = ~WORKQ_MAGIC; | |
| } | |
| slurm_mutex_unlock(&workq->mutex); | |
| END_TIMER2(__func__); | |
| log_flag(THREAD, "%s: [workq@%p] shutdown complete after %s", | |
| __func__, workq, TIMER_STR()); | |
| if (release) | |
| xfree(workq); | |
| } | |
| extern void workq_enqueue(workq_t *workq, workq_allocator_t *alloc, | |
| const workq_priority_t priority, work_func_t func, | |
| const char *func_name, const char *caller, void *arg) | |
| { | |
| workq_work_t *work = NULL; | |
| xassert(priority > WORKQ_PRIORITY_INVALID); | |
| xassert(priority < WORKQ_PRIORITY_INVALID_MAX); | |
| xassert(workq->magic == WORKQ_MAGIC); | |
| xassert(!work || (work->magic != WORK_MAGIC)); | |
| xassert(alloc); | |
| slurm_mutex_lock(&alloc->mutex); | |
| xassert(alloc->magic == ALLOC_MAGIC); | |
| if ((work = _queue_pop(&alloc->queue))) | |
| alloc->stats.hit++; | |
| else | |
| alloc->stats.miss++; | |
| slurm_mutex_unlock(&alloc->mutex); | |
| if (!work) { | |
| work = xmalloc(sizeof(*work)); | |
| } else { | |
| xassert(work->magic == WORK_MAGIC); | |
| xassert(work->alloc == alloc); | |
| xassert(work->state == WORKQ_WORK_STATE_FREE); | |
| } | |
| work->magic = WORK_MAGIC; | |
| work->state = WORKQ_WORK_STATE_ASSIGNED; | |
| work->func = func; | |
| work->arg = arg; | |
| work->func_name = func_name; | |
| work->ts_submitted = timespec_now(); | |
| slurm_mutex_lock(&workq->mutex); | |
| _queue_push(_priority_to_queue(workq, priority), work); | |
| /* Wake up 1 sleeping thread to run the work */ | |
| EVENT_SIGNAL(&workq->run_sleep); | |
| log_flag(THREAD, "%s->%s: [workq@%p] SUBMIT: %s()@%p allocator:%s", | |
| caller, __func__, workq, func_name, work, | |
| (work->alloc ? work->alloc->name : "N/A")); | |
| slurm_mutex_unlock(&workq->mutex); | |
| } | |
| /* Caller must hold alloc->mutex lock */ | |
| static workq_work_t *_alloc_get(workq_allocator_t *alloc, const int index) | |
| { | |
| void *ptr = (((void *) alloc) + sizeof(workq_allocator_t)); | |
| workq_work_t *work = NULL; | |
| xassert(alloc->magic == ALLOC_MAGIC); | |
| xassert(index >= 0); | |
| if ((index >= alloc->count) || (index < 0)) | |
| return NULL; | |
| work = (ptr + (index * sizeof(workq_work_t))); | |
| return work; | |
| } | |
| extern workq_allocator_t *workq_allocator(workq_t *workq, const int count, | |
| const char *name) | |
| { | |
| const size_t alloc_bytes = | |
| (sizeof(workq_allocator_t) + (count * sizeof(workq_work_t))); | |
| workq_allocator_t *alloc = xmalloc(alloc_bytes); | |
| xassert(count > 0); | |
| *alloc = (workq_allocator_t) { | |
| .magic = ALLOC_MAGIC, | |
| .mutex = PTHREAD_MUTEX_INITIALIZER, | |
| .name = name, | |
| .count = count, | |
| }; | |
| for (int i = 0; i < count; i++) { | |
| workq_work_t *work = _alloc_get(alloc, i); | |
| *work = (workq_work_t) { | |
| .magic = WORK_MAGIC, | |
| .state = WORKQ_WORK_STATE_FREE, | |
| .alloc = alloc, | |
| }; | |
| _queue_push(&alloc->queue, work); | |
| } | |
| slurm_mutex_lock(&workq->mutex); | |
| for (int i = 0; i <= ARRAY_SIZE(workq->allocs); i++) { | |
| if (i >= ARRAY_SIZE(workq->allocs)) | |
| fatal_abort("too many allocators!"); | |
| if (!workq->allocs[i]) { | |
| workq->allocs[i] = alloc; | |
| break; | |
| } | |
| } | |
| slurm_mutex_unlock(&workq->mutex); | |
| log_flag(THREAD, "%s: [%s] allocator created count=%d", | |
| __func__, name, count); | |
| return alloc; | |
| } |