Skip to content

Commit e44aef8

Browse files
committed
server: record a games completed stat
Record a game completed statistic and persist it across process restarts by storing it in the local kv store.
1 parent e197874 commit e44aef8

File tree

4 files changed

+149
-24
lines changed

4 files changed

+149
-24
lines changed

cmd/codenames/main.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,12 @@ func main() {
3333
}
3434
log.Printf("[STARTUP] Opening pebble db from directory: %s\n", dir)
3535

36-
db, err := pebble.Open(dir, nil)
36+
db, err := pebble.Open(dir, &pebble.Options{
37+
Merger: &pebble.Merger{
38+
Merge: codenames.PebbleMerge,
39+
Name: "codenameskv",
40+
},
41+
})
3742
if err != nil {
3843
fmt.Fprintf(os.Stderr, "pebble.Open: %s\n", err)
3944
os.Exit(1)

game.go

+17-6
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ type Game struct {
131131
type GameOptions struct {
132132
TimerDurationMS int64 `json:"timer_duration_ms,omitempty"`
133133
EnforceTimer bool `json:"enforce_timer,omitempty"`
134+
Hooks Hooks `json:"-"`
135+
}
136+
137+
type Hooks struct {
138+
Complete func()
134139
}
135140

136141
func (g *Game) StateID() string {
@@ -154,12 +159,10 @@ func (g *Game) checkWinningCondition() {
154159
}
155160
}
156161
if !redRemaining {
157-
winners := Red
158-
g.WinningTeam = &winners
162+
g.win(Red)
159163
}
160164
if !blueRemaining {
161-
winners := Blue
162-
g.WinningTeam = &winners
165+
g.win(Blue)
163166
}
164167
}
165168

@@ -178,6 +181,11 @@ func (g *Game) NextTurn(currentTurn int) bool {
178181
return true
179182
}
180183

184+
func (g *Game) win(team Team) {
185+
g.WinningTeam = &team
186+
g.Hooks.Complete()
187+
}
188+
181189
func (g *Game) Guess(idx int) error {
182190
if idx > len(g.Layout) || idx < 0 {
183191
return fmt.Errorf("index %d is invalid", idx)
@@ -189,8 +197,7 @@ func (g *Game) Guess(idx int) error {
189197
g.Revealed[idx] = true
190198

191199
if g.Layout[idx] == Black {
192-
winners := g.currentTeam().Other()
193-
g.WinningTeam = &winners
200+
g.win(g.currentTeam().Other())
194201
return nil
195202
}
196203

@@ -215,6 +222,10 @@ func newGame(id string, state GameState, opts GameOptions) *Game {
215222
// distinct randomness across games with same seed
216223
randRnd := rand.New(rand.NewSource(state.Seed * int64(state.PermIndex+1)))
217224

225+
if opts.Hooks.Complete == nil {
226+
opts.Hooks.Complete = func() {}
227+
}
228+
218229
game := &Game{
219230
ID: id,
220231
CreatedAt: time.Now(),

server.go

+48-16
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package codenames
33
import (
44
"crypto/subtle"
55
"encoding/json"
6+
"fmt"
67
"html/template"
78
"io"
89
"log"
@@ -31,18 +32,22 @@ type Server struct {
3132

3233
tpl *template.Template
3334
gameIDWords []string
35+
hooks Hooks
3436

3537
mu sync.Mutex
3638
games map[string]*GameHandle
3739
defaultWords []string
3840
mux *http.ServeMux
3941

40-
statOpenRequests int64 // atomic access
41-
statTotalRequests int64 //atomic access
42+
statGamesCompleted int64 // atomic access
43+
statOpenRequests int64 // atomic access
44+
statTotalRequests int64 // atomic access
4245
}
4346

4447
type Store interface {
4548
Save(*Game) error
49+
CounterAdd(string, int64) error
50+
GetCounter(statPrefix string) (int64, error)
4651
}
4752

4853
type GameHandle struct {
@@ -131,7 +136,7 @@ func (s *Server) getGameLocked(gameID string) (*GameHandle, bool) {
131136
if ok {
132137
return gh, ok
133138
}
134-
gh = newHandle(newGame(gameID, randomState(s.defaultWords), GameOptions{}), s.Store)
139+
gh = newHandle(newGame(gameID, randomState(s.defaultWords), GameOptions{Hooks: s.hooks}), s.Store)
135140
s.games[gameID] = gh
136141
return gh, true
137142
}
@@ -151,7 +156,7 @@ func (s *Server) handleGameState(rw http.ResponseWriter, req *http.Request) {
151156
s.mu.Lock()
152157
gh, ok := s.getGameLocked(body.GameID)
153158
if !ok {
154-
gh = newHandle(newGame(body.GameID, randomState(s.defaultWords), GameOptions{}), s.Store)
159+
gh = newHandle(newGame(body.GameID, randomState(s.defaultWords), GameOptions{Hooks: s.hooks}), s.Store)
155160
s.games[body.GameID] = gh
156161
s.mu.Unlock()
157162
writeGame(rw, gh)
@@ -273,6 +278,7 @@ func (s *Server) handleNextGame(rw http.ResponseWriter, req *http.Request) {
273278
opts := GameOptions{
274279
TimerDurationMS: request.TimerDurationMS,
275280
EnforceTimer: request.EnforceTimer,
281+
Hooks: s.hooks,
276282
}
277283

278284
var ok bool
@@ -297,19 +303,18 @@ func (s *Server) handleNextGame(rw http.ResponseWriter, req *http.Request) {
297303
}
298304

299305
type statsResponse struct {
300-
GamesTotal int `json:"games_total"`
301-
GamesInProgress int `json:"games_in_progress"`
302-
GamesCreatedOneHour int `json:"games_created_1h"`
303-
RequestsTotal int64 `json:"requests_total_process_lifetime"`
304-
RequestsInFlight int64 `json:"requests_in_flight"`
306+
GamesCompleted int64 `json:"games_completed"`
307+
MemGamesTotal int `json:"mem_games_total"`
308+
MemGamesInProgress int `json:"mem_games_in_progress"`
309+
MemGamesCreatedOneHour int `json:"mem_games_created_1h"`
310+
RequestsTotal int64 `json:"requests_total_process_lifetime"`
311+
RequestsInFlight int64 `json:"requests_in_flight"`
305312
}
306313

307314
func (s *Server) handleStats(rw http.ResponseWriter, req *http.Request) {
308315
hourAgo := time.Now().Add(-time.Hour)
309316

310317
s.mu.Lock()
311-
defer s.mu.Unlock()
312-
313318
var inProgress, createdWithinAnHour int
314319
for _, gh := range s.games {
315320
gh.mu.Lock()
@@ -321,12 +326,23 @@ func (s *Server) handleStats(rw http.ResponseWriter, req *http.Request) {
321326
}
322327
gh.mu.Unlock()
323328
}
329+
s.mu.Unlock()
330+
331+
// Sum up the count of games completed that's on disk and in-memory.
332+
diskGamesCompleted, err := s.Store.GetCounter("games/completed/")
333+
if err != nil {
334+
http.Error(rw, err.Error(), 400)
335+
return
336+
}
337+
memGamesCompleted := atomic.LoadInt64(&s.statGamesCompleted)
338+
324339
writeJSON(rw, statsResponse{
325-
GamesTotal: len(s.games),
326-
GamesInProgress: inProgress,
327-
GamesCreatedOneHour: createdWithinAnHour,
328-
RequestsTotal: atomic.LoadInt64(&s.statTotalRequests),
329-
RequestsInFlight: atomic.LoadInt64(&s.statOpenRequests),
340+
GamesCompleted: diskGamesCompleted + memGamesCompleted,
341+
MemGamesTotal: len(s.games),
342+
MemGamesInProgress: inProgress,
343+
MemGamesCreatedOneHour: createdWithinAnHour,
344+
RequestsTotal: atomic.LoadInt64(&s.statTotalRequests),
345+
RequestsInFlight: atomic.LoadInt64(&s.statOpenRequests),
330346
})
331347
}
332348

@@ -384,8 +400,11 @@ func (s *Server) Start(games map[string]*Game) error {
384400
s.Store = discardStore{}
385401
}
386402

403+
s.hooks.Complete = func() { atomic.AddInt64(&s.statGamesCompleted, 1) }
404+
387405
if games != nil {
388406
for _, g := range games {
407+
g.GameOptions.Hooks = s.hooks
389408
s.games[g.ID] = newHandle(g, s.Store)
390409
}
391410
}
@@ -396,6 +415,19 @@ func (s *Server) Start(games map[string]*Game) error {
396415
}
397416
}()
398417

418+
// Periodically persist some in-memory stats.
419+
go func() {
420+
const hourFormat = "06010215"
421+
for range time.Tick(time.Minute) {
422+
hourKey := time.Now().UTC().Format(hourFormat) + "utc"
423+
v := atomic.LoadInt64(&s.statGamesCompleted)
424+
if v > 0 {
425+
atomic.AddInt64(&s.statGamesCompleted, -v)
426+
s.Store.CounterAdd(fmt.Sprintf("games/completed/%s", hourKey), v)
427+
}
428+
}
429+
}()
430+
399431
return s.Server.ListenAndServe()
400432
}
401433

store.go

+78-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package codenames
22

33
import (
4+
"bytes"
5+
"encoding/binary"
46
"encoding/json"
57
"fmt"
68
"math"
@@ -62,6 +64,79 @@ func (ps *PebbleStore) Save(g *Game) error {
6264
return err
6365
}
6466

67+
func (ps *PebbleStore) CounterAdd(stat string, v int64) error {
68+
var b [binary.MaxVarintLen64]byte
69+
n := binary.PutVarint(b[:], v)
70+
71+
k := fmt.Sprintf("/stats/counters/%s", stat)
72+
return ps.DB.Merge([]byte(k), b[:n], nil)
73+
}
74+
75+
func (ps *PebbleStore) GetCounter(statPrefix string) (int64, error) {
76+
prefix := []byte(fmt.Sprintf("/stats/counters/%s", statPrefix))
77+
78+
iter := ps.DB.NewIter(nil)
79+
iter.SeekGE(prefix)
80+
81+
var sum int64
82+
for ; iter.Valid() && bytes.HasPrefix(iter.Key(), prefix); iter.Next() {
83+
rawV := iter.Value()
84+
v, n := binary.Varint(rawV)
85+
if n < 0 {
86+
return 0, fmt.Errorf("unable to read stat value: %v for key %q", rawV, iter.Key())
87+
}
88+
sum += v
89+
}
90+
err := iter.Error()
91+
if closeErr := iter.Close(); closeErr != nil {
92+
err = closeErr
93+
}
94+
95+
return sum, err
96+
}
97+
98+
// PebbleMerge implements the pebble.Merge function type.
99+
func PebbleMerge(k, v []byte) (pebble.ValueMerger, error) {
100+
vInt, n := binary.Varint(v)
101+
if n < 0 {
102+
return nil, fmt.Errorf("unable to read merge value: %v", v)
103+
}
104+
//if bytes.HasPrefix(k, []byte("/stats/counters/")) {
105+
return &addValueMerger{v: vInt}, nil
106+
//}
107+
//return nil, fmt.Errorf("unrecognized merge key: %s", pretty.Sprint(k))
108+
}
109+
110+
// addValueMerger implements pebble.ValueMerger by interpreting values as a
111+
// signed varint and adding its operands.
112+
type addValueMerger struct {
113+
v int64
114+
}
115+
116+
func (m *addValueMerger) MergeNewer(value []byte) error {
117+
v, n := binary.Varint(value)
118+
if n < 0 {
119+
return fmt.Errorf("unable to read merge value: %v", value)
120+
}
121+
m.v = m.v + v
122+
return nil
123+
}
124+
125+
func (m *addValueMerger) MergeOlder(value []byte) error {
126+
v, n := binary.Varint(value)
127+
if n < 0 {
128+
return fmt.Errorf("unable to read merge value: %v", value)
129+
}
130+
m.v = m.v + v
131+
return nil
132+
}
133+
134+
func (m *addValueMerger) Finish() ([]byte, error) {
135+
b := make([]byte, binary.MaxVarintLen64)
136+
n := binary.PutVarint(b, m.v)
137+
return b[:n], nil
138+
}
139+
65140
func gameKV(g *Game) (key, value []byte, err error) {
66141
value, err = json.Marshal(g)
67142
if err != nil {
@@ -80,4 +155,6 @@ func mkkey(unixSecs int64, id string) []byte {
80155

81156
type discardStore struct{}
82157

83-
func (ds discardStore) Save(*Game) error { return nil }
158+
func (_ discardStore) Save(*Game) error { return nil }
159+
func (_ discardStore) GetCounter(string) (int64, error) { return 0, nil }
160+
func (_ discardStore) CounterAdd(string, int64) error { return nil }

0 commit comments

Comments
 (0)