package meta import ( "bytes" "context" "errors" "strconv" "time" "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/internal/metaerr" "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" "github.com/cockroachdb/pebble" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) // FilterExpired return expired items from addresses. // Address considered expired if metabase does contain information about expiration and // expiration epoch is less than epoch. func (db *DB) FilterExpired(ctx context.Context, epoch uint64, addresses []oid.Address) ([]oid.Address, error) { var ( startedAt = time.Now() success = true ) defer func() { db.metrics.AddMethodDuration("FilterExpired", time.Since(startedAt), success) }() _, span := tracing.StartSpanFromContext(ctx, "metabase.FilterExpired", trace.WithAttributes( attribute.String("epoch", strconv.FormatUint(epoch, 10)), attribute.Int("addr_count", len(addresses)), )) defer span.End() db.modeMtx.RLock() defer db.modeMtx.RUnlock() if db.mode.NoMetabase() { return nil, ErrDegradedMode } result := make([]oid.Address, 0, len(addresses)) err := db.snapshot(func(s *pebble.Snapshot) error { var e error result, e = selectExpiredObjects(ctx, s, epoch, addresses) return e }) if err != nil { return nil, metaerr.Wrap(err) } success = true return result, nil } func isExpired(ctx context.Context, r pebble.Reader, addr oid.Address, currEpoch uint64) (bool, error) { prefix := []byte{expiredPrefix} it, err := r.NewIter(&pebble.IterOptions{ LowerBound: prefix, OnlyReadGuaranteedDurable: true, }) if err != nil { return false, err } // iteration does in ascending order by expiration epoch. // gc does expired objects collect every epoch, so here should be not so much items. for v := it.First(); v && bytes.HasPrefix(it.Key(), prefix); v = it.Next() { select { case <-ctx.Done(): return false, errors.Join(ctx.Err(), it.Close()) default: } expEpoch, err := expirationEpochFromExpiredKey(it.Key()) if err != nil { return false, errors.Join(err, it.Close()) } if expEpoch >= currEpoch { return false, it.Close() // keys are ordered by epoch, so next items will be discarded anyway. } curAddr, err := addressFromExpiredKey(it.Key()) if err != nil { return false, errors.Join(err, it.Close()) } if curAddr == addr { return true, it.Close() } } return false, it.Close() } func selectExpiredObjects(ctx context.Context, r pebble.Reader, epoch uint64, objects []oid.Address) ([]oid.Address, error) { result := make([]oid.Address, 0) objMap := make(map[oid.Address]struct{}) for _, obj := range objects { objMap[obj] = struct{}{} } prefix := []byte{expiredPrefix} it, err := r.NewIter(&pebble.IterOptions{ LowerBound: prefix, OnlyReadGuaranteedDurable: true, }) if err != nil { return nil, err } // iteration does in ascending order by expiration epoch. // gc does expired objects collect every epoch, so here should be not so much items. for v := it.First(); v && bytes.HasPrefix(it.Key(), prefix); v = it.Next() { select { case <-ctx.Done(): return nil, errors.Join(ctx.Err(), it.Close()) default: } expEpoch, err := expirationEpochFromExpiredKey(it.Key()) if err != nil { return nil, errors.Join(err, it.Close()) } if expEpoch >= epoch { return result, it.Close() // keys are ordered by epoch, so next items will be discarded anyway. } addr, err := addressFromExpiredKey(it.Key()) if err != nil { return nil, errors.Join(err, it.Close()) } if _, ok := objMap[addr]; ok { result = append(result, addr) } } return result, it.Close() } // IterateExpired iterates over all objects in DB which are out of date // relative to epoch. Locked objects are not included (do not confuse // with objects of type LOCK). // // If h returns ErrInterruptIterator, nil returns immediately. // Returns other errors of h directly. func (db *DB) IterateExpired(ctx context.Context, epoch uint64, h ExpiredObjectHandler) error { var ( startedAt = time.Now() success = false ) defer func() { db.metrics.AddMethodDuration("IterateExpired", time.Since(startedAt), success) }() _, span := tracing.StartSpanFromContext(ctx, "metabase.IterateExpired", trace.WithAttributes( attribute.String("epoch", strconv.FormatUint(epoch, 10)), )) defer span.End() db.modeMtx.RLock() defer db.modeMtx.RUnlock() if db.mode.NoMetabase() { return ErrDegradedMode } err := metaerr.Wrap(db.snapshot(func(s *pebble.Snapshot) error { return iterateExpired(ctx, s, epoch, h) })) success = err == nil return err } func iterateExpired(ctx context.Context, r pebble.Reader, epoch uint64, h ExpiredObjectHandler) error { prefix := []byte{expiredPrefix} it, err := r.NewIter(&pebble.IterOptions{ LowerBound: prefix, OnlyReadGuaranteedDurable: true, }) if err != nil { return err } // iteration does in ascending order by expiration epoch. for v := it.First(); v && bytes.HasPrefix(it.Key(), prefix); v = it.Next() { select { case <-ctx.Done(): return errors.Join(ctx.Err(), it.Close()) default: } expEpoch, err := expirationEpochFromExpiredKey(it.Key()) if err != nil { return errors.Join(err, it.Close()) } if expEpoch >= epoch { return it.Close() // keys are ordered by epoch, so next items will be discarded anyway. } addr, err := addressFromExpiredKey(it.Key()) if err != nil { return errors.Join(err, it.Close()) } // Ignore locked objects. // // To slightly optimize performance we can check only REGULAR objects // (only they can be locked), but it's more reliable. isLocked, err := objectLocked(ctx, r, addr.Container(), addr.Object()) if err != nil { return errors.Join(err, it.Close()) } if isLocked { continue } objType, err := firstIrregularObjectType(r, addr.Container(), addr.Object()) if err != nil { return errors.Join(err, it.Close()) } if err := h(&ExpiredObject{ typ: objType, addr: addr, }); err != nil { if errors.Is(err, ErrInterruptIterator) { return it.Close() } return errors.Join(err, it.Close()) } } return it.Close() }