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

     1  package downloads
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"path/filepath"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/go-kit/log"
    12  	"github.com/go-kit/log/level"
    13  	"github.com/grafana/dskit/concurrency"
    14  	"github.com/pkg/errors"
    15  
    16  	"github.com/grafana/loki/pkg/storage/chunk/client/util"
    17  	"github.com/grafana/loki/pkg/storage/stores/indexshipper/index"
    18  	"github.com/grafana/loki/pkg/storage/stores/indexshipper/storage"
    19  	util_log "github.com/grafana/loki/pkg/util/log"
    20  	"github.com/grafana/loki/pkg/util/spanlogger"
    21  )
    22  
    23  // timeout for downloading initial files for a table to avoid leaking resources by allowing it to take all the time.
    24  const (
    25  	downloadTimeout        = 5 * time.Minute
    26  	maxDownloadConcurrency = 50
    27  )
    28  
    29  type Table interface {
    30  	Close()
    31  	ForEach(ctx context.Context, userID string, callback index.ForEachIndexCallback) error
    32  	DropUnusedIndex(ttl time.Duration, now time.Time) (bool, error)
    33  	Sync(ctx context.Context) error
    34  	EnsureQueryReadiness(ctx context.Context, userIDs []string) error
    35  }
    36  
    37  // table is a collection of multiple files created for a same table by various ingesters.
    38  // All the public methods are concurrency safe and take care of mutexes to avoid any data race.
    39  type table struct {
    40  	name              string
    41  	cacheLocation     string
    42  	storageClient     storage.Client
    43  	openIndexFileFunc index.OpenIndexFileFunc
    44  	metrics           *metrics
    45  
    46  	baseUserIndexSet, baseCommonIndexSet storage.IndexSet
    47  
    48  	logger       log.Logger
    49  	indexSets    map[string]IndexSet
    50  	indexSetsMtx sync.RWMutex
    51  }
    52  
    53  // NewTable just creates an instance of table without trying to load files from local storage or object store.
    54  // It is used for initializing table at query time.
    55  func NewTable(name, cacheLocation string, storageClient storage.Client, openIndexFileFunc index.OpenIndexFileFunc, metrics *metrics) Table {
    56  	table := table{
    57  		name:               name,
    58  		cacheLocation:      cacheLocation,
    59  		storageClient:      storageClient,
    60  		baseUserIndexSet:   storage.NewIndexSet(storageClient, true),
    61  		baseCommonIndexSet: storage.NewIndexSet(storageClient, false),
    62  		logger:             log.With(util_log.Logger, "table-name", name),
    63  		openIndexFileFunc:  openIndexFileFunc,
    64  		metrics:            metrics,
    65  		indexSets:          map[string]IndexSet{},
    66  	}
    67  
    68  	return &table
    69  }
    70  
    71  // LoadTable loads a table from local storage(syncs the table too if we have it locally) or downloads it from the shared store.
    72  // It is used for loading and initializing table at startup. It would initialize index sets which already had files locally.
    73  func LoadTable(name, cacheLocation string, storageClient storage.Client, openIndexFileFunc index.OpenIndexFileFunc, metrics *metrics) (Table, error) {
    74  	err := util.EnsureDirectory(cacheLocation)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	filesInfo, err := ioutil.ReadDir(cacheLocation)
    80  	if err != nil {
    81  		return nil, err
    82  	}
    83  
    84  	table := table{
    85  		name:               name,
    86  		cacheLocation:      cacheLocation,
    87  		storageClient:      storageClient,
    88  		baseUserIndexSet:   storage.NewIndexSet(storageClient, true),
    89  		baseCommonIndexSet: storage.NewIndexSet(storageClient, false),
    90  		logger:             log.With(util_log.Logger, "table-name", name),
    91  		indexSets:          map[string]IndexSet{},
    92  		openIndexFileFunc:  openIndexFileFunc,
    93  		metrics:            metrics,
    94  	}
    95  
    96  	level.Debug(table.logger).Log("msg", fmt.Sprintf("opening locally present files for table %s", name), "files", fmt.Sprint(filesInfo))
    97  
    98  	// common index files are outside the directories and user index files are in the directories
    99  	for _, fileInfo := range filesInfo {
   100  		if !fileInfo.IsDir() {
   101  			continue
   102  		}
   103  
   104  		userID := fileInfo.Name()
   105  		userIndexSet, err := NewIndexSet(name, userID, filepath.Join(cacheLocation, userID),
   106  			table.baseUserIndexSet, openIndexFileFunc, loggerWithUserID(table.logger, userID))
   107  		if err != nil {
   108  			return nil, err
   109  		}
   110  
   111  		err = userIndexSet.Init(false)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  
   116  		table.indexSets[userID] = userIndexSet
   117  	}
   118  
   119  	commonIndexSet, err := NewIndexSet(name, "", cacheLocation, table.baseCommonIndexSet,
   120  		openIndexFileFunc, table.logger)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	err = commonIndexSet.Init(false)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	table.indexSets[""] = commonIndexSet
   131  
   132  	return &table, nil
   133  }
   134  
   135  // Close Closes references to all the index.
   136  func (t *table) Close() {
   137  	t.indexSetsMtx.Lock()
   138  	defer t.indexSetsMtx.Unlock()
   139  
   140  	for _, userIndexSet := range t.indexSets {
   141  		userIndexSet.Close()
   142  	}
   143  
   144  	t.indexSets = map[string]IndexSet{}
   145  }
   146  
   147  func (t *table) ForEach(ctx context.Context, userID string, callback index.ForEachIndexCallback) error {
   148  	// iterate through both user and common index
   149  	for _, uid := range []string{userID, ""} {
   150  		indexSet, err := t.getOrCreateIndexSet(ctx, uid, true)
   151  		if err != nil {
   152  			return err
   153  		}
   154  
   155  		if indexSet.Err() != nil {
   156  			t.cleanupBrokenIndexSet(ctx, uid)
   157  			return indexSet.Err()
   158  		}
   159  
   160  		err = indexSet.ForEach(ctx, callback)
   161  		if err != nil {
   162  			return err
   163  		}
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  func (t *table) findExpiredIndexSets(ttl time.Duration, now time.Time) []string {
   170  	t.indexSetsMtx.RLock()
   171  	defer t.indexSetsMtx.RUnlock()
   172  
   173  	var expiredIndexSets []string
   174  	commonIndexSetExpired := false
   175  
   176  	for userID, userIndexSet := range t.indexSets {
   177  		lastUsedAt := userIndexSet.LastUsedAt()
   178  		if lastUsedAt.Add(ttl).Before(now) {
   179  			if userID == "" {
   180  				// add the userID for common index set at the end of the list to make sure it is the last one cleaned up
   181  				// because we remove directories containing the index sets which in case of common index is
   182  				// the parent directory of all the user index sets.
   183  				commonIndexSetExpired = true
   184  			} else {
   185  				expiredIndexSets = append(expiredIndexSets, userID)
   186  			}
   187  		}
   188  	}
   189  
   190  	// common index set should expire only after all the user index sets have expired.
   191  	if commonIndexSetExpired && len(expiredIndexSets) == len(t.indexSets)-1 {
   192  		expiredIndexSets = append(expiredIndexSets, "")
   193  	}
   194  
   195  	return expiredIndexSets
   196  }
   197  
   198  // DropUnusedIndex drops the index set if it has not been queried for at least ttl duration.
   199  // It returns true if the whole table gets dropped.
   200  func (t *table) DropUnusedIndex(ttl time.Duration, now time.Time) (bool, error) {
   201  	indexSetsToCleanup := t.findExpiredIndexSets(ttl, now)
   202  
   203  	if len(indexSetsToCleanup) > 0 {
   204  		t.indexSetsMtx.Lock()
   205  		defer t.indexSetsMtx.Unlock()
   206  		for _, userID := range indexSetsToCleanup {
   207  			// additional check for cleaning up the common index set when it is the only one left.
   208  			// This is just for safety because the index sets could change between findExpiredIndexSets and the actual cleanup.
   209  			if userID == "" && len(t.indexSets) != 1 {
   210  				level.Info(t.logger).Log("msg", "skipping cleanup of common index set because we possibly have unexpired user index sets left")
   211  				continue
   212  			}
   213  
   214  			level.Info(t.logger).Log("msg", fmt.Sprintf("cleaning up expired index set %s", userID))
   215  			err := t.indexSets[userID].DropAllDBs()
   216  			if err != nil {
   217  				return false, err
   218  			}
   219  
   220  			delete(t.indexSets, userID)
   221  		}
   222  
   223  		return len(t.indexSets) == 0, nil
   224  	}
   225  
   226  	return false, nil
   227  }
   228  
   229  // Sync downloads updated and new files from the storage relevant for the table and removes the deleted ones
   230  func (t *table) Sync(ctx context.Context) error {
   231  	level.Debug(t.logger).Log("msg", fmt.Sprintf("syncing files for table %s", t.name))
   232  
   233  	t.indexSetsMtx.RLock()
   234  	defer t.indexSetsMtx.RUnlock()
   235  
   236  	for userID, indexSet := range t.indexSets {
   237  		if err := indexSet.Sync(ctx); err != nil {
   238  			return errors.Wrap(err, fmt.Sprintf("failed to sync index set %s for table %s", userID, t.name))
   239  		}
   240  	}
   241  
   242  	return nil
   243  }
   244  
   245  // getOrCreateIndexSet gets or creates the index set for the userID.
   246  // If it does not exist, it creates a new one and initializes it in a goroutine.
   247  // Caller can use IndexSet.AwaitReady() to wait until the IndexSet gets ready, if required.
   248  // forQuerying must be set to true only getting the index for querying since
   249  // it captures the amount of time it takes to download the index at query time.
   250  func (t *table) getOrCreateIndexSet(ctx context.Context, id string, forQuerying bool) (IndexSet, error) {
   251  	t.indexSetsMtx.RLock()
   252  	indexSet, ok := t.indexSets[id]
   253  	t.indexSetsMtx.RUnlock()
   254  	if ok {
   255  		return indexSet, nil
   256  	}
   257  
   258  	t.indexSetsMtx.Lock()
   259  	defer t.indexSetsMtx.Unlock()
   260  
   261  	indexSet, ok = t.indexSets[id]
   262  	if ok {
   263  		return indexSet, nil
   264  	}
   265  
   266  	var err error
   267  	baseIndexSet := t.baseUserIndexSet
   268  	if id == "" {
   269  		baseIndexSet = t.baseCommonIndexSet
   270  	}
   271  
   272  	// instantiate the index set, add it to the map
   273  	indexSet, err = NewIndexSet(t.name, id, filepath.Join(t.cacheLocation, id), baseIndexSet, t.openIndexFileFunc,
   274  		loggerWithUserID(t.logger, id))
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	t.indexSets[id] = indexSet
   279  
   280  	// initialize the index set in async mode, it would be upto the caller to wait for its readiness using IndexSet.AwaitReady()
   281  	go func() {
   282  		if forQuerying {
   283  			start := time.Now()
   284  			defer func() {
   285  				duration := time.Since(start)
   286  				t.metrics.queryTimeTableDownloadDurationSeconds.WithLabelValues(t.name).Add(duration.Seconds())
   287  				logger := spanlogger.FromContextWithFallback(ctx, loggerWithUserID(t.logger, id))
   288  				level.Info(logger).Log("msg", "downloaded index set at query time", "duration", duration)
   289  			}()
   290  		}
   291  
   292  		err := indexSet.Init(forQuerying)
   293  		if err != nil {
   294  			level.Error(t.logger).Log("msg", fmt.Sprintf("failed to init user index set %s", id), "err", err)
   295  			t.cleanupBrokenIndexSet(ctx, id)
   296  		}
   297  	}()
   298  
   299  	return indexSet, nil
   300  }
   301  
   302  // cleanupBrokenIndexSet if an indexSet with given id exists and is really broken i.e Err() returns a non-nil error
   303  func (t *table) cleanupBrokenIndexSet(ctx context.Context, id string) {
   304  	t.indexSetsMtx.Lock()
   305  	defer t.indexSetsMtx.Unlock()
   306  
   307  	indexSet, ok := t.indexSets[id]
   308  	if !ok || indexSet.Err() == nil {
   309  		return
   310  	}
   311  
   312  	level.Error(util_log.WithContext(ctx, t.logger)).Log("msg", fmt.Sprintf("index set %s has some problem, cleaning it up", id), "err", indexSet.Err())
   313  	if err := indexSet.DropAllDBs(); err != nil {
   314  		level.Error(t.logger).Log("msg", fmt.Sprintf("failed to cleanup broken index set %s", id), "err", err)
   315  	}
   316  
   317  	delete(t.indexSets, id)
   318  }
   319  
   320  // EnsureQueryReadiness ensures that we have downloaded the common index as well as user index for the provided userIDs.
   321  // When ensuring query readiness for a table, we will always download common index set because it can include index for one of the provided user ids.
   322  func (t *table) EnsureQueryReadiness(ctx context.Context, userIDs []string) error {
   323  	commonIndexSet, err := t.getOrCreateIndexSet(ctx, "", false)
   324  	if err != nil {
   325  		return err
   326  	}
   327  	err = commonIndexSet.AwaitReady(ctx)
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	commonIndexSet.UpdateLastUsedAt()
   333  
   334  	missingUserIDs := make([]string, 0, len(userIDs))
   335  	t.indexSetsMtx.RLock()
   336  	for _, userID := range userIDs {
   337  		if userIndexSet, ok := t.indexSets[userID]; !ok {
   338  			missingUserIDs = append(missingUserIDs, userID)
   339  		} else {
   340  			userIndexSet.UpdateLastUsedAt()
   341  		}
   342  	}
   343  	t.indexSetsMtx.RUnlock()
   344  
   345  	return t.downloadUserIndexes(ctx, missingUserIDs)
   346  }
   347  
   348  // downloadUserIndexes downloads user specific index files concurrently.
   349  func (t *table) downloadUserIndexes(ctx context.Context, userIDs []string) error {
   350  	return concurrency.ForEachJob(ctx, len(userIDs), maxDownloadConcurrency, func(ctx context.Context, idx int) error {
   351  		indexSet, err := t.getOrCreateIndexSet(ctx, userIDs[idx], false)
   352  		if err != nil {
   353  			return err
   354  		}
   355  
   356  		return indexSet.AwaitReady(ctx)
   357  	})
   358  }
   359  
   360  func loggerWithUserID(logger log.Logger, userID string) log.Logger {
   361  	if userID == "" {
   362  		return logger
   363  	}
   364  
   365  	return log.With(logger, "user-id", userID)
   366  }