Files
iotlcm/worker-svr/internal/fullsync/contract/keys.go

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
}