Skip to content

feat: make feature store same-project registration deterministic and … - #270

Merged
bruceding merged 2 commits into
masterfrom
features/featurestore-deterministic-registration
Nov 5, 2026
Merged

bruceding merged 2 commits into
masterfrom
features/featurestore-deterministic-registration

Conversation

@symphony09

@symphony09 symphony09 commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

feat: FeatureStore 数据源确定性注册 + 同项目冲突告警

概述

让 FeatureStore 数据源注册结果确定化,并在多份 FeatureStoreConfs 指向同一 project 且字段冲突时打警告,消除冷重启生效配置随机翻转。

动机

fs.Load() 原先按 Go map 随机序遍历配置;底层 SDK 连接池注册表按服务端 datasource 名 first-wins,同 project 多份配置时谁先注册谁生效,随机序导致生效配置跨重启不可复现。

改动(persist/fs/featurestore.go)

  • 收集所有配置键 sort.Strings 后按字典序注册:同 project 固定由字典序最靠前的配置名生效,跨重启稳定。
  • firstConfs[ProjectName] 记录每个 project 的实际生效配置;后续同 project 配置经 diffFeatureStoreConfs(反射逐字段 reflect.DeepEqual)比对,有差异则 log.Warning 指明生效配置名。只比对/打印字段名,绝不打印值(含凭证)。

说明

  • 排序只保证结果可预测,不消除冲突本身;根治仍需配置收敛(同 project 只留一个键或对齐 DSN 相关字段),告警用于暴露问题。
  • ReloadProject 路径未改,确定性由首次 Load 的排序注册保证。

测试

  • GOOS=linux go build ./persist/fs/ 与 go vet 通过。
  • diffFeatureStoreConfs 经临时程序核实:相同配置→空 diff,冲突配置→正确返回差异字段名。

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@symphony09
symphony09 requested a review from bruceding November 4, 2026 08:26

@bruceding bruceding 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.

Thanks for the fix — sorting the conf names does make the cold-start winner deterministic. However, the firstConfs bookkeeping breaks on config hot reloads (see inline comment). Since fs.Load is re-invoked by configloader.reloadConfig while the SDK's connection-pool registry is process-level and first-registration-wins, the warning can credit the opposite config of the one actually in effect. Suggested fix + test case in the inline comment.

Comment thread persist/fs/featurestore.go Outdated
conf := config.FeatureStoreConfs[name]
if firstName, ok := firstConfs[conf.ProjectName]; ok {
if diffFields := diffFeatureStoreConfs(config.FeatureStoreConfs[firstName], conf); len(diffFields) > 0 {
log.Warning(fmt.Sprintf("module=FeatureStore\tname=%q\tmsg=project %s is registered by multiple FeatureStoreConfs, the config of %q is used",

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.

Bug: this warning reports the wrong winning config after a hot reload.

firstConfs is a local variable rebuilt on every Load call, but the state it models — the SDK's connection-pool registry (per the comment at L65-70) — is process-level with first-registration-wins semantics, and fs.Load is invoked repeatedly on config hot reloads (configloader.reloadConfig → fs.Load). Already-loaded confs short-circuit at L94-97 via ReloadProject(), which only re-fetches the project and never re-applies WithHologresLogin / WithHologresPort / WithFeatureDBLogin — so the pool parameters registered at first client creation stay in effect for the lifetime of the process.

Concrete failure timeline:

  1. Cold start with only b = {ProjectName: p, HologresUsername: u2}. b creates the client → the SDK registers project p's pool with u2's credentials.
  2. Hot reload adds a = {ProjectName: p, HologresUsername: u1} ("a" sorts before "b"). In this Load run, the fresh firstConfs records a as the first registrant; a's client creation silently gets b's existing pool (u1 discarded). Then for b, this warning fires:
    project p is registered by multiple FeatureStoreConfs, the config of "a" is used
    — the exact opposite of reality: u2 (registered by b at cold start) is what's actually in effect.
  3. Variant: if the originally-registering conf is later deleted from the config, its credentials silently keep working while the warning credits a conf that never registered anything in this process.

Because the warning concerns credential/port attribution, an operator following it would rotate/inspect the wrong account. It also contradicts the PR's stated goal ("stable and reproducible") which then only holds at cold start.

Suggested fix: track the first actual registration per project at package level (same lifetime as fsInstances, guarded by the already-held fsLoadMu), recording it only in the branch that truly creates a client (L99-148), and compare/warn against that record instead of this run's sort order:

// package-level, guarded by fsLoadMu (held for the whole Load):
// var firstRegisteredPerProject = make(map[string]string)

for _, name := range names {
    conf := config.FeatureStoreConfs[name]
    if firstName, ok := firstRegisteredPerProject[conf.ProjectName]; ok && firstName != name {
        if diffFields := diffFeatureStoreConfs(config.FeatureStoreConfs[firstName], conf); len(diffFields) > 0 {
            log.Warning(... "the config of %q is used" ... firstName)
        }
    }
    // ... existing fsInstances short-circuit ...
    // ... create client ...
    fsInstancesMu.Lock()
    fsInstances[name] = m
    fsInstancesMu.Unlock()
    if _, ok := firstRegisteredPerProject[conf.ProjectName]; !ok {
        firstRegisteredPerProject[conf.ProjectName] = name // recorded only on real registration
    }
}

Test suggestion: load z first, then hot-reload with an added conflicting conf a that sorts earlier; assert the warning still names z as the config in use.

Minor (non-blocking, same lines): diffFields is computed but discarded — the helper's doc comment confirms field names are safe to log, so consider including them in the message (differing fields %v) to make the warning actionable, or simplify the helper to return a bool.

…lict warning

Drop the sorted-key deterministic registration: Load() iterates config.FeatureStoreConfs in its natural map order again. Keep the same-project duplicate detection (diffFeatureStoreConfs) and log.Warning, reworded to state the effective config is non-deterministic across restarts and advise converging the configs, since registration order is no longer forced. Removes the now-unused sort import.
@bruceding
bruceding merged commit 12e2dce into master Nov 5, 2026
3 of 5 checks passed
@bruceding
bruceding deleted the features/featurestore-deterministic-registration branch November 5, 2026 03:42
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.

3 participants