mirror of
https://github.com/zrepl/zrepl.git
synced 2024-11-22 08:23:50 +01:00
WIP: diffing and replication algorithm
This commit is contained in:
parent
181875a89b
commit
0918ef6815
116
cmd/replication/diff.go
Normal file
116
cmd/replication/diff.go
Normal file
@ -0,0 +1,116 @@
|
||||
package replication
|
||||
|
||||
import (
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type ConflictNoCommonAncestor struct {
|
||||
SortedSenderVersions, SortedReceiverVersions []zfs.FilesystemVersion
|
||||
}
|
||||
|
||||
func (c *ConflictNoCommonAncestor) Error() string {
|
||||
return "no common snapshot or suitable bookmark between sender and receiver"
|
||||
}
|
||||
|
||||
type ConflictDiverged struct {
|
||||
SortedSenderVersions, SortedReceiverVersions []zfs.FilesystemVersion
|
||||
CommonAncestor zfs.FilesystemVersion
|
||||
SenderOnly, ReceiverOnly []zfs.FilesystemVersion
|
||||
}
|
||||
|
||||
func (c *ConflictDiverged) Error() string {
|
||||
return "the receiver's latest snapshot is not present on sender"
|
||||
}
|
||||
|
||||
func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []zfs.FilesystemVersion) []zfs.FilesystemVersion {
|
||||
lesser := func(s []zfs.FilesystemVersion) func(i, j int) bool {
|
||||
return func(i, j int) bool {
|
||||
if s[i].CreateTXG < s[j].CreateTXG {
|
||||
return true
|
||||
}
|
||||
if s[i].CreateTXG == s[j].CreateTXG {
|
||||
// Bookmark < Snapshot
|
||||
return s[i].Type == zfs.Bookmark && s[j].Type == zfs.Snapshot
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
if sort.SliceIsSorted(fsvslice, lesser(fsvslice)) {
|
||||
return fsvslice
|
||||
}
|
||||
sorted := make([]zfs.FilesystemVersion, len(fsvslice))
|
||||
copy(sorted, fsvslice)
|
||||
sort.Slice(sorted, lesser(sorted))
|
||||
return sorted
|
||||
}
|
||||
|
||||
// conflict may be a *ConflictDiverged or a *ConflictNoCommonAncestor
|
||||
func IncrementalPath(receiver, sender []zfs.FilesystemVersion) (incPath []zfs.FilesystemVersion, conflict error) {
|
||||
|
||||
if receiver == nil {
|
||||
panic("receiver must not be nil")
|
||||
}
|
||||
if sender == nil {
|
||||
panic("sender must not be nil")
|
||||
}
|
||||
|
||||
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
|
||||
sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender)
|
||||
|
||||
if len(sender) == 0 {
|
||||
return []zfs.FilesystemVersion{}, nil
|
||||
}
|
||||
|
||||
// Find most recent common ancestor by name, preferring snapshots over bookmarks
|
||||
|
||||
mrcaRcv := len(receiver) - 1
|
||||
mrcaSnd := len(sender) - 1
|
||||
|
||||
for mrcaRcv >= 0 && mrcaSnd >= 0 {
|
||||
if receiver[mrcaRcv].Guid == sender[mrcaSnd].Guid {
|
||||
if mrcaSnd-1 >= 0 && sender[mrcaSnd-1].Guid == sender[mrcaSnd].Guid && sender[mrcaSnd-1].Type == zfs.Bookmark {
|
||||
// prefer bookmarks over snapshots as the snapshot might go away sooner
|
||||
mrcaSnd -= 1
|
||||
}
|
||||
break
|
||||
}
|
||||
if receiver[mrcaRcv].CreateTXG < sender[mrcaSnd].CreateTXG {
|
||||
mrcaSnd--
|
||||
} else {
|
||||
mrcaRcv--
|
||||
}
|
||||
}
|
||||
|
||||
if mrcaRcv == -1 || mrcaSnd == -1 {
|
||||
return nil, &ConflictNoCommonAncestor{
|
||||
SortedSenderVersions: sender,
|
||||
SortedReceiverVersions: receiver,
|
||||
}
|
||||
}
|
||||
|
||||
if mrcaRcv != len(receiver)-1 {
|
||||
return nil, &ConflictDiverged{
|
||||
SortedSenderVersions: sender,
|
||||
SortedReceiverVersions: receiver,
|
||||
CommonAncestor: sender[mrcaSnd],
|
||||
SenderOnly: sender[mrcaSnd+1:],
|
||||
ReceiverOnly: receiver[mrcaRcv+1:],
|
||||
}
|
||||
}
|
||||
|
||||
// incPath must not contain bookmarks except initial one,
|
||||
incPath = make([]zfs.FilesystemVersion, 0, len(sender))
|
||||
incPath = append(incPath, sender[mrcaSnd])
|
||||
// it's ok if incPath[0] is a bookmark, but not the subsequent ones in the incPath
|
||||
for i := mrcaSnd + 1; i < len(sender); i++ {
|
||||
if sender[i].Type == zfs.Snapshot && incPath[len(incPath)-1].Guid != sender[i].Guid {
|
||||
incPath = append(incPath, sender[i])
|
||||
}
|
||||
}
|
||||
if len(incPath) == 1 {
|
||||
// nothing to do
|
||||
incPath = incPath[1:]
|
||||
}
|
||||
return incPath, nil
|
||||
}
|
269
cmd/replication/diff_test.go
Normal file
269
cmd/replication/diff_test.go
Normal file
@ -0,0 +1,269 @@
|
||||
package replication_test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zrepl/zrepl/cmd/replication"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func fsvlist(fsv ...string) (r []zfs.FilesystemVersion) {
|
||||
|
||||
r = make([]zfs.FilesystemVersion, len(fsv))
|
||||
for i, f := range fsv {
|
||||
|
||||
// parse the id from fsvlist. it is used to derivce Guid,CreateTXG and Creation attrs
|
||||
split := strings.Split(f, ",")
|
||||
if len(split) != 2 {
|
||||
panic("invalid fsv spec")
|
||||
}
|
||||
id, err := strconv.Atoi(split[1])
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(f, "#") {
|
||||
r[i] = zfs.FilesystemVersion{
|
||||
Name: strings.TrimPrefix(f, "#"),
|
||||
Type: zfs.Bookmark,
|
||||
Guid: uint64(id),
|
||||
CreateTXG: uint64(id),
|
||||
Creation: time.Unix(0, 0).Add(time.Duration(id) * time.Second),
|
||||
}
|
||||
} else if strings.HasPrefix(f, "@") {
|
||||
r[i] = zfs.FilesystemVersion{
|
||||
Name: strings.TrimPrefix(f, "@"),
|
||||
Type: zfs.Snapshot,
|
||||
Guid: uint64(id),
|
||||
CreateTXG: uint64(id),
|
||||
Creation: time.Unix(0, 0).Add(time.Duration(id) * time.Second),
|
||||
}
|
||||
} else {
|
||||
panic("invalid character")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type incPathResult struct {
|
||||
incPath []zfs.FilesystemVersion
|
||||
conflict error
|
||||
}
|
||||
|
||||
type IncrementalPathTest struct {
|
||||
Msg string
|
||||
Receiver, Sender []zfs.FilesystemVersion
|
||||
ExpectIncPath []zfs.FilesystemVersion
|
||||
ExpectNoCommonAncestor bool
|
||||
ExpectDiverged *replication.ConflictDiverged
|
||||
ExpectPanic bool
|
||||
}
|
||||
|
||||
func (tt *IncrementalPathTest) Test(t *testing.T) {
|
||||
|
||||
t.Logf("test: %s", tt.Msg)
|
||||
|
||||
if tt.ExpectPanic {
|
||||
assert.Panics(t, func() {
|
||||
replication.IncrementalPath(tt.Receiver, tt.Sender)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
incPath, conflict := replication.IncrementalPath(tt.Receiver, tt.Sender)
|
||||
|
||||
if tt.ExpectIncPath != nil {
|
||||
assert.Nil(t, conflict)
|
||||
assert.True(t, len(incPath) == 0 || len(incPath) >= 2)
|
||||
assert.Equal(t, tt.ExpectIncPath, incPath)
|
||||
return
|
||||
}
|
||||
if conflict == nil {
|
||||
t.Logf("conflict is (unexpectly) <nil>\nincPath: %#v", incPath)
|
||||
}
|
||||
if tt.ExpectNoCommonAncestor {
|
||||
assert.IsType(t, &replication.ConflictNoCommonAncestor{}, conflict)
|
||||
// TODO check sorting
|
||||
return
|
||||
}
|
||||
if tt.ExpectDiverged != nil {
|
||||
if !assert.IsType(t, &replication.ConflictDiverged{}, conflict) {
|
||||
return
|
||||
}
|
||||
c := conflict.(*replication.ConflictDiverged)
|
||||
// TODO check sorting
|
||||
assert.NotZero(t, c.CommonAncestor)
|
||||
assert.NotEmpty(t, c.ReceiverOnly)
|
||||
assert.Equal(t, tt.ExpectDiverged.ReceiverOnly, c.ReceiverOnly)
|
||||
assert.Equal(t, tt.ExpectDiverged.SenderOnly, c.SenderOnly)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIncrementalPlan_IncrementalSnapshots(t *testing.T) {
|
||||
l := fsvlist
|
||||
|
||||
tbl := []IncrementalPathTest{
|
||||
{
|
||||
Msg: "basic functionality",
|
||||
Receiver: l("@a,1", "@b,2"),
|
||||
Sender: l("@a,1", "@b,2", "@c,3", "@d,4"),
|
||||
ExpectIncPath: l("@b,2", "@c,3", "@d,4"),
|
||||
},
|
||||
{
|
||||
Msg: "no snaps on receiver yields no common ancestor",
|
||||
Receiver: l(),
|
||||
Sender: l("@a,1"),
|
||||
ExpectNoCommonAncestor: true,
|
||||
},
|
||||
{
|
||||
Msg: "no snapshots on sender yields empty incremental path",
|
||||
Receiver: l(),
|
||||
Sender: l(),
|
||||
ExpectIncPath: l(),
|
||||
},
|
||||
{
|
||||
Msg: "nothing to do yields empty incremental path",
|
||||
Receiver: l("@a,1"),
|
||||
Sender: l("@a,1"),
|
||||
ExpectIncPath: l(),
|
||||
},
|
||||
{
|
||||
Msg: "drifting apart",
|
||||
Receiver: l("@a,1", "@b,2"),
|
||||
Sender: l("@c,3", "@d,4"),
|
||||
ExpectNoCommonAncestor: true,
|
||||
},
|
||||
{
|
||||
Msg: "different snapshots on sender and receiver",
|
||||
Receiver: l("@a,1", "@c,2"),
|
||||
Sender: l("@a,1", "@b,3"),
|
||||
ExpectDiverged: &replication.ConflictDiverged{
|
||||
CommonAncestor: l("@a,1")[0],
|
||||
SenderOnly: l("@b,3"),
|
||||
ReceiverOnly: l("@c,2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Msg: "snapshot on receiver not present on sender",
|
||||
Receiver: l("@a,1", "@b,2"),
|
||||
Sender: l("@a,1"),
|
||||
ExpectDiverged: &replication.ConflictDiverged{
|
||||
CommonAncestor: l("@a,1")[0],
|
||||
SenderOnly: l(),
|
||||
ReceiverOnly: l("@b,2"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Msg: "gaps before most recent common ancestor do not matter",
|
||||
Receiver: l("@a,1", "@b,2", "@c,3"),
|
||||
Sender: l("@a,1", "@c,3", "@d,4"),
|
||||
ExpectIncPath: l("@c,3", "@d,4"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tbl {
|
||||
test.Test(t)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIncrementalPlan_BookmarksSupport(t *testing.T) {
|
||||
l := fsvlist
|
||||
|
||||
tbl := []IncrementalPathTest{
|
||||
{
|
||||
Msg: "bookmarks are used",
|
||||
Receiver: l("@a,1"),
|
||||
Sender: l("#a,1", "@b,2"),
|
||||
ExpectIncPath: l("#a,1", "@b,2"),
|
||||
},
|
||||
{
|
||||
Msg: "boomarks are stripped from incPath (cannot send incrementally)",
|
||||
Receiver: l("@a,1"),
|
||||
Sender: l("#a,1", "#b,2", "@c,3"),
|
||||
ExpectIncPath: l("#a,1", "@c,3"),
|
||||
},
|
||||
{
|
||||
Msg: "bookmarks are preferred over snapshots for start of incPath",
|
||||
Receiver: l("@a,1"),
|
||||
Sender: l("#a,1", "@a,1", "@b,2"),
|
||||
ExpectIncPath: l("#a,1", "@b,2"),
|
||||
},
|
||||
{
|
||||
Msg: "bookmarks are preferred over snapshots for start of incPath (regardless of order)",
|
||||
Receiver: l("@a,1"),
|
||||
Sender: l("@a,1", "#a,1", "@b,2"),
|
||||
ExpectIncPath: l("#a,1", "@b,2"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tbl {
|
||||
test.Test(t)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestSortVersionListByCreateTXGThenBookmarkLTSnapshot(t *testing.T) {
|
||||
|
||||
type Test struct {
|
||||
Msg string
|
||||
Input, Output []zfs.FilesystemVersion
|
||||
}
|
||||
|
||||
l := fsvlist
|
||||
|
||||
tbl := []Test{
|
||||
{
|
||||
"snapshot sorting already sorted",
|
||||
l("@a,1", "@b,2"),
|
||||
l("@a,1", "@b,2"),
|
||||
},
|
||||
{
|
||||
"bookmark sorting already sorted",
|
||||
l("#a,1", "#b,2"),
|
||||
l("#a,1", "#b,2"),
|
||||
},
|
||||
{
|
||||
"snapshot sorting",
|
||||
l("@b,2", "@a,1"),
|
||||
l("@a,1", "@b,2"),
|
||||
},
|
||||
{
|
||||
"bookmark sorting",
|
||||
l("#b,2", "#a,1"),
|
||||
l("#a,1", "#b,2"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tbl {
|
||||
t.Logf("test: %s", test.Msg)
|
||||
inputlen := len(test.Input)
|
||||
sorted := replication.SortVersionListByCreateTXGThenBookmarkLTSnapshot(test.Input)
|
||||
if len(sorted) != inputlen {
|
||||
t.Errorf("lenghts of input and output do not match: %d vs %d", inputlen, len(sorted))
|
||||
continue
|
||||
}
|
||||
if !assert.Equal(t, test.Output, sorted) {
|
||||
continue
|
||||
}
|
||||
last := sorted[0]
|
||||
for _, s := range sorted[1:] {
|
||||
if s.CreateTXG < last.CreateTXG {
|
||||
t.Errorf("must be sorted ascending, got:\n\t%#v", sorted)
|
||||
break
|
||||
}
|
||||
if s.CreateTXG == last.CreateTXG {
|
||||
if last.Type == zfs.Bookmark && s.Type != zfs.Snapshot {
|
||||
t.Errorf("snapshots must come after bookmarks")
|
||||
}
|
||||
}
|
||||
last = s
|
||||
}
|
||||
}
|
||||
|
||||
}
|
209
cmd/replication/replication.go
Normal file
209
cmd/replication/replication.go
Normal file
@ -0,0 +1,209 @@
|
||||
package replication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"io"
|
||||
)
|
||||
|
||||
type ReplicationEndpoint interface {
|
||||
// Does not include placeholder filesystems
|
||||
ListFilesystems() ([]Filesystem, error)
|
||||
ListFilesystemVersions(fs string) ([]zfs.FilesystemVersion, error) // fix depS
|
||||
Sender
|
||||
Receiver
|
||||
}
|
||||
|
||||
type Filesystem struct {
|
||||
Path string
|
||||
ResumeToken string
|
||||
}
|
||||
|
||||
type FilteredError struct{ fs string }
|
||||
|
||||
func (f FilteredError) Error() string { return "endpoint does not allow access to filesystem " + f.fs }
|
||||
|
||||
type SendRequest struct {
|
||||
Filesystem string
|
||||
From, To string
|
||||
// If ResumeToken is not empty, the resume token that CAN be tried for 'zfs send' by the sender
|
||||
// If it does not work, the sender SHOULD clear the resume token on their side
|
||||
// and use From and To instead
|
||||
// If ResumeToken is not empty, the GUIDs of From and To
|
||||
// MUST correspond to those encoded in the ResumeToken.
|
||||
// Otherwise, the Sender MUST return an error.
|
||||
ResumeToken string
|
||||
Compress bool
|
||||
Dedup bool
|
||||
}
|
||||
|
||||
type SendResponse struct {
|
||||
Properties zfs.ZFSProperties // fix dep
|
||||
Stream io.Reader
|
||||
}
|
||||
|
||||
type ReceiveRequest struct {
|
||||
Filesystem string
|
||||
// The resume token used by the sending side.
|
||||
// The receiver MUST discard the saved state on their side if ResumeToken
|
||||
// does not match the zfs property of Filesystem on their side.
|
||||
ResumeToken string
|
||||
}
|
||||
|
||||
type ReplicationMode int
|
||||
|
||||
const (
|
||||
ReplicationModePull ReplicationMode = iota
|
||||
ReplicationModePush
|
||||
)
|
||||
|
||||
type EndpointPair struct {
|
||||
a, b ReplicationEndpoint
|
||||
m ReplicationMode
|
||||
}
|
||||
|
||||
func NewEndpointPairPull(sender, receiver ReplicationEndpoint) EndpointPair {
|
||||
return EndpointPair{sender, receiver, ReplicationModePull}
|
||||
}
|
||||
|
||||
func NewEndpointPairPush(sender, receiver ReplicationEndpoint) EndpointPair {
|
||||
return EndpointPair{receiver, sender, ReplicationModePush}
|
||||
}
|
||||
|
||||
func (p EndpointPair) Sender() ReplicationEndpoint {
|
||||
switch p.m {
|
||||
case ReplicationModePull:
|
||||
return p.a
|
||||
case ReplicationModePush:
|
||||
return p.b
|
||||
}
|
||||
panic("should not be reached")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p EndpointPair) Receiver() ReplicationEndpoint {
|
||||
switch p.m {
|
||||
case ReplicationModePull:
|
||||
return p.b
|
||||
case ReplicationModePush:
|
||||
return p.a
|
||||
}
|
||||
panic("should not be reached")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p EndpointPair) Mode() ReplicationMode {
|
||||
return p.m
|
||||
}
|
||||
|
||||
func Replicate(ctx context.Context, ep EndpointPair, ipr IncrementalPathReplicator) {
|
||||
|
||||
sfss, err := ep.Sender().ListFilesystems()
|
||||
if err != nil {
|
||||
// log error
|
||||
return
|
||||
}
|
||||
|
||||
for _, fs := range sfss {
|
||||
sfsvs, err := ep.Sender().ListFilesystemVersions(fs.Path)
|
||||
rfsvs, err := ep.Receiver().ListFilesystemVersions(fs.Path)
|
||||
if err != nil {
|
||||
if _, ok := err.(FilteredError); ok {
|
||||
// Remote does not map filesystem, don't try to tx it
|
||||
continue
|
||||
}
|
||||
// log and ignore
|
||||
continue
|
||||
}
|
||||
|
||||
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
||||
if conflict != nil {
|
||||
// handle or ignore for now
|
||||
continue
|
||||
}
|
||||
|
||||
ipr.Replicate(ctx, ep.Sender(), ep.Receiver(), NewCopier(), fs, path)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type Sender interface {
|
||||
Send(r SendRequest) (SendResponse, error)
|
||||
}
|
||||
|
||||
type Receiver interface {
|
||||
Receive(r ReceiveRequest) (io.Writer, error)
|
||||
}
|
||||
|
||||
type Copier interface {
|
||||
Copy(writer io.Writer, reader io.Reader) (int64, error)
|
||||
}
|
||||
|
||||
type copier struct{}
|
||||
|
||||
func (copier) Copy(writer io.Writer, reader io.Reader) (int64, error) {
|
||||
return io.Copy(writer, reader)
|
||||
}
|
||||
|
||||
func NewCopier() Copier {
|
||||
return copier{}
|
||||
}
|
||||
|
||||
type IncrementalPathReplicator interface {
|
||||
Replicate(ctx context.Context, sender Sender, receiver Receiver, copier Copier, fs Filesystem, path []zfs.FilesystemVersion)
|
||||
}
|
||||
|
||||
type incrementalPathReplicator struct{}
|
||||
|
||||
func NewIncrementalPathReplicator() IncrementalPathReplicator {
|
||||
return incrementalPathReplicator{}
|
||||
}
|
||||
|
||||
func (incrementalPathReplicator) Replicate(ctx context.Context, sender Sender, receiver Receiver, copier Copier, fs Filesystem, path []zfs.FilesystemVersion) {
|
||||
|
||||
if len(path) == 0 {
|
||||
// nothing to do
|
||||
return
|
||||
}
|
||||
|
||||
usedResumeToken := false
|
||||
|
||||
incrementalLoop:
|
||||
for j := 0; j < len(path)-1; j++ {
|
||||
rt := ""
|
||||
if !usedResumeToken {
|
||||
rt = fs.ResumeToken
|
||||
usedResumeToken = true
|
||||
}
|
||||
sr := SendRequest{
|
||||
Filesystem: fs.Path,
|
||||
From: path[j].String(),
|
||||
To: path[j+1].String(),
|
||||
ResumeToken: rt,
|
||||
}
|
||||
sres, err := sender.Send(sr)
|
||||
if err != nil {
|
||||
// handle and ignore
|
||||
break incrementalLoop
|
||||
}
|
||||
// try to consume stream
|
||||
|
||||
rr := ReceiveRequest{
|
||||
Filesystem: fs.Path,
|
||||
ResumeToken: rt,
|
||||
}
|
||||
recvWriter, err := receiver.Receive(rr)
|
||||
if err != nil {
|
||||
// handle and ignore
|
||||
break incrementalLoop
|
||||
}
|
||||
_, err = copier.Copy(recvWriter, sres.Stream)
|
||||
if err != nil {
|
||||
// handle and ignore
|
||||
break incrementalLoop
|
||||
}
|
||||
|
||||
// handle properties from sres
|
||||
}
|
||||
}
|
152
cmd/replication/replication_test.go
Normal file
152
cmd/replication/replication_test.go
Normal file
@ -0,0 +1,152 @@
|
||||
package replication_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zrepl/zrepl/cmd/replication"
|
||||
"github.com/zrepl/zrepl/zfs"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type IncrementalPathSequenceStep struct {
|
||||
SendRequest replication.SendRequest
|
||||
SendResponse replication.SendResponse
|
||||
SendError error
|
||||
ReceiveRequest replication.ReceiveRequest
|
||||
ReceiveWriter io.Writer
|
||||
ReceiveError error
|
||||
}
|
||||
|
||||
type MockIncrementalPathRecorder struct {
|
||||
T *testing.T
|
||||
Sequence []IncrementalPathSequenceStep
|
||||
Pos int
|
||||
}
|
||||
|
||||
func (m *MockIncrementalPathRecorder) Receive(r replication.ReceiveRequest) (io.Writer, error) {
|
||||
if m.Pos >= len(m.Sequence) {
|
||||
m.T.Fatal("unexpected Receive")
|
||||
}
|
||||
i := m.Sequence[m.Pos]
|
||||
m.Pos++
|
||||
if !assert.Equal(m.T, i.ReceiveRequest, r) {
|
||||
m.T.FailNow()
|
||||
}
|
||||
return i.ReceiveWriter, i.ReceiveError
|
||||
}
|
||||
|
||||
func (m *MockIncrementalPathRecorder) Send(r replication.SendRequest) (replication.SendResponse, error) {
|
||||
if m.Pos >= len(m.Sequence) {
|
||||
m.T.Fatal("unexpected Send")
|
||||
}
|
||||
i := m.Sequence[m.Pos]
|
||||
m.Pos++
|
||||
if !assert.Equal(m.T, i.SendRequest, r) {
|
||||
m.T.FailNow()
|
||||
}
|
||||
return i.SendResponse, i.SendError
|
||||
}
|
||||
|
||||
func (m *MockIncrementalPathRecorder) Finished() bool {
|
||||
return m.Pos == len(m.Sequence)
|
||||
}
|
||||
|
||||
type DiscardCopier struct{}
|
||||
|
||||
func (DiscardCopier) Copy(writer io.Writer, reader io.Reader) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
type IncrementalPathReplicatorTest struct {
|
||||
Msg string
|
||||
Filesystem replication.Filesystem
|
||||
Path []zfs.FilesystemVersion
|
||||
Steps []IncrementalPathSequenceStep
|
||||
}
|
||||
|
||||
func (test *IncrementalPathReplicatorTest) Test(t *testing.T) {
|
||||
|
||||
t.Log(test.Msg)
|
||||
|
||||
rec := &MockIncrementalPathRecorder{
|
||||
T: t,
|
||||
Sequence: test.Steps,
|
||||
}
|
||||
|
||||
ipr := replication.NewIncrementalPathReplicator()
|
||||
ipr.Replicate(
|
||||
context.TODO(),
|
||||
rec,
|
||||
rec,
|
||||
DiscardCopier{},
|
||||
test.Filesystem,
|
||||
test.Path,
|
||||
)
|
||||
|
||||
assert.True(t, rec.Finished())
|
||||
|
||||
}
|
||||
|
||||
func TestIncrementalPathReplicator_Replicate(t *testing.T) {
|
||||
|
||||
tbl := []IncrementalPathReplicatorTest{
|
||||
{
|
||||
Msg: "generic happy place with resume token",
|
||||
Filesystem: replication.Filesystem{
|
||||
Path: "foo/bar",
|
||||
ResumeToken: "blafoo",
|
||||
},
|
||||
Path: fsvlist("@a,1", "@b,2", "@c,3"),
|
||||
Steps: []IncrementalPathSequenceStep{
|
||||
{
|
||||
SendRequest: replication.SendRequest{
|
||||
Filesystem: "foo/bar",
|
||||
From: "@a,1",
|
||||
To: "@b,2",
|
||||
ResumeToken: "blafoo",
|
||||
},
|
||||
},
|
||||
{
|
||||
ReceiveRequest: replication.ReceiveRequest{
|
||||
Filesystem: "foo/bar",
|
||||
ResumeToken: "blafoo",
|
||||
},
|
||||
},
|
||||
{
|
||||
SendRequest: replication.SendRequest{
|
||||
Filesystem: "foo/bar",
|
||||
From: "@b,2",
|
||||
To: "@c,3",
|
||||
},
|
||||
},
|
||||
{
|
||||
ReceiveRequest: replication.ReceiveRequest{
|
||||
Filesystem: "foo/bar",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Msg: "no action on empty sequence",
|
||||
Filesystem: replication.Filesystem{
|
||||
Path: "foo/bar",
|
||||
},
|
||||
Path: fsvlist(),
|
||||
Steps: []IncrementalPathSequenceStep{},
|
||||
},
|
||||
{
|
||||
Msg: "no action on invalid path",
|
||||
Filesystem: replication.Filesystem{
|
||||
Path: "foo/bar",
|
||||
},
|
||||
Path: fsvlist("@justone,1"),
|
||||
Steps: []IncrementalPathSequenceStep{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tbl {
|
||||
test.Test(t)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user