mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-21 14:19:18 +08:00
fix(outbox-dedup): buildSchedulerGroupPayload typed-nil broke dedup_key consistency
#3255 introduced payload-aware dedup_key (sha256 over event_type, account_id, group_id, payload_json). enqueueSchedulerOutbox checks "if payload != nil" before json.Marshal — but Go interface containing a typed-nil map (returned by buildSchedulerGroupPayload(empty)) is NOT == nil at the interface level. So an ungrouped account's account_changed event went through this path: payload := buildSchedulerGroupPayload(account.GroupIDs) // typed-nil map enqueueSchedulerOutbox(..., payload) // interface != nil → json.Marshal(typedNilMap) = "null" → dedup_key hash = sha256(... + "null") While other call sites pass literal nil: enqueueSchedulerOutbox(..., nil) // interface == nil → payloadJSON stays empty → dedup_key hash = sha256(... + "") The two dedup_keys differ for what should be the same logical event, silently degrading dedup effectiveness in bursts on ungrouped accounts. Fix: change buildSchedulerGroupPayload return type from map[string]any to any so empty input returns true untyped-nil. All call sites pass the result straight to enqueueSchedulerOutbox(payload any) — no inspection, no breakage. Adds regression test TestEnqueueSchedulerOutbox_UngroupedAccountDedupesWithLiteralNilPayload asserting (1) typed-nil regression doesn't sneak back, (2) dedup_key for empty-groups payload matches the literal-nil-payload key.
This commit is contained in:
@@ -1882,7 +1882,11 @@ func mergeGroupIDs(a []int64, b []int64) []int64 {
|
||||
return out
|
||||
}
|
||||
|
||||
func buildSchedulerGroupPayload(groupIDs []int64) map[string]any {
|
||||
// buildSchedulerGroupPayload 构造 EventAccountChanged / EventAccountGroupsChanged
|
||||
// 事件的 payload。空 groupIDs 必须返回 untyped nil(any 而非 map[string]any(nil)),
|
||||
// 否则 enqueueSchedulerOutbox 的 "payload != nil" 接口判空会被 typed-nil 欺骗,
|
||||
// 把 payload marshal 成 "null" 写入 dedup_key 哈希,破坏与其他 nil-payload 调用的去重一致性。
|
||||
func buildSchedulerGroupPayload(groupIDs []int64) any {
|
||||
if len(groupIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -90,3 +90,29 @@ func TestSchedulerOutboxRepositoryTryAcquireCleanupLockUnavailable(t *testing.T)
|
||||
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// buildSchedulerGroupPayload 在 groupIDs 为空时必须返回 untyped nil(any),
|
||||
// 否则 enqueueSchedulerOutbox 的 "payload != nil" 接口判空会被 typed-nil 欺骗,
|
||||
// 把 payload marshal 成 "null" 写入 dedup_key 哈希,破坏与其他 nil-payload
|
||||
// 调用的去重一致性。本测试用 ungrouped 账号场景验证两条路径的 dedup_key 一致。
|
||||
func TestEnqueueSchedulerOutbox_UngroupedAccountDedupesWithLiteralNilPayload(t *testing.T) {
|
||||
accountID := int64(42)
|
||||
|
||||
// Path A: 显式 nil payload(如 SetError、SetStatus 等调用模式)
|
||||
keyLiteralNil := schedulerOutboxDedupKey("account_changed", &accountID, nil, nil)
|
||||
|
||||
// Path B: buildSchedulerGroupPayload(account.GroupIDs) 当账号没有任何分组
|
||||
emptyGroupsPayload := buildSchedulerGroupPayload(nil)
|
||||
require.Nil(t, emptyGroupsPayload,
|
||||
"buildSchedulerGroupPayload(empty) must return untyped-nil any to avoid typed-nil marshal")
|
||||
|
||||
// 模拟 enqueueSchedulerOutbox 内部的判空逻辑
|
||||
var payloadJSON []byte
|
||||
if emptyGroupsPayload != nil {
|
||||
t.Fatalf("typed-nil regression: buildSchedulerGroupPayload(empty) interface should be nil")
|
||||
}
|
||||
keyEmptyGroups := schedulerOutboxDedupKey("account_changed", &accountID, nil, payloadJSON)
|
||||
|
||||
require.Equal(t, keyLiteralNil, keyEmptyGroups,
|
||||
"ungrouped-account account_changed must share dedup_key with other nil-payload variants")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user