Compare commits

..

5 Commits

Author SHA1 Message Date
Christian Schwarz 0eb7032735 daemon/control: envconst timeout for control socket server-side timeouts
refs #262
2020-02-17 22:36:32 +01:00
Christian Schwarz 3ff1966cab docs/installation: use && for early exit if build-in-docker step fails 2020-02-17 18:02:04 +01:00
Christian Schwarz a3842155c5 zrepl test filesystems: support snap job type 2020-02-17 18:02:04 +01:00
Christian Schwarz 02b3b4f80c fix some typos 2020-02-17 18:02:04 +01:00
Christian Schwarz 0882290595 README update
- donation links
- package overview
2020-02-17 18:02:04 +01:00
20 changed files with 105 additions and 261 deletions
+21 -9
View File
@@ -1,8 +1,9 @@
[![GitHub license](https://img.shields.io/github/license/zrepl/zrepl.svg)](https://github.com/zrepl/zrepl/blob/master/LICENSE) [![GitHub license](https://img.shields.io/github/license/zrepl/zrepl.svg)](https://github.com/zrepl/zrepl/blob/master/LICENSE)
[![Language: Go](https://img.shields.io/badge/language-Go-6ad7e5.svg)](https://golang.org/) [![Language: Go](https://img.shields.io/badge/language-Go-6ad7e5.svg)](https://golang.org/)
[![User Docs](https://img.shields.io/badge/docs-web-blue.svg)](https://zrepl.github.io) [![User Docs](https://img.shields.io/badge/docs-web-blue.svg)](https://zrepl.github.io)
[![Donate via PayPal](https://img.shields.io/badge/donate-paypal-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96) [![Donate via Patreon](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Fshieldsio-patreon.herokuapp.com%2Fzrepl%2Fpledges&style=flat&color=yellow)](https://www.patreon.com/zrepl)
[![Donate via Liberapay](https://img.shields.io/liberapay/receives/zrepl.svg?logo=liberapay)](https://liberapay.com/zrepl/donate) [![Donate via Liberapay](https://img.shields.io/liberapay/receives/zrepl.svg?logo=liberapay)](https://liberapay.com/zrepl/donate)
[![Donate via PayPal](https://img.shields.io/badge/donate-paypal-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
[![Twitter](https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl) [![Twitter](https://img.shields.io/twitter/url/https/github.com/zrepl/zrepl.svg?style=social)](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
# zrepl # zrepl
@@ -23,6 +24,7 @@ zrepl is a one-stop ZFS backup & replication solution.
If so, think of an expressive configuration example. If so, think of an expressive configuration example.
2. Think of at least one use case that generalizes from your concrete application. 2. Think of at least one use case that generalizes from your concrete application.
3. Open an issue on GitHub with example conf & use case attached. 3. Open an issue on GitHub with example conf & use case attached.
4. **Optional**: [Post a bounty](https://www.bountysource.com/teams/zrepl) on the issue, or [contact Christian Schwarz](https://cschwarz.com) for contract work.
The above does not apply if you already implemented everything. The above does not apply if you already implemented everything.
Check out the *Coding Workflow* section below for details. Check out the *Coding Workflow* section below for details.
@@ -46,11 +48,8 @@ zrepl is written in [Go](https://golang.org) and uses [Go modules](https://githu
The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework. The documentation is written in [ReStructured Text](http://docutils.sourceforge.net/rst.html) using the [Sphinx](https://www.sphinx-doc.org) framework.
To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`. To get started, run `./lazy.sh devsetup` to easily install build dependencies and read `docs/installation.rst -> Compiling from Source`.
`lazy.sh` uses `python3-pip` to fetch the build dependencies for the docs - you might want to use a [venv](https://docs.python.org/3/library/venv.html).
### Overall Architecture If you just want to install the Go dependencies, run `./lazy.sh godep`.
The application architecture is documented as part of the user docs in the *Implementation* section (`docs/content/impl`).
Make sure to develop an understanding how zrepl is typically used by studying the user docs first.
### Project Structure ### Project Structure
@@ -62,14 +61,15 @@ Make sure to develop an understanding how zrepl is typically used by studying th
│   └── samples │   └── samples
├── daemon # the implementation of `zrepl daemon` subcommand ├── daemon # the implementation of `zrepl daemon` subcommand
│   ├── filters │   ├── filters
│   ├── hooks # snapshot hooks
│   ├── job # job implementations │   ├── job # job implementations
│   ├── logging # logging outlets + formatters │   ├── logging # logging outlets + formatters
│   ├── nethelpers │   ├── nethelpers
│   ├── prometheus │   ├── prometheus
│   ├── pruner # pruner implementation │   ├── pruner # pruner implementation
│   ├── snapper # snapshotter implementation │   ├── snapper # snapshotter implementation
├── docs # sphinx-based documentation
├── dist # supplemental material for users & package maintainers ├── dist # supplemental material for users & package maintainers
├── docs # sphinx-based documentation
│   ├── **/*.rst # documentation in reStructuredText │   ├── **/*.rst # documentation in reStructuredText
│   ├── sphinxconf │   ├── sphinxconf
│   │   └── conf.py # sphinx config (see commit 445a280 why its not in docs/) │   │   └── conf.py # sphinx config (see commit 445a280 why its not in docs/)
@@ -78,6 +78,7 @@ Make sure to develop an understanding how zrepl is typically used by studying th
│   └── public_git # checkout of zrepl.github.io managed by above shell script │   └── public_git # checkout of zrepl.github.io managed by above shell script
├── endpoint # implementation of replication endpoints (=> package replication) ├── endpoint # implementation of replication endpoints (=> package replication)
├── logger # our own logger package ├── logger # our own logger package
├── platformtest # test suite for our zfs abstractions (error classification, etc)
├── pruning # pruning rules (the logic, not the actual execution) ├── pruning # pruning rules (the logic, not the actual execution)
│   └── retentiongrid │   └── retentiongrid
├── replication ├── replication
@@ -92,14 +93,13 @@ Make sure to develop an understanding how zrepl is typically used by studying th
│ ├── transportmux # TCP connecter and listener used to split control & data traffic │ ├── transportmux # TCP connecter and listener used to split control & data traffic
│ └── versionhandshake # replication protocol version handshake perfomed on newly established connections │ └── versionhandshake # replication protocol version handshake perfomed on newly established connections
├── tlsconf # abstraction for Go TLS server + client config ├── tlsconf # abstraction for Go TLS server + client config
├── transport # transports implementation ├── transport # transport implementations
│ ├── fromconfig │ ├── fromconfig
│ ├── local │ ├── local
│ ├── ssh │ ├── ssh
│ ├── tcp │ ├── tcp
│ └── tls │ └── tls
├── util ├── util
├── vendor # managed by dep
├── version # abstraction for versions (filled during build by Makefile) ├── version # abstraction for versions (filled during build by Makefile)
└── zfs # zfs(8) wrappers └── zfs # zfs(8) wrappers
``` ```
@@ -134,3 +134,15 @@ There will not be a big refactoring (an attempt was made, but it's destroying to
However, new contributions & patches should fix naming without further notice in the commit message. However, new contributions & patches should fix naming without further notice in the commit message.
### RPC debugging
Optionally, there are various RPC-related environment varibles, that if set to something != `""` will produce additional debug output on stderr:
https://github.com/zrepl/zrepl/blob/master/rpc/rpc_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/dataconn_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/stream/stream_debug.go#L11
https://github.com/zrepl/zrepl/blob/master/rpc/dataconn/heartbeatconn/heartbeatconn_debug.go#L11
+3 -10
View File
@@ -9,7 +9,7 @@ import (
) )
var SignalCmd = &cli.Subcommand{ var SignalCmd = &cli.Subcommand{
Use: "signal [wakeup|reset] JOB [DATA]", Use: "signal [wakeup|reset] JOB",
Short: "wake up a job from wait state or abort its current invocation", Short: "wake up a job from wait state or abort its current invocation",
Run: func(subcommand *cli.Subcommand, args []string) error { Run: func(subcommand *cli.Subcommand, args []string) error {
return runSignalCmd(subcommand.Config(), args) return runSignalCmd(subcommand.Config(), args)
@@ -17,13 +17,8 @@ var SignalCmd = &cli.Subcommand{
} }
func runSignalCmd(config *config.Config, args []string) error { func runSignalCmd(config *config.Config, args []string) error {
if len(args) < 2 || len(args) > 3 { if len(args) != 2 {
return errors.Errorf("Expected 2 arguments: [wakeup|reset|set-concurrency] JOB [DATA]") return errors.Errorf("Expected 2 arguments: [wakeup|reset] JOB")
}
var data string
if len(args) == 3 {
data = args[2]
} }
httpc, err := controlHttpClient(config.Global.Control.SockPath) httpc, err := controlHttpClient(config.Global.Control.SockPath)
@@ -35,11 +30,9 @@ func runSignalCmd(config *config.Config, args []string) error {
struct { struct {
Name string Name string
Op string Op string
Data string
}{ }{
Name: args[1], Name: args[1],
Op: args[0], Op: args[0],
Data: data,
}, },
struct{}{}, struct{}{},
) )
+2
View File
@@ -60,6 +60,8 @@ func runTestFilterCmd(subcommand *cli.Subcommand, args []string) error {
confFilter = j.Filesystems confFilter = j.Filesystems
case *config.PushJob: case *config.PushJob:
confFilter = j.Filesystems confFilter = j.Filesystems
case *config.SnapJob:
confFilter = j.Filesystems
default: default:
return fmt.Errorf("job type %T does not have filesystems filter", j) return fmt.Errorf("job type %T does not have filesystems filter", j)
} }
+2 -14
View File
@@ -8,7 +8,6 @@ import (
"io" "io"
"net" "net"
"net/http" "net/http"
"strconv"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -48,8 +47,6 @@ func (j *controlJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool) { r
func (j *controlJob) SenderConfig() *endpoint.SenderConfig { return nil } func (j *controlJob) SenderConfig() *endpoint.SenderConfig { return nil }
func (j *controlJob) SetConcurrency(concurrency int) error { return errors.Errorf("not supported") }
var promControl struct { var promControl struct {
requestBegin *prometheus.CounterVec requestBegin *prometheus.CounterVec
requestFinished *prometheus.HistogramVec requestFinished *prometheus.HistogramVec
@@ -129,7 +126,6 @@ func (j *controlJob) Run(ctx context.Context) {
type reqT struct { type reqT struct {
Name string Name string
Op string Op string
Data string
} }
var req reqT var req reqT
if decoder(&req) != nil { if decoder(&req) != nil {
@@ -142,14 +138,6 @@ func (j *controlJob) Run(ctx context.Context) {
err = j.jobs.wakeup(req.Name) err = j.jobs.wakeup(req.Name)
case "reset": case "reset":
err = j.jobs.reset(req.Name) err = j.jobs.reset(req.Name)
case "set-concurrency":
var concurrency int
concurrency, err = strconv.Atoi(req.Data) // shadow
if err != nil {
// fallthrough outer
} else {
err = j.jobs.setConcurrency(req.Name, concurrency)
}
default: default:
err = fmt.Errorf("operation %q is invalid", req.Op) err = fmt.Errorf("operation %q is invalid", req.Op)
} }
@@ -159,8 +147,8 @@ func (j *controlJob) Run(ctx context.Context) {
server := http.Server{ server := http.Server{
Handler: mux, Handler: mux,
// control socket is local, 1s timeout should be more than sufficient, even on a loaded system // control socket is local, 1s timeout should be more than sufficient, even on a loaded system
WriteTimeout: 1 * time.Second, WriteTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_WRITE_TIMEOUT", 1*time.Second),
ReadTimeout: 1 * time.Second, ReadTimeout: envconst.Duration("ZREPL_DAEMON_CONTROL_READ_TIMEOUT", 1*time.Second),
} }
outer: outer:
-12
View File
@@ -176,18 +176,6 @@ func (s *jobs) reset(job string) error {
return wu() return wu()
} }
func (s *jobs) setConcurrency(jobName string, concurrency int) error {
s.m.RLock()
defer s.m.RUnlock()
job, ok := s.jobs[jobName]
if !ok {
return errors.Errorf("Job %q does not exist", job)
}
return job.SetConcurrency(concurrency)
}
const ( const (
jobNamePrometheus = "_prometheus" jobNamePrometheus = "_prometheus"
jobNameControl = "_control" jobNameControl = "_control"
+4 -28
View File
@@ -55,12 +55,9 @@ const (
type activeSideTasks struct { type activeSideTasks struct {
state ActiveSideState state ActiveSideState
concurrency int
// valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone // valid for state ActiveSideReplicating, ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
replicationReport driver.ReportFunc replicationReport driver.ReportFunc
replicationCancel context.CancelFunc replicationCancel context.CancelFunc
replicationSetConcurrency driver.SetConcurrencyFunc
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone // valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
prunerSender, prunerReceiver *pruner.Pruner prunerSender, prunerReceiver *pruner.Pruner
@@ -281,8 +278,6 @@ func activeSide(g *config.Global, in *config.ActiveJob, configJob interface{}) (
return nil, errors.Wrap(err, "invalid job name") return nil, errors.Wrap(err, "invalid job name")
} }
j.tasks.concurrency = 1 // FIXME
switch v := configJob.(type) { switch v := configJob.(type) {
case *config.PushJob: case *config.PushJob:
j.mode, err = modePushFromConfig(g, v, j.name) // shadow j.mode, err = modePushFromConfig(g, v, j.name) // shadow
@@ -380,22 +375,6 @@ func (j *ActiveSide) SenderConfig() *endpoint.SenderConfig {
return push.senderConfig return push.senderConfig
} }
func (j *ActiveSide) SetConcurrency(concurrency int) (err error) {
j.updateTasks(func(tasks *activeSideTasks) {
if tasks.replicationSetConcurrency != nil {
err = tasks.replicationSetConcurrency(concurrency) // no shadow
if err == nil {
tasks.concurrency = concurrency
}
} else {
// FIXME this is not great, should always be able to set it
err = errors.Errorf("cannot set while not replicating")
}
})
return err
}
func (j *ActiveSide) Run(ctx context.Context) { func (j *ActiveSide) Run(ctx context.Context) {
log := GetLogger(ctx) log := GetLogger(ctx)
ctx = logging.WithSubsystemLoggers(ctx, log) ctx = logging.WithSubsystemLoggers(ctx, log)
@@ -457,14 +436,11 @@ func (j *ActiveSide) do(ctx context.Context) {
ctx, repCancel := context.WithCancel(ctx) ctx, repCancel := context.WithCancel(ctx)
var repWait driver.WaitFunc var repWait driver.WaitFunc
j.updateTasks(func(tasks *activeSideTasks) { j.updateTasks(func(tasks *activeSideTasks) {
// reset it (almost) // reset it
old := *tasks *tasks = activeSideTasks{}
*tasks = activeSideTasks{
concurrency: old.concurrency,
}
tasks.replicationCancel = repCancel tasks.replicationCancel = repCancel
tasks.replicationReport, repWait, tasks.replicationSetConcurrency = replication.Do( tasks.replicationReport, repWait = replication.Do(
ctx, tasks.concurrency, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()), ctx, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
) )
tasks.state = ActiveSideReplicating tasks.state = ActiveSideReplicating
}) })
-1
View File
@@ -40,7 +40,6 @@ type Job interface {
// must return the root of that subtree as rfs and ok = true // must return the root of that subtree as rfs and ok = true
OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool) OwnedDatasetSubtreeRoot() (rfs *zfs.DatasetPath, ok bool)
SenderConfig() *endpoint.SenderConfig SenderConfig() *endpoint.SenderConfig
SetConcurrency(concurrency int) error
} }
type Type string type Type string
-2
View File
@@ -163,8 +163,6 @@ func (j *PassiveSide) SenderConfig() *endpoint.SenderConfig {
func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {} func (*PassiveSide) RegisterMetrics(registerer prometheus.Registerer) {}
func (*PassiveSide) SetConcurrency(concurrency int) error { return errors.Errorf("not supported") }
func (j *PassiveSide) Run(ctx context.Context) { func (j *PassiveSide) Run(ctx context.Context) {
log := GetLogger(ctx) log := GetLogger(ctx)
-2
View File
@@ -117,8 +117,6 @@ outer:
} }
} }
func (*SnapJob) SetConcurrency(concurrency int) error { return errors.Errorf("not supported") }
// Adaptor that implements pruner.History around a pruner.Target. // Adaptor that implements pruner.History around a pruner.Target.
// The ReplicationCursor method is Get-op only and always returns // The ReplicationCursor method is Get-op only and always returns
// the filesystem's most recent version's GUID. // the filesystem's most recent version's GUID.
-3
View File
@@ -5,7 +5,6 @@ import (
"net" "net"
"net/http" "net/http"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
@@ -52,8 +51,6 @@ func (j *prometheusJob) OwnedDatasetSubtreeRoot() (p *zfs.DatasetPath, ok bool)
func (j *prometheusJob) SenderConfig() *endpoint.SenderConfig { return nil } func (j *prometheusJob) SenderConfig() *endpoint.SenderConfig { return nil }
func (j *prometheusJob) SetConcurrency(concurrency int) error { return errors.Errorf("not supported") }
func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {} func (j *prometheusJob) RegisterMetrics(registerer prometheus.Registerer) {}
func (j *prometheusJob) Run(ctx context.Context) { func (j *prometheusJob) Run(ctx context.Context) {
+3 -3
View File
@@ -101,9 +101,9 @@ and serves as a reference for build dependencies and procedure:
:: ::
git clone https://github.com/zrepl/zrepl.git git clone https://github.com/zrepl/zrepl.git && \
cd zrepl cd zrepl && \
sudo docker build -t zrepl_build -f build.Dockerfile . sudo docker build -t zrepl_build -f build.Dockerfile . && \
sudo docker run -it --rm \ sudo docker run -it --rm \
-v "${PWD}:/src" \ -v "${PWD}:/src" \
--user "$(id -u):$(id -g)" \ --user "$(id -u):$(id -g)" \
+7 -54
View File
@@ -89,8 +89,6 @@ type attempt struct {
// if both are nil, it must be assumed that Planner.Plan is active // if both are nil, it must be assumed that Planner.Plan is active
planErr *timedError planErr *timedError
fss []*fs fss []*fs
concurrency int
} }
type timedError struct { type timedError struct {
@@ -172,12 +170,11 @@ type step struct {
type ReportFunc func() *report.Report type ReportFunc func() *report.Report
type WaitFunc func(block bool) (done bool) type WaitFunc func(block bool) (done bool)
type SetConcurrencyFunc func(concurrency int) error
var maxAttempts = envconst.Int64("ZREPL_REPLICATION_MAX_ATTEMPTS", 3) var maxAttempts = envconst.Int64("ZREPL_REPLICATION_MAX_ATTEMPTS", 3)
var reconnectHardFailTimeout = envconst.Duration("ZREPL_REPLICATION_RECONNECT_HARD_FAIL_TIMEOUT", 10*time.Minute) var reconnectHardFailTimeout = envconst.Duration("ZREPL_REPLICATION_RECONNECT_HARD_FAIL_TIMEOUT", 10*time.Minute)
func Do(ctx context.Context, initialConcurrency int, planner Planner) (ReportFunc, WaitFunc, SetConcurrencyFunc) { func Do(ctx context.Context, planner Planner) (ReportFunc, WaitFunc) {
log := getLog(ctx) log := getLog(ctx)
l := chainlock.New() l := chainlock.New()
run := &run{ run := &run{
@@ -185,8 +182,6 @@ func Do(ctx context.Context, initialConcurrency int, planner Planner) (ReportFun
startedAt: time.Now(), startedAt: time.Now(),
} }
concurrencyChanges := make(chan concurrencyChange)
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
defer close(done) defer close(done)
@@ -203,21 +198,15 @@ func Do(ctx context.Context, initialConcurrency int, planner Planner) (ReportFun
run.waitReconnect.SetZero() run.waitReconnect.SetZero()
run.waitReconnectError = nil run.waitReconnectError = nil
prevConcurrency := initialConcurrency // FIXME default concurrency
if prev != nil {
prevConcurrency = prev.concurrency
}
// do current attempt // do current attempt
cur := &attempt{ cur := &attempt{
l: l, l: l,
startedAt: time.Now(), startedAt: time.Now(),
planner: planner, planner: planner,
concurrency: prevConcurrency,
} }
run.attempts = append(run.attempts, cur) run.attempts = append(run.attempts, cur)
run.l.DropWhile(func() { run.l.DropWhile(func() {
cur.do(ctx, prev, concurrencyChanges) cur.do(ctx, prev)
}) })
prev = cur prev = cur
if ctx.Err() != nil { if ctx.Err() != nil {
@@ -288,25 +277,10 @@ func Do(ctx context.Context, initialConcurrency int, planner Planner) (ReportFun
defer run.l.Lock().Unlock() defer run.l.Lock().Unlock()
return run.report() return run.report()
} }
setConcurrency := func(concurrency int) (reterr error) { return report, wait
var wg sync.WaitGroup
wg.Add(1)
concurrencyChanges <- concurrencyChange{ concurrency, func(err error) {
defer wg.Done()
reterr = err // shadow
}}
wg.Wait()
return reterr
}
return report, wait, setConcurrency
} }
type concurrencyChange struct { func (a *attempt) do(ctx context.Context, prev *attempt) {
value int
resultCallback func(error)
}
func (a *attempt) do(ctx context.Context, prev *attempt, setConcurrency <-chan concurrencyChange) {
pfss, err := a.planner.Plan(ctx) pfss, err := a.planner.Plan(ctx)
errTime := time.Now() errTime := time.Now()
defer a.l.Lock().Unlock() defer a.l.Lock().Unlock()
@@ -380,8 +354,8 @@ func (a *attempt) do(ctx context.Context, prev *attempt, setConcurrency <-chan c
} }
// invariant: prevs contains an entry for each unambigious correspondence // invariant: prevs contains an entry for each unambigious correspondence
stepQueue := newStepQueue(a.concurrency) stepQueue := newStepQueue()
defer stepQueue.Start()() defer stepQueue.Start(1)() // TODO parallel replication
var fssesDone sync.WaitGroup var fssesDone sync.WaitGroup
for _, f := range a.fss { for _, f := range a.fss {
fssesDone.Add(1) fssesDone.Add(1)
@@ -390,27 +364,8 @@ func (a *attempt) do(ctx context.Context, prev *attempt, setConcurrency <-chan c
f.do(ctx, stepQueue, prevs[f]) f.do(ctx, stepQueue, prevs[f])
}(f) }(f)
} }
changeConcurrencyDone := make(chan struct{})
go func() {
for {
select {
case change := <-setConcurrency:
err := stepQueue.SetConcurrency(change.value)
go change.resultCallback(err)
if err == nil {
a.l.Lock()
a.concurrency = change.value
a.l.Unlock()
}
case <-changeConcurrencyDone:
return
// not waiting for ctx.Done here, the main job are the fsses
}
}
}()
a.l.DropWhile(func() { a.l.DropWhile(func() {
fssesDone.Wait() fssesDone.Wait()
close(changeConcurrencyDone)
}) })
a.finishedAt = time.Now() a.finishedAt = time.Now()
} }
@@ -506,8 +461,7 @@ func (fs *fs) do(ctx context.Context, pq *stepQueue, prev *fs) {
// lock must not be held while executing step in order for reporting to work // lock must not be held while executing step in order for reporting to work
fs.l.DropWhile(func() { fs.l.DropWhile(func() {
targetDate := s.step.TargetDate() targetDate := s.step.TargetDate()
ctx, done := pq.WaitReady(ctx, fs, targetDate)() defer pq.WaitReady(fs, targetDate)()
defer done()
err = s.step.Step(ctx) // no shadow err = s.step.Step(ctx) // no shadow
errTime = time.Now() // no shadow errTime = time.Now() // no shadow
}) })
@@ -528,7 +482,6 @@ func (r *run) report() *report.Report {
WaitReconnectSince: r.waitReconnect.begin, WaitReconnectSince: r.waitReconnect.begin,
WaitReconnectUntil: r.waitReconnect.end, WaitReconnectUntil: r.waitReconnect.end,
WaitReconnectError: r.waitReconnectError.IntoReportError(), WaitReconnectError: r.waitReconnectError.IntoReportError(),
// Concurrency: r.concurrency,
} }
for i := range report.Attempts { for i := range report.Attempts {
report.Attempts[i] = r.attempts[i].report() report.Attempts[i] = r.attempts[i].report()
@@ -151,7 +151,7 @@ func TestReplication(t *testing.T) {
ctx := context.Background() ctx := context.Background()
mp := &mockPlanner{} mp := &mockPlanner{}
getReport, wait, _ := Do(ctx, 1, mp) getReport, wait := Do(ctx, mp)
begin := time.Now() begin := time.Now()
fireAt := []time.Duration{ fireAt := []time.Duration{
// the following values are relative to the start // the following values are relative to the start
+45 -103
View File
@@ -2,8 +2,6 @@ package driver
import ( import (
"container/heap" "container/heap"
"fmt"
"sync"
"time" "time"
"github.com/zrepl/zrepl/util/chainlock" "github.com/zrepl/zrepl/util/chainlock"
@@ -13,88 +11,51 @@ type stepQueueRec struct {
ident interface{} ident interface{}
targetDate time.Time targetDate time.Time
wakeup chan StepCompletedFunc wakeup chan StepCompletedFunc
cancelDueToConcurrencyDownsize interace{}
} }
type stepQueue struct { type stepQueue struct {
stop chan struct{} stop chan struct{}
reqs chan stepQueueRec reqs chan stepQueueRec
// l protects all members except the channels above
l *chainlock.L
pendingCond *sync.Cond
// ident => queueItem
pending *stepQueueHeap
active *stepQueueHeap
queueItems map[interface{}]*stepQueueHeapItem // for tracking used idents in both pending and active
// stopped is used for cancellation of "wake" goroutine
stopped bool
concurrency int
} }
type stepQueueHeapItem struct { type stepQueueHeapItem struct {
idx int idx int
req *stepQueueRec req stepQueueRec
}
type stepQueueHeap struct {
items []*stepQueueHeapItem
reverse bool // never change after pushing first element
} }
type stepQueueHeap []*stepQueueHeapItem
func (h stepQueueHeap) Less(i, j int) bool { func (h stepQueueHeap) Less(i, j int) bool {
res := h.items[i].req.targetDate.Before(h.items[j].req.targetDate) return h[i].req.targetDate.Before(h[j].req.targetDate)
if h.reverse {
return !res
}
return res
} }
func (h stepQueueHeap) Swap(i, j int) { func (h stepQueueHeap) Swap(i, j int) {
h.items[i], h.items[j] = h.items[j], h.items[i] h[i], h[j] = h[j], h[i]
h.items[i].idx = i h[i].idx = i
h.items[j].idx = j h[j].idx = j
} }
func (h stepQueueHeap) Len() int { func (h stepQueueHeap) Len() int {
return len(h.items) return len(h)
} }
func (h *stepQueueHeap) Push(elem interface{}) { func (h *stepQueueHeap) Push(elem interface{}) {
hitem := elem.(*stepQueueHeapItem) hitem := elem.(*stepQueueHeapItem)
hitem.idx = h.Len() hitem.idx = h.Len()
h.items = append(h.items, hitem) *h = append(*h, hitem)
} }
func (h *stepQueueHeap) Pop() interface{} { func (h *stepQueueHeap) Pop() interface{} {
elem := h.items[h.Len()-1] elem := (*h)[h.Len()-1]
elem.idx = -1 elem.idx = -1
h.items = h.items[:h.Len()-1] *h = (*h)[:h.Len()-1]
return elem return elem
} }
// returned stepQueue must be closed with method Close // returned stepQueue must be closed with method Close
func newStepQueue(concurrency int) *stepQueue { func newStepQueue() *stepQueue {
l := chainlock.New()
q := &stepQueue{ q := &stepQueue{
stop: make(chan struct{}), stop: make(chan struct{}),
reqs: make(chan stepQueueRec), reqs: make(chan stepQueueRec),
l: l,
pendingCond: l.NewCond(),
// priority queue
pending: &stepQueueHeap{reverse: false},
active: &stepQueueHeap{reverse: true},
// ident => queueItem
queueItems: make(map[interface{}]*stepQueueHeapItem),
// stopped is used for cancellation of "wake" goroutine
stopped: false,
}
err := q.setConcurrencyLocked(concurrency)
if err != nil {
panic(err)
} }
return q return q
} }
@@ -104,12 +65,25 @@ func newStepQueue(concurrency int) *stepQueue {
// //
// No WaitReady calls must be active at the time done is called // No WaitReady calls must be active at the time done is called
// The behavior of calling WaitReady after done was called is undefined // The behavior of calling WaitReady after done was called is undefined
func (q *stepQueue) Start() (done func()) { func (q *stepQueue) Start(concurrency int) (done func()) {
if concurrency < 1 {
panic("concurrency must be >= 1")
}
// l protects pending and queueItems
l := chainlock.New()
pendingCond := l.NewCond()
// priority queue
pending := &stepQueueHeap{}
// ident => queueItem
queueItems := make(map[interface{}]*stepQueueHeapItem)
// stopped is used for cancellation of "wake" goroutine
stopped := false
active := 0
go func() { // "stopper" goroutine go func() { // "stopper" goroutine
<-q.stop <-q.stop
defer q.l.Lock().Unlock() defer l.Lock().Unlock()
q.stopped = true stopped = true
q.pendingCond.Broadcast() pendingCond.Broadcast()
}() }()
go func() { // "reqs" goroutine go func() { // "reqs" goroutine
for { for {
@@ -123,52 +97,41 @@ func (q *stepQueue) Start() (done func()) {
} }
case req := <-q.reqs: case req := <-q.reqs:
func() { func() {
defer q.l.Lock().Unlock() defer l.Lock().Unlock()
if _, ok := q.queueItems[req.ident]; ok { if _, ok := queueItems[req.ident]; ok {
panic("WaitReady must not be called twice for the same ident") panic("WaitReady must not be called twice for the same ident")
} }
qitem := &stepQueueHeapItem{ qitem := &stepQueueHeapItem{
req: req, req: req,
} }
q.queueItems[req.ident] = qitem queueItems[req.ident] = qitem
heap.Push(q.pending, qitem) heap.Push(pending, qitem)
q.pendingCond.Broadcast() pendingCond.Broadcast()
}() }()
} }
} }
}() }()
go func() { // "wake" goroutine go func() { // "wake" goroutine
defer q.l.Lock().Unlock() defer l.Lock().Unlock()
for { for {
for !q.stopped && (q.active.Len() >= q.concurrency || q.pending.Len() == 0) { for !stopped && (active >= concurrency || pending.Len() == 0) {
q.pendingCond.Wait() pendingCond.Wait()
} }
if q.stopped { if stopped {
return return
} }
if q.pending.Len() <= 0 { if pending.Len() <= 0 {
return return
} }
active++
next := heap.Pop(pending).(*stepQueueHeapItem).req
delete(queueItems, next.ident)
// pop from tracked items next.wakeup <- func() {
next := heap.Pop(q.pending).(*stepQueueHeapItem) defer l.Lock().Unlock()
active--
next.req.cancelDueToConcurrencyDownsize = pendingCond.Broadcast()
heap.Push(q.active, next)
next.req.wakeup <- func() {
defer q.l.Lock().Unlock()
//
qitem := &stepQueueHeapItem{
req: req,
}
// delete(q.queueItems, next.req.ident) // def
q.pendingCond.Broadcast()
} }
} }
}() }()
@@ -198,24 +161,3 @@ func (q *stepQueue) WaitReady(ident interface{}, targetDate time.Time) StepCompl
} }
return q.sendAndWaitForWakeup(ident, targetDate) return q.sendAndWaitForWakeup(ident, targetDate)
} }
// caller must hold lock
func (q *stepQueue) setConcurrencyLocked(newConcurrency int) error {
if !(newConcurrency >= 1) {
return fmt.Errorf("concurrency must be >= 1 but requested %v", newConcurrency)
}
q.concurrency = newConcurrency
q.pendingCond.Broadcast() // wake up waiters who could make progress
for q.active.Len() > q.concurrency {
item := heap.Pop(q.active).(*stepQueueHeapItem)
item.req.cancelDueToConcurrencyDownsize()
heap.Push(q.pending, item)
}
return nil
}
func (q *stepQueue) SetConcurrency(new int) error {
defer q.l.Lock().Unlock()
return q.setConcurrencyLocked(new)
}
@@ -17,7 +17,7 @@ import (
// (relies on scheduler responsivity of < 500ms) // (relies on scheduler responsivity of < 500ms)
func TestPqNotconcurrent(t *testing.T) { func TestPqNotconcurrent(t *testing.T) {
var ctr uint32 var ctr uint32
q := newStepQueue(1) q := newStepQueue()
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(4) wg.Add(4)
go func() { go func() {
@@ -29,7 +29,7 @@ func TestPqNotconcurrent(t *testing.T) {
}() }()
// give goroutine "1" 500ms to enter queue, get the active slot and enter time.Sleep // give goroutine "1" 500ms to enter queue, get the active slot and enter time.Sleep
defer q.Start()() defer q.Start(1)()
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
// while "1" is still running, queue in "2", "3" and "4" // while "1" is still running, queue in "2", "3" and "4"
@@ -78,8 +78,7 @@ func (r record) String() string {
// times for each step are close together. // times for each step are close together.
func TestPqConcurrent(t *testing.T) { func TestPqConcurrent(t *testing.T) {
concurrency := 5 q := newStepQueue()
q := newStepQueue(concurrency)
var wg sync.WaitGroup var wg sync.WaitGroup
filesystems := 100 filesystems := 100
stepsPerFS := 20 stepsPerFS := 20
@@ -105,7 +104,8 @@ func TestPqConcurrent(t *testing.T) {
records <- recs records <- recs
}(fs) }(fs)
} }
defer q.Start()() concurrency := 5
defer q.Start(concurrency)()
wg.Wait() wg.Wait()
close(records) close(records)
t.Logf("loop done") t.Logf("loop done")
-1
View File
@@ -262,7 +262,6 @@ func (p *Planner) doPlanning(ctx context.Context) ([]*Filesystem, error) {
return nil, err return nil, err
} }
sfss := slfssres.GetFilesystems() sfss := slfssres.GetFilesystems()
// no progress here since we could run in a live-lock on connectivity issues
rlfssres, err := p.receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{}) rlfssres, err := p.receiver.ListFilesystems(ctx, &pdu.ListFilesystemReq{})
if err != nil { if err != nil {
+2 -2
View File
@@ -8,6 +8,6 @@ import (
"github.com/zrepl/zrepl/replication/driver" "github.com/zrepl/zrepl/replication/driver"
) )
func Do(ctx context.Context, initialConcurrency int, planner driver.Planner) (driver.ReportFunc, driver.WaitFunc, driver.SetConcurrencyFunc) { func Do(ctx context.Context, planner driver.Planner) (driver.ReportFunc, driver.WaitFunc) {
return driver.Do(ctx, initialConcurrency, planner) return driver.Do(ctx, planner)
} }
-1
View File
@@ -10,7 +10,6 @@ type Report struct {
WaitReconnectSince, WaitReconnectUntil time.Time WaitReconnectSince, WaitReconnectUntil time.Time
WaitReconnectError *TimedError WaitReconnectError *TimedError
Attempts []*AttemptReport Attempts []*AttemptReport
Concurrency int
} }
var _, _ = json.Marshal(&Report{}) var _, _ = json.Marshal(&Report{})
+1 -1
View File
@@ -49,7 +49,7 @@ type Wire interface {
// No data that could otherwise be Read is lost as a consequence of this call. // No data that could otherwise be Read is lost as a consequence of this call.
// The use case for this API is abortive connection shutdown. // The use case for this API is abortive connection shutdown.
// To provide any value over draining Wire using io.Read, an implementation // To provide any value over draining Wire using io.Read, an implementation
// will likely use out-of-bounds messaging mechanisms. // will likely use out-of-band messaging mechanisms.
// TODO WaitForPeerClose() (supported bool, err error) // TODO WaitForPeerClose() (supported bool, err error)
} }
+1 -1
View File
@@ -32,7 +32,7 @@ func TLSListenerFactoryFromConfig(c *config.Global, in *config.TLSServe) (transp
serverCert, err := tls.LoadX509KeyPair(in.Cert, in.Key) serverCert, err := tls.LoadX509KeyPair(in.Cert, in.Key)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "cannot parse cer/key pair") return nil, errors.Wrap(err, "cannot parse cert/key pair")
} }
clientCNs := make(map[string]struct{}, len(in.ClientCNs)) clientCNs := make(map[string]struct{}, len(in.ClientCNs))