internal/rollout/bucketing.go
Rama main · réplica de lectura del repositorio original.
// Package rollout implements the deterministic device bucketing behind progressive
// rollouts. Manifest and asset resolution share these functions so the two paths can
// never disagree about which cohort a device belongs to. No per-device state is stored:
// the bucket is a salted hash of the persistent EAS-Client-ID.
package rollout
import (
"crypto/sha256"
"encoding/binary"
"math"
)
// Bucket maps a device to a stable position in [0, 1) for the given salt. An empty
// clientID returns 1.0 so a device that sends no EAS-Client-ID is never in a rollout.
func Bucket(clientID string, salt string) float64 {
if clientID == "" {
return 1.0
}
sum := sha256.Sum256([]byte(clientID + ":" + salt))
return float64(binary.BigEndian.Uint64(sum[:8])) / float64(math.MaxUint64)
}
// InBucket reports whether the device falls inside a rollout serving percentage% of
// devices. Monotonic in percentage: a device inside at N stays inside at every M > N,
// so increasing a rollout never flips an already-served device back out.
func InBucket(clientID string, salt string, percentage int) bool {
return Bucket(clientID, salt) < float64(percentage)/100
}
// UpdateSalt derives the fixed bucketing salt of a per-update rollout. Channel rollouts
// use the rollout row's UUID instead; the "update:" prefix keeps the two salt spaces
// structurally disjoint so the mechanisms draw independent cohorts.
func UpdateSalt(appId string, branchName string, runtimeVersion string, updateId string) string {
return "update:" + appId + ":" + branchName + ":" + runtimeVersion + ":" + updateId
}