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  }