fix(playlists): pin candidate order before jitter to make daily builds deterministic
scoreAndSortCandidates drew per-candidate jitter by slice position, but the candidate query (LoadRadioCandidatesV2) has ORDER BY random() arms and no stable outer ordering, so DB row order varies call-to-call. When the recency spread between candidates is smaller than the ±jitter (small or recency-clustered libraries), two same-day rebuilds assigned jitter to different tracks and reordered near-ties — so the build was not actually deterministic-within-a-day as documented. Pre-existing latent flake in TestBuildSystemPlaylists_DailyNonceDeterminism (passed in isolation / by luck in CI; deterministically reproduced when the system-build tests run in sequence). Confirmed independent of the You-might-like change by neutralizing buildYouMightLike — the flake persisted. Fix: sort the candidate slice by track id before assigning jitter, so the jitter for a track is a function of (track, day) alone, independent of DB return order. Verified: full playlists package green 4/4 and the build-test sequence green 5/5 (was 0/4 before). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
package playlists
|
package playlists
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
@@ -180,8 +181,21 @@ func scoreAndSortCandidates(cands []recommendation.Candidate, userID pgtype.UUID
|
|||||||
c recommendation.Candidate
|
c recommendation.Candidate
|
||||||
score float64
|
score float64
|
||||||
}
|
}
|
||||||
pairs := make([]scored, len(cands))
|
// Pin candidate order by track id before drawing jitter. The candidate
|
||||||
for i, c := range cands {
|
// query (LoadRadioCandidatesV2) has ORDER BY random() arms and no
|
||||||
|
// stable outer ordering, so DB row order varies call-to-call. Since
|
||||||
|
// the i-th candidate gets the i-th seeded jitter draw, an unstable
|
||||||
|
// input order would assign different jitter to the same track across
|
||||||
|
// same-day rebuilds and reorder near-ties — breaking the daily
|
||||||
|
// determinism this function promises. Sorting by id first makes the
|
||||||
|
// jitter assignment a function of (track, day) alone.
|
||||||
|
ordered := make([]recommendation.Candidate, len(cands))
|
||||||
|
copy(ordered, cands)
|
||||||
|
sort.SliceStable(ordered, func(i, j int) bool {
|
||||||
|
return uuidLessPL(ordered[i].Track.ID, ordered[j].Track.ID)
|
||||||
|
})
|
||||||
|
pairs := make([]scored, len(ordered))
|
||||||
|
for i, c := range ordered {
|
||||||
pairs[i] = scored{c: c, score: recommendation.Score(c.Inputs, systemMixWeights, now, rng.Float64)}
|
pairs[i] = scored{c: c, score: recommendation.Score(c.Inputs, systemMixWeights, now, rng.Float64)}
|
||||||
}
|
}
|
||||||
sort.SliceStable(pairs, func(i, j int) bool {
|
sort.SliceStable(pairs, func(i, j int) bool {
|
||||||
@@ -709,6 +723,12 @@ func insertSystemPlaylist(ctx context.Context, qtx *dbq.Queries, userID pgtype.U
|
|||||||
// (pgtype.UUID's String method exists on some pgx versions but not others;
|
// (pgtype.UUID's String method exists on some pgx versions but not others;
|
||||||
// also the playlists package needs this independent of test helpers.)
|
// also the playlists package needs this independent of test helpers.)
|
||||||
// Suffix `PL` distinguishes it from any test helper named uuidString.
|
// Suffix `PL` distinguishes it from any test helper named uuidString.
|
||||||
|
// uuidLessPL reports whether a sorts before b by raw 16-byte value. Used
|
||||||
|
// to pin candidate order deterministically before jitter assignment.
|
||||||
|
func uuidLessPL(a, b pgtype.UUID) bool {
|
||||||
|
return bytes.Compare(a.Bytes[:], b.Bytes[:]) < 0
|
||||||
|
}
|
||||||
|
|
||||||
func uuidStringPL(u pgtype.UUID) string {
|
func uuidStringPL(u pgtype.UUID) string {
|
||||||
if !u.Valid {
|
if !u.Valid {
|
||||||
return "<nil>"
|
return "<nil>"
|
||||||
|
|||||||
Reference in New Issue
Block a user