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 }