Compare commits

..

19 Commits

Author SHA1 Message Date
JMoVS ad8be226fd fix small typo 2018-10-22 11:32:37 +02:00
Christian Schwarz 9b3e5c38e2 docs: fix changelog + invocations of wakeup subcommand 2018-10-22 11:27:00 +02:00
Christian Schwarz 7e1c5f5d1f docs: discourage use of ssh+stdinserver transport due to inferior error handling 2018-10-22 11:25:16 +02:00
Christian Schwarz 98bc8d1717 daemon/job: explicit notice of ZREPL_JOB_WATCHDOG_TIMEOUT environment variable on cancellation 2018-10-22 11:03:31 +02:00
Christian Schwarz 2889a5d5ff client/status: current bytes/second + spinning progress bar 2018-10-21 23:15:21 +02:00
Christian Schwarz 0b8c19c620 docs/tutorial: switch to push setup & use mutual TLS (2 machines) 2018-10-21 22:20:35 +02:00
Christian Schwarz a62b475f46 docs/transport/tls: document self-signed certs procedure for 2-machine setup 2018-10-21 22:20:07 +02:00
Christian Schwarz 1691839c6b replication: handle context cancellation errors as GlobalError 2018-10-21 19:06:35 +02:00
Christian Schwarz 36265ff349 fixup 438f950be3: forgotten ErrorCount in printf 2018-10-21 18:37:57 +02:00
Christian Schwarz 94427d334b replication + pruner + watchdog: adjust timeouts based on practical experience 2018-10-21 18:37:57 +02:00
Christian Schwarz b2844569c8 replication: rewrite error handling + simplify state machines
* Remove explicity state machine code for all but replication.Replication
* Introduce explicit error types that satisfy interfaces which provide
  sufficient information for replication.Replication to make intelligent
  retry + queuing decisions

  * Temporary()
  * LocalToFS()

* Remove the queue and replace it with a simple array that we sort each
  time (yay no generics :( )
2018-10-21 18:37:57 +02:00
Christian Schwarz ae5e60b1ae client/status: display problems as wrapped + indented if they do not fit the current line 2018-10-21 17:50:08 +02:00
Christian Schwarz fffda09f67 replication + pruner: progress markers during planning 2018-10-21 17:50:08 +02:00
Christian Schwarz 5ec7a5c078 pruner: report: fix broken checks for state (wrong precedence rules) 2018-10-21 13:37:08 +02:00
Christian Schwarz 190c7270d9 daemon/active + watchdog: simplify control flow using explicit ActiveSideState 2018-10-21 12:53:34 +02:00
Christian Schwarz f704b28cad daemon/job: track active side state explicitly 2018-10-21 12:52:48 +02:00
Christian Schwarz 5efeec1819 daemon/control: stop logging status endpoint requests 2018-10-20 12:50:31 +02:00
Christian Schwarz 438f950be3 pruner: improve cancellation + error handling strategy
Pruner now backs off as soon as there is an error, making that error the
Error field in the pruner report.
The error is also stored in the specific *fs that failed, and we
maintain an error counter per *fs to de-prioritize those fs that failed.
Like with replication, the de-prioritization on errors is to avoid '
getting stuck' with an individual filesystem until the watchdog hits.
2018-10-20 12:46:43 +02:00
Christian Schwarz 50c1549865 pruner: fixup 69bfcb7bed: add missing progress updates for watchdog 2018-10-20 10:58:22 +02:00
24 changed files with 1116 additions and 670 deletions
Generated
+3 -3
View File
@@ -174,15 +174,15 @@
revision = "391d2c78c8404a9683d79f75dd24ab53040f89f7"
[[projects]]
digest = "1:c2ba1c9dc003c15856e4529dac028cacba08ee8924300f058b3467cde9acf7a9"
digest = "1:1bcbb0a7ad8d3392d446eb583ae5415ff987838a8f7331a36877789be20667e6"
name = "github.com/problame/go-streamrpc"
packages = [
".",
"internal/pdu",
]
pruneopts = ""
revision = "de6f6a4041c77f700f02d8fe749e54efa50811f7"
version = "v0.4"
revision = "d5d111e014342fe1c37f0b71cc37ec5f2afdfd13"
version = "v0.5"
[[projects]]
branch = "master"
+1 -1
View File
@@ -66,7 +66,7 @@ ignored = [ "github.com/inconshreveable/mousetrap" ]
[[constraint]]
name = "github.com/problame/go-streamrpc"
version = "0.4.0"
version = "0.5.0"
-1
View File
@@ -22,7 +22,6 @@ SUBPKGS += pruning/retentiongrid
SUBPKGS += replication
SUBPKGS += replication/fsrep
SUBPKGS += replication/pdu
SUBPKGS += replication/internal/queue
SUBPKGS += replication/internal/diff
SUBPKGS += tlsconf
SUBPKGS += util
+126 -39
View File
@@ -22,6 +22,41 @@ import (
"time"
)
type bytesProgressHistory struct {
changeCounter int
lastChangeAt time.Time
last int64
bpsIncreaseExpAvg float64
}
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
if currentVal < p.last {
*p = bytesProgressHistory{
last: currentVal,
lastChangeAt: time.Now(),
}
}
defer func() {
p.last = currentVal
}()
if time.Now().Sub(p.lastChangeAt) > 3 *time.Second { // FIXME depends on refresh frequency
p.changeCounter = 0
p.bpsIncreaseExpAvg = 0
}
if currentVal != p.last {
p.changeCounter++
p.lastChangeAt = time.Now()
}
byteIncrease := float64(currentVal - p.last)
if byteIncrease < 0 {
byteIncrease = 0
}
const factor = 0.1
p.bpsIncreaseExpAvg = (1-factor) * p.bpsIncreaseExpAvg + factor *byteIncrease
return int64(p.bpsIncreaseExpAvg), p.changeCounter
}
type tui struct {
x, y int
indent int
@@ -29,10 +64,14 @@ type tui struct {
lock sync.Mutex //For report and error
report map[string]job.Status
err error
replicationProgress map[string]*bytesProgressHistory // by job name
}
func newTui() tui {
return tui{}
return tui{
replicationProgress: make(map[string]*bytesProgressHistory, 0),
}
}
func (t *tui) moveCursor(x, y int) {
@@ -40,9 +79,11 @@ func (t *tui) moveCursor(x, y int) {
t.y += y
}
const INDENT_MULTIPLIER = 4
func (t *tui) moveLine(dl int, col int) {
t.y += dl
t.x = t.indent*4 + col
t.x = t.indent*INDENT_MULTIPLIER + col
}
func (t *tui) write(text string) {
@@ -60,6 +101,40 @@ func (t *tui) printf(text string, a ...interface{}) {
t.write(fmt.Sprintf(text, a...))
}
func wrap(s string, width int) string {
var b strings.Builder
for len(s) > 0 {
rem := width
if rem > len(s) {
rem = len(s)
}
if idx := strings.IndexAny(s, "\n\r"); idx != -1 && idx < rem {
rem = idx+1
}
untilNewline := strings.TrimSpace(s[:rem])
s = s[rem:]
if len(untilNewline) == 0 {
continue
}
b.WriteString(untilNewline)
b.WriteString("\n")
}
return strings.TrimSpace(b.String())
}
func (t *tui) printfDrawIndentedAndWrappedIfMultiline(format string, a ...interface{}) {
whole := fmt.Sprintf(format, a...)
width, _ := termbox.Size()
if !strings.ContainsAny(whole, "\n\r") && t.x + len(whole) <= width {
t.printf(format, a...)
} else {
t.addIndent(1)
t.newline()
t.write(wrap(whole, width - INDENT_MULTIPLIER*t.indent))
t.addIndent(-1)
}
}
func (t *tui) newline() {
t.moveLine(1, 0)
}
@@ -74,6 +149,7 @@ func (t *tui) addIndent(indent int) {
t.moveLine(0, 0)
}
var statusFlags struct {
Raw bool
}
@@ -167,6 +243,15 @@ loop:
}
func (t *tui) getReplicationProgresHistory(jobName string) *bytesProgressHistory {
p, ok := t.replicationProgress[jobName]
if !ok {
p = &bytesProgressHistory{}
t.replicationProgress[jobName] = p
}
return p
}
func (t *tui) draw() {
t.lock.Lock()
defer t.lock.Unlock()
@@ -226,7 +311,7 @@ func (t *tui) draw() {
t.printf("Replication:")
t.newline()
t.addIndent(1)
t.renderReplicationReport(pushStatus.Replication)
t.renderReplicationReport(pushStatus.Replication, t.getReplicationProgresHistory(k))
t.addIndent(-1)
t.printf("Pruning Sender:")
@@ -246,7 +331,7 @@ func (t *tui) draw() {
termbox.Flush()
}
func (t *tui) renderReplicationReport(rep *replication.Report) {
func (t *tui) renderReplicationReport(rep *replication.Report, history *bytesProgressHistory) {
if rep == nil {
t.printf("...\n")
return
@@ -271,7 +356,8 @@ func (t *tui) renderReplicationReport(rep *replication.Report) {
t.printf("Status: %s", state)
t.newline()
if rep.Problem != "" {
t.printf("Problem: %s", rep.Problem)
t.printf("Problem: ")
t.printfDrawIndentedAndWrappedIfMultiline("%s", rep.Problem)
t.newline()
}
if rep.SleepUntil.After(time.Now()) && !state.IsTerminal() {
@@ -297,9 +383,10 @@ func (t *tui) renderReplicationReport(rep *replication.Report) {
transferred += fstx
total += fstotal
}
rate, changeCount := history.Update(transferred)
t.write("Progress: ")
t.drawBar(80, transferred, total)
t.write(fmt.Sprintf(" %s / %s", ByteCountBinary(transferred), ByteCountBinary(total)))
t.drawBar(50, transferred, total, changeCount)
t.write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(transferred), ByteCountBinary(total), ByteCountBinary(rate)))
t.newline()
}
@@ -310,7 +397,7 @@ func (t *tui) renderReplicationReport(rep *replication.Report) {
}
}
for _, fs := range all {
printFilesystemStatus(fs, t, fs == rep.Active, maxFSLen)
t.printFilesystemStatus(fs, fs == rep.Active, maxFSLen)
}
}
@@ -391,8 +478,8 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
for _, fs := range all {
t.write(rightPad(fs.Filesystem, maxFSname, " "))
t.write(" ")
if fs.Error != "" {
t.printf("ERROR: %s\n", fs.Error) // whitespace is padding
if fs.LastError != "" {
t.printf("ERROR (%d): %s\n", fs.ErrorCount, fs.LastError) // whitespace is padding
continue
}
@@ -400,11 +487,11 @@ func (t *tui) renderPrunerReport(r *pruner.Report) {
len(fs.DestroyList), len(fs.SnapshotList))
if fs.completed {
t.printf( "Completed %s\n", pruneRuleActionStr)
t.printf( "Completed %s\n", pruneRuleActionStr)
continue
}
t.write("Pending ") // whitespace is padding 10
t.write("Pending ") // whitespace is padding 10
if len(fs.DestroyList) == 1 {
t.write(fs.DestroyList[0].Name)
} else {
@@ -456,7 +543,10 @@ func leftPad(str string, length int, pad string) string {
return times(pad, length-len(str)) + str
}
func (t *tui) drawBar(length int, bytes, totalBytes int64) {
var arrowPositions = `>\|/`
// changeCount = 0 indicates stall / no progresss
func (t *tui) drawBar(length int, bytes, totalBytes int64, changeCount int) {
var completedLength int
if totalBytes > 0 {
completedLength = int(int64(length) * bytes / totalBytes)
@@ -469,7 +559,7 @@ func (t *tui) drawBar(length int, bytes, totalBytes int64) {
t.write("[")
t.write(times("=", completedLength))
t.write(">")
t.write( string(arrowPositions[changeCount%len(arrowPositions)]))
t.write(times("-", length-completedLength))
t.write("]")
}
@@ -477,19 +567,17 @@ func (t *tui) drawBar(length int, bytes, totalBytes int64) {
func StringStepState(s fsrep.StepState) string {
switch s {
case fsrep.StepReplicationReady: return "Ready"
case fsrep.StepReplicationRetry: return "Retry"
case fsrep.StepMarkReplicatedReady: return "MarkReady"
case fsrep.StepMarkReplicatedRetry: return "MarkRetry"
case fsrep.StepPermanentError: return "PermanentError"
case fsrep.StepCompleted: return "Completed"
default:
return fmt.Sprintf("UNKNOWN %d", s)
}
}
func filesystemStatusString(rep *fsrep.Report, active bool, fsWidth int) (line string, bytes, totalBytes int64) {
bytes = int64(0)
totalBytes = int64(0)
func (t *tui) printFilesystemStatus(rep *fsrep.Report, active bool, maxFS int) {
bytes := int64(0)
totalBytes := int64(0)
for _, s := range rep.Pending {
bytes += s.Bytes
totalBytes += s.ExpectedBytes
@@ -499,36 +587,35 @@ func filesystemStatusString(rep *fsrep.Report, active bool, fsWidth int) (line s
totalBytes += s.ExpectedBytes
}
next := ""
if rep.Problem != "" {
next = " problem: " + rep.Problem
} else if len(rep.Pending) > 0 {
if rep.Pending[0].From != "" {
next = fmt.Sprintf(" next: %s => %s", rep.Pending[0].From, rep.Pending[0].To)
} else {
next = fmt.Sprintf(" next: %s (full)", rep.Pending[0].To)
}
}
status := fmt.Sprintf("%s (step %d/%d, %s/%s)%s",
status := fmt.Sprintf("%s (step %d/%d, %s/%s)",
rep.Status,
len(rep.Completed), len(rep.Pending) + len(rep.Completed),
ByteCountBinary(bytes), ByteCountBinary(totalBytes),
next,
)
activeIndicator := " "
if active {
activeIndicator = "*"
}
line = fmt.Sprintf("%s %s %s",
t.printf("%s %s %s ",
activeIndicator,
rightPad(rep.Filesystem, fsWidth, " "),
rightPad(rep.Filesystem, maxFS, " "),
status)
return line, bytes, totalBytes
}
func printFilesystemStatus(rep *fsrep.Report, t *tui, active bool, maxFS int) {
totalStatus, _, _ := filesystemStatusString(rep, active, maxFS)
t.write(totalStatus)
next := ""
if rep.Problem != "" {
next = rep.Problem
} else if len(rep.Pending) > 0 {
if rep.Pending[0].From != "" {
next = fmt.Sprintf("next: %s => %s", rep.Pending[0].From, rep.Pending[0].To)
} else {
next = fmt.Sprintf("next: %s (full)", rep.Pending[0].To)
}
}
t.printfDrawIndentedAndWrappedIfMultiline("%s", next)
t.newline()
}
+3 -2
View File
@@ -100,10 +100,11 @@ func (j *controlJob) Run(ctx context.Context) {
}}})
mux.Handle(ControlJobEndpointStatus,
requestLogger{log: log, handler: jsonResponder{func() (interface{}, error) {
// don't log requests to status endpoint, too spammy
jsonResponder{func() (interface{}, error) {
s := j.jobs.status()
return s, nil
}}})
}})
mux.Handle(ControlJobEndpointSignal,
requestLogger{log: log, handler: jsonRequestResponder{func(decoder jsonDecoder) (interface{}, error) {
+84 -40
View File
@@ -6,19 +6,18 @@ import (
"github.com/problame/go-streamrpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/zrepl/zrepl/config"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/job/reset"
"github.com/zrepl/zrepl/daemon/job/wakeup"
"github.com/zrepl/zrepl/daemon/transport/connecter"
"github.com/zrepl/zrepl/daemon/filters"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/daemon/pruner"
"github.com/zrepl/zrepl/daemon/snapper"
"github.com/zrepl/zrepl/daemon/transport/connecter"
"github.com/zrepl/zrepl/endpoint"
"github.com/zrepl/zrepl/replication"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/watchdog"
"github.com/zrepl/zrepl/zfs"
"sync"
"github.com/zrepl/zrepl/daemon/logging"
"github.com/zrepl/zrepl/daemon/snapper"
"time"
)
@@ -38,10 +37,29 @@ type ActiveSide struct {
tasks activeSideTasks
}
//go:generate enumer -type=ActiveSideState
type ActiveSideState int
const (
ActiveSideReplicating ActiveSideState = 1 << iota
ActiveSidePruneSender
ActiveSidePruneReceiver
ActiveSideDone // also errors
)
type activeSideTasks struct {
state ActiveSideState
// valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
replication *replication.Replication
replicationCancel context.CancelFunc
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
prunerSender, prunerReceiver *pruner.Pruner
// valid for state ActiveSidePruneReceiver, ActiveSideDone
prunerSenderCancel, prunerReceiverCancel context.CancelFunc
}
@@ -254,11 +272,8 @@ func (j *ActiveSide) do(ctx context.Context) {
// allow cancellation of an invocation (this function)
ctx, cancelThisRun := context.WithCancel(ctx)
defer cancelThisRun()
runDone := make(chan struct{})
defer close(runDone)
go func() {
select {
case <-runDone:
case <-reset.Wait(ctx):
log.Info("reset received, cancelling current invocation")
cancelThisRun()
@@ -266,53 +281,74 @@ func (j *ActiveSide) do(ctx context.Context) {
}
}()
// watchdog
// The code after this watchdog goroutine is sequential and transitions the state from
// ActiveSideReplicating -> ActiveSidePruneSender -> ActiveSidePruneReceiver -> ActiveSideDone
// If any of those sequential tasks 'gets stuck' (livelock, no progress), the watchdog will eventually
// cancel its context.
// If the task is written to support context cancellation, it will return immediately (in permanent error state),
// and the sequential code above transitions to the next state.
go func() {
// if no progress after 1 minute, kill the task
wdto := envconst.Duration("ZREPL_JOB_WATCHDOG_TIMEOUT", 1*time.Minute)
log.WithField("watchdog_timeout", wdto.String()).Debug("starting watchdog")
wdto := envconst.Duration("ZREPL_JOB_WATCHDOG_TIMEOUT", 10*time.Minute)
jitter := envconst.Duration("ZREPL_JOB_WATCHDOG_JITTER", 1*time.Second)
// shadowing!
log := log.WithField("watchdog_timeout", wdto.String())
log.Debug("starting watchdog")
defer log.Debug("watchdog stopped")
t := time.NewTicker(wdto)
defer t.Stop()
var (
rep, prunerSender, prunerReceiver watchdog.Progress
)
for {
select {
case <-runDone:
return
case <-ctx.Done():
return
case <-t.C: // fall
}
log := log.WithField("watchdog_timeout", wdto.String()) // shadowing!
j.updateTasks(func(tasks *activeSideTasks) {
if tasks.replication != nil &&
!tasks.replication.Progress.ExpectProgress(&rep) &&
!tasks.replication.State().IsTerminal() {
log.Error("replication did not make progress, cancelling")
tasks.replicationCancel()
}
if tasks.prunerSender != nil &&
!tasks.prunerSender.Progress.ExpectProgress(&prunerSender) &&
!tasks.prunerSender.State().IsTerminal() {
log.Error("pruner:sender did not make progress, cancelling")
tasks.prunerSenderCancel()
}
if tasks.prunerReceiver != nil &&
!tasks.prunerReceiver.Progress.ExpectProgress(&prunerReceiver) &&
!tasks.prunerReceiver.State().IsTerminal() {
log.Error("pruner:receiver did not make progress, cancelling")
tasks.prunerReceiverCancel()
// Since cancelling a task will cause the sequential code to transition to the next state immediately,
// we cannot check for its progress right then (no fallthrough).
// Instead, we return (not continue because we are in a closure) and give the new state another
// ZREPL_JOB_WATCHDOG_TIMEOUT interval to try make some progress.
log.WithField("state", tasks.state).Debug("watchdog firing")
const WATCHDOG_ENVCONST_NOTICE = " (adjust ZREPL_JOB_WATCHDOG_TIMEOUT env variable if inappropriate)"
switch tasks.state {
case ActiveSideReplicating:
log.WithField("replication_progress", tasks.replication.Progress.String()).
Debug("check replication progress")
if tasks.replication.Progress.CheckTimeout(wdto, jitter) {
log.Error("replication did not make progress, cancelling" + WATCHDOG_ENVCONST_NOTICE)
tasks.replicationCancel()
return
}
case ActiveSidePruneSender:
log.WithField("prune_sender_progress", tasks.replication.Progress.String()).
Debug("check pruner_sender progress")
if tasks.prunerSender.Progress.CheckTimeout(wdto, jitter) {
log.Error("pruner_sender did not make progress, cancelling" + WATCHDOG_ENVCONST_NOTICE)
tasks.prunerSenderCancel()
return
}
case ActiveSidePruneReceiver:
log.WithField("prune_receiver_progress", tasks.replication.Progress.String()).
Debug("check pruner_receiver progress")
if tasks.prunerReceiver.Progress.CheckTimeout(wdto, jitter) {
log.Error("pruner_receiver did not make progress, cancelling" + WATCHDOG_ENVCONST_NOTICE)
tasks.prunerReceiverCancel()
return
}
case ActiveSideDone:
// ignore, ctx will be Done() in a few milliseconds and the watchdog will exit
default:
log.WithField("state", tasks.state).
Error("watchdog implementation error: unknown active side state")
}
})
log.WithField("replication_progress", rep.String()).
WithField("pruner_sender_progress", prunerSender.String()).
WithField("pruner_receiver_progress", prunerReceiver.String()).
Debug("watchdog did run")
}
}()
@@ -337,6 +373,7 @@ func (j *ActiveSide) do(ctx context.Context) {
*tasks = activeSideTasks{}
tasks.replicationCancel = repCancel
tasks.replication = replication.NewReplication(j.promRepStateSecs, j.promBytesReplicated)
tasks.state = ActiveSideReplicating
})
log.Info("start replication")
tasks.replication.Drive(ctx, sender, receiver)
@@ -353,6 +390,7 @@ func (j *ActiveSide) do(ctx context.Context) {
tasks := j.updateTasks(func(tasks *activeSideTasks) {
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
tasks.prunerSenderCancel = senderCancel
tasks.state = ActiveSidePruneSender
})
log.Info("start pruning sender")
tasks.prunerSender.Prune()
@@ -369,10 +407,16 @@ func (j *ActiveSide) do(ctx context.Context) {
tasks := j.updateTasks(func(tasks *activeSideTasks) {
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
tasks.prunerReceiverCancel = receiverCancel
tasks.state = ActiveSidePruneReceiver
})
log.Info("start pruning receiver")
tasks.prunerReceiver.Prune()
log.Info("finished pruning receiver")
receiverCancel()
}
j.updateTasks(func(tasks *activeSideTasks) {
tasks.state = ActiveSideDone
})
}
+66
View File
@@ -0,0 +1,66 @@
// Code generated by "enumer -type=ActiveSideState"; DO NOT EDIT.
package job
import (
"fmt"
)
const (
_ActiveSideStateName_0 = "ActiveSideReplicatingActiveSidePruneSender"
_ActiveSideStateName_1 = "ActiveSidePruneReceiver"
_ActiveSideStateName_2 = "ActiveSideDone"
)
var (
_ActiveSideStateIndex_0 = [...]uint8{0, 21, 42}
_ActiveSideStateIndex_1 = [...]uint8{0, 23}
_ActiveSideStateIndex_2 = [...]uint8{0, 14}
)
func (i ActiveSideState) String() string {
switch {
case 1 <= i && i <= 2:
i -= 1
return _ActiveSideStateName_0[_ActiveSideStateIndex_0[i]:_ActiveSideStateIndex_0[i+1]]
case i == 4:
return _ActiveSideStateName_1
case i == 8:
return _ActiveSideStateName_2
default:
return fmt.Sprintf("ActiveSideState(%d)", i)
}
}
var _ActiveSideStateValues = []ActiveSideState{1, 2, 4, 8}
var _ActiveSideStateNameToValueMap = map[string]ActiveSideState{
_ActiveSideStateName_0[0:21]: 1,
_ActiveSideStateName_0[21:42]: 2,
_ActiveSideStateName_1[0:23]: 4,
_ActiveSideStateName_2[0:14]: 8,
}
// ActiveSideStateString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func ActiveSideStateString(s string) (ActiveSideState, error) {
if val, ok := _ActiveSideStateNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to ActiveSideState values", s)
}
// ActiveSideStateValues returns all values of the enum
func ActiveSideStateValues() []ActiveSideState {
return _ActiveSideStateValues
}
// IsAActiveSideState returns "true" if the value is listed in the enum definition. "false" otherwise
func (i ActiveSideState) IsAActiveSideState() bool {
for _, v := range _ActiveSideStateValues {
if i == v {
return true
}
}
return false
}
+99 -64
View File
@@ -11,8 +11,10 @@ import (
"github.com/zrepl/zrepl/replication/pdu"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/watchdog"
"github.com/problame/go-streamrpc"
"net"
"sort"
"strings"
"sync"
"time"
)
@@ -69,8 +71,7 @@ type Pruner struct {
err error
// State Exec
prunePending []*fs
pruneCompleted []*fs
execQueue *execQueue
}
type PrunerFactory struct {
@@ -116,7 +117,7 @@ func NewPrunerFactory(in config.PruningSenderReceiver, promPruneSecs *prometheus
f := &PrunerFactory{
senderRules: keepRulesSender,
receiverRules: keepRulesReceiver,
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 4 * time.Second),
retryWait: envconst.Duration("ZREPL_PRUNER_RETRY_INTERVAL", 10 * time.Second),
considerSnapAtCursorReplicated: considerSnapAtCursorReplicated,
promPruneSecs: promPruneSecs,
}
@@ -223,7 +224,8 @@ type Report struct {
type FSReport struct {
Filesystem string
SnapshotList, DestroyList []SnapshotReport
Error string
ErrorCount int
LastError string
}
type SnapshotReport struct {
@@ -238,26 +240,17 @@ func (p *Pruner) Report() *Report {
r := Report{State: p.state.String()}
if p.state & PlanWait|ExecWait != 0 {
if p.state & (PlanWait|ExecWait) != 0 {
r.SleepUntil = p.sleepUntil
}
if p.state & PlanWait|ExecWait|ErrPerm != 0 {
if p.state & (PlanWait|ExecWait|ErrPerm) != 0 {
if p.err != nil {
r.Error = p.err.Error()
}
}
if p.state & Plan|PlanWait == 0 {
return &r
}
r.Pending = make([]FSReport, len(p.prunePending))
for i, fs := range p.prunePending{
r.Pending[i] = fs.Report()
}
r.Completed = make([]FSReport, len(p.pruneCompleted))
for i, fs := range p.pruneCompleted{
r.Completed[i] = fs.Report()
if p.execQueue != nil {
r.Pending, r.Completed = p.execQueue.Report()
}
return &r
@@ -289,14 +282,11 @@ type fs struct {
destroyList []pruning.Snapshot
mtx sync.RWMutex
// for Plan
err error
}
func (f *fs) Update(err error) {
f.mtx.Lock()
defer f.mtx.Unlock()
f.err = err
// only during Exec state, also used by execQueue
execErrLast error
execErrCount int
}
func (f *fs) Report() FSReport {
@@ -305,8 +295,9 @@ func (f *fs) Report() FSReport {
r := FSReport{}
r.Filesystem = f.path
if f.err != nil {
r.Error = f.err.Error()
r.ErrorCount = f.execErrCount
if f.execErrLast != nil {
r.LastError = f.execErrLast.Error()
}
r.SnapshotList = make([]SnapshotReport, len(f.snaps))
@@ -344,6 +335,14 @@ func (s snapshot) Replicated() bool { return s.replicated }
func (s snapshot) Date() time.Time { return s.date }
type Error interface {
error
Temporary() bool
}
var _ Error = net.Error(nil)
var _ Error = streamrpc.Error(nil)
func shouldRetry(e error) bool {
if neterr, ok := e.(net.Error); ok {
return neterr.Temporary()
@@ -372,6 +371,10 @@ func onErr(u updater, e error) state {
func statePlan(a *args, u updater) state {
ctx, target, receiver := a.ctx, a.target, a.receiver
var ka *watchdog.KeepAlive
u(func(pruner *Pruner) {
ka = &pruner.Progress
})
tfss, err := target.ListFilesystems(ctx)
if err != nil {
@@ -379,7 +382,6 @@ func statePlan(a *args, u updater) state {
}
pfss := make([]*fs, len(tfss))
fsloop:
for i, tfs := range tfss {
l := GetLogger(ctx).WithField("fs", tfs.Path)
@@ -394,12 +396,10 @@ fsloop:
tfsvs, err := target.ListFilesystemVersions(ctx, tfs.Path)
if err != nil {
l.WithError(err).Error("cannot list filesystem versions")
if shouldRetry(err) {
return onErr(u, err)
}
pfs.err = err
continue fsloop
return onErr(u, err)
}
// no progress here since we could run in a live-lock (must have used target AND receiver before progress)
pfs.snaps = make([]pruning.Snapshot, 0, len(tfsvs))
rcReq := &pdu.ReplicationCursorReq{
@@ -411,17 +411,13 @@ fsloop:
rc, err := receiver.ReplicationCursor(ctx, rcReq)
if err != nil {
l.WithError(err).Error("cannot get replication cursor")
if shouldRetry(err) {
return onErr(u, err)
}
pfs.err = err
continue fsloop
return onErr(u, err)
}
if rc.GetError() != "" {
l.WithField("reqErr", rc.GetError()).Error("cannot get replication cursor")
pfs.err = fmt.Errorf("%s", rc.GetError())
continue fsloop
return onErr(u, err)
}
ka.MadeProgress()
// scan from older to newer, all snapshots older than cursor are interpreted as replicated
@@ -445,9 +441,11 @@ fsloop:
}
creation, err := tfsv.CreationAsTime()
if err != nil {
pfs.err = fmt.Errorf("%s%s has invalid creation date: %s", tfs, tfsv.RelName(), err)
l.WithError(pfs.err).Error("")
continue fsloop
err := fmt.Errorf("%s%s has invalid creation date: %s", tfs, tfsv.RelName(), err)
l.WithError(err).
WithField("tfsv", tfsv.RelName()).
Error("error with fileesystem version")
return onErr(u, err)
}
// note that we cannot use CreateTXG because target and receiver could be on different pools
atCursor := tfsv.Guid == rc.GetGuid()
@@ -459,23 +457,21 @@ fsloop:
})
}
if preCursor {
pfs.err = fmt.Errorf("replication cursor not found in prune target filesystem versions")
l.WithError(pfs.err).Error("")
continue fsloop
err := fmt.Errorf("replication cursor not found in prune target filesystem versions")
l.Error(err.Error())
return onErr(u, err)
}
// Apply prune rules
pfs.destroyList = pruning.PruneSnapshots(pfs.snaps, a.rules)
ka.MadeProgress()
}
return u(func(pruner *Pruner) {
pruner.Progress.MadeProgress()
pruner.execQueue = newExecQueue(len(pfss))
for _, pfs := range pfss {
if pfs.err != nil {
pruner.pruneCompleted = append(pruner.pruneCompleted, pfs)
} else {
pruner.prunePending = append(pruner.prunePending, pfs)
}
pruner.execQueue.Put(pfs, nil, false)
}
pruner.state = Exec
}).statefunc()
@@ -485,17 +481,15 @@ func stateExec(a *args, u updater) state {
var pfs *fs
state := u(func(pruner *Pruner) {
if len(pruner.prunePending) == 0 {
pfs = pruner.execQueue.Pop()
if pfs == nil {
nextState := Done
for _, pfs := range pruner.pruneCompleted {
if pfs.err != nil {
nextState = ErrPerm
}
if pruner.execQueue.HasCompletedFSWithErrors() {
nextState = ErrPerm
}
pruner.state = nextState
return
}
pfs = pruner.prunePending[0]
})
if state != Exec {
return state.statefunc()
@@ -509,21 +503,62 @@ func stateExec(a *args, u updater) state {
WithField("destroy_snap", destroyList[i].Name).
Debug("policy destroys snapshot")
}
pfs.Update(nil)
req := pdu.DestroySnapshotsReq{
Filesystem: pfs.path,
Snapshots: destroyList,
}
_, err := a.target.DestroySnapshots(a.ctx, &req)
pfs.Update(err)
if err != nil && shouldRetry(err) {
GetLogger(a.ctx).WithField("fs", pfs.path).Debug("destroying snapshots")
res, err := a.target.DestroySnapshots(a.ctx, &req)
if err != nil {
u(func(pruner *Pruner) {
pruner.execQueue.Put(pfs, err, false)
})
return onErr(u, err)
}
// check if all snapshots were destroyed
destroyResults := make(map[string]*pdu.DestroySnapshotRes)
for _, fsres := range res.Results {
destroyResults[fsres.Snapshot.Name] = fsres
}
err = nil
destroyFails := make([]*pdu.DestroySnapshotRes, 0)
for _, reqDestroy := range destroyList {
res, ok := destroyResults[reqDestroy.Name]
if !ok {
err = fmt.Errorf("missing destroy-result for %s", reqDestroy.RelName())
break
} else if res.Error != "" {
destroyFails = append(destroyFails, res)
}
}
if err == nil && len(destroyFails) > 0 {
names := make([]string, len(destroyFails))
pairs := make([]string, len(destroyFails))
allSame := true
lastMsg := destroyFails[0].Error
for i := 0; i < len(destroyFails); i++{
allSame = allSame && destroyFails[i].Error == lastMsg
relname := destroyFails[i].Snapshot.RelName()
names[i] = relname
pairs[i] = fmt.Sprintf("(%s: %s)", relname, destroyFails[i].Error)
}
if allSame {
err = fmt.Errorf("destroys failed %s: %s",
strings.Join(names, ", "), lastMsg)
} else {
err = fmt.Errorf("destroys failed: %s", strings.Join(pairs, ", "))
}
}
u(func(pruner *Pruner) {
pruner.execQueue.Put(pfs, err, err == nil)
})
if err != nil {
GetLogger(a.ctx).WithError(err).Error("target could not destroy snapshots")
return onErr(u, err)
}
// if it's not retryable, treat is like as being done
return u(func(pruner *Pruner) {
pruner.pruneCompleted = append(pruner.pruneCompleted, pfs)
pruner.prunePending = pruner.prunePending[1:]
pruner.Progress.MadeProgress()
}).statefunc()
}
+89
View File
@@ -0,0 +1,89 @@
package pruner
import (
"sort"
"strings"
"sync"
)
type execQueue struct {
mtx sync.Mutex
pending, completed []*fs
}
func newExecQueue(cap int) *execQueue {
q := execQueue{
pending: make([]*fs, 0, cap),
completed: make([]*fs, 0, cap),
}
return &q
}
func (q *execQueue) Report() (pending, completed []FSReport) {
q.mtx.Lock()
defer q.mtx.Unlock()
pending = make([]FSReport, len(q.pending))
for i, fs := range q.pending {
pending[i] = fs.Report()
}
completed = make([]FSReport, len(q.completed))
for i, fs := range q.completed {
completed[i] = fs.Report()
}
return pending, completed
}
func (q *execQueue) HasCompletedFSWithErrors() bool {
q.mtx.Lock()
defer q.mtx.Unlock()
for _, fs := range q.completed {
if fs.execErrLast != nil {
return true
}
}
return false
}
func (q *execQueue) Pop() *fs {
if len(q.pending) == 0 {
return nil
}
fs := q.pending[0]
q.pending = q.pending[1:]
return fs
}
func(q *execQueue) Put(fs *fs, err error, done bool) {
fs.mtx.Lock()
fs.execErrLast = err
if err != nil {
fs.execErrCount++
}
if done || (err != nil && !shouldRetry(fs.execErrLast)) {
fs.mtx.Unlock()
q.mtx.Lock()
q.completed = append(q.completed, fs)
q.mtx.Unlock()
return
}
fs.mtx.Unlock()
q.mtx.Lock()
// inefficient priority q
q.pending = append(q.pending, fs)
sort.SliceStable(q.pending, func(i, j int) bool {
q.pending[i].mtx.Lock()
defer q.pending[i].mtx.Unlock()
q.pending[j].mtx.Lock()
defer q.pending[j].mtx.Unlock()
if q.pending[i].execErrCount != q.pending[j].execErrCount {
return q.pending[i].execErrCount < q.pending[j].execErrCount
}
return strings.Compare(q.pending[i].path, q.pending[j].path) == -1
})
q.mtx.Unlock()
}
+5 -11
View File
@@ -133,16 +133,14 @@ func TestPruner_Prune(t *testing.T) {
},
listVersionsErrs: map[string][]error{
"zroot/foo": {
stubNetErr{msg: "fakeerror1", temporary: true}, // should be classified as temporaty
stubNetErr{msg: "fakeerror1", temporary: true},
stubNetErr{msg: "fakeerror2", temporary: true,},
},
},
destroyErrs: map[string][]error{
"zroot/foo": {
fmt.Errorf("permanent error"),
},
"zroot/bar": {
stubNetErr{msg: "fakeerror3", temporary: true},
"zroot/baz": {
stubNetErr{msg: "fakeerror3", temporary: true}, // first error puts it back in the queue
stubNetErr{msg:"permanent error"}, // so it will be last when pruner gives up due to permanent err
},
},
destroyed: make(map[string][]string),
@@ -178,9 +176,6 @@ func TestPruner_Prune(t *testing.T) {
"zroot/foo": {
stubNetErr{msg: "fakeerror4", temporary: true},
},
"zroot/baz": {
fmt.Errorf("permanent error2"),
},
},
}
@@ -199,9 +194,8 @@ func TestPruner_Prune(t *testing.T) {
p.Prune()
exp := map[string][]string{
"zroot/foo": {"drop_c"},
"zroot/bar": {"drop_g"},
// drop_c is prohibited by failing destroy
// drop_i is prohibiteed by failing ReplicationCursor call
}
assert.Equal(t, exp, target.destroyed)
+9 -6
View File
@@ -42,13 +42,15 @@ Notes to Package Maintainers
* If the daemon crashes, the stack trace produced by the Go runtime and possibly diagnostic output of zrepl will be written to stderr.
This behavior is independent from the ``stdout`` outlet type.
Please make sure the stderr output of the daemon is captured to a file.
Rotation should not be necessary because stderr is not written to under normal circumstances.
Please make sure the stderr output of the daemon is captured somewhere.
To conserve precious stack traces, make sure that multiple service restarts do not directly discard previous stderr output.
* Make it obvious for users how to set the ``GOTRACEBACK`` environment variable to ``GOTRACEBACK=crash``.
This functionality will cause SIGABRT on panics and can be used to capture a coredump of the panicking process.
To that extend, make sure that your package build system, your OS's coredump collection and the Go delve debugger work together.
Use your build system to package the Go program in `this tutorial on Go coredumps and the delve debugger <https://rakyll.org/coredumps/>`_ , and make sure the symbol resolution etc. work on coredumps captured from the binary produced by your build system. (Special focus on symbol stripping, etc.)
* Use of ``ssh+stdinserver`` :ref:`transport <transport-ssh+stdinserver>` is no longer encouraged.
Please encourage users to use the new ``tcp`` or ``tls`` transports.
You might as well mention some of the :ref:`tunneling options listed here <transport-tcp-tunneling>`.
Changes
~~~~~~~
@@ -56,10 +58,10 @@ Changes
* |feature| :issue:`55` : Push replication (see :ref:`push job <job-push>` and :ref:`sink job <job-sink>`)
* |feature| :ref:`TCP Transport <transport-tcp>`
* |feature| :ref:`TCP + TLS client authentication transport <transport-tcp+tlsclientauth>`
* |feature| :issue:`78` TODO MERGE COMMIT Replication protocol rewrite
* |feature| :issue:`78` :commit:`074f989` : Replication protocol rewrite
* Uses ``github.com/problame/go-streamrpc`` for RPC layer
* |break| zrepl 0.1 and restart on both sides of a replication setup is required
* |break| Protocol breakage, update and restart of all zrepl daemons is required
* |feature| :issue:`83`: Improved error handling of network-level errors (zrepl retries instead of failing the entire job)
* |bugfix| :issue:`75` :issue:`81`: use connection timeouts and protocol-level heartbeats
* |break| |break_config|: mappings are no longer supported
@@ -71,7 +73,8 @@ Changes
* |feature| :issue:`69`: include manually created snapshots in replication
* |break_config| ``manual`` and ``periodic`` :ref:`snapshotting types <job-snapshotting-spec>`
* |feature| ``zrepl wakeup JOB`` subcommand to trigger *just* replication
* |feature| ``zrepl signal wakeup JOB`` subcommand to trigger replication + pruning
* |feature| ``zrepl signal reset JOB`` subcommand to abort current replication + pruning
* |feature| |break| |break_config| New pruning system
@@ -85,7 +88,7 @@ Changes
* |feature| |break| Bookmark pruning is no longer necessary
* Per filesystem, zrepl creates a single bookmark (``#zrepl_replication_cursor``) and moves it forward with the most recent successfully replicated snapshot on the receiving side.
* Old bookmarks created prior to zrepl 0.1 (named like their corresponding snapshot) must be deleted manually.
* Old bookmarks created by prior versions of zrepl (named like their corresponding snapshot) must be deleted manually.
* |break_config| ``keep_bookmarks`` parameter of the ``grid`` keep rule has been removed
* |feature| ``zrepl status`` for live-updating replication progress (it's really cool!)
+1 -1
View File
@@ -140,7 +140,7 @@ There is also a ``manual`` snapshotting type, which covers the following use cas
* Run scripts before and after taking snapshots (like locking database tables).
We are working on better integration for this use case: see :issue:`74`.
Note that you will have to trigger replication manually using the ``zrepl wakeup JOB`` subcommand in that case.
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
::
+1 -1
View File
@@ -100,7 +100,7 @@ the left edge of the leftmost (first) interval is the ``creation`` date of the y
All intervals to its right describe time intervals further in the past.
Each interval carries a maximum number of snapshots to keep.
It is secified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
It is specified via ``(keep=N)``, where ``N`` is either ``all`` (all snapshots are kept) or a positive integer.
The default value is **keep=1**.
The following procedure happens during pruning:
+79
View File
@@ -10,6 +10,8 @@ On the passive (serving) side, the transport also provides the **client identity
this string is used for access control and separation of filesystem sub-trees in :ref:`sink jobs <job-sink>`.
Transports are specified in the ``connect`` or ``serve`` section of a job definition.
.. contents::
.. ATTENTION::
The **client identities must be valid ZFS dataset path components**
@@ -27,6 +29,8 @@ This transport may also be used in conjunction with network-layer encryption and
To make the IP-based client authentication effective, such solutions should provide authenticated IP addresses.
Some options to consider:
.. _transport-tcp-tunneling:
* `WireGuard <https://www.wireguard.com/>`_: Linux-focussed, in-kernel TLS
* `OpenVPN <https://openvpn.net/>`_: Cross-platform VPN, uses tun on \*nix
* `IPSec <https://en.wikipedia.org/wiki/IPsec>`_: Properly standardized, in-kernel network-layer VPN
@@ -73,6 +77,7 @@ Connect
The ``tls`` transport uses TCP + TLS with client authentication using client certificates.
The client identity is the common name (CN) presented in the client certificate.
It is recommended to set up a dedicated CA infrastructure for this transport, e.g. using OpenVPN's `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_.
For a simple 2-machine setup, see the :ref:`instructions below<transport-tcp+tlsclientauth-2machineopenssl>`.
The implementation uses `Go's TLS library <https://golang.org/pkg/crypto/tls/>`_.
Since Go binaries are statically linked, you or your distribution need to recompile zrepl when vulnerabilities in that library are disclosed.
@@ -122,6 +127,75 @@ The ``server_cn`` specifies the expected common name (CN) of the server's certif
It overrides the hostname specified in ``address``.
The connection fails if either do not match.
.. _transport-tcp+tlsclientauth-2machineopenssl:
Self-Signed Certificates
~~~~~~~~~~~~~~~~~~~~~~~~
Tools like `EasyRSA <https://github.com/OpenVPN/easy-rsa>`_ make it easy to manage CA infrastructure for multiple clients, e.g. a central zrepl backup server (in sink mode).
However, for a two-machine setup, self-signed certificates distributed using an out-of-band mechanism will also work just fine:
Suppose you have a push-mode setup, with `backups.example.com` running the :ref:`sink job <job-sink>`, and `prod.example.com` running the :ref:`push job <job-push>`.
Run the following OpenSSL commands on each host, substituting HOSTNAME in both filenames and the interactive input prompt by OpenSSL:
.. code-block:: bash
:emphasize-lines: 1-5,24
openssl req -x509 -sha256 -nodes \
-newkey rsa:4096 \
-days 365 \
-keyout HOSTNAME.key \
-out HOSTNAME.crt
#Generating a 4096 bit RSA private key
#................++++
#.++++
#writing new private key to 'backups.key'
#-----
#You are about to be asked to enter information that will be incorporated
#into your certificate request.
#What you are about to enter is what is called a Distinguished Name or a DN.
#There are quite a few fields but you can leave some blank
#For some fields there will be a default value,
#If you enter '.', the field will be left blank.
#-----
#Country Name (2 letter code) [XX]:
#State or Province Name (full name) []:
#Locality Name (eg, city) [Default City]:
#Organization Name (eg, company) [Default Company Ltd]:
#Organizational Unit Name (eg, section) []:
#Common Name (eg, your name or your server's hostname) []:HOSTNAME
#Email Address []:
Now copy each machine's ``HOSTNAME.crt`` to the other machine's ``/etc/zrepl/HOSTNAME.crt``, for example using `scp`.
The serve & connect configuration will thus look like the following:
::
# on backups.example.com
- type: sink
serve:
type: tls
listen: ":8888"
ca: "/etc/zrepl/prod.example.com.crt"
cert: "/etc/zrepl/backups.example.com.crt"
key: "/etc/zrepl/backups.example.com.key"
client_cns:
- "prod.example.com"
...
# on prod.example.com
- type: push
connect:
type: tls
address:"backups.example.com:8888"
ca: /etc/zrepl/backups.example.com.crt
cert: /etc/zrepl/prod.example.com.crt
key: /etc/zrepl/prod.example.com.key
server_cn: "backups.example.com"
...
.. _transport-ssh+stdinserver:
``ssh+stdinserver`` Transport
@@ -130,6 +204,11 @@ The connection fails if either do not match.
``ssh+stdinserver`` is inspired by `git shell <https://git-scm.com/docs/git-shell>`_ and `Borg Backup <https://borgbackup.readthedocs.io/en/stable/deployment.html>`_.
It is provided by the Go package ``github.com/problame/go-netssh``.
.. ATTENTION::
``ssh+stdinserver`` has inferior error detection and handling compared to the ``tcp`` and ``tls`` transports.
If you require tested timeout & retry handling, use ``tcp`` or ``tls`` transports, or help improve package go-netssh.
.. _transport-ssh+stdinserver-serve:
Serve
+97 -71
View File
@@ -6,7 +6,7 @@ Tutorial
========
This tutorial shows how zrepl can be used to implement a ZFS-based pull backup.
This tutorial shows how zrepl can be used to implement a ZFS-based push backup.
We assume the following scenario:
* Production server ``prod`` with filesystems to back up:
@@ -17,12 +17,12 @@ We assume the following scenario:
* Backup server ``backups`` with
* Filesystem ``storage/zrepl/pull/prod`` + children dedicated to backups of ``prod``
* Filesystem ``storage/zrepl/sink/prod`` + children dedicated to backups of ``prod``
Our backup solution should fulfill the following requirements:
* Periodically snapshot the filesystems on ``prod`` *every 10 minutes*
* Incrementally replicate these snapshots to ``storage/zrepl/pull/prod/*`` on ``backups``
* Incrementally replicate these snapshots to ``storage/zrepl/sink/prod/*`` on ``backups``
* Keep only very few snapshots on ``prod`` to save disk space
* Keep a fading history (24 hourly, 30 daily, 6 monthly) of snapshots on ``backups``
@@ -31,74 +31,70 @@ Analysis
We can model this situation as two jobs:
* A **source job** on ``prod``
* A **push job** on ``prod``
* Creates the snapshots
* Keeps a short history of snapshots to enable incremental replication to ``backups``
* Accepts connections from ``backups``
* Keeps a short history of local snapshots to enable incremental replication to ``backups``
* Connects to the ``zrepl daemon`` process on ``backups``
* Pushes snapshots ``backups``
* Prunes snapshots on ``backups`` after replication is complete
* A **pull job** on ``backups``
* A **sink job** on ``backups``
* Connects to the ``zrepl daemon`` process on ``prod``
* Pulls the snapshots to ``storage/zrepl/pull/prod/*``
* Fades out snapshots in ``storage/zrepl/pull/prod/*`` as they age
Why doesn't the **pull job** create the snapshots before pulling?
As is the case with all distributed systems, the link between ``prod`` and ``backups`` might be down for an hour or two.
We do not want to sacrifice our required backup resolution of 10 minute intervals for a temporary connection outage.
When the link comes up again, ``backups`` will catch up with the snapshots taken by ``prod`` in the meantime, without a gap in our backup history.
* Accepts connections & responds to requests from ``prod``
* Limits client ``prod`` access to filesystem sub-tree ``storage/zrepl/sink/prod``
Install zrepl
-------------
Follow the :ref:`OS-specific installation instructions <installation>` and come back here.
Configure server ``backups``
----------------------------
We define a **pull job** named ``pull_prod`` in ``/etc/zrepl/zrepl.yml`` or ``/usr/local/etc/zrepl/zrepl.yml`` on host ``backups`` : ::
jobs:
- name: pull_prod
type: pull
connect:
type: tcp
address: "192.168.2.20:2342"
root_fs: "storage/zrepl/pull/prod"
interval: 10m
pruning:
keep_sender:
- type: not_replicated
- type: last_n
count: 10
keep_receiver:
- type: grid
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
regex: "^zrepl_"
interval: 10m
The ``connect`` section instructs the zrepl daemon to use plain TCP transport.
Check out the :ref:`transports <transport>` section for alternatives that support encryption.
.. _tutorial-configure-prod:
Configure server ``prod``
Generate TLS Certificates
-------------------------
We define a corresponding **source job** named ``source_backups`` in ``/etc/zrepl/zrepl.yml`` or ``/usr/local/etc/zrepl/zrepl.yml`` on host ``prod`` : ::
We use the `TLS client authentication transport <transport-tcp+tlsclientauth>` to protect our data on the wire.
To get things going quickly, we skip setting up a CA and generate two self-signed certificates as described :ref:`here <transport-tcp+tlsclientauth-2machineopenssl>`.
Again, for convenience, We generate the key pairs on our local machine and distribute them using ssh:
.. code-block:: bash
:emphasize-lines: 6,13
openssl req -x509 -sha256 -nodes \
-newkey rsa:4096 \
-days 365 \
-keyout backups.key \
-out backups.crt
# ... and use "backups" as Common Name (CN)
openssl req -x509 -sha256 -nodes \
-newkey rsa:4096 \
-days 365 \
-keyout prod.key \
-out prod.crt
# ... and use "prod" as Common Name (CN)
ssh root@backups "mkdir /etc/zrepl"
scp backups.key backups.crt prod.crt root@backups:/etc/zrepl
ssh root@prod "mkdir /etc/zrepl"
scp prod.key prod.crt backups.crt root@prod:/etc/zrepl
Configure server ``prod``
----------------------------
We define a **push job** named ``prod_to_backups`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` : ::
jobs:
- name: source_backups
type: source
serve:
type: tcp
listen: ":2342"
clients: {
"192.168.2.10" : "backups"
}
- name: prod_to_backups
type: push
connect:
type: tls
address: "backups.example.com:8888"
ca: /etc/zrepl/backups.crt
cert: /etc/zrepl/prod.crt
key: /etc/zrepl/prod.key
server_cn: "backups"
filesystems: {
"zroot/var/db:": true,
"zroot/usr/home<": true,
@@ -108,39 +104,69 @@ We define a corresponding **source job** named ``source_backups`` in ``/etc/zrep
type: periodic
prefix: zrepl_
interval: 10m
pruning:
keep_sender:
- type: not_replicated
- type: last_n
count: 10
keep_receiver:
- type: grid
grid: 1x1h(keep=all) | 24x1h | 30x1d | 6x30d
regex: "^zrepl_"
.. _tutorial-configure-prod:
Configure server ``prod``
-------------------------
We define a corresponding **sink job** named ``sink`` in ``/etc/zrepl/zrepl.yml`` on host ``prod`` : ::
jobs:
- name: sink
type: sink
serve:
type: tls
listen: ":8888"
ca: "/etc/zrepl/prod.crt"
cert: "/etc/zrepl/backups.crt"
key: "/etc/zrepl/backups.key"
client_cns:
- "prod"
root_fs: "storage/zrepl/sink"
The ``serve`` section whitelists ``backups``'s IP address ``192.168.2.10`` and assigns it the client identity ``backups`` which will show up in the logs.
Again, check the :ref:`docs for encrypted transports <transport>`.
Apply Configuration Changes
---------------------------
We need to restart the zrepl daemon on **both** ``prod`` and ``backups``.
This is :ref:`OS-specific <usage-zrepl-daemon-restarting>`.
We use ``zrepl configcheck`` before to catch any configuration errors: no output indicates that everything is fine.
If that is the case, restart the zrepl daemon on **both** ``prod`` and ``backups`` using ``service zrepl restart`` or ``systemctl restart zrepl``.
Watch it Work
-------------
Run ``zrepl status`` on ``prod`` to monitor the replication and pruning activity.
Additionally, you can check the detailed structured logs of the `zrepl daemon` process and use GNU *watch* to view the snapshots present on both machines.
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup prod_to_backups`` on ``prod``.
If you like tmux, here is a handy script that works on FreeBSD: ::
pkg install gnu-watch tmux
tmux new-window
tmux split-window "tail -f /var/log/zrepl.log"
tmux split-window "gnu-watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
tmux select-layout tiled
tmux new -s zrepl -d
tmux split-window -t zrepl "tail -f /var/log/messages"
tmux split-window -t zrepl "gnu-watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
tmux split-window -t zrepl "zrepl status"
tmux select-layout -t zrepl tiled
tmux attach -t zrepl
The Linux equivalent might look like this: ::
# make sure tmux is installed & let's assume you use systemd + journald
tmux new-window
tmux split-window "journalctl -f -u zrepl.service"
tmux split-window "watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
tmux select-layout tiled
tmux new -s zrepl -d
tmux split-window -t zrepl "journalctl -f -u zrepl.service"
tmux split-window -t zrepl "watch 'zfs list -t snapshot -o name,creation -s creation | grep zrepl_'"
tmux split-window -t zrepl "zrepl status"
tmux select-layout -t zrepl tiled
tmux attach -t zrepl
Summary
-------
+4 -2
View File
@@ -27,8 +27,10 @@ CLI Overview
- show job activity, or with ``--raw`` for JSON output
* - ``zrepl stdinserver``
- see :ref:`transport-ssh+stdinserver`
* - ``zrepl wakeup JOB``
- manually trigger replication + pruning
* - ``zrepl signal wakeup JOB``
- manually trigger replication + pruning of JOB
* - ``zrepl signal reset JOB``
- manually abort current replication + pruning of JOB
* - ``zrepl configcheck``
- check if config can be parsed without errors
+179 -165
View File
@@ -71,38 +71,30 @@ type Report struct {
Completed, Pending []*StepReport
}
//go:generate stringer -type=State
//go:generate enumer -type=State
type State uint
const (
Ready State = 1 << iota
Retry
PermanentError
Completed
)
func (s State) fsrsf() state {
m := map[State]state{
Ready: stateReady,
Retry: stateRetry,
PermanentError: nil,
Completed: nil,
}
return m[s]
}
func (s State) IsErrorState() bool {
return s & (Retry|PermanentError) != 0
type Error interface {
error
Temporary() bool
ContextErr() bool
LocalToFS() bool
}
type Replication struct {
promBytesReplicated prometheus.Counter
fs string
// lock protects all fields below it in this struct, but not the data behind pointers
lock sync.Mutex
state State
fs string
err error
err Error
completed, pending []*ReplicationStep
}
@@ -112,13 +104,35 @@ func (f *Replication) State() State {
return f.state
}
func (f *Replication) Err() error {
func (f *Replication) FS() string { return f.fs }
// returns zero value time.Time{} if no more pending steps
func (f *Replication) NextStepDate() time.Time {
if len(f.pending) == 0 {
return time.Time{}
}
return f.pending[0].to.SnapshotTime()
}
func (f *Replication) Err() Error {
f.lock.Lock()
defer f.lock.Unlock()
if f.state & (Retry|PermanentError) != 0 {
return f.err
return f.err
}
func (f *Replication) CanRetry() bool {
f.lock.Lock()
defer f.lock.Unlock()
if f.state == Completed {
return false
}
return nil
if f.state != Ready {
panic(fmt.Sprintf("implementation error: %v", f.state))
}
if f.err == nil {
return true
}
return f.err.Temporary()
}
func (f *Replication) UpdateSizeEsitmate(ctx context.Context, sender Sender) error {
@@ -162,26 +176,39 @@ func (b *ReplicationBuilder) Done() (r *Replication) {
return r
}
func NewReplicationWithPermanentError(fs string, err error) *Replication {
type ReplicationConflictError struct {
Err error
}
func (e *ReplicationConflictError) Timeout() bool { return false }
func (e *ReplicationConflictError) Temporary() bool { return false }
func (e *ReplicationConflictError) Error() string { return fmt.Sprintf("permanent error: %s", e.Err.Error()) }
func (e *ReplicationConflictError) LocalToFS() bool { return true }
func (e *ReplicationConflictError) ContextErr() bool { return false }
func NewReplicationConflictError(fs string, err error) *Replication {
return &Replication{
state: PermanentError,
state: Completed,
fs: fs,
err: err,
err: &ReplicationConflictError{Err: err},
}
}
//go:generate stringer -type=StepState
//go:generate enumer -type=StepState
type StepState uint
const (
StepReplicationReady StepState = 1 << iota
StepReplicationRetry
StepMarkReplicatedReady
StepMarkReplicatedRetry
StepPermanentError
StepCompleted
)
func (s StepState) IsTerminal() bool { return s == StepCompleted }
type FilesystemVersion interface {
SnapshotTime() time.Time
GetName() string // name without @ or #
@@ -204,7 +231,7 @@ type ReplicationStep struct {
expectedSize int64 // 0 means no size estimate present / possible
}
func (f *Replication) TakeStep(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver) (post State, nextStepDate time.Time) {
func (f *Replication) Retry(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver) Error {
var u updater = func(fu func(*Replication)) State {
f.lock.Lock()
@@ -214,76 +241,108 @@ func (f *Replication) TakeStep(ctx context.Context, ka *watchdog.KeepAlive, send
}
return f.state
}
var s state = u(nil).fsrsf()
pre := u(nil)
preTime := time.Now()
s = s(ctx, ka, sender, receiver, u)
delta := time.Now().Sub(preTime)
post = u(func(f *Replication) {
if len(f.pending) == 0 {
return
}
nextStepDate = f.pending[0].to.SnapshotTime()
})
getLogger(ctx).
WithField("fs", f.fs).
WithField("transition", fmt.Sprintf("%s => %s", pre, post)).
WithField("duration", delta).
Debug("fsr step taken")
return post, nextStepDate
}
type updater func(func(fsr *Replication)) State
type state func(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver, u updater) state
func stateReady(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver, u updater) state {
var current *ReplicationStep
s := u(func(f *Replication) {
pre := u(nil)
getLogger(ctx).WithField("fsrep_state", pre).Debug("begin fsrep.Retry")
defer func() {
post := u(nil)
getLogger(ctx).WithField("fsrep_transition", post).Debug("end fsrep.Retry")
}()
st := u(func(f *Replication) {
if len(f.pending) == 0 {
f.state = Completed
return
}
current = f.pending[0]
})
if s != Ready {
return s.fsrsf()
if st == Completed {
return nil
}
if st != Ready {
panic(fmt.Sprintf("implementation error: %v", st))
}
stepState := current.doReplication(ctx, ka, sender, receiver)
stepCtx := WithLogger(ctx, getLogger(ctx).WithField("step", current))
getLogger(stepCtx).Debug("take step")
err := current.Retry(stepCtx, ka, sender, receiver)
if err != nil {
getLogger(stepCtx).WithError(err).Error("step could not be completed")
}
return u(func(f *Replication) {
switch stepState {
case StepCompleted:
f.completed = append(f.completed, current)
f.pending = f.pending[1:]
if len(f.pending) > 0 {
f.state = Ready
} else {
f.state = Completed
}
case StepReplicationRetry:
fallthrough
case StepMarkReplicatedRetry:
f.state = Retry
case StepPermanentError:
f.state = PermanentError
f.err = errors.New("a replication step failed with a permanent error")
default:
panic(f)
u(func(fsr *Replication) {
if err != nil {
f.err = &StepError{stepStr: current.String(), err: err}
return
}
}).fsrsf()
if err == nil && current.state != StepCompleted {
panic(fmt.Sprintf("implementation error: %v", current.state))
}
f.err = nil
f.completed = append(f.completed, current)
f.pending = f.pending[1:]
if len(f.pending) > 0 {
f.state = Ready
} else {
f.state = Completed
}
})
var retErr Error = nil
u(func(fsr *Replication) {
retErr = fsr.err
})
return retErr
}
func stateRetry(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver, u updater) state {
return u(func(fsr *Replication) {
fsr.state = Ready
}).fsrsf()
type updater func(func(fsr *Replication)) State
type state func(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver, u updater) state
type StepError struct {
stepStr string
err error
}
var _ Error = &StepError{}
func (e StepError) Error() string {
if e.LocalToFS() {
return fmt.Sprintf("step %s failed: %s", e.stepStr, e.err)
}
return e.err.Error()
}
func (e StepError) Timeout() bool {
if neterr, ok := e.err.(net.Error); ok {
return neterr.Timeout()
}
return false
}
func (e StepError) Temporary() bool {
if neterr, ok := e.err.(net.Error); ok {
return neterr.Temporary()
}
return false
}
func (e StepError) LocalToFS() bool {
if _, ok := e.err.(net.Error); ok {
return false
}
return true // conservative approximation: we'd like to check for specific errors returned over RPC here...
}
func (e StepError) ContextErr() bool {
switch e.err {
case context.Canceled:
return true
case context.DeadlineExceeded:
return true
}
return false
}
func (fsr *Replication) Report() *Report {
@@ -295,9 +354,8 @@ func (fsr *Replication) Report() *Report {
Status: fsr.state.String(),
}
if fsr.state&PermanentError != 0 {
if fsr.err != nil && fsr.err.LocalToFS() {
rep.Problem = fsr.err.Error()
return &rep
}
rep.Completed = make([]*StepReport, len(fsr.completed))
@@ -309,70 +367,48 @@ func (fsr *Replication) Report() *Report {
rep.Pending[i] = fsr.pending[i].Report()
}
if fsr.state&Retry != 0 {
if len(rep.Pending) != 0 { // should always be true for Retry == true?
rep.Problem = rep.Pending[0].Problem
}
}
return &rep
}
func shouldRetry(err error) bool {
switch err {
case io.EOF:
fallthrough
case io.ErrUnexpectedEOF:
fallthrough
case io.ErrClosedPipe:
return true
func (s *ReplicationStep) Retry(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver) error {
switch s.state {
case StepReplicationReady:
return s.doReplication(ctx, ka, sender, receiver)
case StepMarkReplicatedReady:
return s.doMarkReplicated(ctx, ka, sender)
case StepCompleted:
return nil
}
if _, ok := err.(net.Error); ok {
return true
}
return false
panic(fmt.Sprintf("implementation error: %v", s.state))
}
func (s *ReplicationStep) doReplication(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver) StepState {
func (s *ReplicationStep) Error() error {
if s.state & (StepReplicationReady|StepMarkReplicatedReady) != 0 {
return s.err
}
return nil
}
func (s *ReplicationStep) doReplication(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver) error {
if s.state != StepReplicationReady {
panic(fmt.Sprintf("implementation error: %v", s.state))
}
fs := s.parent.fs
log := getLogger(ctx).
WithField("filesystem", fs).
WithField("step", s.String())
updateStateError := func(err error) StepState {
s.lock.Lock()
defer s.lock.Unlock()
s.err = err
if shouldRetry(s.err) {
s.state = StepReplicationRetry
return s.state
}
s.state = StepPermanentError
return s.state
}
updateStateCompleted := func() StepState {
s.lock.Lock()
defer s.lock.Unlock()
s.err = nil
s.state = StepMarkReplicatedReady
return s.state
}
log := getLogger(ctx)
sr := s.buildSendRequest(false)
log.Debug("initiate send request")
sres, sstream, err := sender.Send(ctx, sr)
if err != nil {
log.WithError(err).Error("send request failed")
return updateStateError(err)
return err
}
if sstream == nil {
err := errors.New("send request did not return a stream, broken endpoint implementation")
return updateStateError(err)
return err
}
s.byteCounter = util.NewByteCounterReader(sstream)
@@ -400,42 +436,23 @@ func (s *ReplicationStep) doReplication(ctx context.Context, ka *watchdog.KeepAl
// - an unexpected exit of ZFS on the sending side
// - an unexpected exit of ZFS on the receiving side
// - a connectivity issue
return updateStateError(err)
return err
}
log.Debug("receive finished")
ka.MadeProgress()
updateStateCompleted()
s.state = StepMarkReplicatedReady
return s.doMarkReplicated(ctx, ka, sender)
}
func (s *ReplicationStep) doMarkReplicated(ctx context.Context, ka *watchdog.KeepAlive, sender Sender) StepState {
func (s *ReplicationStep) doMarkReplicated(ctx context.Context, ka *watchdog.KeepAlive, sender Sender) error {
log := getLogger(ctx).
WithField("filesystem", s.parent.fs).
WithField("step", s.String())
updateStateError := func(err error) StepState {
s.lock.Lock()
defer s.lock.Unlock()
s.err = err
if shouldRetry(s.err) {
s.state = StepMarkReplicatedRetry
return s.state
}
s.state = StepPermanentError
return s.state
if s.state != StepMarkReplicatedReady {
panic(fmt.Sprintf("implementation error: %v", s.state))
}
updateStateCompleted := func() StepState {
s.lock.Lock()
defer s.lock.Unlock()
s.state = StepCompleted
return s.state
}
log := getLogger(ctx)
log.Debug("advance replication cursor")
req := &pdu.ReplicationCursorReq{
@@ -449,25 +466,22 @@ func (s *ReplicationStep) doMarkReplicated(ctx context.Context, ka *watchdog.Kee
res, err := sender.ReplicationCursor(ctx, req)
if err != nil {
log.WithError(err).Error("error advancing replication cursor")
return updateStateError(err)
return err
}
if res.GetError() != "" {
err := fmt.Errorf("cannot advance replication cursor: %s", res.GetError())
log.Error(err.Error())
return updateStateError(err)
return err
}
ka.MadeProgress()
return updateStateCompleted()
s.state = StepCompleted
return err
}
func (s *ReplicationStep) updateSizeEstimate(ctx context.Context, sender Sender) error {
fs := s.parent.fs
log := getLogger(ctx).
WithField("filesystem", fs).
WithField("step", s.String())
log := getLogger(ctx)
sr := s.buildSendRequest(true)
+50
View File
@@ -0,0 +1,50 @@
// Code generated by "enumer -type=State"; DO NOT EDIT.
package fsrep
import (
"fmt"
)
const _StateName = "ReadyCompleted"
var _StateIndex = [...]uint8{0, 5, 14}
func (i State) String() string {
i -= 1
if i >= State(len(_StateIndex)-1) {
return fmt.Sprintf("State(%d)", i+1)
}
return _StateName[_StateIndex[i]:_StateIndex[i+1]]
}
var _StateValues = []State{1, 2}
var _StateNameToValueMap = map[string]State{
_StateName[0:5]: 1,
_StateName[5:14]: 2,
}
// StateString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func StateString(s string) (State, error) {
if val, ok := _StateNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to State values", s)
}
// StateValues returns all values of the enum
func StateValues() []State {
return _StateValues
}
// IsAState returns "true" if the value is listed in the enum definition. "false" otherwise
func (i State) IsAState() bool {
for _, v := range _StateValues {
if i == v {
return true
}
}
return false
}
-29
View File
@@ -1,29 +0,0 @@
// Code generated by "stringer -type=State"; DO NOT EDIT.
package fsrep
import "strconv"
const (
_State_name_0 = "ReadyRetry"
_State_name_1 = "PermanentError"
_State_name_2 = "Completed"
)
var (
_State_index_0 = [...]uint8{0, 5, 10}
)
func (i State) String() string {
switch {
case 1 <= i && i <= 2:
i -= 1
return _State_name_0[_State_index_0[i]:_State_index_0[i+1]]
case i == 4:
return _State_name_1
case i == 8:
return _State_name_2
default:
return "State(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
+61
View File
@@ -0,0 +1,61 @@
// Code generated by "enumer -type=StepState"; DO NOT EDIT.
package fsrep
import (
"fmt"
)
const (
_StepStateName_0 = "StepReplicationReadyStepMarkReplicatedReady"
_StepStateName_1 = "StepCompleted"
)
var (
_StepStateIndex_0 = [...]uint8{0, 20, 43}
_StepStateIndex_1 = [...]uint8{0, 13}
)
func (i StepState) String() string {
switch {
case 1 <= i && i <= 2:
i -= 1
return _StepStateName_0[_StepStateIndex_0[i]:_StepStateIndex_0[i+1]]
case i == 4:
return _StepStateName_1
default:
return fmt.Sprintf("StepState(%d)", i)
}
}
var _StepStateValues = []StepState{1, 2, 4}
var _StepStateNameToValueMap = map[string]StepState{
_StepStateName_0[0:20]: 1,
_StepStateName_0[20:43]: 2,
_StepStateName_1[0:13]: 4,
}
// StepStateString retrieves an enum value from the enum constants string name.
// Throws an error if the param is not part of the enum.
func StepStateString(s string) (StepState, error) {
if val, ok := _StepStateNameToValueMap[s]; ok {
return val, nil
}
return 0, fmt.Errorf("%s does not belong to StepState values", s)
}
// StepStateValues returns all values of the enum
func StepStateValues() []StepState {
return _StepStateValues
}
// IsAStepState returns "true" if the value is listed in the enum definition. "false" otherwise
func (i StepState) IsAStepState() bool {
for _, v := range _StepStateValues {
if i == v {
return true
}
}
return false
}
-35
View File
@@ -1,35 +0,0 @@
// Code generated by "stringer -type=StepState"; DO NOT EDIT.
package fsrep
import "strconv"
const (
_StepState_name_0 = "StepReplicationReadyStepReplicationRetry"
_StepState_name_1 = "StepMarkReplicatedReady"
_StepState_name_2 = "StepMarkReplicatedRetry"
_StepState_name_3 = "StepPermanentError"
_StepState_name_4 = "StepCompleted"
)
var (
_StepState_index_0 = [...]uint8{0, 20, 40}
)
func (i StepState) String() string {
switch {
case 1 <= i && i <= 2:
i -= 1
return _StepState_name_0[_StepState_index_0[i]:_StepState_index_0[i+1]]
case i == 4:
return _StepState_name_1
case i == 8:
return _StepState_name_2
case i == 16:
return _StepState_name_3
case i == 32:
return _StepState_name_4
default:
return "StepState(" + strconv.FormatInt(int64(i), 10) + ")"
}
}
-122
View File
@@ -1,122 +0,0 @@
package queue
import (
"sort"
"time"
. "github.com/zrepl/zrepl/replication/fsrep"
)
type replicationQueueItem struct {
// duplicates fsr.state to avoid accessing and locking fsr
state State
// duplicates fsr.current.nextStepDate to avoid accessing & locking fsr
nextStepDate time.Time
errorStateEnterCount int
fsr *Replication
}
type ReplicationQueue []*replicationQueueItem
func NewReplicationQueue() *ReplicationQueue {
q := make(ReplicationQueue, 0)
return &q
}
func (q ReplicationQueue) Len() int { return len(q) }
func (q ReplicationQueue) Swap(i, j int) { q[i], q[j] = q[j], q[i] }
type lessmapEntry struct {
prio int
less func(a, b *replicationQueueItem) bool
}
var lessmap = map[State]lessmapEntry{
Ready: {
prio: 0,
less: func(a, b *replicationQueueItem) bool {
return a.nextStepDate.Before(b.nextStepDate)
},
},
Retry: {
prio: 1,
less: func(a, b *replicationQueueItem) bool {
return a.errorStateEnterCount < b.errorStateEnterCount
},
},
}
func (q ReplicationQueue) Less(i, j int) bool {
a, b := q[i], q[j]
al, aok := lessmap[a.state]
if !aok {
panic(a)
}
bl, bok := lessmap[b.state]
if !bok {
panic(b)
}
if al.prio != bl.prio {
return al.prio < bl.prio
}
return al.less(a, b)
}
func (q *ReplicationQueue) sort() (done []*Replication) {
// pre-scan for everything that is not ready
newq := make(ReplicationQueue, 0, len(*q))
done = make([]*Replication, 0, len(*q))
for _, qitem := range *q {
if _, ok := lessmap[qitem.state]; !ok {
done = append(done, qitem.fsr)
} else {
newq = append(newq, qitem)
}
}
sort.Stable(newq) // stable to avoid flickering in reports
*q = newq
return done
}
// next remains valid until the next call to GetNext()
func (q *ReplicationQueue) GetNext() (done []*Replication, next *ReplicationQueueItemHandle) {
done = q.sort()
if len(*q) == 0 {
return done, nil
}
next = &ReplicationQueueItemHandle{(*q)[0]}
return done, next
}
func (q *ReplicationQueue) Add(fsr *Replication) {
*q = append(*q, &replicationQueueItem{
fsr: fsr,
state: fsr.State(),
})
}
func (q *ReplicationQueue) Foreach(fu func(*ReplicationQueueItemHandle)) {
for _, qitem := range *q {
fu(&ReplicationQueueItemHandle{qitem})
}
}
type ReplicationQueueItemHandle struct {
i *replicationQueueItem
}
func (h ReplicationQueueItemHandle) GetFSReplication() *Replication {
return h.i.fsr
}
func (h ReplicationQueueItemHandle) Update(newState State, nextStepDate time.Time) {
h.i.state = newState
h.i.nextStepDate = nextStepDate
if h.i.state.IsErrorState() {
h.i.errorStateEnterCount++
}
}
+150 -58
View File
@@ -10,14 +10,15 @@ import (
"github.com/zrepl/zrepl/daemon/job/wakeup"
"github.com/zrepl/zrepl/util/envconst"
"github.com/zrepl/zrepl/util/watchdog"
"github.com/problame/go-streamrpc"
"math/bits"
"net"
"sort"
"sync"
"time"
"github.com/zrepl/zrepl/replication/fsrep"
. "github.com/zrepl/zrepl/replication/internal/diff"
. "github.com/zrepl/zrepl/replication/internal/queue"
"github.com/zrepl/zrepl/replication/pdu"
)
@@ -70,9 +71,9 @@ type Replication struct {
state State
// Working, WorkingWait, Completed, ContextDone
queue *ReplicationQueue
queue []*fsrep.Replication
completed []*fsrep.Replication
active *ReplicationQueueItemHandle
active *fsrep.Replication // == queue[0] or nil, unlike in Report
// for PlanningError, WorkingWait and ContextError and Completed
err error
@@ -87,7 +88,7 @@ type Report struct {
SleepUntil time.Time
Completed []*fsrep.Report
Pending []*fsrep.Report
Active *fsrep.Report
Active *fsrep.Report // not contained in Pending, unlike in struct Replication
}
func NewReplication(secsPerState *prometheus.HistogramVec, bytesReplicated *prometheus.CounterVec) *Replication {
@@ -193,17 +194,22 @@ func resolveConflict(conflict error) (path []*pdu.FilesystemVersion, msg string)
return nil, "no automated way to handle conflict type"
}
var RetryInterval = envconst.Duration("ZREPL_REPLICATION_RETRY_INTERVAL", 4 * time.Second)
var RetryInterval = envconst.Duration("ZREPL_REPLICATION_RETRY_INTERVAL", 10 * time.Second)
type Error interface {
error
Temporary() bool
}
var _ Error = fsrep.Error(nil)
var _ Error = net.Error(nil)
var _ Error = streamrpc.Error(nil)
func isPermanent(err error) bool {
switch err {
case context.Canceled: return true
case context.DeadlineExceeded: return true
if e, ok := err.(Error); ok {
return !e.Temporary()
}
if operr, ok := err.(net.Error); ok {
return !operr.Temporary()
}
return false
return true
}
func statePlanning(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver, u updater) state {
@@ -214,8 +220,10 @@ func statePlanning(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, r
handlePlanningError := func(err error) state {
return u(func(r *Replication) {
r.err = err
if isPermanent(err) {
ge := GlobalError{Err: err, Temporary: !isPermanent(err)}
log.WithError(ge).Error("encountered global error while planning replication")
r.err = ge
if !ge.Temporary {
r.state = PermanentError
} else {
r.sleepUntil = time.Now().Add(RetryInterval)
@@ -229,6 +237,7 @@ func statePlanning(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, r
log.WithError(err).Error("error listing sender filesystems")
return handlePlanningError(err)
}
// no progress here since we could run in a live-lock on connectivity issues
rfss, err := receiver.ListFilesystems(ctx)
if err != nil {
@@ -236,7 +245,9 @@ func statePlanning(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, r
return handlePlanningError(err)
}
q := NewReplicationQueue()
ka.MadeProgress() // for both sender and receiver
q := make([]*fsrep.Replication, 0, len(sfss))
mainlog := log
for _, fs := range sfss {
@@ -249,11 +260,12 @@ func statePlanning(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, r
log.WithError(err).Error("cannot get remote filesystem versions")
return handlePlanningError(err)
}
ka.MadeProgress()
if len(sfsvs) < 1 {
err := errors.New("sender does not have any versions")
log.Error(err.Error())
q.Add(fsrep.NewReplicationWithPermanentError(fs.Path, err))
q = append(q, fsrep.NewReplicationConflictError(fs.Path, err))
continue
}
@@ -278,6 +290,7 @@ func statePlanning(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, r
} else {
rfsvs = []*pdu.FilesystemVersion{}
}
ka.MadeProgress()
path, conflict := IncrementalPath(rfsvs, sfsvs)
if conflict != nil {
@@ -291,8 +304,9 @@ func statePlanning(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, r
log.WithField("problem", msg).Error("cannot resolve conflict")
}
}
ka.MadeProgress()
if path == nil {
q.Add(fsrep.NewReplicationWithPermanentError(fs.Path, conflict))
q = append(q, fsrep.NewReplicationConflictError(fs.Path, conflict))
continue
}
@@ -309,14 +323,16 @@ func statePlanning(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, r
}
}
qitem := fsrfsm.Done()
ka.MadeProgress()
log.Debug("compute send size estimate")
if err = qitem.UpdateSizeEsitmate(ctx, sender); err != nil {
log.WithError(err).Error("error computing size estimate")
return handlePlanningError(err)
}
ka.MadeProgress()
q.Add(qitem)
q = append(q, qitem)
}
ka.MadeProgress()
@@ -351,48 +367,125 @@ func statePlanningError(ctx context.Context, ka *watchdog.KeepAlive, sender Send
}).rsf()
}
type GlobalError struct {
Err error
Temporary bool
}
func (e GlobalError) Error() string {
errClass := "temporary"
if !e.Temporary {
errClass = "permanent"
}
return fmt.Sprintf("%s global error: %s", errClass, e.Err)
}
type FilesystemsReplicationFailedError struct {
FilesystemsWithError []*fsrep.Replication
}
func (e FilesystemsReplicationFailedError) Error() string {
allSame := true
lastErr := e.FilesystemsWithError[0].Err().Error()
for _, fs := range e.FilesystemsWithError {
fsErr := fs.Err().Error()
allSame = allSame && lastErr == fsErr
}
fsstr := "multiple filesystems"
if len(e.FilesystemsWithError) == 1 {
fsstr = fmt.Sprintf("filesystem %s", e.FilesystemsWithError[0].FS())
}
errorStr := lastErr
if !allSame {
errorStr = "multiple different errors"
}
return fmt.Sprintf("%s could not be replicated: %s", fsstr, errorStr)
}
func stateWorking(ctx context.Context, ka *watchdog.KeepAlive, sender Sender, receiver Receiver, u updater) state {
var active *ReplicationQueueItemHandle
var active *fsrep.Replication
rsfNext := u(func(r *Replication) {
done, next := r.queue.GetNext()
r.completed = append(r.completed, done...)
if next == nil {
r.state = Completed
r.err = nil
newq := make([]*fsrep.Replication, 0, len(r.queue))
for i := range r.queue {
if r.queue[i].CanRetry() {
newq = append(newq, r.queue[i])
} else {
r.completed = append(r.completed, r.queue[i])
}
}
r.active = next
active = next
sort.SliceStable(newq, func(i, j int) bool {
return newq[i].NextStepDate().Before(newq[j].NextStepDate())
})
r.queue = newq
if len(r.queue) == 0 {
r.state = Completed
fsWithErr := FilesystemsReplicationFailedError{ // prepare it
FilesystemsWithError: make([]*fsrep.Replication, 0, len(r.completed)),
}
for _, fs := range r.completed {
if fs.CanRetry() {
panic(fmt.Sprintf("implementation error: completed contains retryable FS %s %#v",
fs.FS(), fs.Err()))
}
if fs.Err() != nil {
fsWithErr.FilesystemsWithError = append(fsWithErr.FilesystemsWithError, fs)
}
}
if len(fsWithErr.FilesystemsWithError) > 0 {
r.err = fsWithErr
r.state = PermanentError
}
return
}
active = r.queue[0] // do not dequeue: if it's done, it will be sorted the next time we check for more work
r.active = active
}).rsf()
if active == nil {
return rsfNext
}
state, nextStepDate := active.GetFSReplication().TakeStep(ctx, ka, sender, receiver)
activeCtx := fsrep.WithLogger(ctx, getLogger(ctx).WithField("fs", active.FS()))
err := active.Retry(activeCtx, ka, sender, receiver)
u(func(r *Replication) {
active.Update(state, nextStepDate)
r.active = nil
}).rsf()
select {
case <-ctx.Done():
return u(func(r *Replication) {
r.err = ctx.Err()
r.state = PermanentError
}).rsf()
default:
}
if err := active.GetFSReplication().Err(); err != nil {
return u(func(r *Replication) {
r.err = err
if isPermanent(err) {
if err != nil {
if err.ContextErr() && ctx.Err() != nil {
getLogger(ctx).WithError(err).
Info("filesystem replication was cancelled")
u(func(r*Replication) {
r.err = GlobalError{Err: err, Temporary: false}
r.state = PermanentError
} else {
})
} else if err.LocalToFS() {
getLogger(ctx).WithError(err).
Error("filesystem replication encountered a filesystem-specific error")
// we stay in this state and let the queuing logic above de-prioritize this failing FS
} else if err.Temporary() {
getLogger(ctx).WithError(err).
Error("filesystem encountered a non-filesystem-specific temporary error, enter retry-wait")
u(func(r *Replication) {
r.err = GlobalError{Err: err, Temporary: true}
r.sleepUntil = time.Now().Add(RetryInterval)
r.state = WorkingWait
}
}).rsf()
}).rsf()
} else {
getLogger(ctx).WithError(err).
Error("encountered a permanent non-filesystem-specific error")
u(func(r *Replication) {
r.err = GlobalError{Err: err, Temporary: false}
r.state = PermanentError
}).rsf()
}
}
return u(nil).rsf()
@@ -404,7 +497,7 @@ func stateWorkingWait(ctx context.Context, ka *watchdog.KeepAlive, sender Sender
sleepUntil = r.sleepUntil
})
t := time.NewTimer(RetryInterval)
getLogger(ctx).WithField("until", sleepUntil).Info("retry wait after replication step error")
getLogger(ctx).WithField("until", sleepUntil).Info("retry wait after error")
defer t.Stop()
select {
case <-ctx.Done():
@@ -433,27 +526,26 @@ func (r *Replication) Report() *Report {
SleepUntil: r.sleepUntil,
}
if r.state&(Planning|PlanningError|PermanentError) != 0 {
if r.err != nil {
rep.Problem = r.err.Error()
}
if r.err != nil {
rep.Problem = r.err.Error()
}
if r.state&(Planning|PlanningError) != 0 {
return &rep
}
rep.Pending = make([]*fsrep.Report, 0, r.queue.Len())
rep.Pending = make([]*fsrep.Report, 0, len(r.queue))
rep.Completed = make([]*fsrep.Report, 0, len(r.completed)) // room for active (potentially)
var active *fsrep.Replication
// since r.active == r.queue[0], do not contain it in pending output
pending := r.queue
if r.active != nil {
active = r.active.GetFSReplication()
rep.Active = active.Report()
rep.Active = r.active.Report()
pending = r.queue[1:]
}
for _, fsr := range pending {
rep.Pending= append(rep.Pending, fsr.Report())
}
r.queue.Foreach(func(h *ReplicationQueueItemHandle) {
fsr := h.GetFSReplication()
if active != fsr {
rep.Pending = append(rep.Pending, fsr.Report())
}
})
for _, fsr := range r.completed {
rep.Completed = append(rep.Completed, fsr.Report())
}
+9 -19
View File
@@ -6,36 +6,26 @@ import (
"time"
)
type Progress struct {
type KeepAlive struct {
mtx sync.Mutex
lastUpd time.Time
}
func (p *Progress) String() string {
return fmt.Sprintf("last update at %s", p.lastUpd)
}
func (p *Progress) madeProgressSince(p2 *Progress) bool {
if p.lastUpd.IsZero() && p2.lastUpd.IsZero() {
return false
func (p *KeepAlive) String() string {
if p.lastUpd.IsZero() {
return fmt.Sprintf("never updated")
}
return p.lastUpd.After(p2.lastUpd)
}
type KeepAlive struct {
mtx sync.Mutex
p Progress
return fmt.Sprintf("last update at %s", p.lastUpd)
}
func (k *KeepAlive) MadeProgress() {
k.mtx.Lock()
defer k.mtx.Unlock()
k.p.lastUpd = time.Now()
k.lastUpd = time.Now()
}
func (k *KeepAlive) ExpectProgress(last *Progress) (madeProgress bool) {
func (k *KeepAlive) CheckTimeout(timeout time.Duration, jitter time.Duration) (didTimeOut bool) {
k.mtx.Lock()
defer k.mtx.Unlock()
madeProgress = k.p.madeProgressSince(last)
*last = k.p
return madeProgress
return k.lastUpd.Add(timeout - jitter).Before(time.Now())
}