github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbfs/libpages/stats_file_based.go (about) 1 // Copyright 2018 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libpages 6 7 import ( 8 "context" 9 "database/sql" 10 11 "os" 12 "path/filepath" 13 "sort" 14 "time" 15 16 "github.com/keybase/client/go/kbfs/data" 17 "github.com/keybase/client/go/kbfs/ioutil" 18 "github.com/keybase/client/go/kbfs/tlf" 19 "go.uber.org/zap" 20 ) 21 22 type activity struct { 23 tlfID tlf.ID 24 host string 25 } 26 27 type fileBasedActivityStatsStorer struct { 28 root string 29 logger *zap.Logger 30 ch chan activity 31 } 32 33 const ( 34 dirnameTlfStamps = "tlf-stamps" 35 dirnameHostStamps = "host-stamps" 36 37 fbassChSize = 1000 38 fbassStoreInterval = time.Second * 10 39 ) 40 41 func (s *fileBasedActivityStatsStorer) processLoop() { 42 // We won't worry about cleaning up these two maps since this storer is 43 // meant to be only used for up to ~1000 entries anyway. 44 recentProcessedTlfs := make(map[tlf.ID]time.Time) 45 recentProcessedHosts := make(map[string]time.Time) 46 for a := range s.ch { 47 // The end result we want is a file with mtime set to now. os.Create 48 // uses the O_TRUNC flag which does that for existing files. 49 50 lastProcessed, ok := recentProcessedTlfs[a.tlfID] 51 if !ok || time.Since(lastProcessed) > fbassStoreInterval { 52 if f, err := os.Create(filepath.Join( 53 s.root, dirnameTlfStamps, a.tlfID.String())); err == nil { 54 f.Close() 55 recentProcessedTlfs[a.tlfID] = time.Now() 56 } else { 57 s.logger.Warn("os.Create", zap.Error(err)) 58 } 59 } 60 61 lastProcessed, ok = recentProcessedHosts[a.host] 62 if !ok || time.Since(lastProcessed) > fbassStoreInterval { 63 if f, err := os.Create(filepath.Join( 64 s.root, dirnameHostStamps, a.host)); err == nil { 65 f.Close() 66 recentProcessedHosts[a.host] = time.Now() 67 } else { 68 s.logger.Warn("os.Create", zap.Error(err)) 69 } 70 } 71 } 72 } 73 74 // NewFileBasedActivityStatsStorer creates an ActivityStatsStorer that stores 75 // activities on a local filesystem. 76 // 77 // NOTE that this is meant to be for development and 78 // testing only and does not scale well. 79 func NewFileBasedActivityStatsStorer( 80 rootPath string, logger *zap.Logger) (ActivityStatsStorer, error) { 81 err := os.MkdirAll(filepath.Join(rootPath, dirnameTlfStamps), os.ModeDir|0700) 82 if err != nil { 83 return nil, err 84 } 85 err = os.MkdirAll(filepath.Join(rootPath, dirnameHostStamps), os.ModeDir|0700) 86 if err != nil { 87 return nil, err 88 } 89 s := &fileBasedActivityStatsStorer{ 90 root: rootPath, 91 logger: logger, 92 ch: make(chan activity, fbassChSize), 93 } 94 go s.processLoop() 95 return s, nil 96 } 97 98 // RecordActives implement the ActivityStatsStorer interface. 99 func (s *fileBasedActivityStatsStorer) RecordActives(tlf tlf.ID, host string) { 100 s.ch <- activity{tlfID: tlf, host: host} 101 } 102 103 type fileinfoActivesGetter struct { 104 tlfs []os.FileInfo 105 hosts []os.FileInfo 106 sorted bool 107 } 108 109 func (g *fileinfoActivesGetter) GetActives( 110 dur time.Duration) (tlfs, hosts int, err error) { 111 if !g.sorted { 112 // Sort in decreasing order by time. 113 sort.Slice(g.tlfs, func(i int, j int) bool { 114 return g.tlfs[i].ModTime().After(g.tlfs[j].ModTime()) 115 }) 116 sort.Slice(g.hosts, func(i int, j int) bool { 117 return g.hosts[i].ModTime().After(g.hosts[j].ModTime()) 118 }) 119 g.sorted = true 120 } 121 cutoff := time.Now().Add(-dur) 122 // sort.Search requires a false,false...true,true... sequence. 123 tlfs = sort.Search(len(g.tlfs), func(i int) bool { 124 return cutoff.After(g.tlfs[i].ModTime()) 125 }) 126 hosts = sort.Search(len(g.hosts), func(i int) bool { 127 return cutoff.After(g.hosts[i].ModTime()) 128 }) 129 130 return tlfs, hosts, nil 131 } 132 133 // GetActivesGetter implement the ActivityStatsStorer interface. 134 func (s *fileBasedActivityStatsStorer) GetActivesGetter() ( 135 getter ActivesGetter, err error) { 136 tlfStamps, err := ioutil.ReadDir(filepath.Join(s.root, dirnameTlfStamps)) 137 if err != nil { 138 return nil, err 139 } 140 hostStamps, err := ioutil.ReadDir(filepath.Join(s.root, dirnameHostStamps)) 141 if err != nil { 142 return nil, err 143 } 144 return &fileinfoActivesGetter{ 145 tlfs: tlfStamps, 146 hosts: hostStamps, 147 }, nil 148 } 149 150 // MigrateActivityStatsStorerFromFileBasedToMySQL should only be used as part 151 // of a commandline tool. 152 func MigrateActivityStatsStorerFromFileBasedToMySQL( 153 logger *zap.Logger, fbRootDir string, mysqlDSN string) { 154 logger.Info("open mysql", zap.String("dsn", mysqlDSN)) 155 db, err := sql.Open("mysql", mysqlDSN) 156 if err != nil { 157 logger.Error("open mysql", zap.Error(err)) 158 return 159 } 160 logger.Info("create tables") 161 mysqlStorer := newMySQLActivityStatsStorerNoStart(data.WallClock{}, db, logger) 162 err = mysqlStorer.createTablesIfNotExists(context.Background()) 163 if err != nil { 164 logger.Error("create tables", zap.Error(err)) 165 return 166 } 167 168 logger.Info("tlf stamps") 169 tlfStamps, err := ioutil.ReadDir(filepath.Join(fbRootDir, dirnameTlfStamps)) 170 if err != nil { 171 logger.Error("ReadDir tlf stamps", zap.Error(err)) 172 return 173 } 174 for _, fi := range tlfStamps { 175 tlfID := tlf.ID{} 176 err := tlfID.UnmarshalText([]byte(fi.Name())) 177 if err != nil { 178 logger.Error("skipping stamp file", zap.String("filename", fi.Name()), zap.Error(err)) 179 continue 180 } 181 mysqlStorer.tlfs[tlfID] = fi.ModTime() 182 } 183 184 logger.Info("host stamps") 185 hostStamps, err := ioutil.ReadDir(filepath.Join(fbRootDir, dirnameHostStamps)) 186 if err != nil { 187 logger.Error("ReadDir host stamps", zap.Error(err)) 188 return 189 } 190 for _, fi := range hostStamps { 191 mysqlStorer.hosts[fi.Name()] = fi.ModTime() 192 } 193 194 logger.Info("flush inserts") 195 mysqlStorer.flushInserts() 196 }