move implementation to internal/ directory (#828)

This commit is contained in:
Christian Schwarz
2024-10-18 19:21:17 +02:00
committed by GitHub
parent b9b9ad10cf
commit 908807bd59
360 changed files with 507 additions and 507 deletions
+174
View File
@@ -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
+137
View File
@@ -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)
}
}