fix(cfg): 热重载读侧三点一致性校验——掐断打回出厂与保存吞段(#73)
真机 #73:运行中改配置触发热重载,卷层(dhara)偶发返回旧/短 视图时,引擎把该视图当成文件全文:逐段按缺省解析后整盘打回 出厂默认且不自愈;此后任何一次保存又按缩小的视图做手术式 重写,文件越写越小(台架 1066B→986B,[sntp] 段整体丢失), 即真机对时静默 10.5 小时的元凶。 处置(引擎侧根因无关防御;卷层回滚归 #79/#81 已合并的 FTL 修复域): - poll:读完加三点校验(读长==stat.size、读前后 re-stat 的 size/mtime 一致),判为旧/短视图则 RAM 不动、基线不刷新、 每 2 拍自动重试自愈;首次拒绝留 1 条 DBG_ERROR+syslog, 之后每 30 拍一条 WARN 提示重启收敛。 - save:同型三点校验,视图与 stat 不符返回 -1 且文件不动, 调用方保留 dirty 下拍重试,吞段链在第一次保存即被掐断。 - 边界:「一致但陈旧」的整卷回滚引擎层无法区分,靠重启 收敛,已写入设计说明风险表与文档口径。 测试:新增 TC-CONFIG-058/059/060(旧视图保持 RAM、保存短 视图拒写、读中改写拒绝+日志限速)。cfg_file 239/239; iec104 179、run_par 67、adc_cal 117、sntp 98、 cfg_file_store_mcu 34、net_cfg 48、ftp_cfg 16、prot_set 90、 prot_dim 38 全绿;/keil-build 0 Error 0 Warning。 文档:配置文件设计说明 §(3)/§5.3/风险表/变更记录 v1.17; 单元测试说明 058–060 行;网络配置说明与产品需求 FR-CFG-3 同步口径;台账随新鲜 .map 由 ram_baseline --update 回填。
This commit is contained in:
@@ -29,6 +29,13 @@
|
||||
* len 1/2/4 小端补码)——s_decode_int() 用 strtol + ERANGE +
|
||||
* 宽度上下界校验,s_encode() 带负号输出;负零点偏移从此可
|
||||
* 干净落盘(动因见 docs/任务规划/数据可信.md §4.4 与 §7 决策 3)
|
||||
* v1.6 2026-11-05 DTU 项目组 热重载/保存视图一致性守卫(issue #73 SNTP 补验揪出)——
|
||||
* poll stat→read→复 stat 三点不一致(空读/截断旧视图或读中改写)
|
||||
* 拒绝应用:RAM 与基线不动、2s 重试自愈、首次自检留痕 + 每 30 趟
|
||||
* WARN 限速;save 读视图与 stat 基准不符拒写(断"短视图→整档
|
||||
* 外科重写→非默认段被删"吞段链,台架实测 1066B→986B)。
|
||||
* 台架根因为卷层 dhara 旧视图回滚(≥8/8 复现),引擎侧无法
|
||||
* 分辨"一致但陈旧"的整卷回滚——该形态重启必收敛,见设计说明
|
||||
* 函数说明:引擎状态非重入——调用方须收敛同一任务(defaultTask:poll/save;
|
||||
* 启动期 load 先于任务竞争);FS 卷并发由 FatFS REENTRANT 卷锁保证
|
||||
* (见《配置文件设计说明》D15 A+ 方案)
|
||||
@@ -102,6 +109,8 @@ static cfg_file_stat_t s_last; /* 上次 poll 的 stat */
|
||||
static uint8_t s_stable; /* 连续相同轮询计数(沉降去抖) */
|
||||
static uint8_t s_base_valid; /* 基线是否已建立 */
|
||||
static uint8_t s_snap[CFG_SNAP_MAX];
|
||||
static uint16_t s_reject_streak; /* 连续被拒重载计数(视图不一致;0 = 无进行中
|
||||
拒绝;饱和不回绕,仅用于告警限速节奏) */
|
||||
|
||||
static int s_sec_hdr[CFG_FILE_MAX_SECTIONS]; /* 段头行号(0xFFFF = 未见) */
|
||||
static uint16_t s_key_line[CFG_FILE_MAX_SECTIONS][CFG_FILE_MAX_KEYS]; /* 受管键行号(0xFFFF = 未见) */
|
||||
@@ -1240,6 +1249,26 @@ int cfg_file_save(void) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 2026-11-05 视图一致性守卫(真机台架实证,issue #73 SNTP 补验揪出):
|
||||
* stat 与 read 之间卷层可给出旧视图(dhara 缓存回滚/FTL 视图滞后)。旧实现
|
||||
* 直接按"读到的视图"外科重写整档,视图偏短时非默认段随重写一并被静默删掉
|
||||
* (实测 1066B → 986B,[sntp] 段整段蒸发)。读长与 stat 报长不符、或读期间
|
||||
* 文件被改写 → 拒存:原文件不动、返回 -1;属主任务脏位保留 = 下一趟自然
|
||||
* 重试(defaultTask 50ms 循环 / iec104 下轮 poll 投递,均自带重试语义) */
|
||||
{
|
||||
cfg_file_stat_t st2;
|
||||
|
||||
if (in_len != (size_t)st.size ||
|
||||
cfg_store_stat(cfg_store_config_path(), &st2) != 0 ||
|
||||
st2.size != st.size || st2.mtime != st.mtime) {
|
||||
cfg_store_free(p_in);
|
||||
DBG_ERROR("save: view %luB != stat %luB, aborted (file untouched)",
|
||||
(unsigned long)in_len, (unsigned long)st.size);
|
||||
syslog_post(SYSLOG_SELFTEST, "cfg save view stale");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Pass 1 在独立副本上切分(避免破坏原文的行结束符);
|
||||
* 副本在 p_out 分配前释放,堆峰值维持 p_in + p_out(≤ CFG_HEAP_BUDGET,
|
||||
* 编译期已校验,见文件常量区) */
|
||||
@@ -1371,6 +1400,46 @@ void cfg_file_poll(void) {
|
||||
if (s_load_file(&p_buf, &len) != 0) {
|
||||
return; /* 读失败:保留,下次 poll 重试 */
|
||||
}
|
||||
|
||||
/* 2026-11-05 视图一致性守卫(与 save 侧同族,真机台架实证 issue #73):
|
||||
* stat → read → 再 stat 三点必须一致——读长 ≠ st.size(空读/截断 =
|
||||
* 卷层旧视图;台架 8/8 复现的"热重载打回出厂默认"即空读形态:全段
|
||||
* 静默 memcpy 默认、零解析错误行)或读期间文件被改写 = 读到的内容不
|
||||
* 对应变更令牌。不一致 → 拒绝应用:RAM 副本原样保留、基线不刷新,
|
||||
* 下一趟(2s)重试,视图恢复一致即正常应用(自愈);若持续到重启,
|
||||
* boot 路径全新挂载必然收敛。首次拒绝自检留痕 + ERROR 一行,此后每
|
||||
* 30 趟(约 60s)一条 WARN 计数行(限速,防日志风暴) */
|
||||
{
|
||||
cfg_file_stat_t st2;
|
||||
int stale;
|
||||
|
||||
if (len != (size_t)st.size) {
|
||||
stale = 1; /* 空读/截断/旧视图 */
|
||||
}
|
||||
else if (cfg_store_stat(cfg_store_config_path(), &st2) != 0 ||
|
||||
st2.size != st.size || st2.mtime != st.mtime) {
|
||||
stale = 1; /* 读期间文件消失或被改写 */
|
||||
}
|
||||
else {
|
||||
stale = 0;
|
||||
}
|
||||
if (stale != 0) {
|
||||
cfg_store_free(p_buf);
|
||||
if (s_reject_streak < 0xFFFFu) {
|
||||
s_reject_streak++;
|
||||
}
|
||||
if (s_reject_streak == 1u) {
|
||||
DBG_ERROR("poll: view %luB != stat %luB, HELD (RAM kept)",
|
||||
(unsigned long)len, (unsigned long)st.size);
|
||||
syslog_post(SYSLOG_SELFTEST, "cfg poll view stale");
|
||||
}
|
||||
else if ((s_reject_streak % 30u) == 0u) {
|
||||
DBG_INFO("WARN: cfg reload held %u rounds, reboot to converge",
|
||||
(unsigned)s_reject_streak);
|
||||
}
|
||||
return; /* 基线不刷新 = 同一变更下趟仍触发重试;RAM 不动 */
|
||||
}
|
||||
}
|
||||
for (i = 0; i < (int)CFG_FILE_MAX_SECTIONS; i++) {
|
||||
const cfg_sec_t *p_sec = &s_sections[i];
|
||||
|
||||
@@ -1395,6 +1464,8 @@ void cfg_file_poll(void) {
|
||||
DBG_INFO("poll: config file reloaded");
|
||||
}
|
||||
|
||||
s_reject_streak = 0u; /* 应用成功(含超长重建)= 拒绝期结束 */
|
||||
|
||||
/* 重载后刷新基线(回调触发的写盘不引发二次重载) */
|
||||
{
|
||||
cfg_file_stat_t st2;
|
||||
|
||||
+14
-4
@@ -18,6 +18,10 @@
|
||||
* v1.4 2026-10-25 DTU 项目组 「数据可信」有符号零点类型:新增 CFG_VAL_INT 有符号十进制值类型
|
||||
* (len = 1/2/4,小端补码)——标定零点偏移等负值需有符号
|
||||
* 落盘,动因与取舍见 docs/任务规划/数据可信.md §4.4
|
||||
* v1.5 2026-11-05 DTU 项目组 契约同步视图一致性守卫(issue #73 SNTP 补验揪出):
|
||||
* poll 读视图与 stat 令牌不符 = 拒绝应用(RAM/基线不动、2s
|
||||
* 重试自愈);save 读视图不符 = 拒存返回 -1(原文件不动)。
|
||||
* 细节见 cfg_file.c v1.6 与《配置文件设计说明》变更节
|
||||
*/
|
||||
|
||||
#ifndef CFG_FILE_H
|
||||
@@ -116,7 +120,10 @@ int cfg_file_load(void);
|
||||
* 值原位改写,缺失段/键补写;文件缺失/损坏 → 按 RAM 副本重建
|
||||
* 完整文档(含文件头注释);写盘 = 临时文件 + 原子改名;输出超
|
||||
* CFG_FILE_MAX_LEN → 拒绝保存返回 -1(原文件不动 + 自检上报,
|
||||
* 引擎写盘闸口同拒)
|
||||
* 引擎写盘闸口同拒);读视图与 stat 基准不符(空读/截断/读中
|
||||
* 被改写)同样拒存返回 -1——防按短视图外科重写把文件写小、
|
||||
* 非默认段被静默删除(2026-11-05 视图一致性守卫,调用方脏位
|
||||
* 保留 = 下一趟自然重试)
|
||||
*/
|
||||
int cfg_file_save(void);
|
||||
|
||||
@@ -129,9 +136,12 @@ int cfg_file_save(void);
|
||||
* 函数说明:(size, mtime 变更令牌) 相对基线变化且连续 CFG_FILE_SETTLE_CNT
|
||||
* 次轮询稳定 → 先查 st.size 超 CFG_FILE_MAX_LEN = 损坏 → 工厂默认
|
||||
* + 重建模板 + 自检留痕(不触发段回调:保留在线修复通道,重启
|
||||
* 收敛);否则重载全部段(各段复位工厂默认后解析);挂回调
|
||||
* 段有差异 → 调用其回调(如触发网络重建);重载后刷新基线
|
||||
* (回调触发的写盘不引发二次重载)
|
||||
* 收敛);否则过视图一致性守卫(读长 = st.size 且读前后 (size,
|
||||
* mtime) 不变;不符 = 卷层旧视图/读中被改写 → 拒绝应用:RAM 与
|
||||
* 基线不动,2s 重试自愈,持续到重启则由 load 全新挂载收敛;首次
|
||||
* 拒绝自检留痕,其后每 30 趟一条 WARN)后重载全部段(各段复位
|
||||
* 工厂默认后解析);挂回调段有差异 → 调用其回调(如触发网络重建);
|
||||
* 重载后刷新基线(回调触发的写盘不引发二次重载)
|
||||
*/
|
||||
void cfg_file_poll(void);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user