Cohorts and rollout math¶
Rollouts in SimpleOTA are deterministic. Same (seed, device_id)
always produces the same cohort bucket. This means:
- Ramping
percentagefrom5→25only adds devices to the exposed set; it never re-rolls and demotes anyone. - Re-creating a deployment with the same seed reproduces the same decisions, useful for surgical retries.
- A device's exposure depends only on its
device_id(which you control) and the deployment's seed (which is auto-generated).
The function¶
def cohort_bucket(seed: str, device_id: str) -> int:
"""Return an integer in [0, 100)."""
blob = f"{seed}:{device_id}".encode()
digest = hashlib.sha256(blob).digest()
# Take the first 8 bytes as a big-endian unsigned int, mod 100.
return int.from_bytes(digest[:8], "big") % 100
def is_in_rollout(seed: str, device_id: str, percentage: int) -> bool:
return cohort_bucket(seed, device_id) < percentage
A device is "in the cohort" if cohort_bucket(...) < percentage.
Properties¶
- Uniform: over a large fleet,
cohort_bucketdistributes evenly across[0, 100). - Stable: fixing the seed, the same device always lands in the same bucket forever.
- Independent across deployments: a different seed re-rolls the bucket, so a device's exposure to deployment A says nothing about its exposure to deployment B.
Ramping safely¶
t=0 seed=S, percentage=5 → ~5% of fleet exposed (buckets 0..4)
t=1h seed=S, percentage=25 → adds buckets 5..24 (cumulative 25%)
t=4h seed=S, percentage=100 → all buckets included
Anyone exposed at t=0 is still exposed at t=1h; buckets 0..4 are
strictly inside buckets 0..24. Never change the seed mid-rollout or
this property breaks.
Worked example¶
>>> cohort_bucket("seed-abc", "esp32-0001")
17
>>> is_in_rollout("seed-abc", "esp32-0001", 5)
False
>>> is_in_rollout("seed-abc", "esp32-0001", 25)
True
Device esp32-0001 is in bucket 17. It's not in a 5% canary, is
in a 25% rollout, and stays exposed forever after that point (as long
as the seed doesn't change).