blob: a05fae598c25f8a0d8c644a86caf3f5ea32f2b2d [file]
/*****************************************************************************\
* swait.c - Block until all regular steps of a stepmgr-enabled job
* have ended.
*****************************************************************************
* 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 <errno.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include "slurm/slurm.h"
#include "slurm/slurm_errno.h"
#include "src/common/log.h"
#include "src/common/macros.h"
#include "src/common/read_config.h"
#include "src/common/sluid.h"
#include "src/common/slurm_protocol_api.h"
#include "src/common/slurm_protocol_defs.h"
#include "src/common/workerpool.h"
#include "src/common/xmalloc.h"
#include "src/common/xstring.h"
#include "src/conmgr/conmgr.h"
#include "src/interfaces/conn.h"
#include "src/swait/opt.h"
#define SWAIT_WORKERPOOL_THREADS 3
/*
* exit_lock arbitrates between _on_msg (steps-drained: keep exit_rc=0) and
* _timeout_fire (timeout: exit_rc=1). exit_decided is set by whichever
* callback wins the race; the loser becomes a no-op.
*/
static pthread_mutex_t exit_lock = PTHREAD_MUTEX_INITIALIZER;
static bool exit_decided;
static int exit_rc;
static char *my_cert;
static char *my_host;
static uint16_t my_port;
static char *stepmgr_node;
/*
* query slurmctld for job's stepmgr node
*
* IN/OUT target - step_id selecting the job; job_id is overwritten
* with the per-task id for array-task input.
* RET xmalloc()'d hostname string (caller xfree()s)
*/
static char *_resolve_stepmgr_via_ctld(slurm_step_id_t *target)
{
job_info_msg_t *resp = NULL;
slurm_job_info_t *info = NULL;
char *host = NULL;
bool is_array = false;
if (slurm_load_job(&resp, *target, SHOW_ALL) != SLURM_SUCCESS) {
error("cannot load %pI: %s", target, slurm_strerror(errno));
slurm_free_job_info_msg(resp);
exit(2);
}
if (!resp || (resp->record_count < 1) || !resp->job_array) {
error("cannot load %pI: empty controller response", target);
slurm_free_job_info_msg(resp);
exit(2);
}
for (uint32_t i = 0; i < resp->record_count; i++) {
if (resp->job_array[i].array_task_id != NO_VAL)
is_array = true;
if ((opt.array_task_id != NO_VAL) &&
(resp->job_array[i].array_task_id == opt.array_task_id)) {
info = &resp->job_array[i];
break;
}
}
if (opt.array_task_id != NO_VAL) {
if (!info) {
if (is_array)
error("array task %u_%u: not found",
opt.array_job_id, opt.array_task_id);
else
error("%pI: not an array job", target);
slurm_free_job_info_msg(resp);
exit(2);
}
target->job_id = info->job_id;
} else if (is_array && !target->sluid) {
if (resp->record_count > 1) {
error("%pI is an array job; pass a specific task offset (jobid_task)",
target);
slurm_free_job_info_msg(resp);
exit(2);
}
info = &resp->job_array[0];
opt.array_job_id = info->array_job_id;
opt.array_task_id = info->array_task_id;
target->job_id = info->job_id;
} else {
info = &resp->job_array[0];
target->job_id = info->job_id;
}
if (!(info->bitflags & STEPMGR_ENABLED)) {
error("%pI does not have stepmgr enabled", target);
slurm_free_job_info_msg(resp);
exit(2);
}
if (!info->batch_host || !*info->batch_host) {
if (IS_JOB_PENDING(info))
error("%pI is still pending; swait does not wait for jobs to start",
target);
else
error("%pI: stepmgr host is unknown (controller bug?)",
target);
slurm_free_job_info_msg(resp);
exit(2);
}
host = xstrdup(info->batch_host);
verbose("resolved %pI via controller: stepmgr=%s, JobId=%u",
target, host, info->job_id);
slurm_free_job_info_msg(resp);
return host;
}
/*
* Resolve an array-task's stepmgr from env vars when SLURM_ARRAY_* match.
*
* IN/OUT target - target->job_id is overwritten with the per-task job_id.
* RET xmalloc()'d hostname string, or NULL on any mismatch / missing var.
*/
static char *_array_task_env_stepmgr(slurm_step_id_t *target)
{
const char *array_jid_env = getenv("SLURM_ARRAY_JOB_ID");
const char *array_tid_env = getenv("SLURM_ARRAY_TASK_ID");
const char *jid_env = getenv("SLURM_JOB_ID");
const char *stepmgr = getenv("SLURM_STEPMGR");
unsigned long ajid = 0, atid = 0, jid = 0;
char *end = NULL;
if (!array_jid_env || !*array_jid_env || !array_tid_env ||
!*array_tid_env || !jid_env || !*jid_env || !stepmgr || !*stepmgr)
return NULL;
/* Check if SLURM_ARRAY_JOB_ID matches */
errno = 0;
ajid = strtoul(array_jid_env, &end, 10);
if (errno || *end || (ajid != opt.array_job_id))
return NULL;
/* Check if SLURM_ARRAY_TASK_ID matches */
errno = 0;
atid = strtoul(array_tid_env, &end, 10);
if (errno || *end || (atid != opt.array_task_id))
return NULL;
/* Check if SLURM_JOB_ID matches */
errno = 0;
jid = strtoul(jid_env, &end, 10);
if (errno || *end || !jid || (jid >= MAX_VAL))
return NULL;
target->job_id = (uint32_t) jid;
verbose("resolved array task %u_%u via env: stepmgr=%s, JobId=%lu",
opt.array_job_id, opt.array_task_id, stepmgr, jid);
return xstrdup(stepmgr);
}
/*
* Resolve the job stepmgr
*
* IN/OUT target - step_id selecting the job; job_id is overwritten
* with the per-task id for array-task input.
* RET xmalloc()'d hostname string; caller must xfree() it.
*/
static char *_resolve_stepmgr(slurm_step_id_t *target)
{
const char *stepmgr;
/*
* The stepmgr node that we want to talk to might not be the one in
* SLURM_STEPMGR. Double check that the target job matches what the
* environment says before trusting SLURM_STEPMGR env var. If there's a
* mismatch, resolve stepmgr via ctld.
*/
/* Check if SLURM_ARRAY_TASK_ID/SLURM_ARRAY_TASK_JOB_ID match target */
if (opt.array_task_id != NO_VAL) {
char *host = _array_task_env_stepmgr(target);
if (host)
return host;
goto resolve_via_ctld;
}
/* Check if SLURM_JOB_SLUID matches target */
if (target->sluid) {
const char *sluid_env = getenv("SLURM_JOB_SLUID");
if (!sluid_env || !*sluid_env ||
(str2sluid(sluid_env) != target->sluid))
goto resolve_via_ctld;
stepmgr = getenv("SLURM_STEPMGR");
if (!stepmgr || !*stepmgr)
goto resolve_via_ctld;
verbose("resolved stepmgr via SLURM_STEPMGR env (SLUID match): %s",
stepmgr);
return xstrdup(stepmgr);
}
/* Check if SLURM_JOB_ID matches target */
if (target->job_id != NO_VAL) {
const char *jobid_env = getenv("SLURM_JOB_ID");
unsigned long parsed;
char *end = NULL;
if (!jobid_env || !*jobid_env)
goto resolve_via_ctld;
errno = 0;
parsed = strtoul(jobid_env, &end, 10);
if (errno || *end || !parsed || (parsed >= MAX_VAL) ||
(parsed != target->job_id))
goto resolve_via_ctld;
stepmgr = getenv("SLURM_STEPMGR");
if (!stepmgr || !*stepmgr)
goto resolve_via_ctld;
verbose("resolved stepmgr via SLURM_STEPMGR env (JobId match): %s",
stepmgr);
return xstrdup(stepmgr);
}
resolve_via_ctld:
debug("falling back to controller for stepmgr resolution");
return _resolve_stepmgr_via_ctld(target);
}
/*
* IN node - hostname of the stepmgr stepd
* OUT addr - filled with the resolved address on success
* RET SLURM_SUCCESS or SLURM_ERROR
*/
static int _resolve_stepmgr_addr(const char *node, slurm_addr_t *addr)
{
slurm_node_alias_addrs_t *alias_addrs = NULL;
if (slurm_conf_get_addr(node, addr, 0) == SLURM_SUCCESS)
return SLURM_SUCCESS;
if (slurm_get_node_alias_addrs((char *) node, &alias_addrs) ||
!alias_addrs)
return SLURM_ERROR;
add_remote_nodes_to_conf_tbls(alias_addrs->node_list,
alias_addrs->node_addrs);
slurm_free_node_alias_addrs(alias_addrs);
return slurm_conf_get_addr(node, addr, 0);
}
/*
* conmgr on-message callback: authenticate, dispatch on msg_type, free msg.
* On SRUN_STEPS_DRAINED, sets exit_decided and requests conmgr shutdown.
* IN args - conmgr callback args
* IN msg - unpacked message; freed before return
* IN unpack_rc - non-zero if message unpack failed
* IN arg - unused
* RET SLURM_SUCCESS, SLURM_PROTOCOL_AUTHENTICATION_ERROR, or unpack_rc
*/
static int _on_msg(conmgr_callback_args_t args, slurm_msg_t *msg, int unpack_rc,
void *arg)
{
int rc = SLURM_SUCCESS;
if (unpack_rc) {
if (unpack_rc == SLURM_PROTOCOL_AUTHENTICATION_ERROR)
debug("swait: dropping unauthenticated RPC");
else
debug("swait: dropping malformed RPC: %s",
slurm_strerror(unpack_rc));
rc = unpack_rc;
goto out;
}
if (!msg->auth_ids_set ||
((msg->auth_uid != slurm_conf.slurmd_user_id) &&
(msg->auth_uid != slurm_conf.slurm_user_id))) {
debug("swait: dropping %s from uid %u",
rpc_num2string(msg->msg_type),
msg->auth_ids_set ? msg->auth_uid : (uid_t) -1);
rc = SLURM_PROTOCOL_AUTHENTICATION_ERROR;
goto out;
}
switch (msg->msg_type) {
case SRUN_STEPS_DRAINED:
verbose("received SRUN_STEPS_DRAINED; shutting down");
slurm_mutex_lock(&exit_lock);
exit_decided = true;
slurm_mutex_unlock(&exit_lock);
conmgr_request_shutdown();
break;
default:
debug("swait: unexpected msg type %d/%s",
msg->msg_type, rpc_num2string(msg->msg_type));
break;
}
out:
slurm_free_msg(msg);
return rc;
}
/*
* One-shot --timeout deadline: sets exit_rc=1 and requests conmgr shutdown.
* IN args - conmgr callback args
* IN arg - unused
*/
static void _timeout_fire(conmgr_callback_args_t args, void *arg)
{
if (args.status == CONMGR_WORK_STATUS_CANCELLED)
return;
slurm_mutex_lock(&exit_lock);
if (exit_decided) {
slurm_mutex_unlock(&exit_lock);
return;
}
exit_decided = true;
exit_rc = 1;
slurm_mutex_unlock(&exit_lock);
error("timed out after %u seconds", opt.timeout);
conmgr_request_shutdown();
}
static void *_on_connection(conmgr_callback_args_t conmgr_args, void *arg)
{
log_flag(NET, "%s: [%s] new connection",
__func__, conmgr_con_get_name(conmgr_args.ref));
return conmgr_args.con;
}
/*
* Send REQUEST_STEPS_DRAINED_SUBSCRIBE to the stepmgr at node.
* IN node - hostname of the stepmgr stepd
* IN host - listener hostname swait advertised
* IN port - listener port swait advertised
* IN cert - swait's TLS certificate, or NULL when TLS is off
* RET SLURM_SUCCESS, ESLURM_STEPS_DRAINED (no blocking steps; fast-return),
* EAGAIN (subscriber slots full), or another error code on failure
*/
static int _send_subscribe(const char *node, const char *host, uint16_t port,
const char *cert)
{
slurm_msg_t req_msg, resp_msg;
steps_drained_sub_msg_t data;
int rc = SLURM_SUCCESS;
slurm_msg_t_init(&req_msg);
slurm_msg_t_init(&resp_msg);
slurm_msg_set_r_uid(&req_msg, slurm_conf.slurmd_user_id);
if (_resolve_stepmgr_addr(node, &req_msg.address)) {
rc = SLURM_ERROR;
goto cleanup;
}
data = (steps_drained_sub_msg_t) {
.host = (char *) host,
.port = port,
.step_id = opt.target,
.tls_cert = (char *) cert,
};
req_msg.msg_type = REQUEST_STEPS_DRAINED_SUBSCRIBE;
req_msg.data = &data;
debug("sending REQUEST_STEPS_DRAINED_SUBSCRIBE to %s; listener=%s:%u",
node, host, port);
if (slurm_send_recv_node_msg(&req_msg, &resp_msg, 0)) {
rc = SLURM_ERROR;
goto cleanup;
}
if (resp_msg.msg_type == RESPONSE_SLURM_RC)
rc = ((return_code_msg_t *) resp_msg.data)->return_code;
else
rc = SLURM_UNEXPECTED_MSG_ERROR;
cleanup:
slurm_free_msg_members(&resp_msg);
return rc;
}
/*
* Set up listener to receive SRUN_STEPS_DRAINED from stepmgr
*
* RET SLURM_SUCCESS on success, ESLURM_STEPS_DRAINED if the job has no
* running or pending steps (nothing to wait for), or other non-zero
* error.
*/
static int _setup_steps_drained_listener(void)
{
static const conmgr_events_t swait_events = {
.on_connection = _on_connection,
.on_msg = _on_msg,
};
conmgr_con_flags_t flags = CON_FLAG_NONE;
char *host = NULL;
char *cert = NULL;
uint16_t port = 0;
int fd = -1;
int rc = SLURM_ERROR;
char hostname[HOST_NAME_MAX + 1];
/* Create listening socket that stepmgr can talk to */
if (gethostname(hostname, sizeof(hostname))) {
error("swait: gethostname failed: %m");
return errno ? errno : SLURM_ERROR;
}
host = xstrdup(hostname);
if ((rc = slurm_init_msg_engine_srun_ports(&fd, &port))) {
error("swait: unable to open listen socket: %m");
goto fail;
}
verbose("listening on %s:%u", host, port);
if (conn_tls_enabled()) {
/* stepmgr will need TLS certificate to trust our "server" */
if (!(cert = conn_g_get_own_public_cert())) {
error("swait: conn_g_get_own_public_cert failed");
rc = SLURM_ERROR;
goto fail;
}
flags |= CON_FLAG_TLS_FINGERPRINT;
}
/* Send stepmgr listening socket info so it can talk back to us */
if ((rc = _send_subscribe(stepmgr_node, host, port, cert))) {
if (rc != ESLURM_STEPS_DRAINED) {
error("swait: subscribe RPC failed: %s",
slurm_strerror(rc));
}
goto fail;
}
verbose("subscribed to stepmgr %s", stepmgr_node);
my_host = host;
my_cert = cert;
my_port = port;
/* Start waiting for connections from stepmgr on the listening socket */
if ((rc = conmgr_process_fd_listen(fd, CON_TYPE_RPC, NULL,
&swait_events, flags, NULL))) {
error("swait: conmgr_process_fd_listen failed: %s",
slurm_strerror(rc));
my_host = NULL;
my_cert = NULL;
my_port = 0;
goto fail;
}
/* conmgr owns fd now */
fd = -1;
return SLURM_SUCCESS;
fail:
if (fd >= 0)
(void) close(fd);
xfree(host);
xfree(cert);
return rc;
}
/*
* swait entry point. RET 0 on success, 1 on error.
*/
int main(int argc, char **argv)
{
log_options_t log_opts = LOG_OPTS_STDERR_ONLY;
int setup_rc;
log_init("swait", log_opts, SYSLOG_FACILITY_DAEMON, NULL);
slurm_init(NULL);
parse_command_line(argc, argv);
if (opt.verbose || opt.quiet) {
log_opts.stderr_level += opt.verbose;
log_opts.stderr_level -= opt.quiet;
log_alter(log_opts, SYSLOG_FACILITY_DAEMON, NULL);
}
stepmgr_node = _resolve_stepmgr(&opt.target);
workerpool_init(0, SWAIT_WORKERPOOL_THREADS, NULL);
conmgr_init(0);
setup_rc = _setup_steps_drained_listener();
if (setup_rc == ESLURM_STEPS_DRAINED) {
verbose("steps already drained; exiting without waiting");
} else if (setup_rc == EAGAIN) {
error("stepmgr %s subscriber slots full; try again later",
stepmgr_node);
exit_rc = 2;
} else if (setup_rc) {
error("subscribe to stepmgr %s failed: %s",
stepmgr_node, slurm_strerror(setup_rc));
exit_rc = 2;
} else {
if (opt.timeout > 0) {
verbose("waiting for steps to drain (timeout %us)",
opt.timeout);
conmgr_add_work_delayed_fifo(_timeout_fire, NULL,
opt.timeout, 0);
} else {
verbose("waiting for steps to drain (no timeout)");
}
conmgr_run(true);
}
verbose("exiting rc=%d", exit_rc);
conmgr_fini();
workerpool_fini();
#ifdef MEMORY_LEAK_DEBUG
xfree(stepmgr_node);
xfree(my_host);
xfree(my_cert);
slurm_fini();
#endif /* MEMORY_LEAK_DEBUG */
return exit_rc;
}