github.com/grafana/pyroscope@v1.18.0/pkg/phlaredb/block/block.go (about)

     1  package block
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/go-kit/log"
    15  	"github.com/go-kit/log/level"
    16  	"github.com/grafana/dskit/runutil"
    17  	"github.com/oklog/ulid/v2"
    18  	"github.com/opentracing/opentracing-go"
    19  	"github.com/opentracing/opentracing-go/ext"
    20  	"github.com/pkg/errors"
    21  	"github.com/prometheus/client_golang/prometheus"
    22  	"github.com/thanos-io/objstore"
    23  
    24  	"github.com/grafana/pyroscope/pkg/util/fnv32"
    25  )
    26  
    27  const (
    28  	IndexFilename = "index.tsdb"
    29  	ParquetSuffix = ".parquet"
    30  
    31  	HostnameLabel = "__hostname__"
    32  )
    33  
    34  // DownloadMeta downloads only meta file from bucket by block ID.
    35  // TODO(bwplotka): Differentiate between network error & partial upload.
    36  func DownloadMeta(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID) (Meta, error) {
    37  	rc, err := bkt.Get(ctx, path.Join(id.String(), MetaFilename))
    38  	if err != nil {
    39  		return Meta{}, errors.Wrapf(err, "meta.json bkt get for %s", id.String())
    40  	}
    41  	defer runutil.CloseWithLogOnErr(logger, rc, "download meta bucket client")
    42  
    43  	var m Meta
    44  
    45  	obj, err := io.ReadAll(rc)
    46  	if err != nil {
    47  		return Meta{}, errors.Wrapf(err, "read meta.json for block %s", id.String())
    48  	}
    49  
    50  	if err = json.Unmarshal(obj, &m); err != nil {
    51  		return Meta{}, errors.Wrapf(err, "unmarshal meta.json for block %s", id.String())
    52  	}
    53  
    54  	return m, nil
    55  }
    56  
    57  // Download downloads directory that is meant to be block directory.
    58  func Download(ctx context.Context, logger log.Logger, bucket objstore.Bucket, id ulid.ULID, dst string, options ...objstore.DownloadOption) error {
    59  	sp, ctx := opentracing.StartSpanFromContext(ctx, "block.Download", opentracing.Tag{Key: "ULID", Value: id.String()})
    60  	defer sp.Finish()
    61  
    62  	if err := os.MkdirAll(dst, 0o750); err != nil {
    63  		return errors.Wrap(err, "create dir")
    64  	}
    65  
    66  	if err := objstore.DownloadFile(ctx, logger, bucket, path.Join(id.String(), MetaFilename), filepath.Join(dst, MetaFilename)); err != nil {
    67  		return err
    68  	}
    69  
    70  	ignoredPaths := []string{MetaFilename}
    71  	if err := objstore.DownloadDir(ctx, logger, bucket, id.String(), id.String(), dst, append(options, objstore.WithDownloadIgnoredPaths(ignoredPaths...))...); err != nil {
    72  		return err
    73  	}
    74  
    75  	return nil
    76  }
    77  
    78  func IsBlockDir(path string) (id ulid.ULID, ok bool) {
    79  	id, err := ulid.Parse(filepath.Base(path))
    80  	return id, err == nil
    81  }
    82  
    83  // upload uploads block from given block dir that ends with block id.
    84  // It makes sure cleanup is done on error to avoid partial block uploads.
    85  // TODO(bplotka): Ensure bucket operations have reasonable backoff retries.
    86  // NOTE: Upload updates `meta.Thanos.File` section.
    87  func upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string) error {
    88  	df, err := os.Stat(bdir)
    89  	if err != nil {
    90  		return err
    91  	}
    92  	if !df.IsDir() {
    93  		return errors.Errorf("%s is not a directory", bdir)
    94  	}
    95  
    96  	// Verify dir.
    97  	id, err := ulid.Parse(df.Name())
    98  	if err != nil {
    99  		return errors.Wrap(err, "not a block dir")
   100  	}
   101  
   102  	meta, err := ReadMetaFromDir(bdir)
   103  	if err != nil {
   104  		// No meta or broken meta file.
   105  		return errors.Wrap(err, "read meta")
   106  	}
   107  
   108  	// ensure labels are initialized
   109  	if meta.Labels == nil {
   110  		meta.Labels = make(map[string]string)
   111  	}
   112  
   113  	// add hostname if available
   114  	if hostname, err := os.Hostname(); err == nil {
   115  		meta.Labels[HostnameLabel] = hostname
   116  	}
   117  
   118  	metaEncoded := strings.Builder{}
   119  	if err != nil {
   120  		return errors.Wrap(err, "gather meta file stats")
   121  	}
   122  
   123  	if _, err := meta.WriteTo(&metaEncoded); err != nil {
   124  		return errors.Wrap(err, "encode meta file")
   125  	}
   126  
   127  	// loop through files
   128  	for _, file := range meta.Files {
   129  		if err := objstore.UploadFile(ctx, logger, bkt, path.Join(bdir, file.RelPath), path.Join(id.String(), file.RelPath)); err != nil {
   130  			return cleanUp(logger, bkt, id, errors.Wrapf(err, "uploading file '%s'", file.RelPath))
   131  		}
   132  	}
   133  
   134  	// Meta.json always need to be uploaded as a last item. This will allow to assume block directories without meta file to be pending uploads.
   135  	if err := bkt.Upload(ctx, path.Join(id.String(), MetaFilename), strings.NewReader(metaEncoded.String())); err != nil {
   136  		// Don't call cleanUp here. Despite getting error, meta.json may have been uploaded in certain cases,
   137  		// and even though cleanUp will not see it yet, meta.json may appear in the bucket later.
   138  		// (Eg. S3 is known to behave this way when it returns 503 "SlowDown" error).
   139  		// If meta.json is not uploaded, this will produce partial blocks, but such blocks will be cleaned later.
   140  		return errors.Wrap(err, "upload meta file")
   141  	}
   142  
   143  	return nil
   144  }
   145  
   146  // Upload uploads a TSDB block to the object storage. It verifies basic
   147  // features of Thanos block.
   148  func Upload(ctx context.Context, logger log.Logger, bkt objstore.Bucket, bdir string) error {
   149  	sp, ctx := opentracing.StartSpanFromContext(ctx, "block.Upload", opentracing.Tag{Key: "dir", Value: bdir})
   150  	defer sp.Finish()
   151  	if err := upload(ctx, logger, bkt, bdir); err != nil {
   152  		ext.LogError(sp, err)
   153  		return err
   154  	}
   155  	return nil
   156  }
   157  
   158  func cleanUp(logger log.Logger, bkt objstore.Bucket, id ulid.ULID, err error) error {
   159  	// Cleanup the dir with an uncancelable context.
   160  	cleanErr := Delete(context.Background(), logger, bkt, id)
   161  	if cleanErr != nil {
   162  		return errors.Wrapf(err, "failed to clean block after upload issue. Partial block in system. Err: %s", err.Error())
   163  	}
   164  	return err
   165  }
   166  
   167  // MarkForDeletion creates a file which stores information about when the block was marked for deletion.
   168  func MarkForDeletion(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, details string, warnExist bool, markedForDeletion prometheus.Counter) error {
   169  	deletionMarkFile := path.Join(id.String(), DeletionMarkFilename)
   170  	deletionMarkExists, err := bkt.Exists(ctx, deletionMarkFile)
   171  	if err != nil {
   172  		return errors.Wrapf(err, "check exists %s in bucket", deletionMarkFile)
   173  	}
   174  	if deletionMarkExists {
   175  		if warnExist {
   176  			level.Warn(logger).Log("msg", "requested to mark for deletion, but file already exists; this should not happen; investigate", "err", errors.Errorf("file %s already exists in bucket", deletionMarkFile))
   177  		}
   178  		return nil
   179  	}
   180  
   181  	deletionMark, err := json.Marshal(DeletionMark{
   182  		ID:           id,
   183  		DeletionTime: time.Now().Unix(),
   184  		Version:      DeletionMarkVersion1,
   185  		Details:      details,
   186  	})
   187  	if err != nil {
   188  		return errors.Wrap(err, "json encode deletion mark")
   189  	}
   190  
   191  	if err := bkt.Upload(ctx, deletionMarkFile, bytes.NewBuffer(deletionMark)); err != nil {
   192  		return errors.Wrapf(err, "upload file %s to bucket", deletionMarkFile)
   193  	}
   194  	markedForDeletion.Inc()
   195  	level.Info(logger).Log("msg", "block has been marked for deletion", "block", id)
   196  	return nil
   197  }
   198  
   199  // Delete removes directory that is meant to be block directory.
   200  // NOTE: Always prefer this method for deleting blocks.
   201  //   - We have to delete block's files in the certain order (meta.json first and deletion-mark.json last)
   202  //     to ensure we don't end up with malformed partial blocks. Thanos system handles well partial blocks
   203  //     only if they don't have meta.json. If meta.json is present Thanos assumes valid block.
   204  //   - This avoids deleting empty dir (whole bucket) by mistake.
   205  func Delete(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID) error {
   206  	metaFile := path.Join(id.String(), MetaFilename)
   207  	deletionMarkFile := path.Join(id.String(), DeletionMarkFilename)
   208  
   209  	// Delete block meta file.
   210  	ok, err := bkt.Exists(ctx, metaFile)
   211  	if err != nil {
   212  		return errors.Wrapf(err, "stat %s", metaFile)
   213  	}
   214  
   215  	if ok {
   216  		if err := bkt.Delete(ctx, metaFile); err != nil {
   217  			return errors.Wrapf(err, "delete %s", metaFile)
   218  		}
   219  		level.Debug(logger).Log("msg", "deleted file", "file", metaFile, "bucket", bkt.Name())
   220  	}
   221  
   222  	// Delete the block objects, but skip:
   223  	// - The metaFile as we just deleted. This is required for eventual object storages (list after write).
   224  	// - The deletionMarkFile as we'll delete it at last.
   225  	err = deleteDirRec(ctx, logger, bkt, id.String(), func(name string) bool {
   226  		return name == metaFile || name == deletionMarkFile
   227  	})
   228  	if err != nil {
   229  		return err
   230  	}
   231  
   232  	// Delete block deletion mark.
   233  	ok, err = bkt.Exists(ctx, deletionMarkFile)
   234  	if err != nil {
   235  		return errors.Wrapf(err, "stat %s", deletionMarkFile)
   236  	}
   237  
   238  	if ok {
   239  		if err := bkt.Delete(ctx, deletionMarkFile); err != nil {
   240  			return errors.Wrapf(err, "delete %s", deletionMarkFile)
   241  		}
   242  		level.Debug(logger).Log("msg", "deleted file", "file", deletionMarkFile, "bucket", bkt.Name())
   243  	}
   244  
   245  	return nil
   246  }
   247  
   248  // deleteDirRec removes all objects prefixed with dir from the bucket. It skips objects that return true for the passed keep function.
   249  // NOTE: For objects removal use `block.Delete` strictly.
   250  func deleteDirRec(ctx context.Context, logger log.Logger, bkt objstore.Bucket, dir string, keep func(name string) bool) error {
   251  	return bkt.Iter(ctx, dir, func(name string) error {
   252  		// If we hit a directory, call DeleteDir recursively.
   253  		if strings.HasSuffix(name, objstore.DirDelim) {
   254  			return deleteDirRec(ctx, logger, bkt, name, keep)
   255  		}
   256  		if keep(name) {
   257  			return nil
   258  		}
   259  		if err := bkt.Delete(ctx, name); err != nil {
   260  			return err
   261  		}
   262  		level.Debug(logger).Log("msg", "deleted file", "file", name, "bucket", bkt.Name())
   263  		return nil
   264  	})
   265  }
   266  
   267  // MarkForNoCompact creates a file which marks block to be not compacted.
   268  func MarkForNoCompact(ctx context.Context, logger log.Logger, bkt objstore.Bucket, id ulid.ULID, reason NoCompactReason, details string, markedForNoCompact prometheus.Counter) error {
   269  	m := path.Join(id.String(), NoCompactMarkFilename)
   270  	noCompactMarkExists, err := bkt.Exists(ctx, m)
   271  	if err != nil {
   272  		return errors.Wrapf(err, "check exists %s in bucket", m)
   273  	}
   274  	if noCompactMarkExists {
   275  		level.Warn(logger).Log("msg", "requested to mark for no compaction, but file already exists; this should not happen; investigate", "err", errors.Errorf("file %s already exists in bucket", m))
   276  		return nil
   277  	}
   278  
   279  	noCompactMark, err := json.Marshal(NoCompactMark{
   280  		ID:      id,
   281  		Version: NoCompactMarkVersion1,
   282  
   283  		NoCompactTime: time.Now().Unix(),
   284  		Reason:        reason,
   285  		Details:       details,
   286  	})
   287  	if err != nil {
   288  		return errors.Wrap(err, "json encode no compact mark")
   289  	}
   290  
   291  	if err := bkt.Upload(ctx, m, bytes.NewBuffer(noCompactMark)); err != nil {
   292  		return errors.Wrapf(err, "upload file %s to bucket", m)
   293  	}
   294  	markedForNoCompact.Inc()
   295  	level.Info(logger).Log("msg", "block has been marked for no compaction", "block", id)
   296  	return nil
   297  }
   298  
   299  // HashBlockID returns a 32-bit hash of the block ID useful for
   300  // ring-based sharding.
   301  func HashBlockID(id ulid.ULID) uint32 {
   302  	h := fnv32.New()
   303  	for _, b := range id {
   304  		h = fnv32.AddByte32(h, b)
   305  	}
   306  	return h
   307  }