fix(controller): allow control of running jobs on connected hosts of any status (bug-1941)
Cause: job_ctrl (job.c) sent kill/stop/resume of a running job only when the host's status was exactly 0, so any status bit (CLOSED, CLOSED_BY_ADMIN, BUSY, LOCKED...) refused the request, and the refusal returned -errno, i.e. whatever errno happened to hold (often 0 or -ENOENT). Fix: new job_ctrl_to_host sends the request whenever the worker is connected (endpoint present, no XCE_WORKER_UNAVAIL); an unknown or unconnected worker fails with -E_XCE_BAD_WORKER and an ERROR naming the host. Nothing else changes for jobs on dead hosts. Test: testsuite/test_job_ctrl_hosts.c. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
4d2e80c4bb
commit
f244661b14
4 changed files with 344 additions and 35 deletions
|
|
@ -111,6 +111,7 @@ extern void test_dispatch_commit(void);
|
|||
extern void test_dispatch_fail_terminal(void);
|
||||
extern void test_dispatch_fail_retry(void);
|
||||
extern void test_jobdispatch_no_double_notify(void);
|
||||
extern void test_job_ctrl_hosts(void);
|
||||
extern void test_worker_resync(void);
|
||||
extern void test_standby_state_peer_idx(void);
|
||||
extern void test_late_joiner_catchup_kick(void);
|
||||
|
|
@ -303,6 +304,7 @@ int main(int argc, char* argv[])
|
|||
TEST(test_dispatch_fail_terminal);
|
||||
TEST(test_dispatch_fail_retry);
|
||||
TEST(test_jobdispatch_no_double_notify);
|
||||
TEST(test_job_ctrl_hosts);
|
||||
TEST(test_worker_resync);
|
||||
TEST(test_standby_state_peer_idx);
|
||||
TEST(test_late_joiner_catchup_kick);
|
||||
|
|
|
|||
286
src/controller/testsuite/test_job_ctrl_hosts.c
Normal file
286
src/controller/testsuite/test_job_ctrl_hosts.c
Normal file
|
|
@ -0,0 +1,286 @@
|
|||
/* test_job_ctrl_hosts.c -- which hosts job_ctrl() (job.c) sends a running
|
||||
* job's control request to, and when it refuses.
|
||||
*
|
||||
* bug-1941: job_ctrl() refused kill/stop/resume of a running job unless its
|
||||
* host's status was exactly 0, so a job on a connected host that is closed
|
||||
* (CLOSED, CLOSED_BY_ADMIN), BUSY or locked could not be controlled. Only a
|
||||
* host without a connection (no endpoint, or UNAVAIL) must fail, with
|
||||
* -E_XCE_BAD_WORKER.
|
||||
*
|
||||
* The request is observed on the worker endpoints' outq: xce_jobctrl_req()
|
||||
* is a non-blocking enqueue that succeeds against a zeroed endpoint with no
|
||||
* iomgr (same fixture as test_dispatch_commit.c's g_dc_worker_ep); each
|
||||
* queued packet is decoded back into a Request.
|
||||
*/
|
||||
|
||||
#include <rtti.h>
|
||||
|
||||
#include <capnp_c.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "cluster.h"
|
||||
#include "controller.capnp.h"
|
||||
#include "dataq.h"
|
||||
#include "hook.h"
|
||||
#include "jobj.h"
|
||||
#include "kvmgr.h"
|
||||
#include "netio.h"
|
||||
#include "wobj.h"
|
||||
#include "xce/cluster_def.h"
|
||||
#include "xce/errnox.h"
|
||||
#include "xce/types.h"
|
||||
#include "xcealloc.h"
|
||||
#include "xstring.h"
|
||||
|
||||
#define JCH_MAX_HOSTS 3
|
||||
|
||||
static int g_jch_jobobj_ready;
|
||||
static cluster_t g_jch;
|
||||
static endpoint_t g_jch_ep[JCH_MAX_HOSTS];
|
||||
|
||||
static void jch_setup(void)
|
||||
{
|
||||
kvmgr_params_t int_params = { .key_type = KVMGR_KEY_INT,
|
||||
.nslots = 32, .delta = 16 };
|
||||
kvmgr_params_t str_params = { .key_type = KVMGR_KEY_STRING,
|
||||
.nslots = 32, .delta = 16 };
|
||||
int i;
|
||||
|
||||
if (makeString((char*)"jch-probe") == NULL)
|
||||
xstring_table_init(64);
|
||||
|
||||
if (!g_jch_jobobj_ready) {
|
||||
TEST_ASSERT_EQUAL_INT(0, job_obj_init(32, 32));
|
||||
g_jch_jobobj_ready = 1;
|
||||
}
|
||||
|
||||
memset(&g_jch, 0, sizeof(g_jch));
|
||||
TEST_ASSERT_EQUAL_INT(0,
|
||||
xce_hook_init(&g_jch.hooksys, XCE_CONTROLLER_HOOK_MAX));
|
||||
|
||||
g_jch.jobmgr = makeInstance(&xkvmgr_type, &int_params);
|
||||
g_jch.workermgr = makeInstance(&xkvmgr_type, &str_params);
|
||||
TEST_ASSERT_NOT_NULL(g_jch.jobmgr);
|
||||
TEST_ASSERT_NOT_NULL(g_jch.workermgr);
|
||||
|
||||
for (i = 0; i < JCH_MAX_HOSTS; i++) {
|
||||
memset(&g_jch_ep[i], 0, sizeof(g_jch_ep[i]));
|
||||
g_jch_ep[i].fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Decoded jobcontrol requests found on one endpoint's outq. */
|
||||
typedef struct {
|
||||
int n;
|
||||
uint64_t jobid;
|
||||
uint32_t signo;
|
||||
int32_t force;
|
||||
} jch_obs_t;
|
||||
|
||||
/* Drains ep's outq, decoding every OPCode_jobcontrol request on it. */
|
||||
static jch_obs_t jch_scan(endpoint_t* ep)
|
||||
{
|
||||
jch_obs_t obs = { 0 };
|
||||
data_item_t* it;
|
||||
|
||||
while ((it = endpoint_deq(ep)) != NULL) {
|
||||
io_packet_t* pkt = ITPTR_TO_PACKET(it);
|
||||
struct capn c;
|
||||
struct Request rq;
|
||||
Request_ptr rp;
|
||||
|
||||
capn_init_mem(&c, (const uint8_t*)pkt->data, pkt->sz, 0);
|
||||
rp.p = capn_getp(capn_root(&c), 0, 1);
|
||||
read_Request(&rq, rp);
|
||||
if (rq.opcode == OPCode_jobcontrol
|
||||
&& rq.body_which == Request_body_jobctrlmsg) {
|
||||
struct JobControl jc;
|
||||
|
||||
read_JobControl(&jc, rq.body.jobctrlmsg);
|
||||
obs.n += 1;
|
||||
obs.jobid = jc.jobid;
|
||||
if (jc.u_which == JobControl_u_siginfo) {
|
||||
struct JobSignal js;
|
||||
|
||||
read_JobSignal(&js, jc.u.siginfo);
|
||||
obs.signo = js.signo;
|
||||
obs.force = js.force;
|
||||
}
|
||||
}
|
||||
capn_free(&c);
|
||||
free_packet(pkt);
|
||||
free(pkt);
|
||||
}
|
||||
return obs;
|
||||
}
|
||||
|
||||
static void jch_teardown(void)
|
||||
{
|
||||
int i;
|
||||
|
||||
for (i = 0; i < JCH_MAX_HOSTS; i++)
|
||||
(void)jch_scan(&g_jch_ep[i]);
|
||||
|
||||
if (g_jch.workermgr) dereference(g_jch.workermgr);
|
||||
if (g_jch.jobmgr) dereference(g_jch.jobmgr);
|
||||
xce_hook_fini(&g_jch.hooksys);
|
||||
}
|
||||
|
||||
/* A worker named `name` with host.status `status` and endpoint `ep`. */
|
||||
static void jch_seed_worker(const char* name, int status, endpoint_t* ep)
|
||||
{
|
||||
xce_worker_t wdef;
|
||||
hostattrs_t* attrs;
|
||||
worker_t* w = NULL;
|
||||
|
||||
attrs = calloc(1, sizeof(*attrs));
|
||||
TEST_ASSERT_NOT_NULL(attrs);
|
||||
attrs->hosttype = makeString("LINUX");
|
||||
attrs->hostmodel = makeString("x86");
|
||||
attrs->cpufactor = 1.0f;
|
||||
attrs->maxcpus = 4;
|
||||
memset(&wdef, 0, sizeof(wdef));
|
||||
wdef.name = (char*)name;
|
||||
wdef.attrs = attrs;
|
||||
TEST_ASSERT_NOT_NULL(makeWorker(g_jch.workermgr, &wdef));
|
||||
|
||||
lock(g_jch.workermgr, RTTI_LOCK_READ);
|
||||
key_find(g_jch.workermgr, (void*)name, (void**)&w);
|
||||
unlock(g_jch.workermgr);
|
||||
TEST_ASSERT_NOT_NULL(w);
|
||||
w->ep = ep;
|
||||
w->host.status = status;
|
||||
}
|
||||
|
||||
/* A RUN job whose allocation spans hosts[0..nhosts-1] (hosts[0] is the head
|
||||
* the job was dispatched to). */
|
||||
static void jch_seed_running_job(uint64_t jobid, int nhosts,
|
||||
const char* const* hosts)
|
||||
{
|
||||
job_startinfo_t startinfo;
|
||||
job_submit_data_t submit;
|
||||
xce_job_t jdef;
|
||||
job_t* j = NULL;
|
||||
xce_host_alloc_t** ha;
|
||||
int i;
|
||||
|
||||
memset(&startinfo, 0, sizeof(startinfo));
|
||||
startinfo.cmdline = (char*)"/bin/sleep 100";
|
||||
memset(&submit, 0, sizeof(submit));
|
||||
submit.queue = (char*)"jchq";
|
||||
submit.user = (char*)"tester";
|
||||
submit.start_info = &startinfo;
|
||||
|
||||
memset(&jdef, 0, sizeof(jdef));
|
||||
jdef.jobid = jobid;
|
||||
jdef.state = JOB_STAT_RUN;
|
||||
jdef.submit = &submit;
|
||||
j = makeJob(g_jch.jobmgr, &jdef);
|
||||
TEST_ASSERT_NOT_NULL(j);
|
||||
|
||||
ha = calloc(nhosts, sizeof(*ha));
|
||||
TEST_ASSERT_NOT_NULL(ha);
|
||||
for (i = 0; i < nhosts; i++) {
|
||||
ha[i] = make_host_alloc((char*)hosts[i], 1,
|
||||
make_allocs(1, make_alloc((char*)"slots", NULL, 0,
|
||||
1.0f, 0)));
|
||||
TEST_ASSERT_NOT_NULL(ha[i]);
|
||||
}
|
||||
j->job.allocs = make_job_alloc(jobid, nhosts, ha);
|
||||
TEST_ASSERT_NOT_NULL(j->job.allocs);
|
||||
}
|
||||
|
||||
static int jch_kill(uint64_t jobid, int signo, int force)
|
||||
{
|
||||
xce_signal_t sig = { .signo = signo, .force = force };
|
||||
xce_jobctrl_t ctrl = { .jobid = jobid,
|
||||
.action = JobControl_u_siginfo,
|
||||
.u = { .siginfo = &sig } };
|
||||
|
||||
return job_ctrl(&g_jch, &ctrl);
|
||||
}
|
||||
|
||||
/* bug-1941: a connected host whose status carries CLOSED, CLOSED_BY_ADMIN,
|
||||
* BUSY or a lock bit still gets the control request. */
|
||||
static void test_jc_connected_flagged_host_is_controllable(void)
|
||||
{
|
||||
static const int statuses[] = { XCE_WORKER_CLOSED,
|
||||
XCE_WORKER_CLOSED_BY_ADMIN,
|
||||
XCE_WORKER_BUSY,
|
||||
XCE_WORKER_LOCKEDU,
|
||||
XCE_WORKER_CLOSED_BY_ADMIN
|
||||
| XCE_WORKER_BUSY };
|
||||
size_t k;
|
||||
|
||||
for (k = 0; k < sizeof(statuses) / sizeof(statuses[0]); k++) {
|
||||
const char* hosts[] = { "jch-closed" };
|
||||
uint64_t jobid = 93100 + k;
|
||||
char msg[128];
|
||||
jch_obs_t obs;
|
||||
int rc;
|
||||
|
||||
jch_setup();
|
||||
jch_seed_worker("jch-closed", statuses[k], &g_jch_ep[0]);
|
||||
jch_seed_running_job(jobid, 1, hosts);
|
||||
|
||||
rc = jch_kill(jobid, SIGTERM, 0);
|
||||
obs = jch_scan(&g_jch_ep[0]);
|
||||
|
||||
snprintf(msg, sizeof(msg),
|
||||
"bug-1941: running job on a connected host, status %#x",
|
||||
statuses[k]);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, rc, msg);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(1, obs.n, msg);
|
||||
TEST_ASSERT_EQUAL_UINT64_MESSAGE(jobid, obs.jobid, msg);
|
||||
TEST_ASSERT_EQUAL_UINT32_MESSAGE(SIGTERM, obs.signo, msg);
|
||||
|
||||
jch_teardown();
|
||||
}
|
||||
}
|
||||
|
||||
/* bug-1941: a host that is UNAVAIL, or has no endpoint, fails the request
|
||||
* with -E_XCE_BAD_WORKER and nothing is sent. */
|
||||
static void test_jc_unconnected_host_fails_with_bad_worker(void)
|
||||
{
|
||||
const char* hosts[] = { "jch-dead" };
|
||||
jch_obs_t obs;
|
||||
int rc;
|
||||
|
||||
jch_setup();
|
||||
jch_seed_worker("jch-dead", XCE_WORKER_UNAVAIL, &g_jch_ep[0]);
|
||||
jch_seed_running_job(93200, 1, hosts);
|
||||
|
||||
rc = jch_kill(93200, SIGTERM, 0);
|
||||
obs = jch_scan(&g_jch_ep[0]);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-E_XCE_BAD_WORKER, rc,
|
||||
"bug-1941: control of a job on an UNAVAIL host must fail clearly");
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(0, obs.n,
|
||||
"nothing may be sent to an UNAVAIL host");
|
||||
jch_teardown();
|
||||
|
||||
jch_setup();
|
||||
jch_seed_worker("jch-dead", XCE_WORKER_CLOSED, NULL);
|
||||
jch_seed_running_job(93201, 1, hosts);
|
||||
|
||||
rc = jch_kill(93201, SIGTERM, 0);
|
||||
TEST_ASSERT_EQUAL_INT_MESSAGE(-E_XCE_BAD_WORKER, rc,
|
||||
"bug-1941: control of a job on a host with no endpoint must fail "
|
||||
"clearly");
|
||||
jch_teardown();
|
||||
}
|
||||
|
||||
void test_job_ctrl_hosts(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
printf("\n>> job_ctrl() host selection (bug-1941)\n\n");
|
||||
|
||||
RUN_TEST(test_jc_connected_flagged_host_is_controllable);
|
||||
RUN_TEST(test_jc_unconnected_host_fails_with_bad_worker);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue