blob: 8d5164257ce1b344436233d4b5899e3235db479b [file]
/*****************************************************************************\
* 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;
}