blob: d67cc6e3700fbc3e71759731db336ecbcf306ea3 [file] [edit]
/*****************************************************************************\
* Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
\*****************************************************************************/
#include "config.h"
#include <check.h>
#include <errno.h>
/* Include the interface to check lock ownership with controlled callbacks. */
#include "src/interfaces/runtime.c"
#define TEST_IDX 1
#define THREAD_COUNT 2
static pthread_mutex_t callback_lock = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t callback_cond = PTHREAD_COND_INITIALIZER;
static int active = 0;
static bool release_readers = false;
static bool argument_write_locked = false;
static bool reset_write_locked = false;
static bool export_write_locked = false;
static bool _write_locked(void)
{
int rc = EINVAL;
rc = pthread_rwlock_tryrdlock(&init_lock);
if (!rc)
slurm_rwlock_unlock(&init_lock);
return ((rc == EBUSY) || (rc == EDEADLK));
}
static bool _on_arg(const runtime_arg_t *arg, const uint32_t het_job_inx,
data_t *value, bool set_by_cli, bool set_by_env,
bool set_by_data, bool early_pass)
{
argument_write_locked = _write_locked();
return (argument_write_locked &&
!xstrcmp(data_get_string(value), "value"));
}
static const runtime_arg_t test_arg = {
.argument = "runtime-lock-test",
.environment.sbatch = "RUNTIME_TEST_ARG",
.on_arg = _on_arg,
};
static int _apply_job_desc(const uint32_t het_job_inx,
runtime_context_t context, job_desc_msg_t *job_desc)
{
slurm_mutex_lock(&callback_lock);
active++;
slurm_cond_broadcast(&callback_cond);
while (!release_readers)
slurm_cond_wait(&callback_cond, &callback_lock);
slurm_mutex_unlock(&callback_lock);
return SLURM_SUCCESS;
}
static void _reset_args(const uint32_t het_job_inx)
{
reset_write_locked = _write_locked();
}
static int _apply_env(const uint32_t het_job_inx, const uint32_t het_job_offset,
runtime_context_t context, bool child, char ***envp)
{
export_write_locked = _write_locked();
return export_write_locked ? SLURM_SUCCESS : SLURM_ERROR;
}
static void _setup(void)
{
log_options_t log_opts = LOG_OPTS_INITIALIZER;
log_init("runtime-test", log_opts, 0, NULL);
slurm_rwlock_wrlock(&init_lock);
ops = xcalloc(2, sizeof(*ops));
g_context = xcalloc(2, sizeof(*g_context));
loaded_types = xcalloc(2, sizeof(*loaded_types));
loaded_types[TEST_IDX] = xstrdup("runtime/test");
/* Reset only checks whether the context is non-NULL. */
g_context[TEST_IDX] = (plugin_context_t *) ops;
ops[TEST_IDX].cli_apply_job_desc = _apply_job_desc;
ops[TEST_IDX].cli_apply_env = _apply_env;
ops[TEST_IDX].cli_reset_args = _reset_args;
g_context_cnt = 2;
plugin_inited = PLUGIN_INITED;
runtime_register_arg(TEST_IDX, &test_arg);
slurm_rwlock_unlock(&init_lock);
active = 0;
release_readers = false;
argument_write_locked = false;
reset_write_locked = false;
export_write_locked = false;
}
static void _teardown(void)
{
slurm_rwlock_wrlock(&init_lock);
memset(args, 0, sizeof(args));
args_count = 0;
xfree(loaded_types[TEST_IDX]);
xfree(loaded_types);
xfree(g_context);
xfree(ops);
g_context_cnt = 0;
plugin_inited = PLUGIN_NOT_INITED;
slurm_rwlock_unlock(&init_lock);
log_fini();
}
static void *_apply_thread(void *arg)
{
int *rc = arg;
job_desc_msg_t job_desc = { 0 };
*rc = runtime_g_cli_apply_job_desc(TEST_IDX, 0, RUNTIME_CTXT_SLURMRESTD,
&job_desc);
return NULL;
}
static void _check_unlocked(void)
{
int rc = EINVAL;
rc = pthread_rwlock_trywrlock(&init_lock);
if (!rc)
slurm_rwlock_unlock(&init_lock);
ck_assert_int_eq(rc, 0);
}
START_TEST(test_parallel_apply)
{
pthread_t threads[THREAD_COUNT];
int rc[THREAD_COUNT] = { EINVAL, EINVAL };
int write_rc = EINVAL;
for (int i = 0; i < THREAD_COUNT; i++)
ck_assert_int_eq(pthread_create(&threads[i], NULL,
_apply_thread, &rc[i]),
0);
slurm_mutex_lock(&callback_lock);
while (active < THREAD_COUNT)
slurm_cond_wait(&callback_cond, &callback_lock);
/* Both callbacks overlap, but unloading/reallocation must be excluded. */
write_rc = pthread_rwlock_trywrlock(&init_lock);
if (!write_rc)
slurm_rwlock_unlock(&init_lock);
/* Release the readers before asserting so a failure cannot hang. */
release_readers = true;
slurm_cond_broadcast(&callback_cond);
slurm_mutex_unlock(&callback_lock);
for (int i = 0; i < THREAD_COUNT; i++) {
ck_assert_int_eq(pthread_join(threads[i], NULL), 0);
ck_assert_int_eq(rc[i], SLURM_SUCCESS);
}
ck_assert_int_eq(write_rc, EBUSY);
_check_unlocked();
}
END_TEST
START_TEST(test_argument_writes)
{
char *environment[] = { "RUNTIME_TEST_ARG=value", NULL };
char **envp = environment;
ck_assert_int_eq(runtime_g_cli_process_arg(TEST_IDX, 0,
RUNTIME_OPTVAL_START,
"value", false, false),
SLURM_SUCCESS);
ck_assert(argument_write_locked);
argument_write_locked = false;
ck_assert(runtime_g_cli_process_env(TEST_IDX, 0, RUNTIME_CTXT_SBATCH,
&envp, false));
ck_assert(argument_write_locked);
runtime_g_cli_reset_args(0);
ck_assert(reset_write_locked);
ck_assert_int_eq(runtime_g_cli_apply_env(TEST_IDX, 0, NO_VAL,
RUNTIME_CTXT_SBATCH, false,
NULL),
SLURM_SUCCESS);
ck_assert(export_write_locked);
_check_unlocked();
}
END_TEST
START_TEST(test_error_unlocks)
{
char *environment[] = { "RUNTIME_TEST_ARG=reject", NULL };
char **envp = environment;
ck_assert_int_eq(runtime_g_cli_process_arg(TEST_IDX, 0,
RUNTIME_OPTVAL_START,
"reject", false, false),
SLURM_ERROR);
_check_unlocked();
ck_assert(!runtime_g_cli_process_env(TEST_IDX, 0, RUNTIME_CTXT_SBATCH,
&envp, false));
_check_unlocked();
ck_assert_int_eq(runtime_g_cli_process_arg(TEST_IDX, 0, 0, NULL, false,
false),
ENOENT);
_check_unlocked();
}
END_TEST
int main(void)
{
Suite *suite = suite_create("runtime");
TCase *tc = tcase_create("locking");
SRunner *runner = NULL;
int failed = 0;
tcase_add_checked_fixture(tc, _setup, _teardown);
tcase_set_timeout(tc, 10);
tcase_add_test(tc, test_parallel_apply);
tcase_add_test(tc, test_argument_writes);
tcase_add_test(tc, test_error_unlocks);
suite_add_tcase(suite, tc);
runner = srunner_create(suite);
srunner_run_all(runner, CK_NORMAL);
failed = srunner_ntests_failed(runner);
srunner_free(runner);
return failed ? EXIT_FAILURE : EXIT_SUCCESS;
}