Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 457cbd136b | |||
| 3ff1966cab | |||
| a3842155c5 | |||
| 02b3b4f80c | |||
| 0882290595 |
@@ -1,8 +1,9 @@
|
|||||||
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
[](https://github.com/zrepl/zrepl/blob/master/LICENSE)
|
||||||
[](https://golang.org/)
|
[](https://golang.org/)
|
||||||
[](https://zrepl.github.io)
|
[](https://zrepl.github.io)
|
||||||
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
[](https://www.patreon.com/zrepl)
|
||||||
[](https://liberapay.com/zrepl/donate)
|
[](https://liberapay.com/zrepl/donate)
|
||||||
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=R5QSXJVYHGX96)
|
||||||
[](https://twitter.com/intent/tweet?text=Wow:&url=https%3A%2F%2Fgithub.com%2Fzrepl%2Fzrepl)
|
[](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
@@ -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{}{},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+6
-30
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)" \
|
||||||
|
|||||||
@@ -411,6 +411,7 @@ func ListZFSHoldsAndBookmarks(ctx context.Context, fsfilter zfs.DatasetFilter) (
|
|||||||
for _, fs := range fss {
|
for _, fs := range fss {
|
||||||
err := listZFSHoldsAndBookmarksImplFS(ctx, out, fs)
|
err := listZFSHoldsAndBookmarksImplFS(ctx, out, fs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// FIXME if _, ok := err.(*zfs.DatasetDoesNotExist); ok { noop }
|
||||||
return nil, errors.Wrapf(err, "list holds and bookmarks on %q", fs.ToString())
|
return nil, errors.Wrapf(err, "list holds and bookmarks on %q", fs.ToString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -420,7 +421,8 @@ func ListZFSHoldsAndBookmarks(ctx context.Context, fsfilter zfs.DatasetFilter) (
|
|||||||
func listZFSHoldsAndBookmarksImplFS(ctx context.Context, out *ListHoldsAndBookmarksOutput, fs *zfs.DatasetPath) error {
|
func listZFSHoldsAndBookmarksImplFS(ctx context.Context, out *ListHoldsAndBookmarksOutput, fs *zfs.DatasetPath) error {
|
||||||
fsvs, err := zfs.ZFSListFilesystemVersions(fs, nil)
|
fsvs, err := zfs.ZFSListFilesystemVersions(fs, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "list filesystem versions of %q", fs)
|
// FIXME if _, ok := err.(*zfs.DatasetDoesNotExist); ok { noop }
|
||||||
|
return errors.Wrapf(err, "list filesystem versions of %q", fs.ToString())
|
||||||
}
|
}
|
||||||
for _, v := range fsvs {
|
for _, v := range fsvs {
|
||||||
switch v.Type {
|
switch v.Type {
|
||||||
@@ -429,7 +431,12 @@ func listZFSHoldsAndBookmarksImplFS(ctx context.Context, out *ListHoldsAndBookma
|
|||||||
case zfs.Snapshot:
|
case zfs.Snapshot:
|
||||||
holds, err := zfs.ZFSHolds(ctx, fs.ToString(), v.Name)
|
holds, err := zfs.ZFSHolds(ctx, fs.ToString(), v.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "get holds of %q", v.ToAbsPath(fs))
|
if _, ok := err.(*zfs.DatasetDoesNotExist); ok {
|
||||||
|
holds = []string{}
|
||||||
|
// fallthrough
|
||||||
|
} else {
|
||||||
|
return errors.Wrapf(err, "get holds of %q", v.ToAbsPath(fs))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for _, tag := range holds {
|
for _, tag := range holds {
|
||||||
listZFSHoldsAndBookmarksImplSnapshotTryParseHold(ctx, out, fs, v, tag)
|
listZFSHoldsAndBookmarksImplSnapshotTryParseHold(ctx, out, fs, v, tag)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package tests
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/zrepl/zrepl/platformtest"
|
"github.com/zrepl/zrepl/platformtest"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
)
|
)
|
||||||
@@ -13,35 +14,28 @@ func IdempotentHold(ctx *platformtest.Context) {
|
|||||||
DESTROYROOT
|
DESTROYROOT
|
||||||
CREATEROOT
|
CREATEROOT
|
||||||
+ "foo bar"
|
+ "foo bar"
|
||||||
+ "foo bar@1"
|
+ "foo bar@1 2"
|
||||||
`)
|
|
||||||
defer platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
|
||||||
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@1"
|
|
||||||
- "foo bar@1"
|
|
||||||
- "foo bar"
|
|
||||||
`)
|
`)
|
||||||
|
|
||||||
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||||
v1 := sendArgVersion(fs, "@1")
|
snap := sendArgVersion(fs, "@1 2")
|
||||||
|
|
||||||
tag := "zrepl_platformtest"
|
tag := "zrepl_platformtest"
|
||||||
err := zfs.ZFSHold(ctx, fs, v1, tag)
|
err := zfs.ZFSHold(ctx, fs, snap, tag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = zfs.ZFSHold(ctx, fs, v1, tag)
|
// existing holds
|
||||||
if err != nil {
|
holds, err := zfs.ZFSHolds(ctx, fs, "1 2")
|
||||||
panic(err)
|
require.NoError(ctx, err)
|
||||||
}
|
require.Equal(ctx, []string{tag}, holds)
|
||||||
|
|
||||||
vnonexistent := zfs.ZFSSendArgVersion{
|
|
||||||
RelName: "@nonexistent",
|
|
||||||
GUID: 0xbadf00d,
|
|
||||||
}
|
|
||||||
err = zfs.ZFSHold(ctx, fs, vnonexistent, tag)
|
|
||||||
if err == nil {
|
|
||||||
panic("still expecting error for nonexistent snapshot")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
holds, err = zfs.ZFSHolds(ctx, fs, "non existent")
|
||||||
|
ctx.Logf("holds=%v", holds)
|
||||||
|
ctx.Logf("errT=%T", err)
|
||||||
|
ctx.Logf("err=%s", err)
|
||||||
|
notExist, ok := err.(*zfs.DatasetDoesNotExist)
|
||||||
|
require.True(ctx, ok)
|
||||||
|
require.Equal(ctx, fs+"@non existent", notExist.Path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/platformtest"
|
||||||
|
"github.com/zrepl/zrepl/zfs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ListHolds(ctx *platformtest.Context) {
|
||||||
|
|
||||||
|
platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||||
|
DESTROYROOT
|
||||||
|
CREATEROOT
|
||||||
|
+ "foo bar"
|
||||||
|
+ "foo bar@1"
|
||||||
|
`)
|
||||||
|
defer platformtest.Run(ctx, platformtest.PanicErr, ctx.RootDataset, `
|
||||||
|
R zfs release zrepl_platformtest "${ROOTDS}/foo bar@1"
|
||||||
|
- "foo bar@1"
|
||||||
|
- "foo bar"
|
||||||
|
`)
|
||||||
|
|
||||||
|
fs := fmt.Sprintf("%s/foo bar", ctx.RootDataset)
|
||||||
|
v1 := sendArgVersion(fs, "@1")
|
||||||
|
|
||||||
|
tag := "zrepl_platformtest"
|
||||||
|
err := zfs.ZFSHold(ctx, fs, v1, tag)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = zfs.ZFSHold(ctx, fs, v1, tag)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vnonexistent := zfs.ZFSSendArgVersion{
|
||||||
|
RelName: "@nonexistent",
|
||||||
|
GUID: 0xbadf00d,
|
||||||
|
}
|
||||||
|
err = zfs.ZFSHold(ctx, fs, vnonexistent, tag)
|
||||||
|
if err == nil {
|
||||||
|
panic("still expecting error for nonexistent snapshot")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -31,4 +31,5 @@ var Cases = []Case{
|
|||||||
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden,
|
SendArgsValidationEncryptedSendOfUnencryptedDatasetForbidden,
|
||||||
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
SendArgsValidationResumeTokenEncryptionMismatchForbidden,
|
||||||
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
SendArgsValidationResumeTokenDifferentFilesystemForbidden,
|
||||||
|
ListHolds,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -77,9 +77,8 @@ func (r record) String() string {
|
|||||||
// Hence, perform some statistics on the wakeup times and assert that the mean wakeup
|
// Hence, perform some statistics on the wakeup times and assert that the mean wakeup
|
||||||
// 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")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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{})
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ success:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the snapshot does not exist, the returned error is of type *DatasetDoesNotExist
|
||||||
func ZFSHolds(ctx context.Context, fs, snap string) ([]string, error) {
|
func ZFSHolds(ctx context.Context, fs, snap string) ([]string, error) {
|
||||||
if err := validateZFSFilesystem(fs); err != nil {
|
if err := validateZFSFilesystem(fs); err != nil {
|
||||||
return nil, errors.Wrap(err, "`fs` is not a valid filesystem path")
|
return nil, errors.Wrap(err, "`fs` is not a valid filesystem path")
|
||||||
|
|||||||
Reference in New Issue
Block a user