Skip to content

Commit 31d56c2

Browse files
make the codes smaller
1 parent 3a577f0 commit 31d56c2

File tree

2 files changed

+148
-7
lines changed

2 files changed

+148
-7
lines changed

internal/rendezvous/codes.go

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ type CodeEntry struct {
2222
}
2323

2424
type Store struct {
25-
mu sync.RWMutex
26-
codes map[string]*CodeEntry // code -> entry
27-
appToCode map[string]string // appID -> code
28-
pregen chan string // optional pregen pool
29-
ttl time.Duration
30-
words []string
25+
mu sync.RWMutex
26+
codes map[string]*CodeEntry // code -> entry
27+
appToCode map[string]string // appID -> code
28+
pregen chan string // optional pregen pool
29+
ttl time.Duration
30+
words []string
31+
perm []int // permutation of [0..len(words)-1], set once
32+
singleIdx int // rotating cursor for single-word picks
33+
pairA, pairB int // rotating cursors for two-word pairs
3134
}
3235

3336
func NewStore(words []string, ttl time.Duration, pregenCap int) *Store {
@@ -40,6 +43,19 @@ func NewStore(words []string, ttl time.Duration, pregenCap int) *Store {
4043
ttl: ttl,
4144
words: words,
4245
}
46+
if n := len(words); n > 0 {
47+
s.perm = make([]int, n)
48+
for i := 0; i < n; i++ {
49+
s.perm[i] = i
50+
}
51+
// Fisher–Yates with crypto/rand
52+
for i := n - 1; i > 0; i-- {
53+
j, err := randInt(i + 1)
54+
if err == nil && j != i {
55+
s.perm[i], s.perm[j] = s.perm[j], s.perm[i]
56+
}
57+
}
58+
}
4359
if pregenCap > 0 {
4460
s.pregen = make(chan string, pregenCap)
4561
go s.pregenerator()
@@ -177,6 +193,84 @@ func (s *Store) newUniqueCode() (string, error) {
177193
}
178194

179195
func (s *Store) generateCode() (string, error) {
196+
// We need to see what's currently allocated, and advance our fair cursors.
197+
s.mu.Lock()
198+
defer s.mu.Unlock()
199+
200+
// 1) Try single-word pool.
201+
if code, ok := s.pickSingleLocked(); ok {
202+
return code, nil
203+
}
204+
// 2) All single words are currently allocated -> try two-word pool.
205+
if code, ok := s.pickPairLocked(); ok {
206+
return code, nil
207+
}
208+
// 3) Extremely unlikely to get here. Fall back to legacy generator (4 words + 4 digits).
209+
return s.generateLegacyUnlocked()
210+
}
211+
212+
// pickSingleLocked returns an unused single-word code if available.
213+
// Requires s.mu to be held (write lock) because it advances s.singleIdx.
214+
func (s *Store) pickSingleLocked() (string, bool) {
215+
n := len(s.words)
216+
if n == 0 {
217+
return "", false
218+
}
219+
if len(s.perm) != n {
220+
// Safety: build a basic permutation if not set (shouldn't happen).
221+
s.perm = make([]int, n)
222+
for i := range s.perm {
223+
s.perm[i] = i
224+
}
225+
}
226+
for tried := 0; tried < n; tried++ {
227+
idx := s.perm[(s.singleIdx+tried)%n]
228+
w := s.words[idx]
229+
if _, inUse := s.codes[w]; !inUse {
230+
// Reserve this candidate by advancing the cursor; actual insertion happens in CreateCode.
231+
s.singleIdx = (s.singleIdx + tried + 1) % n
232+
return w, true
233+
}
234+
}
235+
return "", false
236+
}
237+
238+
// pickPairLocked returns an unused two-word code "w1-w2" if available.
239+
// Requires s.mu to be held (write lock) because it advances s.pairA/B.
240+
func (s *Store) pickPairLocked() (string, bool) {
241+
n := len(s.words)
242+
if n == 0 {
243+
return "", false
244+
}
245+
if len(s.perm) != n {
246+
s.perm = make([]int, n)
247+
for i := range s.perm {
248+
s.perm[i] = i
249+
}
250+
}
251+
// Scan n*n pairs in a rotating manner.
252+
for scanned := 0; scanned < n*n; scanned++ {
253+
a := s.words[s.perm[s.pairA]]
254+
b := s.words[s.perm[s.pairB]]
255+
code := a + "-" + b
256+
257+
// Move pairB then roll into pairA
258+
s.pairB++
259+
if s.pairB >= n {
260+
s.pairB = 0
261+
s.pairA = (s.pairA + 1) % n
262+
}
263+
264+
if _, inUse := s.codes[code]; !inUse {
265+
return code, true
266+
}
267+
}
268+
return "", false
269+
}
270+
271+
// generateLegacyUnlocked reproduces the old style code (4 words + 4 digits).
272+
// Requires s.mu to be held (write lock) only because we share randInt; it doesn't mutate s.
273+
func (s *Store) generateLegacyUnlocked() (string, error) {
180274
const wordsPerCode = 4
181275
if len(s.words) < 2 {
182276
return "", errors.New("wordlist too small")
@@ -194,7 +288,7 @@ func (s *Store) generateCode() (string, error) {
194288
return "", err
195289
}
196290
parts = append(parts, fmt.Sprintf("%04d", d))
197-
return strings.Join(parts, "-"), nil
291+
return strings.ToLower(strings.Join(parts, "-")), nil
198292
}
199293

200294
func randInt(n int) (int, error) {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package rendezvous
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
)
8+
9+
func Test_SingleThenPairs(t *testing.T) {
10+
words := []string{"alpha", "bravo", "charlie"}
11+
s := NewStore(words, 5*time.Minute, 0)
12+
13+
seen := map[string]bool{}
14+
var singles int
15+
16+
// Allocate more than len(words) to force pairs.
17+
for i := 0; i < len(words)+2; i++ {
18+
code, appID, _, err := s.CreateCode(context.Background())
19+
if err != nil {
20+
t.Fatalf("create: %v", err)
21+
}
22+
if seen[code] {
23+
t.Fatalf("duplicate code: %s", code)
24+
}
25+
seen[code] = true
26+
if appID == "" {
27+
t.Fatalf("missing appID")
28+
}
29+
if countHyphens(code) == 0 {
30+
singles++
31+
}
32+
}
33+
34+
if singles != len(words) {
35+
t.Fatalf("expected %d singles first, got %d", len(words), singles)
36+
}
37+
}
38+
39+
func countHyphens(s string) int {
40+
n := 0
41+
for _, r := range s {
42+
if r == '-' {
43+
n++
44+
}
45+
}
46+
return n
47+
}

0 commit comments

Comments
 (0)