move implementation to internal/ directory (#828)
This commit is contained in:
committed by
GitHub
parent
b9b9ad10cf
commit
908807bd59
@@ -0,0 +1,933 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/daemon/logging/trace"
|
||||
"github.com/zrepl/zrepl/internal/util/envconst"
|
||||
"github.com/zrepl/zrepl/internal/util/nodefault"
|
||||
"github.com/zrepl/zrepl/internal/util/semaphore"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
type AbstractionType string
|
||||
|
||||
// Implementation note:
|
||||
// There are a lot of exhaustive switches on AbstractionType in the code base.
|
||||
// When adding a new abstraction type, make sure to search and update them!
|
||||
const (
|
||||
AbstractionStepHold AbstractionType = "step-hold"
|
||||
AbstractionLastReceivedHold AbstractionType = "last-received-hold"
|
||||
AbstractionTentativeReplicationCursorBookmark AbstractionType = "tentative-replication-cursor-bookmark-v2"
|
||||
AbstractionReplicationCursorBookmarkV1 AbstractionType = "replication-cursor-bookmark-v1"
|
||||
AbstractionReplicationCursorBookmarkV2 AbstractionType = "replication-cursor-bookmark-v2"
|
||||
)
|
||||
|
||||
var AbstractionTypesAll = AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionLastReceivedHold: true,
|
||||
AbstractionTentativeReplicationCursorBookmark: true,
|
||||
AbstractionReplicationCursorBookmarkV1: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
|
||||
// Implementation Note:
|
||||
// Whenever you add a new accessor, adjust AbstractionJSON.MarshalJSON accordingly
|
||||
type Abstraction interface {
|
||||
GetType() AbstractionType
|
||||
GetFS() string
|
||||
GetName() string
|
||||
GetFullPath() string
|
||||
GetJobID() *JobID // may return nil if the abstraction does not have a JobID
|
||||
GetCreateTXG() uint64
|
||||
GetFilesystemVersion() zfs.FilesystemVersion
|
||||
String() string
|
||||
// destroy the abstraction: either releases the hold or destroys the bookmark
|
||||
Destroy(context.Context) error
|
||||
json.Marshaler
|
||||
}
|
||||
|
||||
func AbstractionEquals(a, b Abstraction) bool {
|
||||
if (a != nil) != (b != nil) {
|
||||
return false
|
||||
}
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
var aJobId, bJobId JobID
|
||||
if aJid := a.GetJobID(); aJid != nil {
|
||||
aJobId = *aJid
|
||||
}
|
||||
if bJid := b.GetJobID(); bJid != nil {
|
||||
bJobId = *bJid
|
||||
}
|
||||
return a.GetType() == b.GetType() &&
|
||||
a.GetFS() == b.GetFS() &&
|
||||
a.GetName() == b.GetName() &&
|
||||
a.GetFullPath() == b.GetFullPath() &&
|
||||
aJobId == bJobId &&
|
||||
a.GetCreateTXG() == b.GetCreateTXG() &&
|
||||
zfs.FilesystemVersionEqualIdentity(a.GetFilesystemVersion(), b.GetFilesystemVersion()) &&
|
||||
a.String() == b.String()
|
||||
}
|
||||
|
||||
func (t AbstractionType) Validate() error {
|
||||
switch t {
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return nil
|
||||
default:
|
||||
return errors.Errorf("unknown abstraction type %q", t)
|
||||
}
|
||||
}
|
||||
|
||||
func (t AbstractionType) MustValidate() error {
|
||||
if err := t.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type AbstractionJSON struct{ Abstraction }
|
||||
|
||||
var _ json.Marshaler = (*AbstractionJSON)(nil)
|
||||
|
||||
func (a AbstractionJSON) MarshalJSON() ([]byte, error) {
|
||||
type S struct {
|
||||
Type AbstractionType
|
||||
FS string
|
||||
Name string
|
||||
FullPath string
|
||||
JobID *JobID // may return nil if the abstraction does not have a JobID
|
||||
CreateTXG uint64
|
||||
FilesystemVersion zfs.FilesystemVersion
|
||||
String string
|
||||
}
|
||||
v := S{
|
||||
Type: a.Abstraction.GetType(),
|
||||
FS: a.Abstraction.GetFS(),
|
||||
Name: a.Abstraction.GetName(),
|
||||
FullPath: a.Abstraction.GetFullPath(),
|
||||
JobID: a.Abstraction.GetJobID(),
|
||||
CreateTXG: a.Abstraction.GetCreateTXG(),
|
||||
FilesystemVersion: a.Abstraction.GetFilesystemVersion(),
|
||||
String: a.Abstraction.String(),
|
||||
}
|
||||
return json.Marshal(v)
|
||||
}
|
||||
|
||||
type AbstractionTypeSet map[AbstractionType]bool
|
||||
|
||||
func AbstractionTypeSetFromStrings(sts []string) (AbstractionTypeSet, error) {
|
||||
ats := make(map[AbstractionType]bool, len(sts))
|
||||
for i, t := range sts {
|
||||
at := AbstractionType(t)
|
||||
if err := at.Validate(); err != nil {
|
||||
return nil, errors.Wrapf(err, "invalid abstraction type #%d %q", i+1, t)
|
||||
}
|
||||
ats[at] = true
|
||||
}
|
||||
return ats, nil
|
||||
}
|
||||
|
||||
func (s AbstractionTypeSet) ContainsAll(q AbstractionTypeSet) bool {
|
||||
for k := range q {
|
||||
if _, ok := s[k]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s AbstractionTypeSet) ContainsAnyOf(q AbstractionTypeSet) bool {
|
||||
for k := range q {
|
||||
if _, ok := s[k]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s AbstractionTypeSet) String() string {
|
||||
sts := make([]string, 0, len(s))
|
||||
for i := range s {
|
||||
sts = append(sts, string(i))
|
||||
}
|
||||
sort.Strings(sts)
|
||||
return strings.Join(sts, ",")
|
||||
}
|
||||
|
||||
func (s AbstractionTypeSet) Validate() error {
|
||||
for k := range s {
|
||||
if err := k.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Use the `BookmarkExtractor()` method of each abstraction type in this set
|
||||
// to try extract an abstraction from the given FilesystemVersion.
|
||||
//
|
||||
// Abstraction types in this set that don't have a bookmark extractor are skipped.
|
||||
//
|
||||
// Panics if more than one abstraction type matches.
|
||||
func (s AbstractionTypeSet) ExtractBookmark(dp *zfs.DatasetPath, v *zfs.FilesystemVersion) Abstraction {
|
||||
matched := make(AbstractionTypeSet, 1)
|
||||
var matchedAbs Abstraction
|
||||
for absType := range s {
|
||||
extractor := absType.BookmarkExtractor()
|
||||
if extractor == nil {
|
||||
continue
|
||||
}
|
||||
abstraction := extractor(dp, *v)
|
||||
if abstraction != nil {
|
||||
matched[absType] = true
|
||||
matchedAbs = abstraction
|
||||
}
|
||||
}
|
||||
if len(matched) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(matched) == 1 {
|
||||
if matchedAbs == nil {
|
||||
panic("loop above should always set matchedAbs if there is a match")
|
||||
}
|
||||
return matchedAbs
|
||||
}
|
||||
panic(fmt.Sprintf("abstraction types extractors should not overlap: %s", matched))
|
||||
}
|
||||
|
||||
type BookmarkExtractor func(fs *zfs.DatasetPath, v zfs.FilesystemVersion) Abstraction
|
||||
|
||||
// returns nil if the abstraction type is not bookmark-based
|
||||
func (t AbstractionType) BookmarkExtractor() BookmarkExtractor {
|
||||
switch t {
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return TentativeReplicationCursorExtractor
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return ReplicationCursorV1Extractor
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return ReplicationCursorV2Extractor
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
default:
|
||||
panic(fmt.Sprintf("unimpl: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
type HoldExtractor = func(fs *zfs.DatasetPath, v zfs.FilesystemVersion, tag string) Abstraction
|
||||
|
||||
// returns nil if the abstraction type is not hold-based
|
||||
func (t AbstractionType) HoldExtractor() HoldExtractor {
|
||||
switch t {
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return nil
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return nil
|
||||
case AbstractionStepHold:
|
||||
return StepHoldExtractor
|
||||
case AbstractionLastReceivedHold:
|
||||
return LastReceivedHoldExtractor
|
||||
default:
|
||||
panic(fmt.Sprintf("unimpl: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
func (t AbstractionType) BookmarkNamer() func(fs string, guid uint64, jobId JobID) (string, error) {
|
||||
switch t {
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return TentativeReplicationCursorBookmarkName
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
panic("shouldn't be creating new ones")
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return ReplicationCursorBookmarkName
|
||||
case AbstractionStepHold:
|
||||
return nil
|
||||
case AbstractionLastReceivedHold:
|
||||
return nil
|
||||
default:
|
||||
panic(fmt.Sprintf("unimpl: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
func (t AbstractionType) IsSnapshotOrBookmark() bool {
|
||||
switch t {
|
||||
case AbstractionTentativeReplicationCursorBookmark:
|
||||
return true
|
||||
case AbstractionReplicationCursorBookmarkV1:
|
||||
return true
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
return true
|
||||
case AbstractionStepHold:
|
||||
return false
|
||||
case AbstractionLastReceivedHold:
|
||||
return false
|
||||
default:
|
||||
panic(fmt.Sprintf("unimpl: %q", t))
|
||||
}
|
||||
}
|
||||
|
||||
type ListZFSHoldsAndBookmarksQuery struct {
|
||||
FS ListZFSHoldsAndBookmarksQueryFilesystemFilter
|
||||
// What abstraction types should match (any contained in the set)
|
||||
What AbstractionTypeSet
|
||||
|
||||
// The output for the query must satisfy _all_ (AND) requirements of all fields in this query struct.
|
||||
|
||||
// if not nil: JobID of the hold or bookmark in question must be equal
|
||||
// else: JobID of the hold or bookmark can be any value
|
||||
JobID *JobID
|
||||
|
||||
// zero-value means any CreateTXG is acceptable
|
||||
CreateTXG CreateTXGRange
|
||||
|
||||
// Number of concurrently queried filesystems. Must be >= 1
|
||||
Concurrency int64
|
||||
}
|
||||
|
||||
type CreateTXGRangeBound struct {
|
||||
CreateTXG uint64
|
||||
Inclusive *nodefault.Bool // must not be nil
|
||||
}
|
||||
|
||||
// A non-empty range of CreateTXGs
|
||||
//
|
||||
// If both Since and Until are nil, any CreateTXG is acceptable
|
||||
type CreateTXGRange struct {
|
||||
// if not nil: The hold's snapshot or the bookmark's createtxg must be greater than (or equal) Since
|
||||
// else: CreateTXG of the hold or bookmark can be any value accepted by Until
|
||||
Since *CreateTXGRangeBound
|
||||
// if not nil: The hold's snapshot or the bookmark's createtxg must be less than (or equal) Until
|
||||
// else: CreateTXG of the hold or bookmark can be any value accepted by Since
|
||||
Until *CreateTXGRangeBound
|
||||
}
|
||||
|
||||
// FS == nil XOR Filter == nil
|
||||
type ListZFSHoldsAndBookmarksQueryFilesystemFilter struct {
|
||||
FS *string
|
||||
Filter zfs.DatasetFilter
|
||||
}
|
||||
|
||||
func (q *ListZFSHoldsAndBookmarksQuery) Validate() error {
|
||||
if err := q.FS.Validate(); err != nil {
|
||||
return errors.Wrap(err, "FS")
|
||||
}
|
||||
if q.JobID != nil {
|
||||
q.JobID.MustValidate() // FIXME
|
||||
}
|
||||
if err := q.CreateTXG.Validate(); err != nil {
|
||||
return errors.Wrap(err, "CreateTXGRange")
|
||||
}
|
||||
if err := q.What.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if q.Concurrency < 1 {
|
||||
return errors.New("Concurrency must be >= 1")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var createTXGRangeBoundAllowCreateTXG0 = envconst.Bool("ZREPL_ENDPOINT_LIST_ABSTRACTIONS_QUERY_CREATETXG_RANGE_BOUND_ALLOW_0", false)
|
||||
|
||||
func (i *CreateTXGRangeBound) Validate() error {
|
||||
if err := i.Inclusive.ValidateNoDefault(); err != nil {
|
||||
return errors.Wrap(err, "Inclusive")
|
||||
}
|
||||
if i.CreateTXG == 0 && !createTXGRangeBoundAllowCreateTXG0 {
|
||||
return errors.New("CreateTXG must be non-zero")
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (f *ListZFSHoldsAndBookmarksQueryFilesystemFilter) Validate() error {
|
||||
if f == nil {
|
||||
return nil
|
||||
}
|
||||
fsSet := f.FS != nil
|
||||
filterSet := f.Filter != nil
|
||||
if fsSet && filterSet || !fsSet && !filterSet {
|
||||
return fmt.Errorf("must set FS or Filter field, but fsIsSet=%v and filterIsSet=%v", fsSet, filterSet)
|
||||
}
|
||||
if fsSet {
|
||||
if err := zfs.EntityNamecheck(*f.FS, zfs.EntityTypeFilesystem); err != nil {
|
||||
return errors.Wrap(err, "FS invalid")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *ListZFSHoldsAndBookmarksQueryFilesystemFilter) Filesystems(ctx context.Context) ([]string, error) {
|
||||
if err := f.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if f.FS != nil {
|
||||
return []string{*f.FS}, nil
|
||||
}
|
||||
if f.Filter != nil {
|
||||
dps, err := zfs.ZFSListMapping(ctx, f.Filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fss := make([]string, len(dps))
|
||||
for i, dp := range dps {
|
||||
fss[i] = dp.ToString()
|
||||
}
|
||||
return fss, nil
|
||||
}
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func (r *CreateTXGRange) Validate() error {
|
||||
if r.Since != nil {
|
||||
if err := r.Since.Validate(); err != nil {
|
||||
return errors.Wrap(err, "Since")
|
||||
}
|
||||
}
|
||||
if r.Until != nil {
|
||||
if err := r.Until.Validate(); err != nil {
|
||||
return errors.Wrap(err, "Until")
|
||||
}
|
||||
}
|
||||
if _, err := r.effectiveBounds(); err != nil {
|
||||
return errors.Wrapf(err, "specified range %s is semantically invalid", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// inclusive-inclusive bounds
|
||||
type effectiveBounds struct {
|
||||
sinceInclusive uint64
|
||||
sinceUnbounded bool
|
||||
untilInclusive uint64
|
||||
untilUnbounded bool
|
||||
}
|
||||
|
||||
// callers must have validated r.Since and r.Until before calling this method
|
||||
func (r *CreateTXGRange) effectiveBounds() (bounds effectiveBounds, err error) {
|
||||
|
||||
bounds.sinceUnbounded = r.Since == nil
|
||||
bounds.untilUnbounded = r.Until == nil
|
||||
|
||||
if r.Since == nil && r.Until == nil {
|
||||
return bounds, nil
|
||||
}
|
||||
|
||||
if r.Since != nil {
|
||||
bounds.sinceInclusive = r.Since.CreateTXG
|
||||
if !r.Since.Inclusive.B {
|
||||
if r.Since.CreateTXG == math.MaxUint64 {
|
||||
return bounds, errors.Errorf("Since-exclusive (%v) must be less than math.MaxUint64 (%v)",
|
||||
r.Since.CreateTXG, uint64(math.MaxUint64))
|
||||
}
|
||||
bounds.sinceInclusive++
|
||||
}
|
||||
}
|
||||
|
||||
if r.Until != nil {
|
||||
bounds.untilInclusive = r.Until.CreateTXG
|
||||
if !r.Until.Inclusive.B {
|
||||
if r.Until.CreateTXG == 0 {
|
||||
return bounds, errors.Errorf("Until-exclusive (%v) must be greater than 0", r.Until.CreateTXG)
|
||||
}
|
||||
bounds.untilInclusive--
|
||||
}
|
||||
}
|
||||
|
||||
if !bounds.sinceUnbounded && !bounds.untilUnbounded {
|
||||
if bounds.sinceInclusive >= bounds.untilInclusive {
|
||||
return bounds, errors.Errorf("effective range bounds are [%v,%v] which is empty or invalid", bounds.sinceInclusive, bounds.untilInclusive)
|
||||
}
|
||||
// fallthrough
|
||||
}
|
||||
|
||||
return bounds, nil
|
||||
}
|
||||
|
||||
func (r *CreateTXGRange) String() string {
|
||||
var buf strings.Builder
|
||||
if r.Since == nil {
|
||||
fmt.Fprintf(&buf, "~")
|
||||
} else {
|
||||
if err := r.Since.Inclusive.ValidateNoDefault(); err != nil {
|
||||
fmt.Fprintf(&buf, "?")
|
||||
} else if r.Since.Inclusive.B {
|
||||
fmt.Fprintf(&buf, "[")
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "(")
|
||||
}
|
||||
fmt.Fprintf(&buf, "%d", r.Since.CreateTXG)
|
||||
}
|
||||
|
||||
fmt.Fprintf(&buf, ",")
|
||||
|
||||
if r.Until == nil {
|
||||
fmt.Fprintf(&buf, "~")
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "%d", r.Until.CreateTXG)
|
||||
if err := r.Until.Inclusive.ValidateNoDefault(); err != nil {
|
||||
fmt.Fprintf(&buf, "?")
|
||||
} else if r.Until.Inclusive.B {
|
||||
fmt.Fprintf(&buf, "]")
|
||||
} else {
|
||||
fmt.Fprintf(&buf, ")")
|
||||
}
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// panics if not .Validate()
|
||||
func (r *CreateTXGRange) IsUnbounded() bool {
|
||||
if err := r.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
bounds, err := r.effectiveBounds()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bounds.sinceUnbounded && bounds.untilUnbounded
|
||||
}
|
||||
|
||||
// panics if not .Validate()
|
||||
func (r *CreateTXGRange) Contains(qCreateTxg uint64) bool {
|
||||
if err := r.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
bounds, err := r.effectiveBounds()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
sinceMatches := bounds.sinceUnbounded || bounds.sinceInclusive <= qCreateTxg
|
||||
untilMatches := bounds.untilUnbounded || qCreateTxg <= bounds.untilInclusive
|
||||
|
||||
return sinceMatches && untilMatches
|
||||
}
|
||||
|
||||
type ListAbstractionsError struct {
|
||||
FS string
|
||||
Snap string
|
||||
What string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ListAbstractionsError) Error() string {
|
||||
if e.FS == "" {
|
||||
return fmt.Sprintf("list endpoint abstractions: %s: %s", e.What, e.Err)
|
||||
} else {
|
||||
v := e.FS
|
||||
if e.Snap != "" {
|
||||
v = fmt.Sprintf("%s@%s", e.FS, e.Snap)
|
||||
}
|
||||
return fmt.Sprintf("list endpoint abstractions on %q: %s: %s", v, e.What, e.Err)
|
||||
}
|
||||
}
|
||||
|
||||
type putListAbstractionErr func(err error, fs string, what string)
|
||||
type putListAbstraction func(a Abstraction)
|
||||
|
||||
type ListAbstractionsErrors []ListAbstractionsError
|
||||
|
||||
func (e ListAbstractionsErrors) Error() string {
|
||||
if len(e) == 0 {
|
||||
panic(e)
|
||||
}
|
||||
if len(e) == 1 {
|
||||
return fmt.Sprintf("list endpoint abstractions: %s", e[0])
|
||||
}
|
||||
msgs := make([]string, len(e))
|
||||
for i := range e {
|
||||
msgs[i] = e[i].Error()
|
||||
}
|
||||
return fmt.Sprintf("list endpoint abstractions: multiple errors:\n%s", strings.Join(msgs, "\n"))
|
||||
}
|
||||
|
||||
func ListAbstractions(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (out []Abstraction, outErrs []ListAbstractionsError, err error) {
|
||||
outChan, outErrsChan, drainDone, err := ListAbstractionsStreamed(ctx, query)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer drainDone()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for a := range outChan {
|
||||
out = append(out, a)
|
||||
}
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for err := range outErrsChan {
|
||||
outErrs = append(outErrs, err)
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
return out, outErrs, nil
|
||||
}
|
||||
|
||||
// if err != nil, the returned channels are both nil
|
||||
// if err == nil, both channels must be fully drained by the caller to avoid leaking goroutines.
|
||||
// After draining is done, the caller must call the returned drainDone func.
|
||||
func ListAbstractionsStreamed(ctx context.Context, query ListZFSHoldsAndBookmarksQuery) (_ <-chan Abstraction, _ <-chan ListAbstractionsError, drainDone func(), _ error) {
|
||||
|
||||
// impl note: structure the query processing in such a way that
|
||||
// a minimum amount of zfs shell-outs needs to be done
|
||||
|
||||
if err := query.Validate(); err != nil {
|
||||
return nil, nil, nil, errors.Wrap(err, "validate query")
|
||||
}
|
||||
|
||||
fss, err := query.FS.Filesystems(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, nil, errors.Wrap(err, "list filesystems")
|
||||
}
|
||||
|
||||
outErrs := make(chan ListAbstractionsError)
|
||||
out := make(chan Abstraction)
|
||||
|
||||
errCb := func(err error, fs string, what string) {
|
||||
outErrs <- ListAbstractionsError{Err: err, FS: fs, What: what}
|
||||
}
|
||||
emitAbstraction := func(a Abstraction) {
|
||||
jobIdMatches := query.JobID == nil || a.GetJobID() == nil || *a.GetJobID() == *query.JobID
|
||||
|
||||
createTXGMatches := query.CreateTXG.Contains(a.GetCreateTXG())
|
||||
|
||||
if jobIdMatches && createTXGMatches {
|
||||
out <- a
|
||||
}
|
||||
}
|
||||
|
||||
sem := semaphore.New(int64(query.Concurrency))
|
||||
ctx, endTask := trace.WithTask(ctx, "list-abstractions-streamed-producer")
|
||||
go func() {
|
||||
defer close(out)
|
||||
defer close(outErrs)
|
||||
|
||||
_, add, wait := trace.WithTaskGroup(ctx, "list-abstractions-impl-fs")
|
||||
defer wait()
|
||||
for _, fs := range fss {
|
||||
fs := fs // capture by copy
|
||||
add(func(ctx context.Context) {
|
||||
g, err := sem.Acquire(ctx)
|
||||
if err != nil {
|
||||
errCb(err, fs, err.Error())
|
||||
return
|
||||
}
|
||||
func() {
|
||||
defer g.Release()
|
||||
listAbstractionsImplFS(ctx, fs, &query, emitAbstraction, errCb)
|
||||
}()
|
||||
})
|
||||
}
|
||||
}()
|
||||
|
||||
drainDone = func() {
|
||||
endTask()
|
||||
}
|
||||
|
||||
return out, outErrs, drainDone, nil
|
||||
}
|
||||
|
||||
func listAbstractionsImplFS(ctx context.Context, fs string, query *ListZFSHoldsAndBookmarksQuery, emitCandidate putListAbstraction, errCb putListAbstractionErr) {
|
||||
fsp, err := zfs.NewDatasetPath(fs)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if len(query.What) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
whatTypes := zfs.VersionTypeSet{}
|
||||
for what := range query.What {
|
||||
if e := what.BookmarkExtractor(); e != nil {
|
||||
whatTypes[zfs.Bookmark] = true
|
||||
}
|
||||
if e := what.HoldExtractor(); e != nil {
|
||||
whatTypes[zfs.Snapshot] = true
|
||||
}
|
||||
}
|
||||
fsvs, err := zfs.ZFSListFilesystemVersions(ctx, fsp, zfs.ListFilesystemVersionsOptions{
|
||||
Types: whatTypes,
|
||||
})
|
||||
if err != nil {
|
||||
errCb(err, fs, "list filesystem versions")
|
||||
return
|
||||
}
|
||||
|
||||
for at := range query.What {
|
||||
bmE := at.BookmarkExtractor()
|
||||
holdE := at.HoldExtractor()
|
||||
if bmE == nil && holdE == nil || bmE != nil && holdE != nil {
|
||||
panic("implementation error: extractors misconfigured for " + at)
|
||||
}
|
||||
for _, v := range fsvs {
|
||||
if v.Type == zfs.Bookmark && bmE != nil {
|
||||
if a := bmE(fsp, v); a != nil {
|
||||
emitCandidate(a)
|
||||
}
|
||||
}
|
||||
if v.Type == zfs.Snapshot && holdE != nil && query.CreateTXG.Contains(v.GetCreateTXG()) && (!v.UserRefs.Valid || v.UserRefs.Value > 0) { // FIXME review v.UserRefsValid
|
||||
holds, err := zfs.ZFSHolds(ctx, fsp.ToString(), v.Name)
|
||||
if err != nil {
|
||||
errCb(err, v.ToAbsPath(fsp), "get hold on snap")
|
||||
continue
|
||||
}
|
||||
for _, tag := range holds {
|
||||
if a := holdE(fsp, v, tag); a != nil {
|
||||
emitCandidate(a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type BatchDestroyResult struct {
|
||||
Abstraction
|
||||
DestroyErr error
|
||||
}
|
||||
|
||||
var _ json.Marshaler = (*BatchDestroyResult)(nil)
|
||||
|
||||
func (r BatchDestroyResult) MarshalJSON() ([]byte, error) {
|
||||
err := ""
|
||||
if r.DestroyErr != nil {
|
||||
err = r.DestroyErr.Error()
|
||||
}
|
||||
s := struct {
|
||||
Abstraction AbstractionJSON
|
||||
DestroyErr string
|
||||
}{
|
||||
AbstractionJSON{r.Abstraction},
|
||||
err,
|
||||
}
|
||||
return json.Marshal(s)
|
||||
}
|
||||
|
||||
func BatchDestroy(ctx context.Context, abs []Abstraction) <-chan BatchDestroyResult {
|
||||
// hold-based batching: per snapshot
|
||||
// bookmark-based batching: none possible via CLI
|
||||
// => not worth the trouble for now, will be worth it once we start using channel programs
|
||||
// => TODO: actual batching using channel programs
|
||||
res := make(chan BatchDestroyResult, len(abs))
|
||||
go func() {
|
||||
for _, a := range abs {
|
||||
res <- BatchDestroyResult{
|
||||
a,
|
||||
a.Destroy(ctx),
|
||||
}
|
||||
}
|
||||
close(res)
|
||||
}()
|
||||
return res
|
||||
}
|
||||
|
||||
type StalenessInfo struct {
|
||||
ConstructedWithQuery ListZFSHoldsAndBookmarksQuery
|
||||
Live []Abstraction
|
||||
Stale []Abstraction
|
||||
}
|
||||
|
||||
type fsAndJobId struct {
|
||||
fs string
|
||||
jobId JobID
|
||||
}
|
||||
|
||||
type ListStaleQueryError struct {
|
||||
error
|
||||
}
|
||||
|
||||
// returns *ListStaleQueryError if the given query cannot be used for determining staleness info
|
||||
func ListStale(ctx context.Context, q ListZFSHoldsAndBookmarksQuery) (*StalenessInfo, error) {
|
||||
if !q.CreateTXG.IsUnbounded() {
|
||||
// we must determine the most recent step per FS, can't allow that
|
||||
return nil, &ListStaleQueryError{errors.New("ListStale cannot have Until != nil set on query")}
|
||||
}
|
||||
|
||||
// if asking for step holds must also ask for replication cursor bookmarks (for firstNotStale)
|
||||
ifAnyThenAll := AbstractionTypeSet{
|
||||
AbstractionStepHold: true,
|
||||
AbstractionReplicationCursorBookmarkV2: true,
|
||||
}
|
||||
if q.What.ContainsAnyOf(ifAnyThenAll) && !q.What.ContainsAll(ifAnyThenAll) {
|
||||
return nil, &ListStaleQueryError{errors.Errorf("ListStale requires query to ask for all of %s", ifAnyThenAll.String())}
|
||||
}
|
||||
|
||||
// ----------------- done validating query for listStaleFiltering -----------------------
|
||||
|
||||
qAbs, absErr, err := ListAbstractions(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(absErr) > 0 {
|
||||
// can't go on here because we can't determine the most recent step
|
||||
return nil, ListAbstractionsErrors(absErr)
|
||||
}
|
||||
|
||||
si := listStaleFiltering(qAbs, q.CreateTXG.Since)
|
||||
si.ConstructedWithQuery = q
|
||||
return si, nil
|
||||
}
|
||||
|
||||
type fsAjobAtype struct {
|
||||
fsAndJobId
|
||||
Type AbstractionType
|
||||
}
|
||||
|
||||
// For step holds and bookmarks, only those older than the most recent replication cursor
|
||||
// of their (filesystem,job) are considered because younger ones cannot be stale by definition
|
||||
// (if we destroy them, we might actually lose the hold on the `To` for an ongoing incremental replication)
|
||||
//
|
||||
// For replication cursors and last-received-holds, only the most recent one is kept.
|
||||
//
|
||||
// the returned StalenessInfo.ConstructedWithQuery is not set
|
||||
func listStaleFiltering(abs []Abstraction, sinceBound *CreateTXGRangeBound) *StalenessInfo {
|
||||
|
||||
var noJobId []Abstraction
|
||||
by := make(map[fsAjobAtype][]Abstraction)
|
||||
for _, a := range abs {
|
||||
if a.GetJobID() == nil {
|
||||
noJobId = append(noJobId, a)
|
||||
continue
|
||||
}
|
||||
faj := fsAjobAtype{fsAndJobId{a.GetFS(), *a.GetJobID()}, a.GetType()}
|
||||
l := by[faj]
|
||||
l = append(l, a)
|
||||
by[faj] = l
|
||||
}
|
||||
|
||||
type stepFirstNotStaleCandidate struct {
|
||||
cursor *Abstraction
|
||||
step *Abstraction
|
||||
}
|
||||
stepFirstNotStaleCandidates := make(map[fsAndJobId]stepFirstNotStaleCandidate) // empty map => will always return nil
|
||||
for _, a := range abs {
|
||||
if a.GetJobID() == nil {
|
||||
continue // already put those into always-live list noJobId in above loop
|
||||
}
|
||||
key := fsAndJobId{a.GetFS(), *a.GetJobID()}
|
||||
c := stepFirstNotStaleCandidates[key]
|
||||
|
||||
switch a.GetType() {
|
||||
// stepFirstNotStaleCandidate.cursor
|
||||
case AbstractionReplicationCursorBookmarkV2:
|
||||
if c.cursor == nil || (*c.cursor).GetCreateTXG() < a.GetCreateTXG() {
|
||||
a := a
|
||||
c.cursor = &a
|
||||
}
|
||||
|
||||
// stepFirstNotStaleCandidate.step
|
||||
case AbstractionStepHold:
|
||||
if c.step == nil || (*c.step).GetCreateTXG() < a.GetCreateTXG() {
|
||||
a := a
|
||||
c.step = &a
|
||||
}
|
||||
|
||||
// not interested in the others
|
||||
default:
|
||||
continue // not relevant
|
||||
|
||||
}
|
||||
|
||||
stepFirstNotStaleCandidates[key] = c
|
||||
}
|
||||
|
||||
ret := &StalenessInfo{
|
||||
Live: noJobId,
|
||||
Stale: []Abstraction{},
|
||||
}
|
||||
|
||||
for k := range by {
|
||||
l := by[k]
|
||||
|
||||
if k.Type == AbstractionStepHold {
|
||||
// all step holds older than the most recent cursor are stale, others are always live
|
||||
|
||||
// if we don't have a replication cursor yet, use untilBound = nil
|
||||
// to consider all steps stale (...at first)
|
||||
var untilBound *CreateTXGRangeBound
|
||||
{
|
||||
sfnsc := stepFirstNotStaleCandidates[k.fsAndJobId]
|
||||
|
||||
// If there's a replication cursor, use it as a cutoff between live and stale
|
||||
// for both cursors and holds.
|
||||
// If there's no cursor, we are in initial replication and only need to keep
|
||||
// the most recent step hold live, which hold the .To of the initial send step.
|
||||
if sfnsc.cursor != nil {
|
||||
untilBound = &CreateTXGRangeBound{
|
||||
CreateTXG: (*sfnsc.cursor).GetCreateTXG(),
|
||||
// if we have a cursor, can throw away step hold on both From and To
|
||||
Inclusive: &nodefault.Bool{B: true},
|
||||
}
|
||||
} else if sfnsc.step != nil {
|
||||
untilBound = &CreateTXGRangeBound{
|
||||
CreateTXG: (*sfnsc.step).GetCreateTXG(),
|
||||
// if we don't have a cursor, the step most recent step hold is our
|
||||
// initial replication cursor and it's possibly still live (interrupted initial replication)
|
||||
Inclusive: &nodefault.Bool{B: false},
|
||||
}
|
||||
} else {
|
||||
untilBound = nil // consider everything stale
|
||||
}
|
||||
}
|
||||
staleRange := CreateTXGRange{
|
||||
Since: sinceBound,
|
||||
Until: untilBound,
|
||||
}
|
||||
|
||||
// partition by staleRange
|
||||
for _, a := range l {
|
||||
if staleRange.Contains(a.GetCreateTXG()) {
|
||||
ret.Stale = append(ret.Stale, a)
|
||||
} else {
|
||||
ret.Live = append(ret.Live, a)
|
||||
}
|
||||
}
|
||||
|
||||
} else if k.Type == AbstractionReplicationCursorBookmarkV2 || k.Type == AbstractionLastReceivedHold {
|
||||
// all cursors but the most recent cursor are stale by definition (we always _move_ them)
|
||||
// NOTE: must not use firstNotStale in this branch, not computed for these types
|
||||
|
||||
// sort descending (highest createtxg first), then cut off
|
||||
sort.Slice(l, func(i, j int) bool {
|
||||
return l[i].GetCreateTXG() > l[j].GetCreateTXG()
|
||||
})
|
||||
if len(l) > 0 {
|
||||
ret.Live = append(ret.Live, l[0])
|
||||
ret.Stale = append(ret.Stale, l[1:]...)
|
||||
}
|
||||
} else {
|
||||
ret.Live = append(ret.Live, l...)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user