fix(controller): a queue added by a live config upgrade exists without a restart (bug-1469)

build_queues() only ran at boot, so a queue a live config upgrade added
had no runtime object in c->qmgr (and the scheduler/apiserver never heard
of it) until the controller restarted. The checkpoint-ordering half of
bug-1469 was fixed in 959b0cda; this is the remaining half.

- reconcile_config_queues(c, notify): creates every queue in the active
  config that has no runtime object, leaves existing ones untouched,
  returns the number created.
- config_activate_committed() calls it (with notify) after every
  successful activation -- verified-marker and no-marker paths alike.
- persist_notify_queue_updated_no_wal(): fires HOOK_DETAIL_QUEUE_UPDATE
  under the t_apply_seam_notifying guard, so the scheduler/apiserver are
  told while no WAL_QUEUE_CTRL is appended -- config queues are
  config-owned, built on every member from the committed config.
- build_queues() now delegates to it without notify, so boot and
  activation are idempotent in either order.

New test_config_queue_reconcile.c (RED before: the added queue was never
created): creates the added queue, leaves the existing object untouched,
notifies once, and a second reconcile/build_queues adds nothing.
Controller suite passes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Rongsong Shen 2026-09-27 06:05:26 +00:00
parent c3a17b19ab
commit 154a169ff3
Signed by untrusted user: rshen
GPG key ID: 80533536B0EB651C
8 changed files with 183 additions and 5 deletions

View file

@ -85,6 +85,7 @@ extern void test_wal_job_dispatch_fact(void);
extern void test_wal_job_start_fact(void);
extern void test_wal_job_finish_fact(void);
extern void test_wal_job_fail_fact(void);
extern void test_config_queue_reconcile(void);
extern void test_wal_job_rusage_fact(void);
extern void test_wal_job_ckpt_fact(void);
extern void test_wal_job_preason_fact(void);
@ -271,6 +272,7 @@ int main(int argc, char* argv[])
TEST(test_wal_job_start_fact);
TEST(test_wal_job_finish_fact);
TEST(test_wal_job_fail_fact);
TEST(test_config_queue_reconcile);
TEST(test_wal_job_rusage_fact);
TEST(test_wal_job_ckpt_fact);
TEST(test_wal_job_preason_fact);

View file

@ -0,0 +1,106 @@
/* test_config_queue_reconcile.c -- bug-1469: a queue added by a live config
* upgrade used to exist only after a restart, because build_queues() ran
* only at boot. reconcile_config_queues() creates the missing runtime queue
* objects when a config is activated, notifies the scheduler/apiserver
* (without a WAL fact -- config queues are config-owned), leaves existing
* queues untouched, and is idempotent (boot's build_queues() uses it too). */
#include <rtti.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <unity/unity.h>
#include "clusterconfig.h"
#include "cluster.h"
#include "hook.h"
#include "myconf.h"
#include "qobj.h"
#include "xce/cluster_def.h"
#include "xstring.h"
static int g_cqr_queue_updates;
static int cqr_queue_spy(int type, int situation, int detail, void* data,
va_list ap)
{
(void)type; (void)data; (void)ap;
if (situation == HOOK_AFTER_EVENT && detail == HOOK_DETAIL_QUEUE_UPDATE)
g_cqr_queue_updates++;
return 0;
}
static queue_t* cqr_find(cluster_t* c, const char* name)
{
queue_t* q = NULL;
lock(c->qmgr, RTTI_LOCK_READ);
key_find(c->qmgr, (void*)name, (void**)&q);
unlock(c->qmgr);
return q;
}
static void test_reconcile_creates_queue_added_by_config(void)
{
kvmgr_params_t str_params = { .key_type = KVMGR_KEY_STRING,
.nslots = 16, .delta = 8 };
cluster_t c;
myconf_t conf;
cluster_config_t cfg;
xce_queue_t qa, qb;
xce_queue_t* queues[2];
queue_t* before;
if (makeString((char*)"cqr-probe") == NULL)
xstring_table_init(64);
memset(&c, 0, sizeof(c));
memset(&conf, 0, sizeof(conf));
memset(&cfg, 0, sizeof(cfg));
TEST_ASSERT_EQUAL_INT(0, xce_hook_init(&c.hooksys, XCE_CONTROLLER_HOOK_MAX));
c.qmgr = makeInstance(&xkvmgr_type, &str_params);
TEST_ASSERT_NOT_NULL(c.qmgr);
memset(&qa, 0, sizeof(qa));
qa.queue = (char*)"cqr-existing";
memset(&qb, 0, sizeof(qb));
qb.queue = (char*)"cqr-added";
queues[0] = &qa;
queues[1] = &qb;
cfg.nqueues = 2;
cfg.queues = queues;
conf.cluster_config = &cfg;
c.myconf = &conf;
/* The old config's queue already exists at runtime. */
TEST_ASSERT_NOT_NULL(create_queue(&c, &qa));
before = cqr_find(&c, "cqr-existing");
TEST_ASSERT_NOT_NULL(before);
g_cqr_queue_updates = 0;
TEST_ASSERT_EQUAL_INT(0, xce_add_hook(&c.hooksys, XCE_CONTROLLER_HOOK_QUEUE,
cqr_queue_spy, NULL));
TEST_ASSERT_EQUAL_INT_MESSAGE(1, reconcile_config_queues(&c, 1),
"bug-1469: the queue added by the new config must be created");
TEST_ASSERT_NOT_NULL(cqr_find(&c, "cqr-added"));
TEST_ASSERT_EQUAL_PTR_MESSAGE(before, cqr_find(&c, "cqr-existing"),
"an existing queue object must be left untouched");
TEST_ASSERT_EQUAL_INT_MESSAGE(1, g_cqr_queue_updates,
"the scheduler/apiserver must be told about the new queue");
/* Idempotent: a second activation (or boot's build_queues) adds nothing. */
TEST_ASSERT_EQUAL_INT(0, reconcile_config_queues(&c, 1));
TEST_ASSERT_EQUAL_INT(1, g_cqr_queue_updates);
TEST_ASSERT_EQUAL_INT(0, build_queues(&c));
TEST_ASSERT_EQUAL_PTR(before, cqr_find(&c, "cqr-existing"));
xce_hook_fini(&c.hooksys);
dereference(c.qmgr);
}
void test_config_queue_reconcile(void)
{
UnitySetTestFile(__FILE__);
RUN_TEST(test_reconcile_creates_queue_added_by_config);
}