Skip to content

feat(flexlb): support Nacos local standby hot updates - #1358

Open
Martin7-1 wants to merge 1 commit into
feature/flexlb-mu-devfrom
feature/flexlb-support-local-standby-hot-update
Open

feat(flexlb): support Nacos local standby hot updates#1358
Martin7-1 wants to merge 1 commit into
feature/flexlb-mu-devfrom
feature/flexlb-support-local-standby-hot-update

Conversation

@Martin7-1

Copy link
Copy Markdown
Collaborator

Background

FlexLB currently reads configuration from Nacos at startup, but Local Standby capacity and expiration tuning cannot take effect without a restart. This change makes the supported Local Standby tuning fields update at runtime while keeping enable/disable switches static.

Implementation

  • Consolidate Nacos update handling in ConfigService with validated, typed projections and last-known-good protection.
  • Reuse the projection listener for log level and Local Standby consumers instead of per-setting provider classes.
  • Hot-update only Local Standby capacity and TTL controls:
    • maximum_entries
    • capacity_multiplier
    • ttl_ms
    • minimum_ttl_ms
    • ttl_reduction_start_ratio
  • Update Local Standby cache capacity, expiration policy, and prediction-cache expiration atomically for each accepted projection.
  • Keep autoSwitchEnabled and other enable/disable settings startup-only.
  • Document listener locking, update ordering, and the Nacos hot-update design.

Validation

  • ./mvnw test
  • ./mvnw spotless:check -Pspotless-check
  • Built and started the internal-profile jar with ./mvnw -Pinternal -pl flexlb-api -am package -DskipTests.
  • E2E on the FlexLB test deployment:
    • Nacos update reduced the Local Standby capacity from 1,803,000 to 1 without restarting the process.
    • Invalid minimum_ttl_ms > ttl_ms was rejected and the last-known-good configuration remained active.
    • Restoring the original Nacos document restored capacity to 1,803,000; health remained 200 throughout.

Compatibility and rollout

  • Existing configuration is unchanged at startup.
  • Only the five Local Standby capacity/TTL fields above are runtime mutable.
  • Other configuration updates continue to use the shared ConfigService validation and notification path.

Rollback

Restore the prior Nacos configuration values. The next accepted update restores the previous Local Standby runtime settings without requiring a restart.

@Martin7-1
Martin7-1 removed the request for review from jianglan89 August 31, 2026 07:23

@LLLLKKKK LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review - PR #1358

Status: BLOCKING

Summary: P0/0 · P1/1 · P2/14 · P3/8

Reviewed: commit dd4571eaa7e5 · 2026-08-31 16:13 UTC+8

Blocking Issues

P1

  • 关闭 KVCM 的 Nacos 推送会持续闭锁该进程的全部配置热更新,含日志等级 @ rtp_llm/flexlb/flexlb-common/src/main/java/org/flexlb/config/LocalStandbyRuntimeSettings.java:24
    • 建议:不要让「运行时无法生效」的字段拥有否决权:KVCM 缺失/被关闭时让投影退化为返回上一次成功值(等价 no-op,applier 不触发)并记一条 WARN 说明「关闭 KVCM 需重启生效」,仅在五个热更字段取值越界时才否决整次推送;把 kvcm 前置检查限制在启动期 ModelServiceConfiguration 校验路径。补两条用例:运行时推送 kvcm.enabled=false 后,同批次及后续推送中的 flexlbLogLevel 仍能生效,且该 disable 意图能落进快照供下次重启使用。若确实要保留否决语义,必须在 06-configuration-and-observability.md 明确「运行时不可关闭 kvcm,此类推送会拒绝整份配置」,提供替代 kill switch,并补覆盖该路径的单测。

Non-blocking Suggestions

P2

  • HBM 估算不可用时运行时下调的容量配置被静默丢弃 @ rtp_llm/flexlb/flexlb-cache/src/main/java/org/flexlb/cache/match/localstandby/LocalStandbyCacheManager.java:209
    • 建议:在 updateRuntimeSettings 中先按新 settings 无条件夹紧一次索引上限(估算不可用时直接 updateMaximumEntries(settings.maximumEntries())),再让 60s 周期任务收敛到基于 HBM 的估算值;estimate <= 0 分支至少打一条 warn 说明跳过原因。补一个单测:workerStatusProvider 返回空集合时运行时下调 maximum_entries,断言 maximumEntryCount() 等于新配置值。
  • 容量刷新可被定时线程与配置通知线程并发执行,新上限可能被旧配置覆盖 @ rtp_llm/flexlb/flexlb-cache/src/main/java/org/flexlb/cache/match/localstandby/LocalStandbyCacheManager.java:213
    • 建议:把 refreshCapacityLimits() 声明为 synchronized,或用专用 lock 包住「读 settings → 估算 → 写上限」临界区,也可统一投递到单线程 executor 串行执行,保证 :213 读到的 settings 与 :216 写入的上限来自同一版本配置。补一条定时刷新与配置推送并发不回退的用例。
  • 运行期投影校验范围小于启动期校验,非热更字段的非法值成为重启地雷 @ rtp_llm/flexlb/flexlb-common/src/main/java/org/flexlb/config/ModelServiceConfiguration.java:81
    • 建议:在 ConfigService 再注册一个覆盖整个 modelServiceConfig 的「校验专用」投影(applier 为空实现),复用与 validateServiceRoute/validateLocalStandby 等价的规则,使「运行期能接受」与「重启能启动」保持一致;并补一条「推送非热更字段的非法值会被拒绝并保留 last-known-good」的单测。对合法但无法运行时生效的字段变化,至少 log.warn 明确提示「已接受但需重启生效」。
  • 递归合并同时改变了启动期 env+Nacos 的嵌套解析语义,文档与测试只覆盖运行期 @ rtp_llm/flexlb/flexlb-common/src/main/java/org/flexlb/config/ConfigService.java:154
    • 建议:在 06-configuration-and-observability.md 启动期优先级第 3 条同样写明「嵌套对象逐字段递归合并、数组整体替换、显式 null 覆盖」;并在 ConfigServiceTest 增加启动期用例:env 提供含非默认 kvcmMODEL_SERVICE_CONFIG、Nacos 提供省略 kvcmmodelServiceConfig,断言最终 kvcm 取值,把该语义固化。
  • 递归合并对数组与 JSON null 的整体替换语义缺少回归测试 @ rtp_llm/flexlb/flexlb-common/src/test/java/org/flexlb/service/config/NacosConfigSourceTest.java:133
    • 建议:增加用例:初始配置含多个 role_endpoints,推送只含单元素 role_endpointsmodelServiceConfig,断言合并后列表长度为 1;再推送 {"modelServiceConfig":{"kvcm":null}} 断言该推送被拒并保留 last-known-good,固化「非对象字段整体替换」的契约。
  • 新的唯一校验权威 LocalStandbyRuntimeSettings 缺少聚焦单测 @ rtp_llm/flexlb/flexlb-common/src/main/java/org/flexlb/config/LocalStandbyRuntimeSettings.java:40
    • 建议:新增 LocalStandbyRuntimeSettingsTest,用参数化用例覆盖 5 个字段各自的边界(0、负值、NaN/Infinity、比值取 0 与 1、minimumTtlMs == ttlMscapacityMultiplier == 1.0),断言 from((LocalStandbyConfig) null) 返回与 LocalStandbyConfig.DEFAULT_* 一致的默认值,并覆盖 from(FlexlbConfig) 两条拒绝分支及其消息。
  • NacosConfigSourceTest 缺少 @AfterEach,断言失败会把静态配置源泄漏给同 JVM 后续用例 @ rtp_llm/flexlb/flexlb-common/src/test/java/org/flexlb/service/config/NacosConfigSourceTest.java:162
    • 建议:在 NacosConfigSourceTest 增加与 ConfigServiceTest 一致的 @AfterEach:用字段保存创建出的 ConfigService 并在 teardown 中无条件 close()(或用 try-finally 包裹断言),使静态 CONFIG_SOURCES 在失败路径上也被清理。两个测试类对同一静态资源应使用同一套清理范式。
  • 仅测试使用的构造重载迫使生产代码保留不可达 null 分支,并可静默关闭热更新 @ rtp_llm/flexlb/flexlb-cache/src/main/java/org/flexlb/cache/match/localstandby/LocalStandbyCacheManager.java:49
    • 建议:移除这两个重载与随之而来的 null 判空,测试统一改用完整构造并传入 mock(ConfigService.class)LocalStandbyCacheManagerTest:166-175 已经这么写,Mockito 默认对 addUpdateListener 不做任何事,与当前 null 路径等价)。若确需可选依赖,改用 ObjectProvider<ConfigService> 并在跳过订阅时 log.info 说明原因,让「热更新未启用」成为可观测状态。
  • manager 到 cacheIndex 的 TTL 透传链路缺少断言,两个同类型 long 参数写反不会被发现 @ rtp_llm/flexlb/flexlb-cache/src/test/java/org/flexlb/cache/match/localstandby/LocalStandbyCacheManagerTest.java:156
    • 建议:在该用例后补一个断言块:先 addRoutedRequestBlocks 写入映射,再通过订阅回调推入 ttl_ms/minimum_ttl_ms 极小的运行时配置,然后断言 findMatchingEngines 不再命中且 mappingCount() 归零,使 TTL 透传与传参顺序具备可失败的覆盖。
  • DefaultRouter 的 null 兜底与下游策略的非空前提矛盾,且回滚路径改动无可失败覆盖 @ rtp_llm/flexlb/flexlb-sync/src/main/java/org/flexlb/balance/scheduler/DefaultRouter.java:140
    • 建议:明确不变量并统一实现:既然 RouteService.route() 保证 setConfig 先于 router.route(),可在 DefaultRouter 入口一次取值并 Objects.requireNonNull 快速失败,把 getLoadBalancer 简化为只接收 FlexlbConfig;若保留兜底则收敛为 BalanceContext 上的单一访问器供各调用方共用。同时补一条用例:stub getConfig() 返回策略与 configService.loadBalanceConfig() 不同的快照,令第二个角色选路失败,verify 回滚发生在该快照对应的 LoadBalancer 上且 loadBalanceConfig() 从未被调用。
  • RouteService.cancel 的配置来源变更零测试覆盖 @ rtp_llm/flexlb/flexlb-sync/src/main/java/org/flexlb/service/RouteService.java:59
    • 建议:新增最小 RouteServiceTest,覆盖三种情形:getConfig() 返回 enableQueueing=true 的快照(断言 cancel() 与 future 异常完成)、返回 enableQueueing=false 的快照(断言不取消)、返回 null(断言回退到 configService.loadBalanceConfig())。
  • 预测缓存过期窗口复用索引 ttl_ms,热更新下调会静默削减对比观测 @ rtp_llm/flexlb/flexlb-cache/src/main/java/org/flexlb/cache/match/localstandby/LocalStandbyComparisonService.java:137
    • 建议:为预测关联窗口使用独立配置字段,或至少给一个下界(如 max(ttlMs, 最小反馈等待时长));并在因过期导致对比缺失时通过 CacheMetricsReporter 上报计数指标,使这种降级可观测。
  • 推送被拒与 applier 失败均不可定位,且失败时仍打印 Applied 成功日志 @ rtp_llm/flexlb/flexlb-common/src/main/java/org/flexlb/config/ConfigService.java:113
    • 建议:from(LocalStandbyConfig) 的异常消息带上首个非法字段名与实际值(如 minimum_ttl_ms=200 > ttl_ms=100);ModelServiceConfiguration:91 改为 new IllegalArgumentException(msg, e) 保留 cause(若 ModelServiceConfigurationTest 依赖 hasRootCauseMessage 需同步调整);拒绝推送的 error 日志带上异常对象以保留 stack;notifyListeners 统计失败数并返回,存在失败时不再输出 Applied 成功 info;给订阅增加名字维度(如 addUpdateListener(name, projection, applier))并在失败日志中输出;通过现有 metrics 上报「推送被拒次数」与「applier 失败次数」。
  • 订阅注册期的首次回放未受保护,与更新期错误语义不一致 @ rtp_llm/flexlb/flexlb-common/src/main/java/org/flexlb/config/ConfigService.java:74
    • 建议:明确注册期契约并写入 javadoc:若选择 fail-fast,则保证投影异常信息足以定位字段,并在文档中说明「注册期投影失败 = 启动失败」;若不希望如此,则把首次回放失败按运行期处理(记录并保留订阅,等下次推送重试)。补一条「注册期 projection 抛异常」的单测固化该行为。

P3

  • 运行时缩短 TTL 后未主动触发清理,索引占用收敛延迟 @ rtp_llm/flexlb/flexlb-cache/src/main/java/org/flexlb/cache/match/localstandby/LocalStandbyCacheIndex.java:130
    • 建议:与 updateMaximumEntries 保持对称:在 updateExpirationSettings 末尾同样调用 requestHighWatermarkCleanupIfNeeded(),或在检测到 TTL 相比旧值缩短时把 checksSinceLastCleanup 直接推到阈值,使下一次后台检查即执行批量清理。
  • applier 部分失败后回退到上次成功值的推送会被静默跳过 @ rtp_llm/flexlb/flexlb-common/src/main/java/org/flexlb/config/ConfigService.java:222
    • 建议:为每个订阅记录「上次通知失败」标记,失败后下一次推送无论投影值是否变化都重新投递;或在 addUpdateListener javadoc 中明确要求 applier 必须原子(先全部校验、再一次性写入),并说明违反该约定的后果。
  • 构造函数中注册订阅造成 this-escape 并在 bean 构造期发起 worker 扫描 @ rtp_llm/flexlb/flexlb-cache/src/main/java/org/flexlb/cache/match/localstandby/LocalStandbyCacheManager.java:81
    • 建议:把订阅注册从构造函数移到 @PostConstruct(或 SmartInitializingSingleton),让构造函数只做字段赋值,既消除 this-escape,也避免在 bean 构造期查询尚未同步完成的 worker 状态。
  • disabled 分支重复手写五个默认值常量 @ rtp_llm/flexlb/flexlb-cache/src/main/java/org/flexlb/cache/match/localstandby/LocalStandbyCacheManager.java:67
    • 建议:disabled 分支直接调用 LocalStandbyRuntimeSettings.from((LocalStandbyConfig) null),让默认值只有一个权威来源。
  • 比较缓存 TTL 用例依赖挂钟余量,且负向断言可因错误原因通过 @ rtp_llm/flexlb/flexlb-cache/src/test/java/org/flexlb/cache/match/localstandby/LocalStandbyComparisonServiceTest.java:81
    • 建议:把延长方向的 TTL 提高到数十秒量级(如 30_000)以消除绝对时间预算;在「过期」用例缩短 TTL 之前先断言一次预测可被取回,使负向断言只能因过期而成立。若要彻底去掉墙钟依赖,可让生产代码支持注入 Caffeine Ticker,测试中用假时钟显式推进时间。
  • publishesOnlyChangedValidatedRuntimeSettings 中存在无效赋值 @ rtp_llm/flexlb/flexlb-common/src/test/java/org/flexlb/service/config/ConfigServiceTest.java:208
    • 建议:在 :211 之后补一条 assertThat(service.loadBalanceConfig()).isNotSameAs(lastKnownGood);,让 :208 的赋值产生实际断言价值,明确区分「快照已替换」与「投影未变化、不重复通知」两个语义;若不打算补断言,则删除 :208 的无效赋值以免误导读者。
  • 新增的 @SuppressWarnings("unchecked") 冗余且与同 PR 内等价写法不一致 @ rtp_llm/flexlb/flexlb-sync/src/test/java/org/flexlb/service/monitor/FlexlbLogManagerTest.java:41
    • 建议:删除该注解以保持同 PR 内一致;若某个编译配置确实报警,应在最小作用域(局部变量声明)上压制并注明原因,而不是整个方法。
  • addUpdateListener 中的两段 updateLock 对正确性无额外作用 @ rtp_llm/flexlb/flexlb-common/src/main/java/org/flexlb/config/ConfigService.java:71
    • 建议:保留 notificationLock 的串行化语义,把 addUpdateListener 中的 updateLock 收敛为只包裹 updateListeners 的注册(读取 currentConfig 直接走 AtomicReference),并在注释中明确 updateLock 的保护范围仅为「快照替换 + 订阅列表」以及与构造期 initializeConfigSources 的互斥。

Checklist Findings (12 fail / 26 total)

General Principles Checklist

  • [6.1] Architecture — 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全 → issue 递归合并同时改变了启动期 env+Nacos 的嵌套解析语义,文档与测试只覆盖运行期
    mergeObjectFieldsmergeConfig 复用,而 mergeConfig 同时服务启动路径 initializeConfigSources(:90)与运行期 receiveConfigUpdate(:107)。EnvironmentConfigSource:40 序列化完整 FlexlbConfig 作为基线,因此启动时 Nacos 的 modelServiceConfig 从「整对象替换 env 值」变为「逐字段深合并」:若 Nacos DataId 有意省略 kvcm 以关闭 KVCM,而 env MODEL_SERVICE_CONFIGkvcm.enabled=true,该字段现在会被继承,cache-match 模式在下次重启时反转。文档 :21-22 只在运行期写了「递归覆盖」,启动期第 3 条(:18-20)未更新;letsNacosOverrideEnvironmentModelServiceConfig(:142-155)只断言 service_id,无法区分两种语义。
  • [6.1] Architecture — 分层边界:新概念在正确层级,不泄漏内部 → issue 关闭 KVCM 的 Nacos 推送会持续闭锁该进程的全部配置热更新,含日志等级
    from(FlexlbConfig)kvcm == null || !kvcm.isEnabled() 时抛 IllegalArgumentException(:24-26)。该投影在 KVCM 启用时必然注册(CacheMatchConfiguration:36localStandbyEnabled == kvcmEnabledLocalStandbyCacheManager:81LocalStandbyComparisonService:55)。任一投影抛异常即跳过 currentConfig.setConfigService:107-111),整次推送被拒。NacosConfigSource:128-134 每次投递完整 DataId,故 kvcm.enabled=false 一旦写入 DataId,后续每次推送都被同样拒绝:FlexlbLogManager:34-35 注册的 flexlbLogLevel/enableStdoutLog,以及 enableQueueing、负载均衡策略等热更字段被长期
  • [6.1] Architecture — 可观测性:日志/指标/超时可操作、非噪声 → issue 推送被拒与 applier 失败均不可定位,且失败时仍打印 Applied 成功日志
    三处叠加:(1) 五个字段的 9 个校验谓词共用固定文案 "local standby runtime settings are invalid"(LocalStandbyRuntimeSettings:49),不含字段名与实际值;ModelServiceConfiguration:89-94 重新包装时未传 cause,原始信息被完全丢弃;receiveConfigUpdate 的 catch 只打 e.getMessage()(:116-119),无 cause 与 stack。(2) notifyListeners 捕获异常后仅 log.error("...but a runtime listener failed")(:129),不含订阅名,多订阅共存时无法判断是谁失败;随后 :114 仍无条件打印 Applied FlexLB configuration update,基于关键字的告警易误判。(3) 没有「推送被拒次数」「applier 失败次数」指标,而 applier 失败后快照已提交、运行态仍是旧值,这种不一致完全不可监控。
  • [6.1] Architecture — 回滚路径:风险行为存在运维回滚手段 → issue 推送被拒与 applier 失败均不可定位,且失败时仍打印 Applied 成功日志
    三处叠加:(1) 五个字段的 9 个校验谓词共用固定文案 "local standby runtime settings are invalid"(LocalStandbyRuntimeSettings:49),不含字段名与实际值;ModelServiceConfiguration:89-94 重新包装时未传 cause,原始信息被完全丢弃;receiveConfigUpdate 的 catch 只打 e.getMessage()(:116-119),无 cause 与 stack。(2) notifyListeners 捕获异常后仅 log.error("...but a runtime listener failed")(:129),不含订阅名,多订阅共存时无法判断是谁失败;随后 :114 仍无条件打印 Applied FlexLB configuration update,基于关键字的告警易误判。(3) 没有「推送被拒次数」「applier 失败次数」指标,而 applier 失败后快照已提交、运行态仍是旧值,这种不一致完全不可监控。
  • [6.1] Architecture — 状态不变量:创建/更新/失败/重试/回滚路径有效 → issue 构造函数中注册订阅造成 this-escape 并在 bean 构造期发起 worker 扫描
    addUpdateListener 会立即回放(ConfigService:74),即在构造函数内同步调用 this::updateRuntimeSettings,进而读 cacheIndexserviceRoutesworkerStatusProvider 并触发一次全量 worker 容量扫描(refreshCapacityLimits)。当前只因为注册恰好是构造函数最后一条语句才安全;任何字段赋值顺序调整都会退化为 NPE。LocalStandbyComparisonService:55 是同一模式。
  • [6.1] Architecture — 错误语义:fail-fast/retry/fallback/silent 行为显式 → issue applier 部分失败后回退到上次成功值的推送会被静默跳过
    prepareUpdateObjects.equals(currentValue, updatedValue) 时返回空 Runnable(:222-224),而 currentValue 只在 listener.accept 成功后推进(:225-228)。若某个 applier 在写入部分运行时状态之后抛异常,currentValue 仍停留在旧值 U 而真实运行时状态已变成 V;随后运维把配置回滚为 U 时投影值等于 currentValue,applier 不会被调用,运行时状态永久停在 V。类 javadoc(:194-200)与文档 :82-83 只承诺「下一次仍不同于上次成功值的推送会重试」,未覆盖这条回滚路径。当前两个 applier 恰好都接近原子,因此暂无可达触发点,属框架级隐性契约缺口。
  • [6.1] Software Engineering — DRY:重复非平凡逻辑被抽取或显式复用 → issue disabled 分支重复手写五个默认值常量
    LocalStandbyRuntimeSettings.from(LocalStandbyConfig) 在入参为 null 时已用 new LocalStandbyConfig() 取默认值(:34),而 LocalStandbyConfig 的字段初值正是 DEFAULT_TTL_MS 等 5 个常量,与 disabled 分支手写的 5 个 DEFAULT_*(:67-72)完全等价。同一份默认值现在有两处来源,将来任一 DEFAULT_* 变更或新增字段时容易漏改其中一处。
  • [6.1] Software Engineering — KISS/YAGNI:无投机性抽象 → issue addUpdateListener 中的两段 updateLock 对正确性无额外作用
    addUpdateListener 全程持有 notificationLock,而 updateListenerscurrentConfig 在运行期的唯一其他访问者 receiveConfigUpdate 同样以 notificationLock 为最外层锁,initializeConfigSources 只在构造期执行(对象未发布,不可能与 addUpdateListener 并发),且 currentConfig 本身是 AtomicReference。因此 :71 与 :75 两段 synchronized (updateLock) 虽然无害,但不改变任何可见性或互斥结论,只增加读者判断「两把锁各自保护什么」的成本。
  • [6.1] Software Engineering — SRP:模块/类职责单一 → issue 预测缓存过期窗口复用索引 ttl_ms,热更新下调会静默削减对比观测
    updateRuntimeSettings 直接用 settings.ttlMs()setExpiresAfter(:138-140)。但 ttl_ms 的语义是「块映射保鲜期」,而 pendingLocalStandbyPredictions 的窗口语义是「等待引擎反馈的最长时间」,两者只是历史上共用同一字段(构造期 :52 已如此)。改成热更新后,运维为控制索引容量把 ttl_ms 调小会同时缩短反馈关联窗口:尚未收到 CacheHitFeedback 的预测被提前淘汰,buildCacheHitComparisonprediction == null 分支(:94-96)返回 localStandby=null,cache_hit_comparison 观测静默缺失,既无日志也无指标可区分「未预测」与「预测已过期」。
  • [6.1] Tests — 新逻辑有聚焦单测 + 相关集成/smoke 测试 → issue publishesOnlyChangedValidatedRuntimeSettings 中存在无效赋值
    :208 的 FlexlbConfig lastKnownGood = service.loadBalanceConfig(); 在 :212 被重新赋值前从未被读取,是一次死赋值;中间只有 source.emit("{\"enableQueueing\":true}")(:210)与 assertThat(updates).containsExactly(9)(:211)。从命名看作者原本想断言「仅 enableQueueing 变化时快照被替换、但投影值未变因而不触发 applier」,实际只断言了后半句,读者容易误以为「投影未变」等于「快照未变」。
  • [6.1] Tests — 边界 case 覆盖(空、单元素、最大值) → issue 比较缓存 TTL 用例依赖挂钟余量,且负向断言可因错误原因通过
    retainsPendingPredictionUsingExtendedRuntimeTtl 在 :81 把 TTL 更新为 1_000ms,随后 putbuildCacheHitComparison(...).get(1, SECONDS) 之间夹入 Thread.sleep(20)(:83),整条链路绝对预算只有 1s,CI 上一次 full GC 或调度抖动就可能让条目提前过期而误报失败。另一侧 expiresPendingPredictionUsingUpdatedRuntimeTtlassertNull(...localStandby())(:61)在「预测根本没写入缓存」时同样会通过,缺少写入成功的前置断言。同分片 LocalStandbyCacheIndexTest:37-38 已示范注入时间戳的确定性写法。

RTP-LLM Checklist

  • [I] 代码质量 — 同一功能用统一工具函数 → issue 新增的 @SuppressWarnings("unchecked") 冗余且与同 PR 内等价写法不一致
    appliesConfigUpdates 新增方法级 @SuppressWarnings("unchecked")(:41),但其内部只是把 invocation.getArgument(0) 赋给 Function<FlexlbConfig, Object>(:52-53);getArgument 声明为 <T> T getArgument(int),由目标类型推断,调用点不产生 unchecked 警告。本 PR 中 LocalStandbyCacheManagerTest:167-173LocalStandbyComparisonServiceTest:46-51、70-75 使用完全相同的 doAnswer 模式却均未加该注解,同一 PR 内出现两种写法。

Strengths

  • 提交时序正确且修正了旧语义错误:ConfigService.java:106-112updateLock 内先对所有订阅求值投影再 currentConfig.set,任一投影抛异常即整次推送被拒并保留 last-known-good;改动前存在「快照已 set 却记录 keeping last-known-good」的矛盾语义,本次一并修好。
  • prepareUpdate 是纯函数(只算投影并返回 Runnable,不改状态),使「先校验全部投影、再提交快照」这一不变量在部分求值时也成立,且用户 applier 不在快照锁内执行。
  • 失败重试语义精确:currentValue 只在 applier 成功后推进(ConfigService.java:225-228),ConfigServiceTest.continuesNotifyingOtherRuntimeSettingsWhenOneApplierFailsattempted=[9,10,10,11] / delivered=[9,10,11] 两条独立序列把该语义完全钉死。
  • 并发不变量有确定性护栏:两个门闩用例分别锁定「注册与推送竞态不丢更新」与「先提交快照(:330 断言 maxRetryCount==10)+ 回调串行(maximumConcurrentCallbacks==1)+ 顺序 9/10/11」,所有等待都有 1s 上界;两处 await(100ms)).isFalse() 是安全方向断言,不会因 CI 负载升高误报。
  • TTL 热更新的并发发布方式正确:三字段收敛为不可变 ExpirationSettings record 并以单个 volatile 引用整体替换(LocalStandbyCacheIndex.java:33、131、306),effectiveTtlNanos() 先快照到局部变量再多次读取(:189-205),彻底消除逐字段 volatile 的跨字段撕裂读。
  • 递归合并修复了一个真实缺陷:此前 merged.setAll(overrides) 会让仅含 kvcm.local_standby 的推送整体替换 modelServiceConfig、丢掉 service_id/role_endpointsNacosConfigSourceTest:195-201 精准断言非法推送后仅推 maximum_entries 时其余四字段仍继承 (500, 10.0, 300_000, 100_000, 0.8),覆盖了本 PR 风险最高的改动。
  • DefaultRouter.routeByRoleType(:117)与 rollBackRoutingFailure(:161)共享同一份请求级快照,消除了「选点用新策略、回滚用旧策略」导致回滚到另一个 LoadBalancer、泄漏本地增量状态的隐患。
  • 校验权威收敛到 LocalStandbyRuntimeSettings.from:容量/TTL 五字段的规则从 ModelServiceConfiguration 内联条件移出,避免启动期与运行期规则漂移,对外错误信息保持不变。
  • LocalStandbyCacheIndexTest.appliesUpdatedTtlToExistingMappings(:31-41)用注入的查询时间点 System.nanoTime() + 2ms 而非 sleep 断言过期,并同时验证 mappingCount 回收,是本 PR 最确定的时间相关用例,值得作为该模块范式。
  • 锁契约有文档:两把锁职责、notificationLock → updateLock 获取顺序、「用户 applier 不在快照锁内执行」、以及可运行时生效字段与必须重启字段的逐字段清单都写入类注释(ConfigService.java:30-40)与架构文档(:78-84、:56-59)。

throw new IllegalArgumentException("modelServiceConfig must not be null");
}
ServiceRoute serviceRoute = config.getModelServiceConfig();
if (serviceRoute.getKvcm() == null || !serviceRoute.getKvcm().isEnabled()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 关闭 KVCM 的 Nacos 推送会持续闭锁该进程的全部配置热更新,含日志等级

from(FlexlbConfig)kvcm == null || !kvcm.isEnabled() 时抛 IllegalArgumentException(:24-26)。该投影在 KVCM 启用时必然注册(CacheMatchConfiguration:36localStandbyEnabled == kvcmEnabledLocalStandbyCacheManager:81LocalStandbyComparisonService:55)。任一投影抛异常即跳过 currentConfig.setConfigService:107-111),整次推送被拒。NacosConfigSource:128-134 每次投递完整 DataId,故 kvcm.enabled=false 一旦写入 DataId,后续每次推送都被同样拒绝:FlexlbLogManager:34-35 注册的 flexlbLogLevel/enableStdoutLog,以及 enableQueueing、负载均衡策略等热更字段被长期

建议: 不要让「运行时无法生效」的字段拥有否决权:KVCM 缺失/被关闭时让投影退化为返回上一次成功值(等价 no-op,applier 不触发)并记一条 WARN 说明「关闭 KVCM 需重启生效」,仅在五个热更字段取值越界时才否决整次推送;把 kvcm 前置检查限制在启动期 ModelServiceConfiguration 校验路径。补两条用例:运行时推送 kvcm.enabled=false 后,同批次及后续推送中的 flexlbLogLevel 仍能生效,且该 disable 意图能落进快照供下次重启使用。若确实要保留否决语义,必须在 06-configuration-and-observability.md 明确「运行时不可关闭 kvcm,此类推送会拒绝整份配置」,提供替代 kill switch,并补覆盖该路径的单测。

Checklist: [6.1] 分层边界:新概念在正确层级,不泄漏内部

@LLLLKKKK LLLLKKKK left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI Code Review - PR #1358 (non-blocking suggestions)

22 条 P2/P3 建议,不阻塞合并。阻塞判定与完整摘要见上一条 review。

@@ -204,7 +210,8 @@ void refreshCapacityLimits() {
return;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📍 实际位置 rtp_llm/flexlb/flexlb-cache/src/main/java/org/flexlb/cache/match/localstandby/LocalStandbyCacheManager.java:209(不在 diff 展示范围内,就近挂载)

[P2] HBM 估算不可用时运行时下调的容量配置被静默丢弃

updateRuntimeSettings(:263-268)应用 maximum_entries/capacity_multiplier 的唯一手段是 refreshCapacityLimits(),而后者在 estimatedHbmBlockCapacity <= 0 时直接 return(:209-211),既不调用 cacheIndex.updateMaximumEntries 也不打印任何日志。calculateWorkerBlockCapacity(:279-291)在 worker 未存活、cacheStatus 为空或 totalKvCache/blockSize <= 0 时返回 0,即启动初期或集群状态同步中断时必然发生。此时索引沿用旧上限、mappingCount 可继续增长到旧上限,运维侧无任何线索说明本次配置未落地,与文档 :56-59「运行时生效」的表述有落差。

建议:updateRuntimeSettings 中先按新 settings 无条件夹紧一次索引上限(估算不可用时直接 updateMaximumEntries(settings.maximumEntries())),再让 60s 周期任务收敛到基于 HBM 的估算值;estimate <= 0 分支至少打一条 warn 说明跳过原因。补一个单测:workerStatusProvider 返回空集合时运行时下调 maximum_entries,断言 maximumEntryCount() 等于新配置值。

}

long newMaximumEntries = calculateMaximumEntries(estimatedHbmBlockCapacity);
LocalStandbyRuntimeSettings settings = runtimeSettings;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 容量刷新可被定时线程与配置通知线程并发执行,新上限可能被旧配置覆盖

本 PR 使 refreshCapacityLimits() 同时成为 @Scheduled(fixedDelay = 60_000) 定时任务(:201)与配置通知线程的同步调用点(:267),而方法内「读 runtimeSettings(:213)→ 估算 → 写 updateMaximumEntries(:216)」这段临界区无任何互斥。交错场景:定时线程在 :213 读到旧 settings,通知线程随后写入新 settings 并完成 updateMaximumEntries(new),定时线程再执行 :216 把上限写回旧值。后果是刚生效的运行时上限被回退,最长需等下一个 60s 周期才自愈;:218 的 "from X to Y" 日志也会打印出误导性配对。

建议:refreshCapacityLimits() 声明为 synchronized,或用专用 lock 包住「读 settings → 估算 → 写上限」临界区,也可统一投递到单线程 executor 串行执行,保证 :213 读到的 settings 与 :216 写入的上限来自同一版本配置。补一条定时刷新与配置推送并发不回退的用例。

@@ -80,21 +80,18 @@ private void validateLocalStandby(LocalStandbyConfig localStandby) {
}
if (localStandby.getBlockSize() < 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 运行期投影校验范围小于启动期校验,非热更字段的非法值成为重启地雷

validateLocalStandby 仍在启动期校验 block_sizeasync_queue_capacityhash_thread_counthash_queue_capacity(:81-88),而运行期只做 Jackson 反序列化 + 投影校验(ConfigService:107),投影 LocalStandbyRuntimeSettings 只含五个热更字段。推送 {"modelServiceConfig":{"kvcm":{"local_standby":{"hash_thread_count":0}}}} 会通过投影、提交进快照并打出 Applied FlexLB configuration update,运行期无任何拒绝或告警;但 Nacos DataId 已持久化该内容,下次重启时 :86 抛 IllegalArgumentException 使 modelMetaConfig bean 创建失败,形成重启即启动失败。本 PR 正鼓励运维直接编辑 kvcm.local_standby,命中概率被放大。

建议:ConfigService 再注册一个覆盖整个 modelServiceConfig 的「校验专用」投影(applier 为空实现),复用与 validateServiceRoute/validateLocalStandby 等价的规则,使「运行期能接受」与「重启能启动」保持一致;并补一条「推送非热更字段的非法值会被拒绝并保留 last-known-good」的单测。对合法但无法运行时生效的字段变化,至少 log.warn 明确提示「已接受但需重启生效」。

return config;
}

private void mergeObjectFields(ObjectNode base, ObjectNode overrides) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 递归合并同时改变了启动期 env+Nacos 的嵌套解析语义,文档与测试只覆盖运行期

mergeObjectFieldsmergeConfig 复用,而 mergeConfig 同时服务启动路径 initializeConfigSources(:90)与运行期 receiveConfigUpdate(:107)。EnvironmentConfigSource:40 序列化完整 FlexlbConfig 作为基线,因此启动时 Nacos 的 modelServiceConfig 从「整对象替换 env 值」变为「逐字段深合并」:若 Nacos DataId 有意省略 kvcm 以关闭 KVCM,而 env MODEL_SERVICE_CONFIGkvcm.enabled=true,该字段现在会被继承,cache-match 模式在下次重启时反转。文档 :21-22 只在运行期写了「递归覆盖」,启动期第 3 条(:18-20)未更新;letsNacosOverrideEnvironmentModelServiceConfig(:142-155)只断言 service_id,无法区分两种语义。

建议:06-configuration-and-observability.md 启动期优先级第 3 条同样写明「嵌套对象逐字段递归合并、数组整体替换、显式 null 覆盖」;并在 ConfigServiceTest 增加启动期用例:env 提供含非默认 kvcmMODEL_SERVICE_CONFIG、Nacos 提供省略 kvcmmodelServiceConfig,断言最终 kvcm 取值,把该语义固化。

Checklist: [6.1] 兼容性:外部 HTTP/RPC API、持久数据、配置、环境迁移安全

}

@Test
void appliesNestedLocalStandbyCapacityAndTtlUpdateFromNacos() throws Exception {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 递归合并对数组与 JSON null 的整体替换语义缺少回归测试

mergeObjectFields 只在 existingoverride 同为 ObjectNode 时下钻,其余情况在 ConfigService:162 整体替换。三个新增 Nacos 用例(:133、:166、:206)只覆盖「对象字段深合并」与「数值非法被拒」,没有任何断言说明数组字段(基线 localStandbyConfig() 中的 role_endpoints)与 JSON null 是整体替换而非合并。这是深合并最易出错的边界:一旦后续被重构为通用 deep-merge 并开始合并数组,运维缩容 role_endpoints 会得到含已下线 endpoint 的陈旧列表并直接造成错误路由,而当前测试集不会失败。

建议: 增加用例:初始配置含多个 role_endpoints,推送只含单元素 role_endpointsmodelServiceConfig,断言合并后列表长度为 1;再推送 {"modelServiceConfig":{"kvcm":null}} 断言该推送被拒并保留 last-known-good,固化「非对象字段整体替换」的契约。

: LocalStandbyConfig.DEFAULT_CAPACITY_MULTIPLIER;
this.runtimeSettings = enabled
? LocalStandbyRuntimeSettings.from(config)
: new LocalStandbyRuntimeSettings(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] disabled 分支重复手写五个默认值常量

LocalStandbyRuntimeSettings.from(LocalStandbyConfig) 在入参为 null 时已用 new LocalStandbyConfig() 取默认值(:34),而 LocalStandbyConfig 的字段初值正是 DEFAULT_TTL_MS 等 5 个常量,与 disabled 分支手写的 5 个 DEFAULT_*(:67-72)完全等价。同一份默认值现在有两处来源,将来任一 DEFAULT_* 变更或新增字段时容易漏改其中一处。

建议: disabled 分支直接调用 LocalStandbyRuntimeSettings.from((LocalStandbyConfig) null),让默认值只有一个权威来源。

Checklist: [6.1] DRY:重复非平凡逻辑被抽取或显式复用

CacheMatchQuery query = new CacheMatchQuery(
"request-1", List.of(11L), 2_192, List.of(101L), 4_096, RoleType.PREFILL, "default");

listener.get().accept(projection.get().apply(flexlbConfig(1_000, 1_000)));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] 比较缓存 TTL 用例依赖挂钟余量,且负向断言可因错误原因通过

retainsPendingPredictionUsingExtendedRuntimeTtl 在 :81 把 TTL 更新为 1_000ms,随后 putbuildCacheHitComparison(...).get(1, SECONDS) 之间夹入 Thread.sleep(20)(:83),整条链路绝对预算只有 1s,CI 上一次 full GC 或调度抖动就可能让条目提前过期而误报失败。另一侧 expiresPendingPredictionUsingUpdatedRuntimeTtlassertNull(...localStandby())(:61)在「预测根本没写入缓存」时同样会通过,缺少写入成功的前置断言。同分片 LocalStandbyCacheIndexTest:37-38 已示范注入时间戳的确定性写法。

建议: 把延长方向的 TTL 提高到数十秒量级(如 30_000)以消除绝对时间预算;在「过期」用例缩短 TTL 之前先断言一次预测可被取回,使负向断言只能因过期而成立。若要彻底去掉墙钟依赖,可让生产代码支持注入 Caffeine Ticker,测试中用假时钟显式推进时间。

Checklist: [6.1] 边界 case 覆盖(空、单元素、最大值)

}
return maxRetryCount;
}, updates::add);
FlexlbConfig lastKnownGood = service.loadBalanceConfig();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] publishesOnlyChangedValidatedRuntimeSettings 中存在无效赋值

:208 的 FlexlbConfig lastKnownGood = service.loadBalanceConfig(); 在 :212 被重新赋值前从未被读取,是一次死赋值;中间只有 source.emit("{\"enableQueueing\":true}")(:210)与 assertThat(updates).containsExactly(9)(:211)。从命名看作者原本想断言「仅 enableQueueing 变化时快照被替换、但投影值未变因而不触发 applier」,实际只断言了后半句,读者容易误以为「投影未变」等于「快照未变」。

建议: 在 :211 之后补一条 assertThat(service.loadBalanceConfig()).isNotSameAs(lastKnownGood);,让 :208 的赋值产生实际断言价值,明确区分「快照已替换」与「投影未变化、不重复通知」两个语义;若不打算补断言,则删除 :208 的无效赋值以免误导读者。

Checklist: [6.1] 新逻辑有聚焦单测 + 相关集成/smoke 测试

}

@Test
@SuppressWarnings("unchecked")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] 新增的 @SuppressWarnings("unchecked") 冗余且与同 PR 内等价写法不一致

appliesConfigUpdates 新增方法级 @SuppressWarnings("unchecked")(:41),但其内部只是把 invocation.getArgument(0) 赋给 Function<FlexlbConfig, Object>(:52-53);getArgument 声明为 <T> T getArgument(int),由目标类型推断,调用点不产生 unchecked 警告。本 PR 中 LocalStandbyCacheManagerTest:167-173LocalStandbyComparisonServiceTest:46-51、70-75 使用完全相同的 doAnswer 模式却均未加该注解,同一 PR 内出现两种写法。

建议: 删除该注解以保持同 PR 内一致;若某个编译配置确实报警,应在最小作用域(局部变量声明)上压制并注明原因,而不是整个方法。

Checklist: [I] 同一功能用统一工具函数


synchronized (notificationLock) {
T initialValue;
synchronized (updateLock) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] addUpdateListener 中的两段 updateLock 对正确性无额外作用

addUpdateListener 全程持有 notificationLock,而 updateListenerscurrentConfig 在运行期的唯一其他访问者 receiveConfigUpdate 同样以 notificationLock 为最外层锁,initializeConfigSources 只在构造期执行(对象未发布,不可能与 addUpdateListener 并发),且 currentConfig 本身是 AtomicReference。因此 :71 与 :75 两段 synchronized (updateLock) 虽然无害,但不改变任何可见性或互斥结论,只增加读者判断「两把锁各自保护什么」的成本。

建议: 保留 notificationLock 的串行化语义,把 addUpdateListener 中的 updateLock 收敛为只包裹 updateListeners 的注册(读取 currentConfig 直接走 AtomicReference),并在注释中明确 updateLock 的保护范围仅为「快照替换 + 订阅列表」以及与构造期 initializeConfigSources 的互斥。

Checklist: [6.1] KISS/YAGNI:无投机性抽象

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants