cmd/golink: add stat loading and saving to DB

add new DB.LoadStats and DB.SaveStats methods.  DB.SaveSaveStats takes
an incremental count of clicks since the last call.  This is a noop for
FileDB (the full count is still saved in the Link struct on disk), but
will allow for the SqliteDB to store stats in a separate table with
timestamps to display only recent clicks.

Change-Id: I18c7db871854923b873ae100c844940242a32bb3
This commit is contained in:
Will Norris
2022-06-13 17:40:48 -07:00
parent 02e9d55070
commit 0e1d350bb9
2 changed files with 60 additions and 28 deletions
+45
View File
@@ -20,6 +20,10 @@ type Link struct {
Clicks int `json:",omitempty"` // number of times this link has been served
}
// ClickStats is the number of clicks a set of links have received in a given
// time period. It is keyed by link short name, with values of total clicks.
type ClickStats map[string]int
// DB provides storage for Links.
type DB interface {
// LoadAll returns all stored Links.
@@ -36,6 +40,14 @@ type DB interface {
// Save saves a Link.
Save(*Link) error
// LoadStats returns click stats for links.
LoadStats() (ClickStats, error)
// SaveStats records click stats for links. The provided map includes
// incremental clicks that have occurred since the last time SaveStats
// was called.
SaveStats(ClickStats) error
}
// FileDB stores Links in JSON files on disk.
@@ -120,3 +132,36 @@ func (f *FileDB) Save(link *Link) error {
}
return nil
}
func (f *FileDB) LoadStats() (ClickStats, error) {
links, err := db.LoadAll()
if err != nil {
return nil, err
}
stats := make(ClickStats)
for _, link := range links {
if link.Clicks > 0 {
stats[link.Short] = link.Clicks
}
}
return stats, nil
}
func (f *FileDB) SaveStats(stats ClickStats) error {
for short, clicks := range stats {
if clicks <= 0 {
continue
}
link, err := f.Load(short)
if err != nil {
return err
}
link.Clicks += clicks
if err := f.Save(link); err != nil {
return err
}
}
return nil
}
+15 -28
View File
@@ -38,10 +38,10 @@ var (
var stats struct {
mu sync.Mutex
clicks map[string]int // short link -> number of times visited
clicks ClickStats // short link -> number of times visited
// dirty identifies short links with clicks that have not yet been stored.
dirty map[string]bool
// dirty identifies short link clicks that have not yet been stored.
dirty ClickStats
}
//go:embed link-snapshot.json
@@ -140,20 +140,16 @@ func init() {
// initStats initializes the in-memory stats counter with counts from db.
func initStats() error {
links, err := db.LoadAll()
stats.mu.Lock()
defer stats.mu.Unlock()
clicks, err := db.LoadStats()
if err != nil {
return err
}
stats.mu.Lock()
defer stats.mu.Unlock()
stats.clicks = make(map[string]int)
for _, link := range links {
if link.Clicks > 0 {
stats.clicks[link.Short] = link.Clicks
}
}
stats.clicks = clicks
stats.dirty = make(ClickStats)
return nil
}
@@ -163,19 +159,10 @@ func flushStats() error {
stats.mu.Lock()
defer stats.mu.Unlock()
for short := range stats.dirty {
link, err := db.Load(short)
if err != nil {
return err
}
if link.Clicks != stats.clicks[short] {
link.Clicks = stats.clicks[short]
if err := db.Save(link); err != nil {
return err
}
}
delete(stats.dirty, short)
if err := db.SaveStats(stats.dirty); err != nil {
return err
}
stats.dirty = make(ClickStats)
return nil
}
@@ -260,13 +247,13 @@ func serveGo(w http.ResponseWriter, r *http.Request) {
stats.mu.Lock()
if stats.clicks == nil {
stats.clicks = make(map[string]int)
stats.clicks = make(ClickStats)
}
stats.clicks[link.Short]++
if stats.dirty == nil {
stats.dirty = make(map[string]bool)
stats.dirty = make(ClickStats)
}
stats.dirty[link.Short] = true
stats.dirty[link.Short]++
stats.mu.Unlock()
target, err := expandLink(link.Long, expandEnv{Now: time.Now().UTC(), Path: remainder})