Files
iotlcm/internal/fullsync/contract/keys.go
T
MiFaZhan 2bec257289 chore: 初始开源发布
iotlcm-worker-svr:异步执行中枢(RabbitMQ 消费者 + 全量同步编排)。

- Go module:iotlcm-worker-svr → git.5iot.com/iotlcm-public/iotlcm-worker-svr(214 文件)
- 凭据与内部标识脱敏:MySQL/Redis 凭据、私网与生产 IP、内网域名、
  库名 xiujie_admin → iotlcm、哨兵名占位
- 159 处真实手机号 → 合法 11 位编造号(保留号段与后 4 位,维持互异,
  不破坏「校验 MSISDN 格式」等测试断言)
- 移除内部资产:CI 触发器、Jenkins 触发测试文档、预演门禁与镜像
  digest 记录
- Dockerfile 改用公共基础镜像(golang:1.25-alpine → alpine:3.23),
  移除写死的 git 凭据
- README 重写(架构/全量同步模型/配置/已知失败),补 Apache-2.0
  LICENSE,LF 行尾 + .gitattributes

已知:3 个测试包在源仓同一提交上同样失败(client/upstream、
service/allocate、service/inbound),属既有缺陷,本仓未引入亦未修复。
2026-10-22 16:37:56 +08:00

117 lines
4.2 KiB
Go

package contract
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
)
const (
DefaultFlowPrefix = "flow:"
DefaultSimStatusPrefix = "sim-status:"
)
// Keys is a naming contract only. It does not create indexes or storage.
// Existing run names are byte-for-byte compatible with zset.Store. Global
// run/archive indexes are outside run hash slots and cannot join run-local Lua.
type Keys struct{ prefix string }
func NewKeys(prefix string) (Keys, error) {
if strings.TrimSpace(prefix) == "" {
prefix = DefaultFlowPrefix
}
if strings.ContainsAny(prefix, "{}") {
return Keys{}, invalid("prefix must not contain Redis hash tags")
}
return Keys{prefix: prefix}, nil
}
// The zero value uses the legacy flow prefix, just like NewKeys("").
func (k Keys) Prefix() string {
if k.prefix == "" {
return DefaultFlowPrefix
}
return k.prefix
}
func (k Keys) runKey(runID, suffix string) (string, error) {
if err := ValidateID(runID); err != nil {
return "", err
}
return k.Prefix() + "{" + runID + "}:" + suffix, nil
}
func (k Keys) RunMetaKey(runID string) (string, error) { return k.runKey(runID, "meta") }
func (k Keys) RunChannelsKey(runID string) (string, error) { return k.runKey(runID, "channels") }
func (k Keys) TaskStateKey(runID string) (string, error) { return k.runKey(runID, "task-state") }
func (k Keys) FailedKey(runID string) (string, error) { return k.runKey(runID, "failed") }
func (k Keys) ArchiveStateKey(runID string) (string, error) { return k.runKey(runID, "archive") }
func (k Keys) roundKey(runID string, round int, suffix string) (string, error) {
if round < 1 {
return "", invalid("round must be positive")
}
return k.runKey(runID, fmt.Sprintf("round:%d:%s", round, suffix))
}
func (k Keys) RoundStatsKey(runID string, round int) (string, error) {
return k.roundKey(runID, round, "stats")
}
func (k Keys) roundChannelKey(runID string, round int, kind, chanKey string) (string, error) {
if strings.TrimSpace(chanKey) == "" {
return "", invalid("chanKey is empty")
}
return k.roundKey(runID, round, kind+":"+chanKey)
}
func (k Keys) RoundActiveKey(runID string, round int, chanKey string) (string, error) {
return k.roundChannelKey(runID, round, "active", chanKey)
}
func (k Keys) RoundWaitingKey(runID string, round int, chanKey string) (string, error) {
return k.roundChannelKey(runID, round, "waiting", chanKey)
}
func (k Keys) RoundLeaseKey(runID string, round int, chanKey string) (string, error) {
return k.roundChannelKey(runID, round, "lease", chanKey)
}
func (k Keys) FailedRunsKey() string { return k.Prefix() + "failed:runs" }
func (k Keys) ActiveRunsKey() string { return k.Prefix() + "runs:active" }
func (k Keys) AllRunsKey() string { return k.Prefix() + "runs:all" }
func (k Keys) PendingArchivesKey() string { return k.Prefix() + "archive:pending" }
// Replay action state, leases, idempotency reservations and pending index share
// a dedicated slot, allowing atomic acceptance/discovery. Source/target run
// writes remain in their own slots and require a recoverable replay protocol.
func (k Keys) replayKey(actionID, suffix string) (string, error) {
if err := ValidateID(actionID); err != nil {
return "", err
}
return k.Prefix() + "{replay}:action:" + actionID + ":" + suffix, nil
}
func (k Keys) ReplayActionKey(actionID string) (string, error) { return k.replayKey(actionID, "meta") }
func (k Keys) ReplayLeaseKey(actionID string) (string, error) { return k.replayKey(actionID, "lease") }
func (k Keys) PendingReplayActionsKey() string { return k.Prefix() + "{replay}:pending" }
// Hash the complete tuple so neither delimiter collisions nor arbitrary
// Idempotency-Key characters can change the Redis key or hash slot.
func (k Keys) ReplayIdempotencyKey(sourceRunID, idempotencyKey string) (string, error) {
if err := ValidateID(sourceRunID); err != nil {
return "", err
}
if err := ValidateIdempotencyKey(idempotencyKey); err != nil {
return "", err
}
data, err := json.Marshal([2]string{sourceRunID, idempotencyKey})
if err != nil {
return "", invalid("cannot encode idempotency scope: %v", err)
}
digest := sha256.Sum256(data)
return k.Prefix() + "{replay}:idempotency:" + hex.EncodeToString(digest[:]), nil
}