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  }