daemon/active: implement watchdog to handle stuck replication / pruners

ActiveSide.do() can only run sequentially, i.e. we cannot run
replication and pruning in parallel. Why?

* go-streamrpc only allows one active request at a time
(this is bad design and should be fixed at some point)
* replication and pruning are implemented independently, but work on the
same resources (snapshots)

A: pruning might destroy a snapshot that is planned to be replicated
B: replication might replicate snapshots that should be pruned

We do not have any resource management / locking for A and B, but we
have a use case where users don't want their machine fill up with
snapshots if replication does not work.
That means we _have_ to run the pruners.

A further complication is that we cannot just cancel the replication
context after a timeout and move on to the pruner: it could be initial
replication and we don't know how long it will take.
(And we don't have resumable send & recv yet).

With the previous commits, we can implement the watchdog using context
cancellation.
Note that the 'MadeProgress()' calls can only be placed right before
non-error state transition. Otherwise, we could end up in a live-lock.
This commit is contained in:
Christian Schwarz
2018-10-19 16:27:05 +02:00
parent 4ede99b08c
commit 69bfcb7bed
8 changed files with 240 additions and 35 deletions
+102 -16
View File
@@ -13,6 +13,8 @@ import (
"github.com/zrepl/zrepl/daemon/pruner"
"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"
@@ -38,7 +40,9 @@ type ActiveSide struct {
type activeSideTasks struct {
replication *replication.Replication
replicationCancel context.CancelFunc
prunerSender, prunerReceiver *pruner.Pruner
prunerSenderCancel, prunerReceiverCancel context.CancelFunc
}
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
@@ -262,6 +266,57 @@ func (j *ActiveSide) do(ctx context.Context) {
}
}()
// watchdog
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")
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()
}
})
log.WithField("replication_progress", rep.String()).
WithField("pruner_sender_progress", prunerSender.String()).
WithField("pruner_receiver_progress", prunerReceiver.String()).
Debug("watchdog did run")
}
}()
client, err := j.clientFactory.NewClient()
if err != nil {
log.WithError(err).Error("factory cannot instantiate streamrpc client")
@@ -270,21 +325,52 @@ func (j *ActiveSide) do(ctx context.Context) {
sender, receiver, err := j.mode.SenderReceiver(client)
tasks := j.updateTasks(func(tasks *activeSideTasks) {
// reset it
*tasks = activeSideTasks{}
tasks.replication = replication.NewReplication(j.promRepStateSecs, j.promBytesReplicated)
})
{
select {
case <-ctx.Done():
return
default:
}
ctx, repCancel := context.WithCancel(ctx)
tasks := j.updateTasks(func(tasks *activeSideTasks) {
// reset it
*tasks = activeSideTasks{}
tasks.replicationCancel = repCancel
tasks.replication = replication.NewReplication(j.promRepStateSecs, j.promBytesReplicated)
})
log.Info("start replication")
tasks.replication.Drive(ctx, sender, receiver)
repCancel() // always cancel to free up context resources
}
log.Info("start replication")
tasks.replication.Drive(ctx, sender, receiver)
tasks = j.updateTasks(func(tasks *activeSideTasks) {
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
})
log.Info("start pruning sender")
tasks.prunerSender.Prune()
log.Info("start pruning receiver")
tasks.prunerReceiver.Prune()
{
select {
case <-ctx.Done():
return
default:
}
ctx, senderCancel := context.WithCancel(ctx)
tasks := j.updateTasks(func(tasks *activeSideTasks) {
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
tasks.prunerSenderCancel = senderCancel
})
log.Info("start pruning sender")
tasks.prunerSender.Prune()
senderCancel()
}
{
select {
case <-ctx.Done():
return
default:
}
ctx, receiverCancel := context.WithCancel(ctx)
tasks := j.updateTasks(func(tasks *activeSideTasks) {
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
tasks.prunerReceiverCancel = receiverCancel
})
log.Info("start pruning receiver")
tasks.prunerReceiver.Prune()
receiverCancel()
}
}
+13
View File
@@ -9,6 +9,7 @@ import (
"github.com/zrepl/zrepl/logger"
"github.com/zrepl/zrepl/pruning"
"github.com/zrepl/zrepl/replication/pdu"
"github.com/zrepl/zrepl/util/watchdog"
"net"
"sort"
"sync"
@@ -56,6 +57,8 @@ type args struct {
type Pruner struct {
args args
Progress watchdog.KeepAlive
mtx sync.RWMutex
state State
@@ -175,6 +178,10 @@ func (s State) statefunc() state {
return statemap[s]
}
func (s State) IsTerminal() bool {
return s.statefunc() == nil
}
type updater func(func(*Pruner)) State
type state func(args *args, u updater) state
@@ -249,6 +256,12 @@ func (p *Pruner) Report() *Report {
return &r
}
func (p *Pruner) State() State {
p.mtx.Lock()
defer p.mtx.Unlock()
return p.state
}
type fs struct {
path string