github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/stores/indexshipper/downloads/table_manager.go (about)

     1  package downloads
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"path/filepath"
     8  	"regexp"
     9  	"strconv"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/go-kit/log/level"
    14  	"github.com/prometheus/client_golang/prometheus"
    15  	"github.com/prometheus/common/model"
    16  
    17  	"github.com/grafana/loki/pkg/storage/chunk/client/util"
    18  	"github.com/grafana/loki/pkg/storage/config"
    19  	"github.com/grafana/loki/pkg/storage/stores/indexshipper/index"
    20  	"github.com/grafana/loki/pkg/storage/stores/indexshipper/storage"
    21  	util_log "github.com/grafana/loki/pkg/util/log"
    22  	"github.com/grafana/loki/pkg/validation"
    23  )
    24  
    25  const (
    26  	cacheCleanupInterval = time.Hour
    27  	daySeconds           = int64(24 * time.Hour / time.Second)
    28  )
    29  
    30  // regexp for finding the trailing index bucket number at the end of table name
    31  var extractTableNumberRegex = regexp.MustCompile(`[0-9]+$`)
    32  
    33  type Limits interface {
    34  	AllByUserID() map[string]*validation.Limits
    35  	DefaultLimits() *validation.Limits
    36  }
    37  
    38  // IndexGatewayOwnsTenant is invoked by an IndexGateway instance and answers whether if the given tenant is assigned to this instance or not.
    39  //
    40  // It is only relevant by an IndexGateway in the ring mode and if it returns false for a given tenant, that tenant will be ignored by this IndexGateway during query readiness.
    41  type IndexGatewayOwnsTenant func(tenant string) bool
    42  
    43  type TableManager interface {
    44  	Stop()
    45  	ForEach(ctx context.Context, tableName, userID string, callback index.ForEachIndexCallback) error
    46  }
    47  
    48  type Config struct {
    49  	CacheDir          string
    50  	SyncInterval      time.Duration
    51  	CacheTTL          time.Duration
    52  	QueryReadyNumDays int
    53  	Limits            Limits
    54  }
    55  
    56  type tableManager struct {
    57  	cfg                 Config
    58  	openIndexFileFunc   index.OpenIndexFileFunc
    59  	indexStorageClient  storage.Client
    60  	tableRangesToHandle config.TableRanges
    61  
    62  	tables    map[string]Table
    63  	tablesMtx sync.RWMutex
    64  	metrics   *metrics
    65  
    66  	ctx    context.Context
    67  	cancel context.CancelFunc
    68  	wg     sync.WaitGroup
    69  
    70  	ownsTenant IndexGatewayOwnsTenant
    71  }
    72  
    73  func NewTableManager(cfg Config, openIndexFileFunc index.OpenIndexFileFunc, indexStorageClient storage.Client,
    74  	ownsTenantFn IndexGatewayOwnsTenant, tableRangesToHandle config.TableRanges, reg prometheus.Registerer) (TableManager, error) {
    75  	if err := util.EnsureDirectory(cfg.CacheDir); err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	ctx, cancel := context.WithCancel(context.Background())
    80  	tm := &tableManager{
    81  		cfg:                 cfg,
    82  		openIndexFileFunc:   openIndexFileFunc,
    83  		indexStorageClient:  indexStorageClient,
    84  		tableRangesToHandle: tableRangesToHandle,
    85  		ownsTenant:          ownsTenantFn,
    86  		tables:              make(map[string]Table),
    87  		metrics:             newMetrics(reg),
    88  		ctx:                 ctx,
    89  		cancel:              cancel,
    90  	}
    91  
    92  	// load the existing tables first.
    93  	err := tm.loadLocalTables()
    94  	if err != nil {
    95  		// call Stop to close open file references.
    96  		tm.Stop()
    97  		return nil, err
    98  	}
    99  
   100  	// download the missing tables.
   101  	err = tm.ensureQueryReadiness(ctx)
   102  	if err != nil {
   103  		// call Stop to close open file references.
   104  		tm.Stop()
   105  		return nil, err
   106  	}
   107  
   108  	go tm.loop()
   109  	return tm, nil
   110  }
   111  
   112  func (tm *tableManager) loop() {
   113  	tm.wg.Add(1)
   114  	defer tm.wg.Done()
   115  
   116  	syncTicker := time.NewTicker(tm.cfg.SyncInterval)
   117  	defer syncTicker.Stop()
   118  
   119  	cacheCleanupTicker := time.NewTicker(cacheCleanupInterval)
   120  	defer cacheCleanupTicker.Stop()
   121  
   122  	for {
   123  		select {
   124  		case <-syncTicker.C:
   125  			err := tm.syncTables(tm.ctx)
   126  			if err != nil {
   127  				level.Error(util_log.Logger).Log("msg", "error syncing local boltdb files with storage", "err", err)
   128  			}
   129  
   130  			// we need to keep ensuring query readiness to download every days new table which would otherwise be downloaded only during queries.
   131  			err = tm.ensureQueryReadiness(tm.ctx)
   132  			if err != nil {
   133  				level.Error(util_log.Logger).Log("msg", "error ensuring query readiness of tables", "err", err)
   134  			}
   135  		case <-cacheCleanupTicker.C:
   136  			err := tm.cleanupCache()
   137  			if err != nil {
   138  				level.Error(util_log.Logger).Log("msg", "error cleaning up expired tables", "err", err)
   139  			}
   140  		case <-tm.ctx.Done():
   141  			return
   142  		}
   143  	}
   144  }
   145  
   146  func (tm *tableManager) Stop() {
   147  	tm.cancel()
   148  	tm.wg.Wait()
   149  
   150  	tm.tablesMtx.Lock()
   151  	defer tm.tablesMtx.Unlock()
   152  
   153  	for _, table := range tm.tables {
   154  		table.Close()
   155  	}
   156  }
   157  
   158  func (tm *tableManager) ForEach(ctx context.Context, tableName, userID string, callback index.ForEachIndexCallback) error {
   159  	table, err := tm.getOrCreateTable(tableName)
   160  	if err != nil {
   161  		return err
   162  	}
   163  	return table.ForEach(ctx, userID, callback)
   164  }
   165  
   166  func (tm *tableManager) getOrCreateTable(tableName string) (Table, error) {
   167  	// if table is already there, use it.
   168  	tm.tablesMtx.RLock()
   169  	table, ok := tm.tables[tableName]
   170  	tm.tablesMtx.RUnlock()
   171  
   172  	if !ok {
   173  		tm.tablesMtx.Lock()
   174  		defer tm.tablesMtx.Unlock()
   175  
   176  		// check if some other competing goroutine got the lock before us and created the table, use it if so.
   177  		table, ok = tm.tables[tableName]
   178  		if !ok {
   179  			// table not found, creating one.
   180  			level.Info(util_log.Logger).Log("msg", fmt.Sprintf("downloading all files for table %s", tableName))
   181  
   182  			tablePath := filepath.Join(tm.cfg.CacheDir, tableName)
   183  			err := util.EnsureDirectory(tablePath)
   184  			if err != nil {
   185  				return nil, err
   186  			}
   187  
   188  			table = NewTable(tableName, filepath.Join(tm.cfg.CacheDir, tableName), tm.indexStorageClient, tm.openIndexFileFunc, tm.metrics)
   189  			tm.tables[tableName] = table
   190  		}
   191  	}
   192  
   193  	return table, nil
   194  }
   195  
   196  func (tm *tableManager) syncTables(ctx context.Context) error {
   197  	tm.tablesMtx.RLock()
   198  	defer tm.tablesMtx.RUnlock()
   199  
   200  	start := time.Now()
   201  	var err error
   202  
   203  	defer func() {
   204  		status := statusSuccess
   205  		if err != nil {
   206  			status = statusFailure
   207  		}
   208  
   209  		tm.metrics.tablesSyncOperationTotal.WithLabelValues(status).Inc()
   210  		tm.metrics.tablesDownloadOperationDurationSeconds.Set(time.Since(start).Seconds())
   211  	}()
   212  
   213  	level.Info(util_log.Logger).Log("msg", "syncing tables")
   214  
   215  	for _, table := range tm.tables {
   216  		err := table.Sync(ctx)
   217  		if err != nil {
   218  			return err
   219  		}
   220  	}
   221  
   222  	return nil
   223  }
   224  
   225  func (tm *tableManager) cleanupCache() error {
   226  	tm.tablesMtx.Lock()
   227  	defer tm.tablesMtx.Unlock()
   228  
   229  	level.Info(util_log.Logger).Log("msg", "cleaning tables cache")
   230  
   231  	for name, table := range tm.tables {
   232  		level.Info(util_log.Logger).Log("msg", fmt.Sprintf("cleaning up expired table %s", name))
   233  		isEmpty, err := table.DropUnusedIndex(tm.cfg.CacheTTL, time.Now())
   234  		if err != nil {
   235  			return err
   236  		}
   237  
   238  		if isEmpty {
   239  			delete(tm.tables, name)
   240  		}
   241  	}
   242  
   243  	return nil
   244  }
   245  
   246  // ensureQueryReadiness compares tables required for being query ready with the tables we already have and downloads the missing ones.
   247  func (tm *tableManager) ensureQueryReadiness(ctx context.Context) error {
   248  	start := time.Now()
   249  	distinctUsers := make(map[string]struct{})
   250  
   251  	defer func() {
   252  		level.Info(util_log.Logger).Log("msg", "query readiness setup completed", "duration", time.Since(start), "distinct_users_len", len(distinctUsers))
   253  	}()
   254  
   255  	activeTableNumber := getActiveTableNumber()
   256  
   257  	// find the largest query readiness number
   258  	largestQueryReadinessNum := tm.cfg.QueryReadyNumDays
   259  	if defaultLimits := tm.cfg.Limits.DefaultLimits(); defaultLimits.QueryReadyIndexNumDays > largestQueryReadinessNum {
   260  		largestQueryReadinessNum = defaultLimits.QueryReadyIndexNumDays
   261  	}
   262  
   263  	queryReadinessNumByUserID := make(map[string]int)
   264  	for userID, limits := range tm.cfg.Limits.AllByUserID() {
   265  		if limits.QueryReadyIndexNumDays != 0 {
   266  			queryReadinessNumByUserID[userID] = limits.QueryReadyIndexNumDays
   267  			if limits.QueryReadyIndexNumDays > largestQueryReadinessNum {
   268  				largestQueryReadinessNum = limits.QueryReadyIndexNumDays
   269  			}
   270  		}
   271  	}
   272  
   273  	// return early if no table has to be downloaded for query readiness
   274  	if largestQueryReadinessNum == 0 {
   275  		return nil
   276  	}
   277  
   278  	tables, err := tm.indexStorageClient.ListTables(ctx)
   279  	if err != nil {
   280  		return err
   281  	}
   282  
   283  	for _, tableName := range tables {
   284  		tableNumber, err := extractTableNumberFromName(tableName)
   285  		if err != nil {
   286  			return err
   287  		}
   288  
   289  		if tableNumber == -1 || !tm.tableRangesToHandle.TableNumberInRange(tableNumber) {
   290  			continue
   291  		}
   292  
   293  		// continue if the table is not within query readiness
   294  		if activeTableNumber-tableNumber > int64(largestQueryReadinessNum) {
   295  			continue
   296  		}
   297  
   298  		// list the users that have dedicated index files for this table
   299  		operationStart := time.Now()
   300  		_, usersWithIndex, err := tm.indexStorageClient.ListFiles(ctx, tableName, false)
   301  		if err != nil {
   302  			return err
   303  		}
   304  		listFilesDuration := time.Since(operationStart)
   305  
   306  		// find the users whos index we need to keep ready for querying from this table
   307  		usersToBeQueryReadyFor := tm.findUsersInTableForQueryReadiness(tableNumber, usersWithIndex, queryReadinessNumByUserID)
   308  
   309  		// continue if both user index and common index is not required to be downloaded for query readiness
   310  		if len(usersToBeQueryReadyFor) == 0 && activeTableNumber-tableNumber > int64(tm.cfg.QueryReadyNumDays) {
   311  			continue
   312  		}
   313  
   314  		operationStart = time.Now()
   315  		table, err := tm.getOrCreateTable(tableName)
   316  		if err != nil {
   317  			return err
   318  		}
   319  		createTableDuration := time.Since(operationStart)
   320  
   321  		for _, u := range usersToBeQueryReadyFor {
   322  			distinctUsers[u] = struct{}{}
   323  		}
   324  
   325  		operationStart = time.Now()
   326  		if err := table.EnsureQueryReadiness(ctx, usersToBeQueryReadyFor); err != nil {
   327  			return err
   328  		}
   329  		ensureQueryReadinessDuration := time.Since(operationStart)
   330  
   331  		level.Info(util_log.Logger).Log(
   332  			"msg", "index pre-download for query readiness completed",
   333  			"users_len", len(usersToBeQueryReadyFor),
   334  			"query_readiness_duration", ensureQueryReadinessDuration,
   335  			"table", tableName,
   336  			"create_table_duration", createTableDuration,
   337  			"list_files_duration", listFilesDuration,
   338  		)
   339  	}
   340  
   341  	return nil
   342  }
   343  
   344  // findUsersInTableForQueryReadiness returns the users that needs their index to be query ready based on the tableNumber and
   345  // query readiness number provided per user
   346  func (tm *tableManager) findUsersInTableForQueryReadiness(tableNumber int64, usersWithIndexInTable []string,
   347  	queryReadinessNumByUserID map[string]int) []string {
   348  	activeTableNumber := getActiveTableNumber()
   349  	usersToBeQueryReadyFor := []string{}
   350  
   351  	for _, userID := range usersWithIndexInTable {
   352  		// use the query readiness config for the user if it exists or use the default config
   353  		queryReadyNumDays, ok := queryReadinessNumByUserID[userID]
   354  		if !ok {
   355  			queryReadyNumDays = tm.cfg.Limits.DefaultLimits().QueryReadyIndexNumDays
   356  		}
   357  
   358  		if queryReadyNumDays == 0 {
   359  			continue
   360  		}
   361  
   362  		if tm.ownsTenant != nil && !tm.ownsTenant(userID) {
   363  			continue
   364  		}
   365  
   366  		if activeTableNumber-tableNumber <= int64(queryReadyNumDays) {
   367  			usersToBeQueryReadyFor = append(usersToBeQueryReadyFor, userID)
   368  		}
   369  	}
   370  
   371  	return usersToBeQueryReadyFor
   372  }
   373  
   374  // loadLocalTables loads tables present locally.
   375  func (tm *tableManager) loadLocalTables() error {
   376  	filesInfo, err := ioutil.ReadDir(tm.cfg.CacheDir)
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	for _, fileInfo := range filesInfo {
   382  		if !fileInfo.IsDir() {
   383  			continue
   384  		}
   385  
   386  		tableNumber, err := extractTableNumberFromName(fileInfo.Name())
   387  		if err != nil {
   388  			return err
   389  		}
   390  		if tableNumber == -1 || !tm.tableRangesToHandle.TableNumberInRange(tableNumber) {
   391  			continue
   392  		}
   393  
   394  		level.Info(util_log.Logger).Log("msg", fmt.Sprintf("loading local table %s", fileInfo.Name()))
   395  
   396  		table, err := LoadTable(fileInfo.Name(), filepath.Join(tm.cfg.CacheDir, fileInfo.Name()),
   397  			tm.indexStorageClient, tm.openIndexFileFunc, tm.metrics)
   398  		if err != nil {
   399  			return err
   400  		}
   401  
   402  		tm.tables[fileInfo.Name()] = table
   403  	}
   404  
   405  	return nil
   406  }
   407  
   408  // extractTableNumberFromName extract the table number from a given tableName.
   409  // if the tableName doesn't match the regex, it would return -1 as table number.
   410  func extractTableNumberFromName(tableName string) (int64, error) {
   411  	match := extractTableNumberRegex.Find([]byte(tableName))
   412  	if match == nil {
   413  		return -1, nil
   414  	}
   415  
   416  	tableNumber, err := strconv.ParseInt(string(match), 10, 64)
   417  	if err != nil {
   418  		return -1, err
   419  	}
   420  
   421  	return tableNumber, nil
   422  }
   423  func getActiveTableNumber() int64 {
   424  	return getTableNumberForTime(model.Now())
   425  }
   426  
   427  func getTableNumberForTime(t model.Time) int64 {
   428  	return t.Unix() / daySeconds
   429  }