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 }