github.com/yankunsam/loki/v2@v2.6.3-0.20220817130409-389df5235c27/pkg/storage/stores/indexshipper/compactor/retention/retention.go (about) 1 package retention 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "time" 9 10 "github.com/go-kit/log" 11 "github.com/go-kit/log/level" 12 "github.com/prometheus/client_golang/prometheus" 13 "github.com/prometheus/common/model" 14 "github.com/prometheus/prometheus/model/labels" 15 16 "github.com/grafana/loki/pkg/chunkenc" 17 "github.com/grafana/loki/pkg/storage/chunk" 18 "github.com/grafana/loki/pkg/storage/chunk/client" 19 util_log "github.com/grafana/loki/pkg/util/log" 20 ) 21 22 var chunkBucket = []byte("chunks") 23 24 const ( 25 markersFolder = "markers" 26 ) 27 28 type ChunkRef struct { 29 UserID []byte 30 SeriesID []byte 31 ChunkID []byte 32 From model.Time 33 Through model.Time 34 } 35 36 func (c ChunkRef) String() string { 37 return fmt.Sprintf("UserID: %s , SeriesID: %s , Time: [%s,%s]", c.UserID, c.SeriesID, c.From, c.Through) 38 } 39 40 type ChunkEntry struct { 41 ChunkRef 42 Labels labels.Labels 43 } 44 45 type ChunkEntryCallback func(ChunkEntry) (deleteChunk bool, err error) 46 47 type ChunkIterator interface { 48 ForEachChunk(ctx context.Context, callback ChunkEntryCallback) error 49 } 50 51 type SeriesCleaner interface { 52 // CleanupSeries is for cleaning up the series that do have any chunks left in the index. 53 // It would only be called for the series that have all their chunks deleted without adding new ones. 54 CleanupSeries(userID []byte, lbls labels.Labels) error 55 } 56 57 type chunkIndexer interface { 58 // IndexChunk is for indexing a new chunk that was built from an existing chunk while processing delete requests. 59 // It should return true if the chunk was indexed else false if not. 60 // The implementation could skip indexing a chunk due to it not belonging to the table. 61 // ToDo(Sandeep): We already have a check in the caller of IndexChunk to check if the chunk belongs to the table. 62 // See if we can drop the redundant check in the underlying implementation. 63 IndexChunk(chunk chunk.Chunk) (chunkIndexed bool, err error) 64 } 65 66 type IndexProcessor interface { 67 ChunkIterator 68 chunkIndexer 69 SeriesCleaner 70 } 71 72 var errNoChunksFound = errors.New("no chunks found in table, please check if there are really no chunks and manually drop the table or " + 73 "see if there is a bug causing us to drop whole index table") 74 75 type TableMarker interface { 76 // MarkForDelete marks chunks to delete for a given table and returns if it's empty or modified. 77 MarkForDelete(ctx context.Context, tableName, userID string, indexProcessor IndexProcessor, logger log.Logger) (bool, bool, error) 78 } 79 80 type Marker struct { 81 workingDirectory string 82 expiration ExpirationChecker 83 markerMetrics *markerMetrics 84 chunkClient client.Client 85 markTimeout time.Duration 86 } 87 88 func NewMarker(workingDirectory string, expiration ExpirationChecker, markTimeout time.Duration, chunkClient client.Client, r prometheus.Registerer) (*Marker, error) { 89 metrics := newMarkerMetrics(r) 90 return &Marker{ 91 workingDirectory: workingDirectory, 92 expiration: expiration, 93 markerMetrics: metrics, 94 chunkClient: chunkClient, 95 markTimeout: markTimeout, 96 }, nil 97 } 98 99 // MarkForDelete marks all chunks expired for a given table. 100 func (t *Marker) MarkForDelete(ctx context.Context, tableName, userID string, indexProcessor IndexProcessor, logger log.Logger) (bool, bool, error) { 101 start := time.Now() 102 status := statusSuccess 103 defer func() { 104 t.markerMetrics.tableProcessedDurationSeconds.WithLabelValues(tableName, status).Observe(time.Since(start).Seconds()) 105 level.Debug(logger).Log("msg", "finished to process table", "duration", time.Since(start)) 106 }() 107 level.Debug(logger).Log("msg", "starting to process table") 108 109 empty, modified, err := t.markTable(ctx, tableName, userID, indexProcessor, logger) 110 if err != nil { 111 status = statusFailure 112 return false, false, err 113 } 114 return empty, modified, nil 115 } 116 117 func (t *Marker) markTable(ctx context.Context, tableName, userID string, indexProcessor IndexProcessor, logger log.Logger) (bool, bool, error) { 118 markerWriter, err := NewMarkerStorageWriter(t.workingDirectory) 119 if err != nil { 120 return false, false, fmt.Errorf("failed to create marker writer: %w", err) 121 } 122 123 if ctx.Err() != nil { 124 return false, false, ctx.Err() 125 } 126 127 chunkRewriter := newChunkRewriter(t.chunkClient, tableName, indexProcessor) 128 129 empty, modified, err := markForDelete(ctx, t.markTimeout, tableName, markerWriter, indexProcessor, t.expiration, chunkRewriter, logger) 130 if err != nil { 131 return false, false, err 132 } 133 134 t.markerMetrics.tableMarksCreatedTotal.WithLabelValues(tableName).Add(float64(markerWriter.Count())) 135 if err := markerWriter.Close(); err != nil { 136 return false, false, fmt.Errorf("failed to close marker writer: %w", err) 137 } 138 139 if empty { 140 t.markerMetrics.tableProcessedTotal.WithLabelValues(tableName, userID, tableActionDeleted).Inc() 141 return empty, true, nil 142 } 143 if !modified { 144 t.markerMetrics.tableProcessedTotal.WithLabelValues(tableName, userID, tableActionNone).Inc() 145 return empty, modified, nil 146 } 147 t.markerMetrics.tableProcessedTotal.WithLabelValues(tableName, userID, tableActionModified).Inc() 148 return empty, modified, nil 149 } 150 151 func markForDelete( 152 ctx context.Context, 153 timeout time.Duration, 154 tableName string, 155 marker MarkerStorageWriter, 156 indexFile IndexProcessor, 157 expiration ExpirationChecker, 158 chunkRewriter *chunkRewriter, 159 logger log.Logger, 160 ) (bool, bool, error) { 161 seriesMap := newUserSeriesMap() 162 // tableInterval holds the interval for which the table is expected to have the chunks indexed 163 tableInterval := ExtractIntervalFromTableName(tableName) 164 empty := true 165 modified := false 166 now := model.Now() 167 chunksFound := false 168 169 // This is a fresh context so we know when deletes timeout vs something going 170 // wrong with the other context 171 iterCtx, cancel := ctxForTimeout(timeout) 172 defer cancel() 173 174 err := indexFile.ForEachChunk(iterCtx, func(c ChunkEntry) (bool, error) { 175 chunksFound = true 176 seriesMap.Add(c.SeriesID, c.UserID, c.Labels) 177 178 // see if the chunk is deleted completely or partially 179 if expired, nonDeletedIntervalFilters := expiration.Expired(c, now); expired { 180 if len(nonDeletedIntervalFilters) > 0 { 181 wroteChunks, err := chunkRewriter.rewriteChunk(ctx, c, tableInterval, nonDeletedIntervalFilters) 182 if err != nil { 183 return false, fmt.Errorf("failed to rewrite chunk %s for intervals %+v with error %s", c.ChunkID, nonDeletedIntervalFilters, err) 184 } 185 186 if wroteChunks { 187 // we have re-written chunk to the storage so the table won't be empty and the series are still being referred. 188 empty = false 189 seriesMap.MarkSeriesNotDeleted(c.SeriesID, c.UserID) 190 } 191 } 192 193 modified = true 194 195 // Mark the chunk for deletion only if it is completely deleted, or this is the last table that the chunk is index in. 196 // For a partially deleted chunk, if we delete the source chunk before all the tables which index it are processed then 197 // the retention would fail because it would fail to find it in the storage. 198 if len(nonDeletedIntervalFilters) == 0 || c.Through <= tableInterval.End { 199 if err := marker.Put(c.ChunkID); err != nil { 200 return false, err 201 } 202 } 203 return true, nil 204 } 205 206 // The chunk is not deleted, now see if we can drop its index entry based on end time from tableInterval. 207 // If chunk end time is after the end time of tableInterval, it means the chunk would also be indexed in the next table. 208 // We would now check if the end time of the tableInterval is out of retention period so that 209 // we can drop the chunk entry from this table without removing the chunk from the store. 210 if c.Through.After(tableInterval.End) { 211 if expiration.DropFromIndex(c, tableInterval.End, now) { 212 modified = true 213 return true, nil 214 } 215 } 216 217 empty = false 218 seriesMap.MarkSeriesNotDeleted(c.SeriesID, c.UserID) 219 return false, nil 220 }) 221 if err != nil { 222 if errors.Is(err, context.DeadlineExceeded) && errors.Is(iterCtx.Err(), context.DeadlineExceeded) { 223 // Deletes timed out. Don't return an error so compaction can continue and deletes can be retried 224 level.Warn(logger).Log("msg", "Timed out while running delete") 225 expiration.MarkPhaseTimedOut() 226 } else { 227 return false, false, err 228 } 229 } 230 231 if !chunksFound { 232 return false, false, errNoChunksFound 233 } 234 if empty { 235 return true, true, nil 236 } 237 if ctx.Err() != nil { 238 return false, false, ctx.Err() 239 } 240 241 return false, modified, seriesMap.ForEach(func(info userSeriesInfo) error { 242 if !info.isDeleted { 243 return nil 244 } 245 246 return indexFile.CleanupSeries(info.UserID(), info.lbls) 247 }) 248 } 249 250 func ctxForTimeout(t time.Duration) (context.Context, context.CancelFunc) { 251 if t == 0 { 252 return context.Background(), func() {} 253 } 254 return context.WithTimeout(context.Background(), t) 255 } 256 257 type ChunkClient interface { 258 DeleteChunk(ctx context.Context, userID, chunkID string) error 259 IsChunkNotFoundErr(err error) bool 260 } 261 262 type Sweeper struct { 263 markerProcessor MarkerProcessor 264 chunkClient ChunkClient 265 sweeperMetrics *sweeperMetrics 266 } 267 268 func NewSweeper(workingDir string, deleteClient ChunkClient, deleteWorkerCount int, minAgeDelete time.Duration, r prometheus.Registerer) (*Sweeper, error) { 269 m := newSweeperMetrics(r) 270 p, err := newMarkerStorageReader(workingDir, deleteWorkerCount, minAgeDelete, m) 271 if err != nil { 272 return nil, err 273 } 274 return &Sweeper{ 275 markerProcessor: p, 276 chunkClient: deleteClient, 277 sweeperMetrics: m, 278 }, nil 279 } 280 281 func (s *Sweeper) Start() { 282 s.markerProcessor.Start(func(ctx context.Context, chunkId []byte) error { 283 status := statusSuccess 284 start := time.Now() 285 defer func() { 286 s.sweeperMetrics.deleteChunkDurationSeconds.WithLabelValues(status).Observe(time.Since(start).Seconds()) 287 }() 288 chunkIDString := unsafeGetString(chunkId) 289 userID, err := getUserIDFromChunkID(chunkId) 290 if err != nil { 291 return err 292 } 293 294 err = s.chunkClient.DeleteChunk(ctx, unsafeGetString(userID), chunkIDString) 295 if s.chunkClient.IsChunkNotFoundErr(err) { 296 status = statusNotFound 297 level.Debug(util_log.Logger).Log("msg", "delete on not found chunk", "chunkID", chunkIDString) 298 return nil 299 } 300 if err != nil { 301 level.Error(util_log.Logger).Log("msg", "error deleting chunk", "chunkID", chunkIDString, "err", err) 302 status = statusFailure 303 } 304 return err 305 }) 306 } 307 308 func getUserIDFromChunkID(chunkID []byte) ([]byte, error) { 309 idx := bytes.IndexByte(chunkID, '/') 310 if idx <= 0 { 311 return nil, fmt.Errorf("invalid chunk ID %q", chunkID) 312 } 313 314 return chunkID[:idx], nil 315 } 316 317 func (s *Sweeper) Stop() { 318 s.markerProcessor.Stop() 319 } 320 321 type chunkRewriter struct { 322 chunkClient client.Client 323 tableName string 324 chunkIndexer chunkIndexer 325 } 326 327 func newChunkRewriter(chunkClient client.Client, tableName string, chunkIndexer chunkIndexer) *chunkRewriter { 328 return &chunkRewriter{ 329 chunkClient: chunkClient, 330 tableName: tableName, 331 chunkIndexer: chunkIndexer, 332 } 333 } 334 335 func (c *chunkRewriter) rewriteChunk(ctx context.Context, ce ChunkEntry, tableInterval model.Interval, intervalFilters []IntervalFilter) (bool, error) { 336 userID := unsafeGetString(ce.UserID) 337 chunkID := unsafeGetString(ce.ChunkID) 338 339 chk, err := chunk.ParseExternalKey(userID, chunkID) 340 if err != nil { 341 return false, err 342 } 343 344 chks, err := c.chunkClient.GetChunks(ctx, []chunk.Chunk{chk}) 345 if err != nil { 346 return false, err 347 } 348 349 if len(chks) != 1 { 350 return false, fmt.Errorf("expected 1 entry for chunk %s but found %d in storage", chunkID, len(chks)) 351 } 352 353 wroteChunks := false 354 355 for _, ivf := range intervalFilters { 356 start := ivf.Interval.Start 357 end := ivf.Interval.End 358 359 newChunkData, err := chks[0].Data.Rebound(start, end, ivf.Filter) 360 if err != nil { 361 if errors.Is(err, chunk.ErrSliceNoDataInRange) { 362 level.Info(util_log.Logger).Log("msg", "Rebound leaves an empty chunk", "chunk ref", string(ce.ChunkRef.ChunkID)) 363 // skip empty chunks 364 continue 365 } 366 return false, err 367 } 368 369 if start > tableInterval.End || end < tableInterval.Start { 370 continue 371 } 372 373 facade, ok := newChunkData.(*chunkenc.Facade) 374 if !ok { 375 return false, errors.New("invalid chunk type") 376 } 377 378 newChunk := chunk.NewChunk( 379 userID, chks[0].FingerprintModel(), chks[0].Metric, 380 facade, 381 start, 382 end, 383 ) 384 385 err = newChunk.Encode() 386 if err != nil { 387 return false, err 388 } 389 390 uploadChunk, err := c.chunkIndexer.IndexChunk(newChunk) 391 if err != nil { 392 return false, err 393 } 394 395 // upload chunk only if an entry was written 396 if uploadChunk { 397 err = c.chunkClient.PutChunks(ctx, []chunk.Chunk{newChunk}) 398 if err != nil { 399 return false, err 400 } 401 wroteChunks = true 402 } 403 } 404 405 return wroteChunks, nil 406 }