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 }