E2 Phase 2: external-license-resources 运维面 — stale 标记、last-updated 展示、xceadmin extres status
design.md 3.5 Phase 2(PEN D reason 专用名为可选项,复用 PREASON_NO_RESOURCE
已带资源名,跳过):
① esd stale 标记与 WARN
- esd_source_t 增 stale_limit/last_ok_time;esd_source_is_stale(): 从未有值
不算 stale、时钟超前钳到 0、严格大于(interval==limit 不抖动)、
ESD_STALE_LIMIT_DEFAULT 150;fail-open 保持旧值不变
- 采集失败首轮一次性 WARN,过限后每轮升级为 STALE WARN(带 age/limit/kept
值),恢复即静默
- status 文件 esd_status_write(): daemon/pid/started_at/generated_at/
pushes ok/fail/sources[{name,last_value,last_ok_time,stale}],tmp+rename
原子写;写失败仅状态转换时告警一次,绝不拖垮 poll 循环
② `xce resources` 展示 last-updated
- controller worker.c: EXTERNAL 共享值被接受时盖 last_report——同时盖
live resmgr 对象与 myconf 配置树副本(apiserver 定义 store 只由 join 时
config push 供给,串行的是配置树,缺后者 CLI 永远显示 never)
- scheduler core_events.c: RESOURCES_PUSH 原地更新拷贝 last_report(纯
metadata,resobj.c 比较本就排除)
- apiserver: model 带 last_report + 默认列(SelectFields 只投影默认列,
缺失即 API/CLI 全链不可见);convert cResourceToModel 透传
- apiserver handler.go: OnResourceLocationUpdated 对 host=="" 且定义带
EXTERNAL(0x10) 的推送刷新 store 内 LastReport——config push 快照之外
的活刷新;host 来源值与非 EXTERNAL 资源绝不触碰
- xcecli: LAST_UPDATED 列(-l verbose 表格/详情/JSON 三态),0 显示 "-"
④ `xceadmin extres status`
- 本机读 status 文件(远程读取留待 PR-2,需新 wire 协议;LAST_UPDATED
已覆盖远程新鲜度语义)——--status-file 显式路径或
/var/run/xce/esd-status.json 默认,无 controller 连接依赖
- XceExtresStatusError 本地文件类错误;click default=None 装饰期冻结坑
用调用期解析
TDD:test_extres_resvals +2(配置树盖戳/非 EXTERNAL 不盖戳)、test_esd_core
+4(stale 判式/状态文件)、apiserver models +2(默认列/json tag)、backend
integration +2(EXTERNAL 刷新/非 EXTERNAL 不刷新)、xceadmin +11(渲染/
本地读/容错)。全绿:controller 1360、scheduler 100、apiserver(cgo+nocgo)、
esd 11、xceadmin 385。
t1 E2E 实证:esd join 即推送→API last_report=join 时刻→CLI LAST_UPDATED
"Oct 27 13:52";破坏采集器后 60s 过限转 per-round STALE WARN + status
stale:true + fail-open 保值,恢复脚本即回 false;liccheck 作业提交→运行→
DONE,五 daemon 全健康。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
3eb1b9716d
commit
805288d747
20 changed files with 1179 additions and 47 deletions
|
|
@ -23,6 +23,9 @@
|
|||
* the spawn half (esd_script_collect) is exercised by the t1 E2E with a
|
||||
* real fake-license script, not here. */
|
||||
#include "../src/esd_script.c"
|
||||
/* White-box: the stale predicate and the status-file writer (Phase 2,
|
||||
* design.md 3.5 "运维面"). */
|
||||
#include "../src/esd_status.c"
|
||||
|
||||
static char g_tmpdir[256];
|
||||
|
||||
|
|
@ -58,6 +61,35 @@ static void write_file(const char* dir, const char* name, const char* content)
|
|||
fclose(f);
|
||||
}
|
||||
|
||||
static char* slurp(const char* path)
|
||||
{
|
||||
FILE* f;
|
||||
long len;
|
||||
char* buf;
|
||||
|
||||
f = fopen(path, "r");
|
||||
if (f == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
fseek(f, 0, SEEK_END);
|
||||
len = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
buf = (char*)malloc((size_t)len + 1);
|
||||
if (buf == NULL) {
|
||||
fclose(f);
|
||||
return NULL;
|
||||
}
|
||||
if (fread(buf, 1, (size_t)len, f) != (size_t)len) {
|
||||
fclose(f);
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
buf[len] = '\0';
|
||||
fclose(f);
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* ── esd_config_load ─────────────────────────────────────────────────── */
|
||||
|
||||
static void test_config_load_full_file(void)
|
||||
|
|
@ -185,6 +217,162 @@ static void test_parse_output_garbage_is_nan(void)
|
|||
TEST_ASSERT_TRUE(isnan(v));
|
||||
}
|
||||
|
||||
/* ── Phase 2: stale_limit config parsing ────────────────────────────── */
|
||||
|
||||
static void test_config_stale_limit_parse(void)
|
||||
{
|
||||
esd_config_t cfg;
|
||||
int rc;
|
||||
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
|
||||
/* per-source override wins; a source without one takes the daemon
|
||||
* default; the daemon default itself is optional with a sane built-in
|
||||
* (design.md 3.4: stale_limit in esd.yaml, fail-open keep-old-value). */
|
||||
write_file(g_tmpdir, "esd.yaml",
|
||||
"controller: 172.31.1.30:9990\n"
|
||||
"poll_interval: 10\n"
|
||||
"stale_limit: 200\n"
|
||||
"resources:\n"
|
||||
" - name: lic_a\n"
|
||||
" collector: script\n"
|
||||
" command: /bin/a.sh\n"
|
||||
" stale_limit: 60\n"
|
||||
" - name: lic_b\n"
|
||||
" collector: script\n"
|
||||
" command: /bin/b.sh\n");
|
||||
|
||||
rc = esd_config_load_path(g_tmpdir, "esd.yaml", &cfg);
|
||||
TEST_ASSERT_EQUAL_INT(0, rc);
|
||||
TEST_ASSERT_EQUAL_INT(200, cfg.stale_limit);
|
||||
TEST_ASSERT_EQUAL_INT(60, cfg.src[0].stale_limit);
|
||||
TEST_ASSERT_EQUAL_INT(0, cfg.src[1].stale_limit); /* 0 = use default */
|
||||
|
||||
esd_config_fini(&cfg);
|
||||
}
|
||||
|
||||
/* ── Phase 2: stale predicate ───────────────────────────────────────── */
|
||||
|
||||
static void test_stale_predicate(void)
|
||||
{
|
||||
esd_source_t src;
|
||||
|
||||
memset(&src, 0, sizeof(src));
|
||||
src.name = (char*)"lic";
|
||||
src.have_value = true;
|
||||
src.last_value = 3.0;
|
||||
|
||||
/* Never report a value -> nothing to call stale; admission treats the
|
||||
* pool as 0 through the normal NO_RESOURCE path. */
|
||||
src.last_ok_time = 0;
|
||||
TEST_ASSERT_FALSE(esd_source_is_stale(&src, 100, 60));
|
||||
|
||||
/* Fresh success (10s ago, limit 60) -> not stale. */
|
||||
src.last_ok_time = 90;
|
||||
TEST_ASSERT_FALSE(esd_source_is_stale(&src, 100, 60));
|
||||
|
||||
/* Exactly at the limit -> not stale (strictly-greater rule, so an
|
||||
* interval == stale_limit config never flaps). */
|
||||
src.last_ok_time = 40;
|
||||
TEST_ASSERT_FALSE(esd_source_is_stale(&src, 100, 60));
|
||||
|
||||
/* Past the limit -> stale. */
|
||||
src.last_ok_time = 39;
|
||||
TEST_ASSERT_TRUE(esd_source_is_stale(&src, 100, 60));
|
||||
|
||||
/* A source whose own stale_limit override is set uses it. */
|
||||
src.stale_limit = 10;
|
||||
TEST_ASSERT_TRUE(esd_source_is_stale(&src, 100, 60));
|
||||
src.last_ok_time = 95;
|
||||
TEST_ASSERT_FALSE(esd_source_is_stale(&src, 100, 60));
|
||||
|
||||
/* last_ok_time newer than `now` (clock skew between test harness and
|
||||
* caller) must not read as stale. */
|
||||
src.last_ok_time = 105;
|
||||
TEST_ASSERT_FALSE(esd_source_is_stale(&src, 100, 60));
|
||||
}
|
||||
|
||||
/* ── Phase 2: status file ───────────────────────────────────────────── */
|
||||
|
||||
static void test_status_write_basic(void)
|
||||
{
|
||||
esd_config_t cfg;
|
||||
esd_stats_t stats;
|
||||
char path[512];
|
||||
char* text;
|
||||
time_t now = time(NULL);
|
||||
char expect_lvt[128];
|
||||
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.nsrc = 1;
|
||||
cfg.src = (esd_source_t*)calloc(1, sizeof(esd_source_t));
|
||||
TEST_ASSERT_NOT_NULL(cfg.src);
|
||||
cfg.src[0].name = (char*)"extlic";
|
||||
cfg.src[0].have_value = true;
|
||||
cfg.src[0].last_value = 3.0;
|
||||
/* fresh success: last good value 5s ago, limit default 150 -> not
|
||||
* stale (the marks-stale test below covers the other side) */
|
||||
cfg.src[0].last_ok_time = now - 5;
|
||||
cfg.stale_limit = ESD_STALE_LIMIT_DEFAULT;
|
||||
|
||||
memset(&stats, 0, sizeof(stats));
|
||||
stats.started_at = 1000;
|
||||
stats.pushes_ok = 7;
|
||||
stats.pushes_fail = 1;
|
||||
|
||||
snprintf(path, sizeof(path), "%s/status.json", g_tmpdir);
|
||||
TEST_ASSERT_EQUAL_INT(0, esd_status_write(path, &cfg, &stats));
|
||||
|
||||
text = slurp(path);
|
||||
TEST_ASSERT_NOT_NULL(text);
|
||||
|
||||
/* The fields xceadmin extres status reads, and the stale flag driven
|
||||
* by the same predicate the tests above pin. */
|
||||
TEST_ASSERT_TRUE(strstr(text, "\"daemon\": \"xceesd\"") != NULL);
|
||||
TEST_ASSERT_TRUE(strstr(text, "\"name\": \"extlic\"") != NULL);
|
||||
snprintf(expect_lvt, sizeof(expect_lvt), "\"last_value\": %.17g", 3.0);
|
||||
TEST_ASSERT_TRUE(strstr(text, expect_lvt) != NULL);
|
||||
TEST_ASSERT_TRUE(strstr(text, "\"last_ok_time\": ") != NULL);
|
||||
TEST_ASSERT_TRUE(strstr(text, "\"stale\": false") != NULL);
|
||||
TEST_ASSERT_TRUE(strstr(text, "\"pushes_ok\": 7") != NULL);
|
||||
TEST_ASSERT_TRUE(strstr(text, "\"pushes_fail\": 1") != NULL);
|
||||
|
||||
free(text);
|
||||
free(cfg.src);
|
||||
}
|
||||
|
||||
static void test_status_write_marks_stale(void)
|
||||
{
|
||||
esd_config_t cfg;
|
||||
esd_stats_t stats;
|
||||
char path[512];
|
||||
char* text;
|
||||
time_t now = time(NULL);
|
||||
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
cfg.nsrc = 1;
|
||||
cfg.src = (esd_source_t*)calloc(1, sizeof(esd_source_t));
|
||||
TEST_ASSERT_NOT_NULL(cfg.src);
|
||||
cfg.src[0].name = (char*)"extlic";
|
||||
cfg.src[0].have_value = true;
|
||||
cfg.src[0].last_value = 2.0;
|
||||
/* last success 1 hour ago, limit 150s -> stale. */
|
||||
cfg.src[0].last_ok_time = now - 3600;
|
||||
cfg.src[0].stale_limit = 150;
|
||||
|
||||
memset(&stats, 0, sizeof(stats));
|
||||
|
||||
snprintf(path, sizeof(path), "%s/status.json", g_tmpdir);
|
||||
TEST_ASSERT_EQUAL_INT(0, esd_status_write(path, &cfg, &stats));
|
||||
|
||||
text = slurp(path);
|
||||
TEST_ASSERT_NOT_NULL(text);
|
||||
TEST_ASSERT_TRUE(strstr(text, "\"stale\": true") != NULL);
|
||||
|
||||
free(text);
|
||||
free(cfg.src);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
UNITY_BEGIN();
|
||||
|
|
@ -196,6 +384,10 @@ int main(void)
|
|||
RUN_TEST(test_parse_output_name_value);
|
||||
RUN_TEST(test_parse_output_whitespace_and_negative);
|
||||
RUN_TEST(test_parse_output_garbage_is_nan);
|
||||
RUN_TEST(test_config_stale_limit_parse);
|
||||
RUN_TEST(test_stale_predicate);
|
||||
RUN_TEST(test_status_write_basic);
|
||||
RUN_TEST(test_status_write_marks_stale);
|
||||
|
||||
return UNITY_END();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue