package server import ( "database/sql" "sync" "time" ) var ( // How often to run the user log compaction. compactionInterval = 600 * time.Second // How many log entries to keep when compacting. userKeepLogCount = 100 // How often to run the log pruner. pruneInterval = 24 * time.Hour // Delete entries older than this many days. pruneCutoffDays = 365 ) // Call a function at semi-regular intervals. type cronJob struct { f func() d time.Duration stopCh chan struct{} doneCh chan struct{} } func newCron(f func(), d time.Duration) *cronJob { j := &cronJob{ f: f, d: d, stopCh: make(chan struct{}), doneCh: make(chan struct{}), } go j.run() return j } // Stop the job and wait for it to exit. func (j *cronJob) Stop() { close(j.stopCh) <-j.doneCh } // Wait some time, or be interrupted by Stop(). func (j *cronJob) waitSomeTime() bool { t := time.NewTimer(j.d) defer t.Stop() select { case <-j.stopCh: return false case <-t.C: return true } } func (j *cronJob) run() { defer close(j.doneCh) for { if !j.waitSomeTime() { return } j.f() } } type usernameSet struct { mx sync.Mutex usernames map[string]struct{} } func newUsernameSet() *usernameSet { return &usernameSet{ usernames: make(map[string]struct{}), } } func (s *usernameSet) add(username string) { s.mx.Lock() s.usernames[username] = struct{}{} s.mx.Unlock() } func (s *usernameSet) foreach(f func(string)) { s.mx.Lock() for username := range s.usernames { delete(s.usernames, username) f(username) } s.mx.Unlock() } // Leisurely consolidate logs for users, but only when necessary, and // proceeding at a slow pace. We wake up once per minute, and clean up // the log history for users that have logged in since. func compactLogs(db *sql.DB, stmts statementMap, username string, count int) error { tx, err := db.Begin() if err != nil { return err } if _, err := stmts.get(tx, "consolidate_userlog").Exec(username, count); err != nil { return err } return tx.Commit() } // Remove old entries (both logs and devices) from the database. func pruneLogs(db *sql.DB, stmts statementMap) error { // Run each batch deletion in its own transaction, just to be nice. cutoff := time.Now().AddDate(0, 0, -pruneCutoffDays) for _, stmt := range []string{"prune_userlog", "prune_device_info"} { tx, err := db.Begin() if err != nil { return err } if _, err := stmts.get(tx, stmt).Exec(cutoff); err != nil { tx.Rollback() // nolint return err } if err := tx.Commit(); err != nil { return err } } return nil }