move implementation to internal/ directory (#828)
This commit is contained in:
committed by
GitHub
parent
b9b9ad10cf
commit
908807bd59
@@ -0,0 +1,174 @@
|
||||
package diff
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
. "github.com/zrepl/zrepl/internal/replication/logic/pdu"
|
||||
)
|
||||
|
||||
type ConflictNoCommonAncestor struct {
|
||||
SortedSenderVersions, SortedReceiverVersions []*FilesystemVersion
|
||||
}
|
||||
|
||||
func (c *ConflictNoCommonAncestor) Error() string {
|
||||
var buf strings.Builder
|
||||
buf.WriteString("no common snapshot or suitable bookmark between sender and receiver")
|
||||
if len(c.SortedReceiverVersions) > 0 || len(c.SortedSenderVersions) > 0 {
|
||||
buf.WriteString(":\n sorted sender versions:\n")
|
||||
for _, v := range c.SortedSenderVersions {
|
||||
fmt.Fprintf(&buf, " %s\n", v.RelName())
|
||||
}
|
||||
buf.WriteString(" sorted receiver versions:\n")
|
||||
for _, v := range c.SortedReceiverVersions {
|
||||
fmt.Fprintf(&buf, " %s\n", v.RelName())
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type ConflictDiverged struct {
|
||||
SortedSenderVersions, SortedReceiverVersions []*FilesystemVersion
|
||||
CommonAncestor *FilesystemVersion
|
||||
SenderOnly, ReceiverOnly []*FilesystemVersion
|
||||
}
|
||||
|
||||
func (c *ConflictDiverged) Error() string {
|
||||
var buf strings.Builder
|
||||
buf.WriteString("the receiver's latest snapshot is not present on sender:\n")
|
||||
fmt.Fprintf(&buf, " last common: %s\n", c.CommonAncestor.RelName())
|
||||
fmt.Fprintf(&buf, " sender-only:\n")
|
||||
for _, v := range c.SenderOnly {
|
||||
fmt.Fprintf(&buf, " %s\n", v.RelName())
|
||||
}
|
||||
fmt.Fprintf(&buf, " receiver-only:\n")
|
||||
for _, v := range c.ReceiverOnly {
|
||||
fmt.Fprintf(&buf, " %s\n", v.RelName())
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type ConflictNoSenderSnapshots struct{}
|
||||
|
||||
func (c *ConflictNoSenderSnapshots) Error() string {
|
||||
return "no snapshots available on sender side"
|
||||
}
|
||||
|
||||
type ConflictMostRecentSnapshotAlreadyPresent struct {
|
||||
SortedSenderVersions, SortedReceiverVersions []*FilesystemVersion
|
||||
CommonAncestor *FilesystemVersion
|
||||
}
|
||||
|
||||
func (c *ConflictMostRecentSnapshotAlreadyPresent) Error() string {
|
||||
var buf strings.Builder
|
||||
fmt.Fprintf(&buf, "the most recent sender snapshot is already present on the receiver (guid=%v, name=%q)", c.CommonAncestor.GetGuid(), c.CommonAncestor.RelName())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func SortVersionListByCreateTXGThenBookmarkLTSnapshot(fsvslice []*FilesystemVersion) []*FilesystemVersion {
|
||||
lesser := func(s []*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 == FilesystemVersion_Bookmark && s[j].Type == FilesystemVersion_Snapshot
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
if sort.SliceIsSorted(fsvslice, lesser(fsvslice)) {
|
||||
return fsvslice
|
||||
}
|
||||
sorted := make([]*FilesystemVersion, len(fsvslice))
|
||||
copy(sorted, fsvslice)
|
||||
sort.Slice(sorted, lesser(sorted))
|
||||
return sorted
|
||||
}
|
||||
|
||||
func StripBookmarksFromVersionList(fsvslice []*FilesystemVersion) []*FilesystemVersion {
|
||||
fslice := make([]*FilesystemVersion, 0, len(fsvslice))
|
||||
for _, fv := range fsvslice {
|
||||
if fv.Type != FilesystemVersion_Bookmark {
|
||||
fslice = append(fslice, fv)
|
||||
}
|
||||
}
|
||||
return fslice
|
||||
}
|
||||
|
||||
func IncrementalPath(receiver, sender []*FilesystemVersion) (incPath []*FilesystemVersion, conflict error) {
|
||||
|
||||
// Receive-side bookmarks can't be used as incremental-from,
|
||||
// and don't cause recv to fail if there is a newer bookmark than incremetal-form on the receiver.
|
||||
// So, simply mask them out.
|
||||
// This will also hide them in the report, but it keeps the code in this function simple,
|
||||
// and a user who complains about them missing in a conflict message will likely require
|
||||
// more education about bookmarks than a slightly more accurate error message. They'll get
|
||||
// that when they open an issue.
|
||||
receiver = StripBookmarksFromVersionList(receiver)
|
||||
|
||||
receiver = SortVersionListByCreateTXGThenBookmarkLTSnapshot(receiver)
|
||||
sender = SortVersionListByCreateTXGThenBookmarkLTSnapshot(sender)
|
||||
|
||||
var mrcaCandidate struct {
|
||||
found bool
|
||||
guid uint64
|
||||
r, s int
|
||||
}
|
||||
|
||||
findCandidate:
|
||||
for r := len(receiver) - 1; r >= 0; r-- {
|
||||
for s := len(sender) - 1; s >= 0; s-- {
|
||||
if sender[s].GetGuid() == receiver[r].GetGuid() {
|
||||
mrcaCandidate.guid = sender[s].GetGuid()
|
||||
mrcaCandidate.s = s
|
||||
mrcaCandidate.r = r
|
||||
mrcaCandidate.found = true
|
||||
break findCandidate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handle failure cases
|
||||
if !mrcaCandidate.found {
|
||||
if len(sender) == 0 {
|
||||
return nil, &ConflictNoSenderSnapshots{}
|
||||
} else {
|
||||
return nil, &ConflictNoCommonAncestor{
|
||||
SortedSenderVersions: sender,
|
||||
SortedReceiverVersions: receiver,
|
||||
}
|
||||
}
|
||||
} else if mrcaCandidate.r != len(receiver)-1 {
|
||||
return nil, &ConflictDiverged{
|
||||
SortedSenderVersions: sender,
|
||||
SortedReceiverVersions: receiver,
|
||||
CommonAncestor: sender[mrcaCandidate.s],
|
||||
SenderOnly: sender[mrcaCandidate.s+1:],
|
||||
ReceiverOnly: receiver[mrcaCandidate.r+1:],
|
||||
}
|
||||
}
|
||||
|
||||
// incPath is possible
|
||||
|
||||
// incPath must not contain bookmarks except initial one,
|
||||
incPath = make([]*FilesystemVersion, 0, len(sender))
|
||||
incPath = append(incPath, sender[mrcaCandidate.s])
|
||||
// it's ok if incPath[0] is a bookmark, but not the subsequent ones in the incPath
|
||||
for i := mrcaCandidate.s + 1; i < len(sender); i++ {
|
||||
if sender[i].Type == FilesystemVersion_Snapshot && incPath[len(incPath)-1].Guid != sender[i].Guid {
|
||||
incPath = append(incPath, sender[i])
|
||||
}
|
||||
}
|
||||
if len(incPath) == 1 {
|
||||
// nothing to do
|
||||
return nil, &ConflictMostRecentSnapshotAlreadyPresent{
|
||||
SortedSenderVersions: sender,
|
||||
SortedReceiverVersions: receiver,
|
||||
CommonAncestor: sender[mrcaCandidate.s],
|
||||
}
|
||||
}
|
||||
return incPath, nil
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package diff
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
. "github.com/zrepl/zrepl/internal/replication/logic/pdu"
|
||||
)
|
||||
|
||||
func fsvlist(fsv ...string) (r []*FilesystemVersion) {
|
||||
|
||||
r = make([]*FilesystemVersion, len(fsv))
|
||||
for i, f := range fsv {
|
||||
|
||||
// parse the id from fsvlist. it is used to derive 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)
|
||||
}
|
||||
creation := func(id int) string {
|
||||
return FilesystemVersionCreation(time.Unix(0, 0).Add(time.Duration(id) * time.Second))
|
||||
}
|
||||
if strings.HasPrefix(f, "#") {
|
||||
r[i] = &FilesystemVersion{
|
||||
Name: strings.TrimPrefix(f, "#"),
|
||||
Type: FilesystemVersion_Bookmark,
|
||||
Guid: uint64(id),
|
||||
CreateTXG: uint64(id),
|
||||
Creation: creation(id),
|
||||
}
|
||||
} else if strings.HasPrefix(f, "@") {
|
||||
r[i] = &FilesystemVersion{
|
||||
Name: strings.TrimPrefix(f, "@"),
|
||||
Type: FilesystemVersion_Snapshot,
|
||||
Guid: uint64(id),
|
||||
CreateTXG: uint64(id),
|
||||
Creation: creation(id),
|
||||
}
|
||||
} else {
|
||||
panic("invalid character")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func doTest(receiver, sender []*FilesystemVersion, validate func(incpath []*FilesystemVersion, conflict error)) {
|
||||
p, err := IncrementalPath(receiver, sender)
|
||||
validate(p, err)
|
||||
}
|
||||
|
||||
func TestIncrementalPath_SnapshotsOnly(t *testing.T) {
|
||||
|
||||
l := fsvlist
|
||||
|
||||
// basic functionality
|
||||
doTest(l("@a,1", "@b,2"), l("@a,1", "@b,2", "@c,3", "@d,4"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Equal(t, l("@b,2", "@c,3", "@d,4"), path)
|
||||
})
|
||||
|
||||
// no common ancestor
|
||||
doTest(l(), l("@a,1"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, path)
|
||||
ca, ok := conflict.(*ConflictNoCommonAncestor)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, l("@a,1"), ca.SortedSenderVersions)
|
||||
})
|
||||
doTest(l("@a,1", "@b,2"), l("@c,3", "@d,4"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, path)
|
||||
ca, ok := conflict.(*ConflictNoCommonAncestor)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, l("@a,1", "@b,2"), ca.SortedReceiverVersions)
|
||||
assert.Equal(t, l("@c,3", "@d,4"), ca.SortedSenderVersions)
|
||||
})
|
||||
|
||||
// divergence is detected
|
||||
doTest(l("@a,1", "@b1,2"), l("@a,1", "@b2,3"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, path)
|
||||
cd, ok := conflict.(*ConflictDiverged)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, l("@a,1")[0], cd.CommonAncestor)
|
||||
assert.Equal(t, l("@b1,2"), cd.ReceiverOnly)
|
||||
assert.Equal(t, l("@b2,3"), cd.SenderOnly)
|
||||
})
|
||||
|
||||
// gaps before most recent common ancestor do not matter
|
||||
doTest(l("@a,1", "@b,2", "@c,3"), l("@a,1", "@c,3", "@d,4"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Equal(t, l("@c,3", "@d,4"), path)
|
||||
})
|
||||
|
||||
// nothing to do if fully shared history
|
||||
doTest(l("@a,1", "@b,2"), l("@a,1", "@b,2"), func(incpath []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, incpath)
|
||||
assert.NotNil(t, conflict)
|
||||
_, ok := conflict.(*ConflictMostRecentSnapshotAlreadyPresent)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
// ...but it's sufficient if the most recent snapshot is present
|
||||
doTest(l("@c,3"), l("@a,1", "@b,2", "@c,3"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, path)
|
||||
_, ok := conflict.(*ConflictMostRecentSnapshotAlreadyPresent)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
// no sender snapshots errors: empty receiver
|
||||
doTest(l(), l(), func(incpath []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, incpath)
|
||||
assert.NotNil(t, conflict)
|
||||
t.Logf("%T", conflict)
|
||||
_, ok := conflict.(*ConflictNoSenderSnapshots)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
// no sender snapshots errors: snapshots on receiver
|
||||
doTest(l("@a,1"), l(), func(incpath []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, incpath)
|
||||
assert.NotNil(t, conflict)
|
||||
t.Logf("%T", conflict)
|
||||
_, ok := conflict.(*ConflictNoSenderSnapshots)
|
||||
assert.True(t, ok)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestIncrementalPath_BookmarkSupport(t *testing.T) {
|
||||
l := fsvlist
|
||||
|
||||
// bookmarks are used
|
||||
doTest(l("@a,1"), l("#a,1", "@b,2"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Equal(t, l("#a,1", "@b,2"), path)
|
||||
})
|
||||
|
||||
// bookmarks are stripped from IncrementalPath (cannot send incrementally)
|
||||
doTest(l("@a,1"), l("#a,1", "#b,2", "@c,3"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Equal(t, l("#a,1", "@c,3"), path)
|
||||
})
|
||||
|
||||
// test that snapshots are preferred over bookmarks in IncrementalPath
|
||||
doTest(l("@a,1"), l("#a,1", "@a,1", "@b,2"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Equal(t, l("@a,1", "@b,2"), path)
|
||||
})
|
||||
doTest(l("@a,1"), l("@a,1", "#a,1", "@b,2"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Equal(t, l("@a,1", "@b,2"), path)
|
||||
})
|
||||
|
||||
// test that receive-side bookmarks younger than the most recent common ancestor do not cause a conflict
|
||||
doTest(l("@a,1", "#b,2"), l("@a,1", "@c,3"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.NoError(t, conflict)
|
||||
require.Len(t, path, 2)
|
||||
assert.Equal(t, l("@a,1")[0], path[0])
|
||||
assert.Equal(t, l("@c,3")[0], path[1])
|
||||
})
|
||||
doTest(l("#a,1"), l("@a,1", "@b,2"), func(path []*FilesystemVersion, conflict error) {
|
||||
assert.Nil(t, path)
|
||||
ca, ok := conflict.(*ConflictNoCommonAncestor)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, l(), ca.SortedReceiverVersions, "See comment in IncrementalPath() on why we don't include the boomkmark here")
|
||||
})
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Code generated by "enumer -type=InitialReplicationAutoResolution -trimprefix=InitialReplicationAutoResolution"; DO NOT EDIT.
|
||||
|
||||
package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
_InitialReplicationAutoResolutionName_0 = "MostRecentAll"
|
||||
_InitialReplicationAutoResolutionName_1 = "Fail"
|
||||
)
|
||||
|
||||
var (
|
||||
_InitialReplicationAutoResolutionIndex_0 = [...]uint8{0, 10, 13}
|
||||
_InitialReplicationAutoResolutionIndex_1 = [...]uint8{0, 4}
|
||||
)
|
||||
|
||||
func (i InitialReplicationAutoResolution) String() string {
|
||||
switch {
|
||||
case 1 <= i && i <= 2:
|
||||
i -= 1
|
||||
return _InitialReplicationAutoResolutionName_0[_InitialReplicationAutoResolutionIndex_0[i]:_InitialReplicationAutoResolutionIndex_0[i+1]]
|
||||
case i == 4:
|
||||
return _InitialReplicationAutoResolutionName_1
|
||||
default:
|
||||
return fmt.Sprintf("InitialReplicationAutoResolution(%d)", i)
|
||||
}
|
||||
}
|
||||
|
||||
var _InitialReplicationAutoResolutionValues = []InitialReplicationAutoResolution{1, 2, 4}
|
||||
|
||||
var _InitialReplicationAutoResolutionNameToValueMap = map[string]InitialReplicationAutoResolution{
|
||||
_InitialReplicationAutoResolutionName_0[0:10]: 1,
|
||||
_InitialReplicationAutoResolutionName_0[10:13]: 2,
|
||||
_InitialReplicationAutoResolutionName_1[0:4]: 4,
|
||||
}
|
||||
|
||||
// InitialReplicationAutoResolutionString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func InitialReplicationAutoResolutionString(s string) (InitialReplicationAutoResolution, error) {
|
||||
if val, ok := _InitialReplicationAutoResolutionNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to InitialReplicationAutoResolution values", s)
|
||||
}
|
||||
|
||||
// InitialReplicationAutoResolutionValues returns all values of the enum
|
||||
func InitialReplicationAutoResolutionValues() []InitialReplicationAutoResolution {
|
||||
return _InitialReplicationAutoResolutionValues
|
||||
}
|
||||
|
||||
// IsAInitialReplicationAutoResolution returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i InitialReplicationAutoResolution) IsAInitialReplicationAutoResolution() bool {
|
||||
for _, v := range _InitialReplicationAutoResolutionValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,137 @@
|
||||
syntax = "proto3";
|
||||
option go_package = ".;pdu";
|
||||
|
||||
service Replication {
|
||||
rpc Ping(PingReq) returns (PingRes);
|
||||
rpc ListFilesystems(ListFilesystemReq) returns (ListFilesystemRes);
|
||||
rpc ListFilesystemVersions(ListFilesystemVersionsReq)
|
||||
returns (ListFilesystemVersionsRes);
|
||||
rpc DestroySnapshots(DestroySnapshotsReq) returns (DestroySnapshotsRes);
|
||||
rpc ReplicationCursor(ReplicationCursorReq) returns (ReplicationCursorRes);
|
||||
rpc SendDry(SendReq) returns (SendRes);
|
||||
rpc SendCompleted(SendCompletedReq) returns (SendCompletedRes);
|
||||
// for Send and Recv, see package rpc
|
||||
}
|
||||
|
||||
message ListFilesystemReq {}
|
||||
|
||||
message ListFilesystemRes { repeated Filesystem Filesystems = 1; }
|
||||
|
||||
message Filesystem {
|
||||
string Path = 1;
|
||||
string ResumeToken = 2;
|
||||
bool IsPlaceholder = 3;
|
||||
}
|
||||
|
||||
message ListFilesystemVersionsReq { string Filesystem = 1; }
|
||||
|
||||
message ListFilesystemVersionsRes { repeated FilesystemVersion Versions = 1; }
|
||||
|
||||
message FilesystemVersion {
|
||||
enum VersionType {
|
||||
Snapshot = 0;
|
||||
Bookmark = 1;
|
||||
}
|
||||
VersionType Type = 1;
|
||||
string Name = 2;
|
||||
uint64 Guid = 3;
|
||||
uint64 CreateTXG = 4;
|
||||
string Creation = 5; // RFC 3339
|
||||
}
|
||||
|
||||
message SendReq {
|
||||
string Filesystem = 1;
|
||||
// May be empty / null to request a full transfer of To
|
||||
FilesystemVersion From = 2;
|
||||
FilesystemVersion To = 3;
|
||||
|
||||
// If ResumeToken is not empty, the resume token that CAN be used for 'zfs
|
||||
// send' by the sender. The sender MUST indicate use of ResumeToken in the
|
||||
// reply message SendRes.UsedResumeToken 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.
|
||||
string ResumeToken = 4;
|
||||
|
||||
ReplicationConfig ReplicationConfig = 6;
|
||||
}
|
||||
|
||||
message ReplicationConfig {
|
||||
ReplicationConfigProtection protection = 1;
|
||||
}
|
||||
|
||||
|
||||
message ReplicationConfigProtection {
|
||||
ReplicationGuaranteeKind Initial = 1;
|
||||
ReplicationGuaranteeKind Incremental = 2;
|
||||
}
|
||||
|
||||
enum ReplicationGuaranteeKind {
|
||||
GuaranteeInvalid = 0;
|
||||
GuaranteeResumability = 1;
|
||||
GuaranteeIncrementalReplication = 2;
|
||||
GuaranteeNothing = 3;
|
||||
}
|
||||
|
||||
message Property {
|
||||
string Name = 1;
|
||||
string Value = 2;
|
||||
}
|
||||
|
||||
message SendRes {
|
||||
// Whether the resume token provided in the request has been used or not.
|
||||
// If the SendReq.ResumeToken == "", this field MUST be false.
|
||||
bool UsedResumeToken = 1;
|
||||
|
||||
// Expected stream size determined by dry run, not exact.
|
||||
// 0 indicates that for the given SendReq, no size estimate could be made.
|
||||
uint64 ExpectedSize = 2;
|
||||
}
|
||||
|
||||
message SendCompletedReq {
|
||||
SendReq OriginalReq = 2;
|
||||
}
|
||||
|
||||
message SendCompletedRes {}
|
||||
|
||||
message ReceiveReq {
|
||||
string Filesystem = 1;
|
||||
FilesystemVersion To = 2;
|
||||
|
||||
// If true, the receiver should clear the resume token before performing the
|
||||
// zfs recv of the stream in the request
|
||||
bool ClearResumeToken = 3;
|
||||
|
||||
ReplicationConfig ReplicationConfig = 4;
|
||||
}
|
||||
|
||||
message ReceiveRes {}
|
||||
|
||||
message DestroySnapshotsReq {
|
||||
string Filesystem = 1;
|
||||
// Path to filesystem, snapshot or bookmark to be destroyed
|
||||
repeated FilesystemVersion Snapshots = 2;
|
||||
}
|
||||
|
||||
message DestroySnapshotRes {
|
||||
FilesystemVersion Snapshot = 1;
|
||||
string Error = 2;
|
||||
}
|
||||
|
||||
message DestroySnapshotsRes { repeated DestroySnapshotRes Results = 1; }
|
||||
|
||||
message ReplicationCursorReq { string Filesystem = 1; }
|
||||
|
||||
message ReplicationCursorRes {
|
||||
oneof Result {
|
||||
uint64 Guid = 1;
|
||||
bool Notexist = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message PingReq { string Message = 1; }
|
||||
|
||||
message PingRes {
|
||||
// Echo must be PingReq.Message
|
||||
string Echo = 1;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package pdu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
func (v *FilesystemVersion) GetRelName() string {
|
||||
zv, err := v.ZFSFilesystemVersion()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return zv.String()
|
||||
}
|
||||
|
||||
func (v *FilesystemVersion) RelName() string {
|
||||
zv, err := v.ZFSFilesystemVersion()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return zv.String()
|
||||
}
|
||||
|
||||
func (v FilesystemVersion_VersionType) ZFSVersionType() zfs.VersionType {
|
||||
switch v {
|
||||
case FilesystemVersion_Snapshot:
|
||||
return zfs.Snapshot
|
||||
case FilesystemVersion_Bookmark:
|
||||
return zfs.Bookmark
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected v.Type %#v", v))
|
||||
}
|
||||
}
|
||||
|
||||
func FilesystemVersionFromZFS(fsv *zfs.FilesystemVersion) *FilesystemVersion {
|
||||
var t FilesystemVersion_VersionType
|
||||
switch fsv.Type {
|
||||
case zfs.Bookmark:
|
||||
t = FilesystemVersion_Bookmark
|
||||
case zfs.Snapshot:
|
||||
t = FilesystemVersion_Snapshot
|
||||
default:
|
||||
panic("unknown fsv.Type: " + fsv.Type)
|
||||
}
|
||||
return &FilesystemVersion{
|
||||
Type: t,
|
||||
Name: fsv.Name,
|
||||
Guid: fsv.Guid,
|
||||
CreateTXG: fsv.CreateTXG,
|
||||
Creation: fsv.Creation.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func FilesystemVersionCreation(t time.Time) string {
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
func (v *FilesystemVersion) CreationAsTime() (time.Time, error) {
|
||||
return time.Parse(time.RFC3339, v.Creation)
|
||||
}
|
||||
|
||||
// implement fsfsm.FilesystemVersion
|
||||
func (v *FilesystemVersion) SnapshotTime() time.Time {
|
||||
t, err := v.CreationAsTime()
|
||||
if err != nil {
|
||||
panic(err) // FIXME
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (v *FilesystemVersion) ZFSFilesystemVersion() (*zfs.FilesystemVersion, error) {
|
||||
ct, err := v.CreationAsTime()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &zfs.FilesystemVersion{
|
||||
Type: v.Type.ZFSVersionType(),
|
||||
Name: v.Name,
|
||||
Guid: v.Guid,
|
||||
CreateTXG: v.CreateTXG,
|
||||
Creation: ct,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ReplicationConfigProtectionWithKind(both ReplicationGuaranteeKind) *ReplicationConfigProtection {
|
||||
return &ReplicationConfigProtection{
|
||||
Initial: both,
|
||||
Incremental: both,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc v5.28.0
|
||||
// source: pdu.proto
|
||||
|
||||
package pdu
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.64.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
Replication_Ping_FullMethodName = "/Replication/Ping"
|
||||
Replication_ListFilesystems_FullMethodName = "/Replication/ListFilesystems"
|
||||
Replication_ListFilesystemVersions_FullMethodName = "/Replication/ListFilesystemVersions"
|
||||
Replication_DestroySnapshots_FullMethodName = "/Replication/DestroySnapshots"
|
||||
Replication_ReplicationCursor_FullMethodName = "/Replication/ReplicationCursor"
|
||||
Replication_SendDry_FullMethodName = "/Replication/SendDry"
|
||||
Replication_SendCompleted_FullMethodName = "/Replication/SendCompleted"
|
||||
)
|
||||
|
||||
// ReplicationClient is the client API for Replication service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type ReplicationClient interface {
|
||||
Ping(ctx context.Context, in *PingReq, opts ...grpc.CallOption) (*PingRes, error)
|
||||
ListFilesystems(ctx context.Context, in *ListFilesystemReq, opts ...grpc.CallOption) (*ListFilesystemRes, error)
|
||||
ListFilesystemVersions(ctx context.Context, in *ListFilesystemVersionsReq, opts ...grpc.CallOption) (*ListFilesystemVersionsRes, error)
|
||||
DestroySnapshots(ctx context.Context, in *DestroySnapshotsReq, opts ...grpc.CallOption) (*DestroySnapshotsRes, error)
|
||||
ReplicationCursor(ctx context.Context, in *ReplicationCursorReq, opts ...grpc.CallOption) (*ReplicationCursorRes, error)
|
||||
SendDry(ctx context.Context, in *SendReq, opts ...grpc.CallOption) (*SendRes, error)
|
||||
SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error)
|
||||
}
|
||||
|
||||
type replicationClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewReplicationClient(cc grpc.ClientConnInterface) ReplicationClient {
|
||||
return &replicationClient{cc}
|
||||
}
|
||||
|
||||
func (c *replicationClient) Ping(ctx context.Context, in *PingReq, opts ...grpc.CallOption) (*PingRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(PingRes)
|
||||
err := c.cc.Invoke(ctx, Replication_Ping_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *replicationClient) ListFilesystems(ctx context.Context, in *ListFilesystemReq, opts ...grpc.CallOption) (*ListFilesystemRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListFilesystemRes)
|
||||
err := c.cc.Invoke(ctx, Replication_ListFilesystems_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *replicationClient) ListFilesystemVersions(ctx context.Context, in *ListFilesystemVersionsReq, opts ...grpc.CallOption) (*ListFilesystemVersionsRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ListFilesystemVersionsRes)
|
||||
err := c.cc.Invoke(ctx, Replication_ListFilesystemVersions_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *replicationClient) DestroySnapshots(ctx context.Context, in *DestroySnapshotsReq, opts ...grpc.CallOption) (*DestroySnapshotsRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(DestroySnapshotsRes)
|
||||
err := c.cc.Invoke(ctx, Replication_DestroySnapshots_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *replicationClient) ReplicationCursor(ctx context.Context, in *ReplicationCursorReq, opts ...grpc.CallOption) (*ReplicationCursorRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(ReplicationCursorRes)
|
||||
err := c.cc.Invoke(ctx, Replication_ReplicationCursor_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *replicationClient) SendDry(ctx context.Context, in *SendReq, opts ...grpc.CallOption) (*SendRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SendRes)
|
||||
err := c.cc.Invoke(ctx, Replication_SendDry_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *replicationClient) SendCompleted(ctx context.Context, in *SendCompletedReq, opts ...grpc.CallOption) (*SendCompletedRes, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(SendCompletedRes)
|
||||
err := c.cc.Invoke(ctx, Replication_SendCompleted_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ReplicationServer is the server API for Replication service.
|
||||
// All implementations must embed UnimplementedReplicationServer
|
||||
// for forward compatibility.
|
||||
type ReplicationServer interface {
|
||||
Ping(context.Context, *PingReq) (*PingRes, error)
|
||||
ListFilesystems(context.Context, *ListFilesystemReq) (*ListFilesystemRes, error)
|
||||
ListFilesystemVersions(context.Context, *ListFilesystemVersionsReq) (*ListFilesystemVersionsRes, error)
|
||||
DestroySnapshots(context.Context, *DestroySnapshotsReq) (*DestroySnapshotsRes, error)
|
||||
ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error)
|
||||
SendDry(context.Context, *SendReq) (*SendRes, error)
|
||||
SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error)
|
||||
mustEmbedUnimplementedReplicationServer()
|
||||
}
|
||||
|
||||
// UnimplementedReplicationServer must be embedded to have
|
||||
// forward compatible implementations.
|
||||
//
|
||||
// NOTE: this should be embedded by value instead of pointer to avoid a nil
|
||||
// pointer dereference when methods are called.
|
||||
type UnimplementedReplicationServer struct{}
|
||||
|
||||
func (UnimplementedReplicationServer) Ping(context.Context, *PingReq) (*PingRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Ping not implemented")
|
||||
}
|
||||
func (UnimplementedReplicationServer) ListFilesystems(context.Context, *ListFilesystemReq) (*ListFilesystemRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListFilesystems not implemented")
|
||||
}
|
||||
func (UnimplementedReplicationServer) ListFilesystemVersions(context.Context, *ListFilesystemVersionsReq) (*ListFilesystemVersionsRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListFilesystemVersions not implemented")
|
||||
}
|
||||
func (UnimplementedReplicationServer) DestroySnapshots(context.Context, *DestroySnapshotsReq) (*DestroySnapshotsRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DestroySnapshots not implemented")
|
||||
}
|
||||
func (UnimplementedReplicationServer) ReplicationCursor(context.Context, *ReplicationCursorReq) (*ReplicationCursorRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ReplicationCursor not implemented")
|
||||
}
|
||||
func (UnimplementedReplicationServer) SendDry(context.Context, *SendReq) (*SendRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SendDry not implemented")
|
||||
}
|
||||
func (UnimplementedReplicationServer) SendCompleted(context.Context, *SendCompletedReq) (*SendCompletedRes, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SendCompleted not implemented")
|
||||
}
|
||||
func (UnimplementedReplicationServer) mustEmbedUnimplementedReplicationServer() {}
|
||||
func (UnimplementedReplicationServer) testEmbeddedByValue() {}
|
||||
|
||||
// UnsafeReplicationServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to ReplicationServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeReplicationServer interface {
|
||||
mustEmbedUnimplementedReplicationServer()
|
||||
}
|
||||
|
||||
func RegisterReplicationServer(s grpc.ServiceRegistrar, srv ReplicationServer) {
|
||||
// If the following call pancis, it indicates UnimplementedReplicationServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
|
||||
t.testEmbeddedByValue()
|
||||
}
|
||||
s.RegisterService(&Replication_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Replication_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(PingReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).Ping(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Replication_Ping_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).Ping(ctx, req.(*PingReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Replication_ListFilesystems_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListFilesystemReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).ListFilesystems(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Replication_ListFilesystems_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).ListFilesystems(ctx, req.(*ListFilesystemReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Replication_ListFilesystemVersions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ListFilesystemVersionsReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).ListFilesystemVersions(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Replication_ListFilesystemVersions_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).ListFilesystemVersions(ctx, req.(*ListFilesystemVersionsReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Replication_DestroySnapshots_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(DestroySnapshotsReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).DestroySnapshots(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Replication_DestroySnapshots_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).DestroySnapshots(ctx, req.(*DestroySnapshotsReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Replication_ReplicationCursor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(ReplicationCursorReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).ReplicationCursor(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Replication_ReplicationCursor_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).ReplicationCursor(ctx, req.(*ReplicationCursorReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Replication_SendDry_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SendReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).SendDry(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Replication_SendDry_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).SendDry(ctx, req.(*SendReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Replication_SendCompleted_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SendCompletedReq)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(ReplicationServer).SendCompleted(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: Replication_SendCompleted_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(ReplicationServer).SendCompleted(ctx, req.(*SendCompletedReq))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Replication_ServiceDesc is the grpc.ServiceDesc for Replication service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Replication_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "Replication",
|
||||
HandlerType: (*ReplicationServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "Ping",
|
||||
Handler: _Replication_Ping_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListFilesystems",
|
||||
Handler: _Replication_ListFilesystems_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListFilesystemVersions",
|
||||
Handler: _Replication_ListFilesystemVersions_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "DestroySnapshots",
|
||||
Handler: _Replication_DestroySnapshots_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ReplicationCursor",
|
||||
Handler: _Replication_ReplicationCursor_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SendDry",
|
||||
Handler: _Replication_SendDry_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "SendCompleted",
|
||||
Handler: _Replication_SendCompleted_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "pdu.proto",
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package pdu
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFilesystemVersion_RelName(t *testing.T) {
|
||||
|
||||
type TestCase struct {
|
||||
In *FilesystemVersion
|
||||
Out string
|
||||
Panic bool
|
||||
}
|
||||
|
||||
creat := FilesystemVersionCreation(time.Now())
|
||||
tcs := []TestCase{
|
||||
{
|
||||
In: &FilesystemVersion{
|
||||
Type: FilesystemVersion_Snapshot,
|
||||
Name: "foobar",
|
||||
Creation: creat,
|
||||
},
|
||||
Out: "@foobar",
|
||||
},
|
||||
{
|
||||
In: &FilesystemVersion{
|
||||
Type: FilesystemVersion_Bookmark,
|
||||
Name: "foobar",
|
||||
Creation: creat,
|
||||
},
|
||||
Out: "#foobar",
|
||||
},
|
||||
{
|
||||
In: &FilesystemVersion{
|
||||
Type: 2342,
|
||||
Name: "foobar",
|
||||
Creation: creat,
|
||||
},
|
||||
Panic: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
if tc.Panic {
|
||||
assert.Panics(t, func() {
|
||||
tc.In.RelName()
|
||||
})
|
||||
} else {
|
||||
o := tc.In.RelName()
|
||||
assert.Equal(t, tc.Out, o)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestFilesystemVersion_ZFSFilesystemVersion(t *testing.T) {
|
||||
|
||||
empty := &FilesystemVersion{}
|
||||
_, err := empty.ZFSFilesystemVersion()
|
||||
assert.Error(t, err)
|
||||
|
||||
dateInvalid := &FilesystemVersion{Creation: "foobar"}
|
||||
_, err = dateInvalid.ZFSFilesystemVersion()
|
||||
assert.Error(t, err)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/daemon/logging/trace"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/logger"
|
||||
"github.com/zrepl/zrepl/internal/replication/driver"
|
||||
. "github.com/zrepl/zrepl/internal/replication/logic/diff"
|
||||
"github.com/zrepl/zrepl/internal/replication/logic/pdu"
|
||||
"github.com/zrepl/zrepl/internal/replication/report"
|
||||
"github.com/zrepl/zrepl/internal/util/bytecounter"
|
||||
"github.com/zrepl/zrepl/internal/util/chainlock"
|
||||
"github.com/zrepl/zrepl/internal/util/semaphore"
|
||||
"github.com/zrepl/zrepl/internal/zfs"
|
||||
)
|
||||
|
||||
// Endpoint represents one side of the replication.
|
||||
//
|
||||
// An endpoint is either in Sender or Receiver mode, represented by the correspondingly
|
||||
// named interfaces defined in this package.
|
||||
type Endpoint interface {
|
||||
// Does not include placeholder filesystems
|
||||
ListFilesystems(ctx context.Context, req *pdu.ListFilesystemReq) (*pdu.ListFilesystemRes, error)
|
||||
ListFilesystemVersions(ctx context.Context, req *pdu.ListFilesystemVersionsReq) (*pdu.ListFilesystemVersionsRes, error)
|
||||
DestroySnapshots(ctx context.Context, req *pdu.DestroySnapshotsReq) (*pdu.DestroySnapshotsRes, error)
|
||||
WaitForConnectivity(ctx context.Context) error
|
||||
}
|
||||
|
||||
type Sender interface {
|
||||
Endpoint
|
||||
// If a non-nil io.ReadCloser is returned, it is guaranteed to be closed before
|
||||
// any next call to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
||||
// If the send request is for dry run the io.ReadCloser will be nil
|
||||
Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.ReadCloser, error)
|
||||
SendDry(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, error)
|
||||
SendCompleted(ctx context.Context, r *pdu.SendCompletedReq) (*pdu.SendCompletedRes, error)
|
||||
ReplicationCursor(ctx context.Context, req *pdu.ReplicationCursorReq) (*pdu.ReplicationCursorRes, error)
|
||||
}
|
||||
|
||||
type Receiver interface {
|
||||
Endpoint
|
||||
// Receive sends r and sendStream (the latter containing a ZFS send stream)
|
||||
// to the parent github.com/zrepl/zrepl/replication.Endpoint.
|
||||
Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.ReadCloser) (*pdu.ReceiveRes, error)
|
||||
}
|
||||
|
||||
type Planner struct {
|
||||
sender Sender
|
||||
receiver Receiver
|
||||
policy PlannerPolicy
|
||||
|
||||
promSecsPerState *prometheus.HistogramVec // labels: state
|
||||
promBytesReplicated *prometheus.CounterVec // labels: filesystem
|
||||
}
|
||||
|
||||
func (p *Planner) Plan(ctx context.Context) ([]driver.FS, error) {
|
||||
fss, err := p.doPlanning(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dfss := make([]driver.FS, len(fss))
|
||||
for i := range dfss {
|
||||
dfss[i] = fss[i]
|
||||
}
|
||||
return dfss, nil
|
||||
}
|
||||
|
||||
func (p *Planner) WaitForConnectivity(ctx context.Context) error {
|
||||
var wg sync.WaitGroup
|
||||
doPing := func(endpoint Endpoint, errOut *error) {
|
||||
defer wg.Done()
|
||||
ctx, endTask := trace.WithTaskFromStack(ctx)
|
||||
defer endTask()
|
||||
err := endpoint.WaitForConnectivity(ctx)
|
||||
if err != nil {
|
||||
*errOut = err
|
||||
} else {
|
||||
*errOut = nil
|
||||
}
|
||||
}
|
||||
wg.Add(2)
|
||||
var senderErr, receiverErr error
|
||||
go doPing(p.sender, &senderErr)
|
||||
go doPing(p.receiver, &receiverErr)
|
||||
wg.Wait()
|
||||
if senderErr == nil && receiverErr == nil {
|
||||
return nil
|
||||
} else if senderErr != nil && receiverErr != nil {
|
||||
if senderErr.Error() == receiverErr.Error() {
|
||||
return fmt.Errorf("sender and receiver are not reachable: %s", senderErr.Error())
|
||||
} else {
|
||||
return fmt.Errorf("sender and receiver are not reachable:\n sender: %s\n receiver: %s", senderErr, receiverErr)
|
||||
}
|
||||
} else {
|
||||
var side string
|
||||
var err *error
|
||||
if senderErr != nil {
|
||||
side = "sender"
|
||||
err = &senderErr
|
||||
} else {
|
||||
side = "receiver"
|
||||
err = &receiverErr
|
||||
}
|
||||
return fmt.Errorf("%s is not reachable: %s", side, *err)
|
||||
}
|
||||
}
|
||||
|
||||
type Filesystem struct {
|
||||
sender Sender
|
||||
receiver Receiver
|
||||
policy PlannerPolicy // immutable, it's .ReplicationConfig member is a pointer and copied into messages
|
||||
|
||||
Path string // compat
|
||||
receiverFS, senderFS *pdu.Filesystem // receiverFS may be nil, senderFS never nil
|
||||
promBytesReplicated prometheus.Counter // compat
|
||||
|
||||
sizeEstimateRequestSem *semaphore.S
|
||||
}
|
||||
|
||||
func (f *Filesystem) EqualToPreviousAttempt(other driver.FS) bool {
|
||||
g, ok := other.(*Filesystem)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// TODO: use GUIDs (issued by zrepl, not those from ZFS)
|
||||
return f.Path == g.Path
|
||||
}
|
||||
|
||||
func (f *Filesystem) PlanFS(ctx context.Context) ([]driver.Step, error) {
|
||||
steps, err := f.doPlanning(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsteps := make([]driver.Step, len(steps))
|
||||
for i := range dsteps {
|
||||
dsteps[i] = steps[i]
|
||||
}
|
||||
return dsteps, nil
|
||||
}
|
||||
func (f *Filesystem) ReportInfo() *report.FilesystemInfo {
|
||||
return &report.FilesystemInfo{Name: f.Path} // FIXME compat name
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
sender Sender
|
||||
receiver Receiver
|
||||
|
||||
parent *Filesystem
|
||||
from, to *pdu.FilesystemVersion // from may be nil, indicating full send
|
||||
resumeToken string // empty means no resume token shall be used
|
||||
|
||||
expectedSize uint64 // 0 means no size estimate present / possible
|
||||
|
||||
// byteCounter is nil initially, and set later in Step.doReplication
|
||||
// => concurrent read of that pointer from Step.ReportInfo must be protected
|
||||
byteCounter bytecounter.ReadCloser
|
||||
byteCounterMtx chainlock.L
|
||||
}
|
||||
|
||||
func (s *Step) TargetEquals(other driver.Step) bool {
|
||||
t, ok := other.(*Step)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !s.parent.EqualToPreviousAttempt(t.parent) {
|
||||
panic("Step interface promise broken: parent filesystems must be same")
|
||||
}
|
||||
return s.from.GetGuid() == t.from.GetGuid() &&
|
||||
s.to.GetGuid() == t.to.GetGuid()
|
||||
}
|
||||
|
||||
func (s *Step) TargetDate() time.Time {
|
||||
return s.to.SnapshotTime() // FIXME compat name
|
||||
}
|
||||
|
||||
func (s *Step) Step(ctx context.Context) error {
|
||||
return s.doReplication(ctx)
|
||||
}
|
||||
|
||||
func (s *Step) ReportInfo() *report.StepInfo {
|
||||
|
||||
// get current byteCounter value
|
||||
var byteCounter uint64
|
||||
s.byteCounterMtx.Lock()
|
||||
if s.byteCounter != nil {
|
||||
byteCounter = s.byteCounter.Count()
|
||||
}
|
||||
s.byteCounterMtx.Unlock()
|
||||
|
||||
from := ""
|
||||
if s.from != nil {
|
||||
from = s.from.RelName()
|
||||
}
|
||||
return &report.StepInfo{
|
||||
From: from,
|
||||
To: s.to.RelName(),
|
||||
Resumed: s.resumeToken != "",
|
||||
BytesExpected: s.expectedSize,
|
||||
BytesReplicated: byteCounter,
|
||||
}
|
||||
}
|
||||
|
||||
// caller must ensure policy.Validate() == nil
|
||||
func NewPlanner(secsPerState *prometheus.HistogramVec, bytesReplicated *prometheus.CounterVec, sender Sender, receiver Receiver, policy PlannerPolicy) *Planner {
|
||||
if err := policy.Validate(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &Planner{
|
||||
sender: sender,
|
||||
receiver: receiver,
|
||||
policy: policy,
|
||||
promSecsPerState: secsPerState,
|
||||
promBytesReplicated: bytesReplicated,
|
||||
}
|
||||
}
|
||||
|
||||
func tryAutoresolveConflict(conflict error, policy ConflictResolution) (path []*pdu.FilesystemVersion, reason error) {
|
||||
|
||||
if _, ok := conflict.(*ConflictMostRecentSnapshotAlreadyPresent); ok {
|
||||
// replicatoin is a no-op
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if noCommonAncestor, ok := conflict.(*ConflictNoCommonAncestor); ok {
|
||||
if len(noCommonAncestor.SortedReceiverVersions) == 0 {
|
||||
|
||||
if len(noCommonAncestor.SortedSenderVersions) == 0 {
|
||||
return nil, fmt.Errorf("no snapshots available on sender side")
|
||||
}
|
||||
|
||||
switch policy.InitialReplication {
|
||||
|
||||
case InitialReplicationAutoResolutionMostRecent:
|
||||
|
||||
var mostRecentSnap *pdu.FilesystemVersion
|
||||
for n := len(noCommonAncestor.SortedSenderVersions) - 1; n >= 0; n-- {
|
||||
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
||||
mostRecentSnap = noCommonAncestor.SortedSenderVersions[n]
|
||||
break
|
||||
}
|
||||
}
|
||||
return []*pdu.FilesystemVersion{nil, mostRecentSnap}, nil
|
||||
|
||||
case InitialReplicationAutoResolutionAll:
|
||||
|
||||
path = append(path, nil)
|
||||
|
||||
for n := 0; n < len(noCommonAncestor.SortedSenderVersions); n++ {
|
||||
if noCommonAncestor.SortedSenderVersions[n].Type == pdu.FilesystemVersion_Snapshot {
|
||||
path = append(path, noCommonAncestor.SortedSenderVersions[n])
|
||||
}
|
||||
}
|
||||
return path, nil
|
||||
|
||||
case InitialReplicationAutoResolutionFail:
|
||||
|
||||
return nil, fmt.Errorf("automatic conflict resolution for initial replication is disabled in config")
|
||||
|
||||
default:
|
||||
panic(fmt.Sprintf("unimplemented: %#v", policy.InitialReplication))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, conflict
|
||||
}
|
||||
|
||||
func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
|
||||
|
||||
log := getLogger(ctx)
|
||||
|
||||
log.Info("start planning")
|
||||
|
||||
slfssres, err := p.sender.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("errType", fmt.Sprintf("%T", err)).Error("error listing sender filesystems")
|
||||
return nil, err
|
||||
}
|
||||
sfss := slfssres.GetFilesystems()
|
||||
|
||||
rlfssres, err := p.receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
|
||||
if err != nil {
|
||||
log.WithError(err).WithField("errType", fmt.Sprintf("%T", err)).Error("error listing receiver filesystems")
|
||||
return nil, err
|
||||
}
|
||||
rfss := rlfssres.GetFilesystems()
|
||||
|
||||
sizeEstimateRequestSem := semaphore.New(int64(p.policy.SizeEstimationConcurrency))
|
||||
|
||||
q := make([]*Filesystem, 0, len(sfss))
|
||||
for _, fs := range sfss {
|
||||
|
||||
var receiverFS *pdu.Filesystem
|
||||
for _, rfs := range rfss {
|
||||
if rfs.Path == fs.Path {
|
||||
receiverFS = rfs
|
||||
}
|
||||
}
|
||||
|
||||
var ctr prometheus.Counter
|
||||
if p.promBytesReplicated != nil {
|
||||
ctr = p.promBytesReplicated.WithLabelValues(fs.Path)
|
||||
}
|
||||
|
||||
q = append(q, &Filesystem{
|
||||
sender: p.sender,
|
||||
receiver: p.receiver,
|
||||
policy: p.policy,
|
||||
Path: fs.Path,
|
||||
senderFS: fs,
|
||||
receiverFS: receiverFS,
|
||||
promBytesReplicated: ctr,
|
||||
sizeEstimateRequestSem: sizeEstimateRequestSem,
|
||||
})
|
||||
}
|
||||
|
||||
return q, nil
|
||||
}
|
||||
|
||||
func (fs *Filesystem) doPlanning(ctx context.Context) ([]*Step, error) {
|
||||
|
||||
log := func(ctx context.Context) logger.Logger {
|
||||
return getLogger(ctx).WithField("filesystem", fs.Path)
|
||||
}
|
||||
|
||||
log(ctx).Debug("assessing filesystem")
|
||||
|
||||
if fs.senderFS.IsPlaceholder {
|
||||
log(ctx).Debug("sender filesystem is placeholder")
|
||||
if fs.receiverFS != nil {
|
||||
if fs.receiverFS.IsPlaceholder {
|
||||
// all good, fall through
|
||||
log(ctx).Debug("receiver filesystem is placeholder")
|
||||
} else {
|
||||
err := fmt.Errorf("sender filesystem is placeholder, but receiver filesystem is not")
|
||||
log(ctx).Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
log(ctx).Debug("no steps required for replicating placeholders, the endpoint.Receiver will create a placeholder when we receive the first non-placeholder child filesystem")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sfsvsres, err := fs.sender.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path})
|
||||
if err != nil {
|
||||
log(ctx).WithError(err).Error("cannot get remote filesystem versions")
|
||||
return nil, err
|
||||
}
|
||||
sfsvs := sfsvsres.GetVersions()
|
||||
|
||||
if len(sfsvs) < 1 {
|
||||
err := errors.New("sender does not have any versions")
|
||||
log(ctx).Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rfsvs []*pdu.FilesystemVersion
|
||||
if fs.receiverFS != nil && !fs.receiverFS.GetIsPlaceholder() {
|
||||
rfsvsres, err := fs.receiver.ListFilesystemVersions(ctx, &pdu.ListFilesystemVersionsReq{Filesystem: fs.Path})
|
||||
if err != nil {
|
||||
log(ctx).WithError(err).Error("receiver error")
|
||||
return nil, err
|
||||
}
|
||||
rfsvs = rfsvsres.GetVersions()
|
||||
} else {
|
||||
rfsvs = []*pdu.FilesystemVersion{}
|
||||
}
|
||||
|
||||
var resumeToken *zfs.ResumeToken
|
||||
var resumeTokenRaw string
|
||||
if fs.receiverFS != nil && fs.receiverFS.ResumeToken != "" {
|
||||
resumeTokenRaw = fs.receiverFS.ResumeToken // shadow
|
||||
log(ctx).WithField("receiverFS.ResumeToken", resumeTokenRaw).Debug("decode receiver fs resume token")
|
||||
resumeToken, err = zfs.ParseResumeToken(ctx, resumeTokenRaw) // shadow
|
||||
if err != nil {
|
||||
// TODO in theory, we could do replication without resume token, but that would mean that
|
||||
// we need to discard the resumable state on the receiver's side.
|
||||
// Would be easy by setting UsedResumeToken=false in the RecvReq ...
|
||||
// FIXME / CHECK semantics UsedResumeToken if SendReq.ResumeToken == ""
|
||||
log(ctx).WithError(err).Error("cannot decode resume token, aborting")
|
||||
return nil, err
|
||||
}
|
||||
log(ctx).WithField("token", resumeToken).Debug("decode resume token")
|
||||
}
|
||||
|
||||
var steps []*Step
|
||||
// build the list of replication steps
|
||||
//
|
||||
// prefer to resume any started replication instead of starting over with a normal IncrementalPath
|
||||
//
|
||||
// look for the step encoded in the resume token in the sender's version
|
||||
// if we find that step:
|
||||
// 1. use it as first step (including resume token)
|
||||
// 2. compute subsequent steps by computing incremental path from the token.To version on
|
||||
// ...
|
||||
// that's actually equivalent to simply cutting off earlier versions from rfsvs and sfsvs
|
||||
if resumeToken != nil {
|
||||
|
||||
sfsvs := SortVersionListByCreateTXGThenBookmarkLTSnapshot(sfsvs)
|
||||
|
||||
var fromVersion, toVersion *pdu.FilesystemVersion
|
||||
var toVersionIdx int
|
||||
for idx, sfsv := range sfsvs {
|
||||
if resumeToken.HasFromGUID && sfsv.Guid == resumeToken.FromGUID {
|
||||
if fromVersion != nil && fromVersion.Type == pdu.FilesystemVersion_Snapshot {
|
||||
// prefer snapshots over bookmarks for size estimation
|
||||
} else {
|
||||
fromVersion = sfsv
|
||||
}
|
||||
}
|
||||
if resumeToken.HasToGUID && sfsv.Guid == resumeToken.ToGUID && sfsv.Type == pdu.FilesystemVersion_Snapshot {
|
||||
// `toversion` must always be a snapshot
|
||||
toVersion, toVersionIdx = sfsv, idx
|
||||
}
|
||||
}
|
||||
|
||||
if toVersion == nil {
|
||||
return nil, fmt.Errorf("resume token `toguid` = %v not found on sender (`toname` = %q)", resumeToken.ToGUID, resumeToken.ToName)
|
||||
} else if fromVersion == toVersion {
|
||||
return nil, fmt.Errorf("resume token `fromguid` and `toguid` match same version on sener")
|
||||
}
|
||||
// fromVersion may be nil, toVersion is no nil, encryption matches
|
||||
// good to go this one step!
|
||||
resumeStep := &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
|
||||
from: fromVersion,
|
||||
to: toVersion,
|
||||
|
||||
resumeToken: resumeTokenRaw,
|
||||
}
|
||||
|
||||
// by definition, the resume token _must_ be the receiver's most recent version, if they have any
|
||||
// don't bother checking, zfs recv will produce an error if above assumption is wrong
|
||||
//
|
||||
// thus, subsequent steps are just incrementals on the sender's remaining _snapshots_ (not bookmarks)
|
||||
|
||||
var remainingSFSVs []*pdu.FilesystemVersion
|
||||
for _, sfsv := range sfsvs[toVersionIdx:] {
|
||||
if sfsv.Type == pdu.FilesystemVersion_Snapshot {
|
||||
remainingSFSVs = append(remainingSFSVs, sfsv)
|
||||
}
|
||||
}
|
||||
|
||||
steps = make([]*Step, 0, len(remainingSFSVs)) // shadow
|
||||
steps = append(steps, resumeStep)
|
||||
for i := 0; i < len(remainingSFSVs)-1; i++ {
|
||||
steps = append(steps, &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
from: remainingSFSVs[i],
|
||||
to: remainingSFSVs[i+1],
|
||||
})
|
||||
}
|
||||
} else { // resumeToken == nil
|
||||
path, conflict := IncrementalPath(rfsvs, sfsvs)
|
||||
if conflict != nil {
|
||||
updPath, updConflict := tryAutoresolveConflict(conflict, *fs.policy.ConflictResolution)
|
||||
if updConflict == nil {
|
||||
log(ctx).WithField("conflict", conflict).Info("conflict automatically resolved")
|
||||
|
||||
} else {
|
||||
log(ctx).WithField("conflict", conflict).Error("cannot resolve conflict")
|
||||
}
|
||||
path, conflict = updPath, updConflict
|
||||
}
|
||||
if conflict != nil {
|
||||
return nil, conflict
|
||||
}
|
||||
if len(path) == 0 {
|
||||
steps = nil
|
||||
} else if len(path) == 1 {
|
||||
panic(fmt.Sprintf("len(path) must be two for incremental repl, and initial repl must start with nil, got path[0]=%#v", path[0]))
|
||||
} else {
|
||||
steps = make([]*Step, 0, len(path)) // shadow
|
||||
for i := 0; i < len(path)-1; i++ {
|
||||
steps = append(steps, &Step{
|
||||
parent: fs,
|
||||
sender: fs.sender,
|
||||
receiver: fs.receiver,
|
||||
|
||||
from: path[i], // nil in case of initial repl
|
||||
to: path[i+1],
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(steps) == 0 {
|
||||
log(ctx).Info("planning determined that no replication steps are required")
|
||||
}
|
||||
|
||||
log(ctx).Debug("compute send size estimate")
|
||||
errs := make(chan error, len(steps))
|
||||
fanOutCtx, fanOutCancel := context.WithCancel(ctx)
|
||||
_, fanOutAdd, fanOutWait := trace.WithTaskGroup(fanOutCtx, "compute-size-estimate")
|
||||
defer fanOutCancel()
|
||||
for _, step := range steps {
|
||||
step := step // local copy that is moved into the closure
|
||||
fanOutAdd(func(ctx context.Context) {
|
||||
// TODO instead of the semaphore, rely on resource-exhaustion signaled by the remote endpoint to limit size-estimate requests
|
||||
// Send is handled over rpc/dataconn ATM, which doesn't support the resource exhaustion status codes that gRPC defines
|
||||
guard, err := fs.sizeEstimateRequestSem.Acquire(ctx)
|
||||
if err != nil {
|
||||
fanOutCancel()
|
||||
return
|
||||
}
|
||||
defer guard.Release()
|
||||
|
||||
err = step.updateSizeEstimate(ctx)
|
||||
if err != nil {
|
||||
log(ctx).WithError(err).WithField("step", step).Error("error computing size estimate")
|
||||
fanOutCancel()
|
||||
}
|
||||
errs <- err
|
||||
})
|
||||
}
|
||||
fanOutWait()
|
||||
close(errs)
|
||||
var significantErr error = nil
|
||||
for err := range errs {
|
||||
if err != nil {
|
||||
if significantErr == nil || significantErr == context.Canceled {
|
||||
significantErr = err
|
||||
}
|
||||
}
|
||||
}
|
||||
if significantErr != nil {
|
||||
return nil, significantErr
|
||||
}
|
||||
|
||||
log(ctx).Debug("filesystem planning finished")
|
||||
return steps, nil
|
||||
}
|
||||
|
||||
func (s *Step) updateSizeEstimate(ctx context.Context) error {
|
||||
|
||||
log := getLogger(ctx)
|
||||
|
||||
sr := s.buildSendRequest()
|
||||
|
||||
log.Debug("initiate dry run send request")
|
||||
sres, err := s.sender.SendDry(ctx, sr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("dry run send request failed")
|
||||
return err
|
||||
}
|
||||
if sres == nil {
|
||||
err := fmt.Errorf("dry run send request returned nil send result")
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
s.expectedSize = sres.GetExpectedSize()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Step) buildSendRequest() (sr *pdu.SendReq) {
|
||||
fs := s.parent.Path
|
||||
sr = &pdu.SendReq{
|
||||
Filesystem: fs,
|
||||
From: s.from, // may be nil
|
||||
To: s.to,
|
||||
ResumeToken: s.resumeToken,
|
||||
ReplicationConfig: s.parent.policy.ReplicationConfig,
|
||||
}
|
||||
return sr
|
||||
}
|
||||
|
||||
func (s *Step) doReplication(ctx context.Context) error {
|
||||
|
||||
fs := s.parent.Path
|
||||
|
||||
log := getLogger(ctx).WithField("filesystem", fs)
|
||||
sr := s.buildSendRequest()
|
||||
|
||||
log.Debug("initiate send request")
|
||||
sres, stream, err := s.sender.Send(ctx, sr)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("send request failed")
|
||||
return err
|
||||
}
|
||||
if sres == nil {
|
||||
err := fmt.Errorf("send request returned nil send result")
|
||||
log.Error(err.Error())
|
||||
return err
|
||||
}
|
||||
if stream == nil {
|
||||
err := errors.New("send request did not return a stream, broken endpoint implementation")
|
||||
return err
|
||||
}
|
||||
defer stream.Close()
|
||||
|
||||
// Install a byte counter to track progress + for status report
|
||||
byteCountingStream := bytecounter.NewReadCloser(stream)
|
||||
s.byteCounterMtx.Lock()
|
||||
s.byteCounter = byteCountingStream
|
||||
s.byteCounterMtx.Unlock()
|
||||
defer func() {
|
||||
defer s.byteCounterMtx.Lock().Unlock()
|
||||
if s.parent.promBytesReplicated != nil {
|
||||
s.parent.promBytesReplicated.Add(float64(s.byteCounter.Count()))
|
||||
}
|
||||
}()
|
||||
|
||||
rr := &pdu.ReceiveReq{
|
||||
Filesystem: fs,
|
||||
To: sr.GetTo(),
|
||||
ClearResumeToken: !sres.UsedResumeToken,
|
||||
ReplicationConfig: s.parent.policy.ReplicationConfig,
|
||||
}
|
||||
log.Debug("initiate receive request")
|
||||
_, err = s.receiver.Receive(ctx, rr, byteCountingStream)
|
||||
if err != nil {
|
||||
log.
|
||||
WithError(err).
|
||||
WithField("errType", fmt.Sprintf("%T", err)).
|
||||
WithField("rr", fmt.Sprintf("%v", rr)).
|
||||
Error("receive request failed (might also be error on sender)")
|
||||
// This failure could be due to
|
||||
// - an unexpected exit of ZFS on the sending side
|
||||
// - an unexpected exit of ZFS on the receiving side
|
||||
// - a connectivity issue
|
||||
return err
|
||||
}
|
||||
log.Debug("receive finished")
|
||||
|
||||
log.Debug("tell sender replication completed")
|
||||
_, err = s.sender.SendCompleted(ctx, &pdu.SendCompletedReq{
|
||||
OriginalReq: sr,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("error telling sender that replication completed successfully")
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Step) String() string {
|
||||
if s.from == nil { // FIXME: ZFS semantics are that to is nil on non-incremental send
|
||||
return fmt.Sprintf("%s%s (full)", s.parent.Path, s.to.RelName())
|
||||
} else {
|
||||
return fmt.Sprintf("%s(%s => %s)", s.parent.Path, s.from.RelName(), s.to.RelName())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/daemon/logging"
|
||||
"github.com/zrepl/zrepl/internal/logger"
|
||||
)
|
||||
|
||||
func getLogger(ctx context.Context) logger.Logger {
|
||||
return logging.GetLogger(ctx, logging.SubsysReplication)
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/go-playground/validator"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/zrepl/zrepl/internal/config"
|
||||
"github.com/zrepl/zrepl/internal/replication/logic/pdu"
|
||||
)
|
||||
|
||||
//go:generate enumer -type=InitialReplicationAutoResolution -trimprefix=InitialReplicationAutoResolution
|
||||
type InitialReplicationAutoResolution uint32
|
||||
|
||||
const (
|
||||
InitialReplicationAutoResolutionMostRecent InitialReplicationAutoResolution = 1 << iota
|
||||
InitialReplicationAutoResolutionAll
|
||||
InitialReplicationAutoResolutionFail
|
||||
)
|
||||
|
||||
var initialReplicationAutoResolutionConfigMap = map[InitialReplicationAutoResolution]string{
|
||||
InitialReplicationAutoResolutionMostRecent: "most_recent",
|
||||
InitialReplicationAutoResolutionAll: "all",
|
||||
InitialReplicationAutoResolutionFail: "fail",
|
||||
}
|
||||
|
||||
func InitialReplicationAutoResolutionFromConfig(in string) (InitialReplicationAutoResolution, error) {
|
||||
for v, s := range initialReplicationAutoResolutionConfigMap {
|
||||
if s == in {
|
||||
return v, nil
|
||||
}
|
||||
}
|
||||
l := make([]string, 0, len(initialReplicationAutoResolutionConfigMap))
|
||||
for _, v := range InitialReplicationAutoResolutionValues() {
|
||||
l = append(l, initialReplicationAutoResolutionConfigMap[v])
|
||||
}
|
||||
return 0, fmt.Errorf("invalid value %q, must be one of %s", in, strings.Join(l, ", "))
|
||||
}
|
||||
|
||||
type ConflictResolution struct {
|
||||
InitialReplication InitialReplicationAutoResolution
|
||||
}
|
||||
|
||||
func (c *ConflictResolution) Validate() error {
|
||||
if !c.InitialReplication.IsAInitialReplicationAutoResolution() {
|
||||
return errors.Errorf("must be one of %s", InitialReplicationAutoResolutionValues())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ConflictResolutionFromConfig(in *config.ConflictResolution) (*ConflictResolution, error) {
|
||||
|
||||
initialReplication, err := InitialReplicationAutoResolutionFromConfig(in.InitialReplication)
|
||||
if err != nil {
|
||||
return nil, errors.Errorf("field `initial_replication` is invalid: %q is not one of %v", in.InitialReplication, InitialReplicationAutoResolutionValues())
|
||||
}
|
||||
|
||||
return &ConflictResolution{
|
||||
InitialReplication: initialReplication,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type PlannerPolicy struct {
|
||||
ConflictResolution *ConflictResolution `validate:"ne=nil"`
|
||||
ReplicationConfig *pdu.ReplicationConfig `validate:"ne=nil"`
|
||||
SizeEstimationConcurrency int `validate:"gte=1"`
|
||||
}
|
||||
|
||||
var validate = validator.New()
|
||||
|
||||
func (p PlannerPolicy) Validate() error {
|
||||
if err := validate.Struct(p); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.ConflictResolution.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReplicationConfigFromConfig(in *config.Replication) (*pdu.ReplicationConfig, error) {
|
||||
initial, err := pduReplicationGuaranteeKindFromConfig(in.Protection.Initial)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "field 'initial'")
|
||||
}
|
||||
incremental, err := pduReplicationGuaranteeKindFromConfig(in.Protection.Incremental)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "field 'incremental'")
|
||||
}
|
||||
return &pdu.ReplicationConfig{
|
||||
Protection: &pdu.ReplicationConfigProtection{
|
||||
Initial: initial,
|
||||
Incremental: incremental,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func pduReplicationGuaranteeKindFromConfig(in string) (k pdu.ReplicationGuaranteeKind, _ error) {
|
||||
switch in {
|
||||
case "guarantee_nothing":
|
||||
return pdu.ReplicationGuaranteeKind_GuaranteeNothing, nil
|
||||
case "guarantee_incremental":
|
||||
return pdu.ReplicationGuaranteeKind_GuaranteeIncrementalReplication, nil
|
||||
case "guarantee_resumability":
|
||||
return pdu.ReplicationGuaranteeKind_GuaranteeResumability, nil
|
||||
default:
|
||||
return k, errors.Errorf("%q is not in guarantee_{nothing,incremental,resumability}", in)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user