Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5976264bce | |||
| f28676b8d7 | |||
| 3e6cae1c8f | |||
| a4a2cb2833 | |||
| 97a14dba90 | |||
| 40be626b3a | |||
| 68b895d0bc |
@@ -358,7 +358,7 @@ jobs:
|
|||||||
subcommand: docdep
|
subcommand: docdep
|
||||||
- run:
|
- run:
|
||||||
command: |
|
command: |
|
||||||
git config --global user.email "zreplbot@cschwarz.com"
|
git config --global user.email "me@cschwarz.com"
|
||||||
git config --global user.name "zrepl-github-io-ci"
|
git config --global user.name "zrepl-github-io-ci"
|
||||||
|
|
||||||
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
# https://circleci.com/docs/2.0/add-ssh-key/#adding-multiple-keys-with-blank-hostnames
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resetCmdArgs struct {
|
||||||
|
verbose bool
|
||||||
|
interval time.Duration
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
var ResetCmd = &cli.Subcommand{
|
||||||
|
Use: "reset [-t TOKEN | JOB INVOCATION [replication|snapshotting|prune_sender|prune_receiver]]",
|
||||||
|
Short: "",
|
||||||
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
return runResetCmd(subcommand.Config(), args)
|
||||||
|
},
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVarP(&resetCmdArgs.verbose, "verbose", "v", false, "verbose output")
|
||||||
|
f.DurationVarP(&resetCmdArgs.interval, "poll-interval", "i", 100*time.Millisecond, "poll interval")
|
||||||
|
f.StringVarP(&resetCmdArgs.token, "token", "t", "", "token produced by 'signal' subcommand")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runResetCmd(config *config.Config, args []string) error {
|
||||||
|
|
||||||
|
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var req daemon.ControlJobEndpointResetActiveRequest
|
||||||
|
if resetCmdArgs.token != "" {
|
||||||
|
var token TriggerToken
|
||||||
|
err := token.Decode(resetCmdArgs.token)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot decode token")
|
||||||
|
}
|
||||||
|
req = token.ToReset()
|
||||||
|
} else {
|
||||||
|
jobName := args[0]
|
||||||
|
|
||||||
|
invocationId, err := strconv.ParseUint(args[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parse invocation id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// what := args[2]
|
||||||
|
|
||||||
|
// updated by subsequent requests
|
||||||
|
req = daemon.ControlJobEndpointResetActiveRequest{
|
||||||
|
Job: jobName,
|
||||||
|
ActiveSideResetRequest: job.ActiveSideResetRequest{
|
||||||
|
InvocationId: invocationId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var res job.ActiveSideResetResponse
|
||||||
|
if resetCmdArgs.verbose {
|
||||||
|
pretty.Println("making request", req)
|
||||||
|
}
|
||||||
|
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointResetActive,
|
||||||
|
req,
|
||||||
|
&res,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetCmdArgs.verbose {
|
||||||
|
pretty.Println("got response", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/cli"
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
|
||||||
)
|
|
||||||
|
|
||||||
var SignalCmd = &cli.Subcommand{
|
|
||||||
Use: "signal [wakeup|reset] JOB",
|
|
||||||
Short: "wake up a job from wait state or abort its current invocation",
|
|
||||||
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
|
||||||
return runSignalCmd(subcommand.Config(), args)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
func runSignalCmd(config *config.Config, args []string) error {
|
|
||||||
if len(args) != 2 {
|
|
||||||
return errors.Errorf("Expected 2 arguments: [wakeup|reset] JOB")
|
|
||||||
}
|
|
||||||
|
|
||||||
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointSignal,
|
|
||||||
struct {
|
|
||||||
Name string
|
|
||||||
Op string
|
|
||||||
}{
|
|
||||||
Name: args[1],
|
|
||||||
Op: args[0],
|
|
||||||
},
|
|
||||||
struct{}{},
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon"
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
@@ -42,21 +43,27 @@ func (c *Client) StatusRaw() ([]byte, error) {
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) signal(job, sig string) error {
|
func (c *Client) signal(jobName, sig string) error {
|
||||||
return jsonRequestResponse(c.h, daemon.ControlJobEndpointSignal,
|
return jsonRequestResponse(c.h, daemon.ControlJobEndpointTriggerActive,
|
||||||
struct {
|
struct {
|
||||||
Name string
|
Job string
|
||||||
Op string
|
job.ActiveSideTriggerRequest
|
||||||
}{
|
}{
|
||||||
Name: job,
|
Job: jobName,
|
||||||
Op: sig,
|
ActiveSideTriggerRequest: job.ActiveSideTriggerRequest{
|
||||||
|
What: sig,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
struct{}{},
|
struct{}{},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SignalWakeup(job string) error {
|
func (c *Client) SignalReplication(job string) error {
|
||||||
return c.signal(job, "wakeup")
|
return c.signal(job, "replication")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) SignalSnapshot(job string) error {
|
||||||
|
return c.signal(job, "snapshot")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SignalReset(job string) error {
|
func (c *Client) SignalReset(job string) error {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import (
|
|||||||
type Client interface {
|
type Client interface {
|
||||||
Status() (daemon.Status, error)
|
Status() (daemon.Status, error)
|
||||||
StatusRaw() ([]byte, error)
|
StatusRaw() ([]byte, error)
|
||||||
SignalWakeup(job string) error
|
SignalReplication(job string) error
|
||||||
|
SignalSnapshot(job string) error
|
||||||
SignalReset(job string) error
|
SignalReset(job string) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,16 +71,12 @@ func runStatusV2Command(ctx context.Context, config *config.Config, args []strin
|
|||||||
|
|
||||||
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
mode := statusv2Flags.Mode.Value().(statusv2Mode)
|
||||||
|
|
||||||
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump && mode != StatusV2ModeRaw {
|
if !isatty.IsTerminal(os.Stdout.Fd()) && mode != StatusV2ModeDump {
|
||||||
dumpmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
usemode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeDump)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
rawmode, err := statusv2Flags.Mode.InputForChoice(StatusV2ModeRaw)
|
return errors.Errorf("error: stdout is not a tty, please use --mode %s", usemode)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return errors.Errorf("error: stdout is not a tty, please use --mode %s or --mode %s", dumpmode, rawmode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch mode {
|
switch mode {
|
||||||
|
|||||||
@@ -281,8 +281,8 @@ func interactive(c Client, flag statusFlags) error {
|
|||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
signals := []string{"wakeup", "reset"}
|
signals := []string{"replication", "snapshot", "reset"}
|
||||||
clientFuncs := []func(job string) error{c.SignalWakeup, c.SignalReset}
|
clientFuncs := []func(job string) error{c.SignalReplication, c.SignalSnapshot, c.SignalReset}
|
||||||
sigMod := tview.NewModal()
|
sigMod := tview.NewModal()
|
||||||
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
sigMod.SetBackgroundColor(tcell.ColorDefault)
|
||||||
sigMod.SetBorder(true)
|
sigMod.SetBorder(true)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type bytesProgressHistory struct {
|
|||||||
last *byteProgressMeasurement // pointer as poor man's optional
|
last *byteProgressMeasurement // pointer as poor man's optional
|
||||||
changeCount int
|
changeCount int
|
||||||
lastChange time.Time
|
lastChange time.Time
|
||||||
|
bpsAvg float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64, changeCount int) {
|
||||||
@@ -37,8 +38,11 @@ func (p *bytesProgressHistory) Update(currentVal int64) (bytesPerSecondAvg int64
|
|||||||
deltaT := time.Since(p.last.time)
|
deltaT := time.Since(p.last.time)
|
||||||
rate := float64(deltaV) / deltaT.Seconds()
|
rate := float64(deltaV) / deltaT.Seconds()
|
||||||
|
|
||||||
|
factor := 0.3
|
||||||
|
p.bpsAvg = (1-factor)*p.bpsAvg + factor*rate
|
||||||
|
|
||||||
p.last.time = time.Now()
|
p.last.time = time.Now()
|
||||||
p.last.val = currentVal
|
p.last.val = currentVal
|
||||||
|
|
||||||
return int64(rate), p.changeCount
|
return int64(p.bpsAvg), p.changeCount
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -356,17 +356,9 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
|
|||||||
// Progress: [---------------]
|
// Progress: [---------------]
|
||||||
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
expected, replicated, containsInvalidSizeEstimates := latest.BytesSum()
|
||||||
rate, changeCount := history.Update(replicated)
|
rate, changeCount := history.Update(replicated)
|
||||||
eta := time.Duration(0)
|
|
||||||
if rate > 0 {
|
|
||||||
eta = time.Duration((expected-replicated)/rate) * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Write("Progress: ")
|
t.Write("Progress: ")
|
||||||
t.DrawBar(50, replicated, expected, changeCount)
|
t.DrawBar(50, replicated, expected, changeCount)
|
||||||
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
t.Write(fmt.Sprintf(" %s / %s @ %s/s", ByteCountBinary(replicated), ByteCountBinary(expected), ByteCountBinary(rate)))
|
||||||
if eta != 0 {
|
|
||||||
t.Write(fmt.Sprintf(" (%s remaining)", humanizeDuration(eta)))
|
|
||||||
}
|
|
||||||
t.Newline()
|
t.Newline()
|
||||||
if containsInvalidSizeEstimates {
|
if containsInvalidSizeEstimates {
|
||||||
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
t.Write("NOTE: not all steps could be size-estimated, total estimate is likely imprecise!")
|
||||||
@@ -391,30 +383,6 @@ func renderReplicationReport(t *stringbuilder.B, rep *report.Report, history *by
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func humanizeDuration(duration time.Duration) string {
|
|
||||||
days := int64(duration.Hours() / 24)
|
|
||||||
hours := int64(math.Mod(duration.Hours(), 24))
|
|
||||||
minutes := int64(math.Mod(duration.Minutes(), 60))
|
|
||||||
seconds := int64(math.Mod(duration.Seconds(), 60))
|
|
||||||
|
|
||||||
var parts []string
|
|
||||||
|
|
||||||
force := false
|
|
||||||
chunks := []int64{days, hours, minutes, seconds}
|
|
||||||
for i, chunk := range chunks {
|
|
||||||
if force || chunk > 0 {
|
|
||||||
padding := 0
|
|
||||||
if force {
|
|
||||||
padding = 2
|
|
||||||
}
|
|
||||||
parts = append(parts, fmt.Sprintf("%*d%c", padding, chunk, "dhms"[i]))
|
|
||||||
force = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.Join(parts, " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
func renderPrunerReport(t *stringbuilder.B, r *pruner.Report, fsfilter FilterFunc) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
t.Printf("...\n")
|
t.Printf("...\n")
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TriggerCmd = &cli.Subcommand{
|
||||||
|
Use: "trigger JOB [replication|snapshot]",
|
||||||
|
Short: "",
|
||||||
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
return runTriggerCmd(subcommand.Config(), args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
type TriggerToken struct {
|
||||||
|
// TODO version, daemon invocation id, etc.
|
||||||
|
Job string
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TriggerToken) Encode() string {
|
||||||
|
j, err := json.Marshal(t)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return string(j)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TriggerToken) Decode(s string) error {
|
||||||
|
return json.Unmarshal([]byte(s), t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TriggerToken) ToReset() daemon.ControlJobEndpointResetActiveRequest {
|
||||||
|
return daemon.ControlJobEndpointResetActiveRequest{
|
||||||
|
Job: t.Job,
|
||||||
|
ActiveSideResetRequest: job.ActiveSideResetRequest{
|
||||||
|
InvocationId: t.InvocationId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TriggerToken) ToWait() daemon.ControlJobEndpointWaitActiveRequest {
|
||||||
|
return daemon.ControlJobEndpointWaitActiveRequest{
|
||||||
|
Job: t.Job,
|
||||||
|
ActiveSidePollRequest: job.ActiveSidePollRequest{
|
||||||
|
InvocationId: t.InvocationId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTriggerCmd(config *config.Config, args []string) error {
|
||||||
|
if len(args) != 2 {
|
||||||
|
return errors.Errorf("Expected 2 arguments: [replication|reset|snapshot] JOB")
|
||||||
|
}
|
||||||
|
|
||||||
|
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
jobName := args[0]
|
||||||
|
what := args[1]
|
||||||
|
|
||||||
|
var res job.ActiveSideSignalResponse
|
||||||
|
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointTriggerActive,
|
||||||
|
struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSideTriggerRequest
|
||||||
|
}{
|
||||||
|
Job: jobName,
|
||||||
|
ActiveSideTriggerRequest: job.ActiveSideTriggerRequest{
|
||||||
|
What: what,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&res,
|
||||||
|
)
|
||||||
|
|
||||||
|
token := TriggerToken{
|
||||||
|
Job: jobName,
|
||||||
|
InvocationId: res.InvocationId,
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(token.Encode())
|
||||||
|
return err
|
||||||
|
}
|
||||||
+110
@@ -0,0 +1,110 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kr/pretty"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/zrepl/zrepl/cli"
|
||||||
|
"github.com/zrepl/zrepl/config"
|
||||||
|
"github.com/zrepl/zrepl/daemon"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
|
)
|
||||||
|
|
||||||
|
var waitCmdArgs struct {
|
||||||
|
verbose bool
|
||||||
|
interval time.Duration
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
var WaitCmd = &cli.Subcommand{
|
||||||
|
Use: "wait [-t TOKEN | JOB INVOCATION [replication|snapshotting|prune_sender|prune_receiver]]",
|
||||||
|
Short: "",
|
||||||
|
Run: func(ctx context.Context, subcommand *cli.Subcommand, args []string) error {
|
||||||
|
return runWaitCmd(subcommand.Config(), args)
|
||||||
|
},
|
||||||
|
SetupFlags: func(f *pflag.FlagSet) {
|
||||||
|
f.BoolVarP(&waitCmdArgs.verbose, "verbose", "v", false, "verbose output")
|
||||||
|
f.DurationVarP(&waitCmdArgs.interval, "poll-interval", "i", 100*time.Millisecond, "poll interval")
|
||||||
|
f.StringVarP(&waitCmdArgs.token, "token", "t", "", "token produced by 'signal' subcommand")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func runWaitCmd(config *config.Config, args []string) error {
|
||||||
|
|
||||||
|
httpc, err := controlHttpClient(config.Global.Control.SockPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var req daemon.ControlJobEndpointWaitActiveRequest
|
||||||
|
if waitCmdArgs.token != "" {
|
||||||
|
var token TriggerToken
|
||||||
|
err := token.Decode(resetCmdArgs.token)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "cannot decode token")
|
||||||
|
}
|
||||||
|
req = token.ToWait()
|
||||||
|
} else {
|
||||||
|
|
||||||
|
jobName := args[0]
|
||||||
|
|
||||||
|
invocationId, err := strconv.ParseUint(args[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "parse invocation id")
|
||||||
|
}
|
||||||
|
|
||||||
|
// updated by subsequent requests
|
||||||
|
req = daemon.ControlJobEndpointWaitActiveRequest{
|
||||||
|
Job: jobName,
|
||||||
|
ActiveSidePollRequest: job.ActiveSidePollRequest{
|
||||||
|
InvocationId: invocationId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
doneErr := fmt.Errorf("done")
|
||||||
|
|
||||||
|
pollOnce := func() error {
|
||||||
|
var res job.ActiveSidePollResponse
|
||||||
|
if waitCmdArgs.verbose {
|
||||||
|
pretty.Println("making poll request", req)
|
||||||
|
}
|
||||||
|
err = jsonRequestResponse(httpc, daemon.ControlJobEndpointPollActive,
|
||||||
|
req,
|
||||||
|
&res,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if waitCmdArgs.verbose {
|
||||||
|
pretty.Println("got poll response", res)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Done {
|
||||||
|
return doneErr
|
||||||
|
}
|
||||||
|
|
||||||
|
req.InvocationId = res.InvocationId
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.NewTicker(waitCmdArgs.interval)
|
||||||
|
for range t.C {
|
||||||
|
err := pollOnce()
|
||||||
|
if err == doneErr {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
+4
-12
@@ -13,7 +13,6 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/zrepl/yaml-config"
|
"github.com/zrepl/yaml-config"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/datasizeunit"
|
|
||||||
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
zfsprop "github.com/zrepl/zrepl/zfs/property"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -88,8 +87,6 @@ type SendOptions struct {
|
|||||||
Compressed bool `yaml:"compressed,optional,default=false"`
|
Compressed bool `yaml:"compressed,optional,default=false"`
|
||||||
EmbeddedData bool `yaml:"embbeded_data,optional,default=false"`
|
EmbeddedData bool `yaml:"embbeded_data,optional,default=false"`
|
||||||
Saved bool `yaml:"saved,optional,default=false"`
|
Saved bool `yaml:"saved,optional,default=false"`
|
||||||
|
|
||||||
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type RecvOptions struct {
|
type RecvOptions struct {
|
||||||
@@ -99,15 +96,6 @@ type RecvOptions struct {
|
|||||||
// Reencrypt bool `yaml:"reencrypt"`
|
// Reencrypt bool `yaml:"reencrypt"`
|
||||||
|
|
||||||
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
Properties *PropertyRecvOptions `yaml:"properties,fromdefaults"`
|
||||||
|
|
||||||
BandwidthLimit *BandwidthLimit `yaml:"bandwidth_limit,optional,fromdefaults"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ yaml.Unmarshaler = &datasizeunit.Bits{}
|
|
||||||
|
|
||||||
type BandwidthLimit struct {
|
|
||||||
Max datasizeunit.Bits `yaml:"max,default=-1 B"`
|
|
||||||
BucketCapacity datasizeunit.Bits `yaml:"bucket_capacity,default=128 KiB"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Replication struct {
|
type Replication struct {
|
||||||
@@ -125,6 +113,10 @@ type ReplicationOptionsConcurrency struct {
|
|||||||
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
SizeEstimates int `yaml:"size_estimates,optional,default=4"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *RecvOptions) SetDefault() {
|
||||||
|
*l = RecvOptions{Properties: &PropertyRecvOptions{}}
|
||||||
|
}
|
||||||
|
|
||||||
type PropertyRecvOptions struct {
|
type PropertyRecvOptions struct {
|
||||||
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
Inherit []zfsprop.Property `yaml:"inherit,optional"`
|
||||||
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
Override map[zfsprop.Property]string `yaml:"override,optional"`
|
||||||
|
|||||||
@@ -70,9 +70,9 @@ func TestPrometheusMonitoring(t *testing.T) {
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9811'
|
listen: ':9091'
|
||||||
`)
|
`)
|
||||||
assert.Equal(t, ":9811", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
assert.Equal(t, ":9091", conf.Global.Monitoring[0].Ret.(*PrometheusMonitoring).Listen)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
func TestSyslogLoggingOutletFacility(t *testing.T) {
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
|
|
||||||
jobs:
|
|
||||||
- type: sink
|
|
||||||
name: "limited_sink"
|
|
||||||
root_fs: "fs0"
|
|
||||||
recv:
|
|
||||||
bandwidth_limit:
|
|
||||||
max: 12345 B
|
|
||||||
serve:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
|
|
||||||
- type: push
|
|
||||||
name: "limited_push"
|
|
||||||
connect:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
client_identity: local_backup
|
|
||||||
filesystems: {
|
|
||||||
"root<": true,
|
|
||||||
}
|
|
||||||
send:
|
|
||||||
bandwidth_limit:
|
|
||||||
max: 54321 B
|
|
||||||
bucket_capacity: 1024 B
|
|
||||||
snapshotting:
|
|
||||||
type: manual
|
|
||||||
pruning:
|
|
||||||
keep_sender:
|
|
||||||
- type: last_n
|
|
||||||
count: 1
|
|
||||||
keep_receiver:
|
|
||||||
- type: last_n
|
|
||||||
count: 1
|
|
||||||
|
|
||||||
- type: sink
|
|
||||||
name: "nolimit_sink"
|
|
||||||
root_fs: "fs1"
|
|
||||||
serve:
|
|
||||||
type: local
|
|
||||||
listener_name: localsink
|
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
# quick start section which inlines this example.
|
# quick start section which inlines this example.
|
||||||
#
|
#
|
||||||
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
# CUSTOMIZATIONS YOU WILL LIKELY WANT TO APPLY:
|
||||||
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_drive`
|
# - adjust the name of the production pool `system` in the `filesystems` filter of jobs `snapjob` and `push_to_derive`
|
||||||
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
# - adjust the name of the backup pool `backuppool` in the `backuppool_sink` job
|
||||||
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
# - adjust the occurences of `myhostname` to the name of the system you are backing up (cannot be easily changed once you start replicating)
|
||||||
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
# - make sure the `zrepl_` prefix is not being used by any other zfs tools you might have installed (it likely isn't)
|
||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
|
|
||||||
# This job pushes to the local sink defined in job `backuppool_sink`.
|
# This job pushes to the local sink defined in job `backuppool_sink`.
|
||||||
# We trigger replication manually from the command line / udev rules using
|
# We trigger replication manually from the command line / udev rules using
|
||||||
# `zrepl signal wakeup push_to_drive`
|
# `zrepl signal replication push_to_drive`
|
||||||
- type: push
|
- type: push
|
||||||
name: push_to_drive
|
name: push_to_drive
|
||||||
connect:
|
connect:
|
||||||
|
|||||||
+108
-14
@@ -76,9 +76,26 @@ const (
|
|||||||
ControlJobEndpointPProf string = "/debug/pprof"
|
ControlJobEndpointPProf string = "/debug/pprof"
|
||||||
ControlJobEndpointVersion string = "/version"
|
ControlJobEndpointVersion string = "/version"
|
||||||
ControlJobEndpointStatus string = "/status"
|
ControlJobEndpointStatus string = "/status"
|
||||||
ControlJobEndpointSignal string = "/signal"
|
ControlJobEndpointTriggerActive string = "/signal/active"
|
||||||
|
ControlJobEndpointPollActive string = "/poll/active"
|
||||||
|
ControlJobEndpointResetActive string = "/reset/active"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ControlJobEndpointTriggerActiveRequest struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSideTriggerRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlJobEndpointResetActiveRequest struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSideResetRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
type ControlJobEndpointWaitActiveRequest struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSidePollRequest
|
||||||
|
}
|
||||||
|
|
||||||
func (j *controlJob) Run(ctx context.Context) {
|
func (j *controlJob) Run(ctx context.Context) {
|
||||||
|
|
||||||
log := job.GetLogger(ctx)
|
log := job.GetLogger(ctx)
|
||||||
@@ -130,29 +147,106 @@ func (j *controlJob) Run(ctx context.Context) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}})
|
}})
|
||||||
|
|
||||||
mux.Handle(ControlJobEndpointSignal,
|
mux.Handle(ControlJobEndpointPollActive, requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (v interface{}, err error) {
|
||||||
requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (interface{}, error) {
|
var req ControlJobEndpointWaitActiveRequest
|
||||||
|
if decoder(&req) != nil {
|
||||||
|
return nil, errors.Errorf("decode failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
j.jobs.m.RLock()
|
||||||
|
|
||||||
|
jo, ok := j.jobs.jobs[req.Job]
|
||||||
|
if !ok {
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return struct{}{}, fmt.Errorf("unknown job name %q", req.Job)
|
||||||
|
}
|
||||||
|
|
||||||
|
ajo, ok := jo.(*job.ActiveSide)
|
||||||
|
if !ok {
|
||||||
|
v, err = struct{}{}, fmt.Errorf("job %q is not an active side (it's a %T)", jo.Name(), jo)
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ajo.Poll(req.ActiveSidePollRequest)
|
||||||
|
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
}}})
|
||||||
|
|
||||||
|
mux.Handle(ControlJobEndpointTriggerActive,
|
||||||
|
requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (v interface{}, err error) {
|
||||||
type reqT struct {
|
type reqT struct {
|
||||||
Name string
|
Job string
|
||||||
Op string
|
job.ActiveSideTriggerRequest
|
||||||
}
|
}
|
||||||
var req reqT
|
var req reqT
|
||||||
if decoder(&req) != nil {
|
if decoder(&req) != nil {
|
||||||
return nil, errors.Errorf("decode failed")
|
return nil, errors.Errorf("decode failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
// FIXME dedup the following code with ControlJobEndpointPollActive
|
||||||
switch req.Op {
|
|
||||||
case "wakeup":
|
j.jobs.m.RLock()
|
||||||
err = j.jobs.wakeup(req.Name)
|
|
||||||
case "reset":
|
jo, ok := j.jobs.jobs[req.Job]
|
||||||
err = j.jobs.reset(req.Name)
|
if !ok {
|
||||||
default:
|
j.jobs.m.RUnlock()
|
||||||
err = fmt.Errorf("operation %q is invalid", req.Op)
|
return struct{}{}, fmt.Errorf("unknown job name %q", req.Job)
|
||||||
}
|
}
|
||||||
|
|
||||||
return struct{}{}, err
|
ajo, ok := jo.(*job.ActiveSide)
|
||||||
|
if !ok {
|
||||||
|
v, err = struct{}{}, fmt.Errorf("job %q is not an active side (it's a %T)", jo.Name(), jo)
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ajo.Trigger(req.ActiveSideTriggerRequest)
|
||||||
|
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
|
||||||
}}})
|
}}})
|
||||||
|
|
||||||
|
mux.Handle(ControlJobEndpointResetActive,
|
||||||
|
requestLogger{log: log, handler: jsonRequestResponder{log, func(decoder jsonDecoder) (v interface{}, err error) {
|
||||||
|
type reqT struct {
|
||||||
|
Job string
|
||||||
|
job.ActiveSideResetRequest
|
||||||
|
}
|
||||||
|
var req reqT
|
||||||
|
if decoder(&req) != nil {
|
||||||
|
return nil, errors.Errorf("decode failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME dedup the following code with ControlJobEndpointPollActive
|
||||||
|
|
||||||
|
j.jobs.m.RLock()
|
||||||
|
|
||||||
|
jo, ok := j.jobs.jobs[req.Job]
|
||||||
|
if !ok {
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return struct{}{}, fmt.Errorf("unknown job name %q", req.Job)
|
||||||
|
}
|
||||||
|
|
||||||
|
ajo, ok := jo.(*job.ActiveSide)
|
||||||
|
if !ok {
|
||||||
|
v, err = struct{}{}, fmt.Errorf("job %q is not an active side (it's a %T)", jo.Name(), jo)
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ajo.Reset(req.ActiveSideResetRequest)
|
||||||
|
|
||||||
|
j.jobs.m.RUnlock()
|
||||||
|
|
||||||
|
return res, err
|
||||||
|
|
||||||
|
}}})
|
||||||
|
|
||||||
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
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job"
|
"github.com/zrepl/zrepl/daemon/job"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/version"
|
"github.com/zrepl/zrepl/version"
|
||||||
@@ -132,15 +130,11 @@ type jobs struct {
|
|||||||
|
|
||||||
// m protects all fields below it
|
// m protects all fields below it
|
||||||
m sync.RWMutex
|
m sync.RWMutex
|
||||||
wakeups map[string]wakeup.Func // by Job.Name
|
|
||||||
resets map[string]reset.Func // by Job.Name
|
|
||||||
jobs map[string]job.Job
|
jobs map[string]job.Job
|
||||||
}
|
}
|
||||||
|
|
||||||
func newJobs() *jobs {
|
func newJobs() *jobs {
|
||||||
return &jobs{
|
return &jobs{
|
||||||
wakeups: make(map[string]wakeup.Func),
|
|
||||||
resets: make(map[string]reset.Func),
|
|
||||||
jobs: make(map[string]job.Job),
|
jobs: make(map[string]job.Job),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,28 +184,6 @@ func (s *jobs) status() map[string]*job.Status {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *jobs) wakeup(job string) error {
|
|
||||||
s.m.RLock()
|
|
||||||
defer s.m.RUnlock()
|
|
||||||
|
|
||||||
wu, ok := s.wakeups[job]
|
|
||||||
if !ok {
|
|
||||||
return errors.Errorf("Job %s does not exist", job)
|
|
||||||
}
|
|
||||||
return wu()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *jobs) reset(job string) error {
|
|
||||||
s.m.RLock()
|
|
||||||
defer s.m.RUnlock()
|
|
||||||
|
|
||||||
wu, ok := s.resets[job]
|
|
||||||
if !ok {
|
|
||||||
return errors.Errorf("Job %s does not exist", job)
|
|
||||||
}
|
|
||||||
return wu()
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
jobNamePrometheus = "_prometheus"
|
jobNamePrometheus = "_prometheus"
|
||||||
jobNameControl = "_control"
|
jobNameControl = "_control"
|
||||||
@@ -242,10 +214,6 @@ func (s *jobs) start(ctx context.Context, j job.Job, internal bool) {
|
|||||||
|
|
||||||
s.jobs[jobName] = j
|
s.jobs[jobName] = j
|
||||||
ctx = zfscmd.WithJobID(ctx, j.Name())
|
ctx = zfscmd.WithJobID(ctx, j.Name())
|
||||||
ctx, wakeup := wakeup.Context(ctx)
|
|
||||||
ctx, resetFunc := reset.Context(ctx)
|
|
||||||
s.wakeups[jobName] = wakeup
|
|
||||||
s.resets[jobName] = resetFunc
|
|
||||||
|
|
||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
+199
-43
@@ -8,14 +8,11 @@ import (
|
|||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/common/log"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/job/reset"
|
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -45,6 +42,10 @@ type ActiveSide struct {
|
|||||||
|
|
||||||
tasksMtx sync.Mutex
|
tasksMtx sync.Mutex
|
||||||
tasks activeSideTasks
|
tasks activeSideTasks
|
||||||
|
nextInvocationId uint64
|
||||||
|
activeInvocationId uint64 // 0 <=> inactive
|
||||||
|
trigger chan struct{}
|
||||||
|
reset chan uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:generate enumer -type=ActiveSideState
|
//go:generate enumer -type=ActiveSideState
|
||||||
@@ -57,18 +58,17 @@ const (
|
|||||||
ActiveSideDone // also errors
|
ActiveSideDone // also errors
|
||||||
)
|
)
|
||||||
|
|
||||||
type activeSideTasks struct {
|
type activeSideReplicationAndTriggerRemotePruneSequence struct {
|
||||||
state ActiveSideState
|
state ActiveSideState
|
||||||
|
|
||||||
// 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
|
||||||
|
replicationDone *report.Report
|
||||||
|
|
||||||
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
// valid for state ActiveSidePruneSender, ActiveSidePruneReceiver, ActiveSideDone
|
||||||
prunerSender, prunerReceiver *pruner.Pruner
|
pruneRemote *pruner.Pruner
|
||||||
|
pruneRemoteCancel context.CancelFunc
|
||||||
// valid for state ActiveSidePruneReceiver, ActiveSideDone
|
|
||||||
prunerSenderCancel, prunerReceiverCancel context.CancelFunc
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
func (a *ActiveSide) updateTasks(u func(*activeSideTasks)) activeSideTasks {
|
||||||
@@ -89,7 +89,7 @@ type activeMode interface {
|
|||||||
SenderReceiver() (logic.Sender, logic.Receiver)
|
SenderReceiver() (logic.Sender, logic.Receiver)
|
||||||
Type() Type
|
Type() Type
|
||||||
PlannerPolicy() logic.PlannerPolicy
|
PlannerPolicy() logic.PlannerPolicy
|
||||||
RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{})
|
RunPeriodic(ctx context.Context, wakePeriodic <-chan struct{}, replicationCommon chan<- struct{})
|
||||||
SnapperReport() *snapper.Report
|
SnapperReport() *snapper.Report
|
||||||
ResetConnectBackoff()
|
ResetConnectBackoff()
|
||||||
}
|
}
|
||||||
@@ -131,8 +131,8 @@ func (m *modePush) Type() Type { return TypePush }
|
|||||||
|
|
||||||
func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
func (m *modePush) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||||
|
|
||||||
func (m *modePush) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
func (m *modePush) RunPeriodic(ctx context.Context, wakePeriodic <-chan struct{}, replicationCommon chan<- struct{}) {
|
||||||
m.snapper.Run(ctx, wakeUpCommon)
|
m.snapper.Run(ctx, replicationCommon)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *modePush) SnapperReport() *snapper.Report {
|
func (m *modePush) SnapperReport() *snapper.Report {
|
||||||
@@ -214,10 +214,10 @@ func (*modePull) Type() Type { return TypePull }
|
|||||||
|
|
||||||
func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
func (m *modePull) PlannerPolicy() logic.PlannerPolicy { return *m.plannerPolicy }
|
||||||
|
|
||||||
func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
func (m *modePull) RunPeriodic(ctx context.Context, wakePeriodic <-chan struct{}, replicationCommon chan<- struct{}) {
|
||||||
if m.interval.Manual {
|
if m.interval.Manual {
|
||||||
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
GetLogger(ctx).Info("manual pull configured, periodic pull disabled")
|
||||||
// "waiting for wakeups" is printed in common ActiveSide.do
|
// "waiting for wakeup replications" is printed in common ActiveSide.do
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
t := time.NewTicker(m.interval.Interval)
|
t := time.NewTicker(m.interval.Interval)
|
||||||
@@ -226,12 +226,12 @@ func (m *modePull) RunPeriodic(ctx context.Context, wakeUpCommon chan<- struct{}
|
|||||||
select {
|
select {
|
||||||
case <-t.C:
|
case <-t.C:
|
||||||
select {
|
select {
|
||||||
case wakeUpCommon <- struct{}{}:
|
case replicationCommon <- struct{}{}:
|
||||||
default:
|
default:
|
||||||
GetLogger(ctx).
|
GetLogger(ctx).
|
||||||
WithField("pull_interval", m.interval).
|
WithField("pull_interval", m.interval).
|
||||||
Warn("pull job took longer than pull interval")
|
Warn("pull job took longer than pull interval")
|
||||||
wakeUpCommon <- struct{}{} // block anyways, to queue up the wakeup
|
replicationCommon <- struct{}{} // block anyways, to queue up the wakeup replication
|
||||||
}
|
}
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
@@ -425,50 +425,205 @@ func (j *ActiveSide) Run(ctx context.Context) {
|
|||||||
|
|
||||||
defer log.Info("job exiting")
|
defer log.Info("job exiting")
|
||||||
|
|
||||||
|
type Activity interface {
|
||||||
|
Trigger() (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var periodicActivity Activity
|
||||||
|
var replicationActivity Activity
|
||||||
|
|
||||||
periodicDone := make(chan struct{})
|
periodicDone := make(chan struct{})
|
||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
|
periodicCtx, endTask := trace.WithTask(ctx, "periodic")
|
||||||
defer endTask()
|
defer endTask()
|
||||||
go j.mode.RunPeriodic(periodicCtx, periodicDone)
|
|
||||||
|
|
||||||
invocationCount := 0
|
wakePeriodic := make(chan struct{})
|
||||||
outer:
|
go j.mode.RunPeriodic(periodicCtx, wakePeriodic, periodicDone)
|
||||||
for {
|
|
||||||
log.Info("wait for wakeups")
|
j.trigger = make(chan struct{})
|
||||||
select {
|
j.reset = make(chan uint64)
|
||||||
case <-ctx.Done():
|
j.nextInvocationId = 1
|
||||||
log.WithError(ctx.Err()).Info("context")
|
|
||||||
break outer
|
type WaitTriggerResult interface {
|
||||||
|
|
||||||
case <-wakeup.Wait(ctx):
|
|
||||||
j.mode.ResetConnectBackoff()
|
|
||||||
case <-periodicDone:
|
|
||||||
}
|
}
|
||||||
invocationCount++
|
var t interface{
|
||||||
invocationCtx, endSpan := trace.WithSpan(ctx, fmt.Sprintf("invocation-%d", invocationCount))
|
WaitForTrigger(context.Context) (context.Context, <-chan WaitTriggerResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
log.Info("wait for triggers")
|
||||||
|
|
||||||
|
// j.tasksMtx.Lock()
|
||||||
|
// j.activeInvocationId = j.nextInvocationId
|
||||||
|
// j.nextInvocationId++
|
||||||
|
// thisInvocation := j.activeInvocationId // stack-local, for use in reset-handler goroutine below
|
||||||
|
// j.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
// // setup the goroutine that waits for task resets
|
||||||
|
// // Task resets are converted into cancellations of the invocation context.
|
||||||
|
|
||||||
|
invocationCtx, cancelInvocation := context.WithCancel(invocationCtx)
|
||||||
|
// waitForResetCtx, stopWaitForReset := context.WithCancel(ctx)
|
||||||
|
// var wg sync.WaitGroup
|
||||||
|
// wg.Add(1)
|
||||||
|
// go func() {
|
||||||
|
// defer wg.Done()
|
||||||
|
// select {
|
||||||
|
// case <-waitForResetCtx.Done():
|
||||||
|
// return
|
||||||
|
// case reqResetInvocation := <-j.reset:
|
||||||
|
// l := log.WithField("requested_invocation_id", reqResetInvocation).
|
||||||
|
// WithField("this_invocation_id", thisInvocation)
|
||||||
|
// if reqResetInvocation == thisInvocation {
|
||||||
|
// l.Info("reset received, cancelling current invocation")
|
||||||
|
// cancelInvocation()
|
||||||
|
// } else {
|
||||||
|
// l.Debug("received reset for invocation id that is not us, discarding request")
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }()
|
||||||
|
|
||||||
|
// j.tasksMtx.Lock()
|
||||||
|
// j.activeInvocationId = 0
|
||||||
|
// j.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
invocationCtx, err := t.WaitForTrigger(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(ctx.Err()).Info("error waiting for trigger")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
j.mode.ResetConnectBackoff()
|
||||||
|
|
||||||
|
// setup the invocation context
|
||||||
|
invocationCtx, endSpan := trace.WithSpan(invocationCtx, fmt.Sprintf("invocation-%d", j.nextInvocationId))
|
||||||
|
|
||||||
|
|
||||||
j.do(invocationCtx)
|
j.do(invocationCtx)
|
||||||
|
stopWaitForReset()
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
endSpan()
|
endSpan()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ActiveSidePollRequest struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSidePollResponse struct {
|
||||||
|
Done bool
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) Poll(req ActiveSidePollRequest) (*ActiveSidePollResponse, error) {
|
||||||
|
j.tasksMtx.Lock()
|
||||||
|
defer j.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
waitForId := req.InvocationId
|
||||||
|
if req.InvocationId == 0 {
|
||||||
|
// handle the case where the client doesn't know what the current invocation id is
|
||||||
|
if j.activeInvocationId != 0 {
|
||||||
|
waitForId = j.activeInvocationId
|
||||||
|
} else {
|
||||||
|
waitForId = j.nextInvocationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var done bool
|
||||||
|
if j.activeInvocationId == 0 {
|
||||||
|
done = waitForId < j.nextInvocationId
|
||||||
|
} else {
|
||||||
|
done = waitForId < j.activeInvocationId
|
||||||
|
}
|
||||||
|
res := &ActiveSidePollResponse{Done: done, InvocationId: waitForId}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSideResetRequest struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSideResetResponse struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) Reset(req ActiveSideResetRequest) (*ActiveSideResetResponse, error) {
|
||||||
|
j.tasksMtx.Lock()
|
||||||
|
defer j.tasksMtx.Unlock()
|
||||||
|
|
||||||
|
resetId := req.InvocationId
|
||||||
|
if req.InvocationId == 0 {
|
||||||
|
// handle the case where the client doesn't know what the current invocation id is
|
||||||
|
resetId = j.activeInvocationId
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetId == 0 {
|
||||||
|
return nil, fmt.Errorf("no active invocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetId != j.activeInvocationId {
|
||||||
|
return nil, fmt.Errorf("active invocation (%d) is not the invocation requested for reset (%d); (active invocation '0' indicates no active invocation)", j.activeInvocationId, resetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// non-blocking send (.Run() must not hold mutex while waiting for resets)
|
||||||
|
select {
|
||||||
|
case j.reset <- resetId:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ActiveSideResetResponse{InvocationId: resetId}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSideTriggerRequest struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActiveSideSignalResponse struct {
|
||||||
|
InvocationId uint64
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *ActiveSide) Trigger(req ActiveSideTriggerRequest) (*ActiveSideSignalResponse, error) {
|
||||||
|
// switch req.What {
|
||||||
|
// case "replication":
|
||||||
|
// invocationId, err = j.jobs.doreplication(req.Name)
|
||||||
|
// case "reset":
|
||||||
|
// err = j.jobs.reset(req.Name)
|
||||||
|
// case "snapshot":
|
||||||
|
// err = j.jobs.dosnapshot(req.Name)
|
||||||
|
// default:
|
||||||
|
// err = fmt.Errorf("operation %q is invalid", req.Op)
|
||||||
|
// }
|
||||||
|
|
||||||
|
j.tasksMtx.Lock()
|
||||||
|
var invocationId uint64
|
||||||
|
if j.activeInvocationId != 0 {
|
||||||
|
invocationId = j.activeInvocationId
|
||||||
|
} else {
|
||||||
|
invocationId = j.nextInvocationId
|
||||||
|
}
|
||||||
|
// non-blocking send (.Run() must not hold mutex while waiting for signals)
|
||||||
|
select {
|
||||||
|
case j.trigger <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
j.tasksMtx.Unlock()
|
||||||
|
return &ActiveSideSignalResponse{InvocationId: invocationId}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplicationPlusRemotePruneSequence struct {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (j *ActiveSide) do(ctx context.Context) {
|
func (j *ActiveSide) do(ctx context.Context) {
|
||||||
|
|
||||||
j.mode.ConnectEndpoints(ctx, j.connecter)
|
j.mode.ConnectEndpoints(ctx, j.connecter)
|
||||||
defer j.mode.DisconnectEndpoints()
|
defer j.mode.DisconnectEndpoints()
|
||||||
|
|
||||||
// allow cancellation of an invocation (this function)
|
|
||||||
ctx, cancelThisRun := context.WithCancel(ctx)
|
|
||||||
defer cancelThisRun()
|
|
||||||
go func() {
|
|
||||||
select {
|
|
||||||
case <-reset.Wait(ctx):
|
|
||||||
log.Info("reset received, cancelling current invocation")
|
|
||||||
cancelThisRun()
|
|
||||||
case <-ctx.Done():
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
sender, receiver := j.mode.SenderReceiver()
|
sender, receiver := j.mode.SenderReceiver()
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -492,10 +647,11 @@ func (j *ActiveSide) do(ctx context.Context) {
|
|||||||
GetLogger(ctx).Info("start replication")
|
GetLogger(ctx).Info("start replication")
|
||||||
repWait(true) // wait blocking
|
repWait(true) // wait blocking
|
||||||
repCancel() // always cancel to free up context resources
|
repCancel() // always cancel to free up context resources
|
||||||
|
|
||||||
replicationReport := j.tasks.replicationReport()
|
replicationReport := j.tasks.replicationReport()
|
||||||
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.replicationDone = replicationReport
|
||||||
|
})
|
||||||
endSpan()
|
endSpan()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
func JobsFromConfig(c *config.Config) ([]Job, error) {
|
||||||
@@ -108,13 +107,3 @@ func validateReceivingSidesDoNotOverlap(receivingRootFSs []string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBandwidthLimitConfig(in *config.BandwidthLimit) (c bandwidthlimit.Config, _ error) {
|
|
||||||
if in.Max.ToBytes() > 0 && int64(in.Max.ToBytes()) == 0 {
|
|
||||||
return c, fmt.Errorf("bandwidth limit `max` is too small, must at least specify one byte")
|
|
||||||
}
|
|
||||||
return bandwidthlimit.Config{
|
|
||||||
Max: int64(in.Max.ToBytes()),
|
|
||||||
BucketCapacity: int64(in.BucketCapacity.ToBytes()),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -22,12 +22,7 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
|||||||
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
return nil, errors.Wrap(err, "cannot build filesystem filter")
|
||||||
}
|
}
|
||||||
sendOpts := in.GetSendOptions()
|
sendOpts := in.GetSendOptions()
|
||||||
bwlim, err := buildBandwidthLimitConfig(sendOpts.BandwidthLimit)
|
return &endpoint.SenderConfig{
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build bandwith limit config")
|
|
||||||
}
|
|
||||||
|
|
||||||
sc := &endpoint.SenderConfig{
|
|
||||||
FSF: fsf,
|
FSF: fsf,
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
|
|
||||||
@@ -39,15 +34,7 @@ func buildSenderConfig(in SendingJobConfig, jobID endpoint.JobID) (*endpoint.Sen
|
|||||||
SendCompressed: sendOpts.Compressed,
|
SendCompressed: sendOpts.Compressed,
|
||||||
SendEmbeddedData: sendOpts.EmbeddedData,
|
SendEmbeddedData: sendOpts.EmbeddedData,
|
||||||
SendSaved: sendOpts.Saved,
|
SendSaved: sendOpts.Saved,
|
||||||
|
}, nil
|
||||||
BandwidthLimit: bwlim,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := sc.Validate(); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "cannot build sender config")
|
|
||||||
}
|
|
||||||
|
|
||||||
return sc, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReceivingJobConfig interface {
|
type ReceivingJobConfig interface {
|
||||||
@@ -66,12 +53,6 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
|||||||
}
|
}
|
||||||
|
|
||||||
recvOpts := in.GetRecvOptions()
|
recvOpts := in.GetRecvOptions()
|
||||||
|
|
||||||
bwlim, err := buildBandwidthLimitConfig(recvOpts.BandwidthLimit)
|
|
||||||
if err != nil {
|
|
||||||
return rc, errors.Wrap(err, "cannot build bandwith limit config")
|
|
||||||
}
|
|
||||||
|
|
||||||
rc = endpoint.ReceiverConfig{
|
rc = endpoint.ReceiverConfig{
|
||||||
JobID: jobID,
|
JobID: jobID,
|
||||||
RootWithoutClientComponent: rootFs,
|
RootWithoutClientComponent: rootFs,
|
||||||
@@ -79,8 +60,6 @@ func buildReceiverConfig(in ReceivingJobConfig, jobID endpoint.JobID) (rc endpoi
|
|||||||
|
|
||||||
InheritProperties: recvOpts.Properties.Inherit,
|
InheritProperties: recvOpts.Properties.Inherit,
|
||||||
OverrideProperties: recvOpts.Properties.Override,
|
OverrideProperties: recvOpts.Properties.Override,
|
||||||
|
|
||||||
BandwidthLimit: bwlim,
|
|
||||||
}
|
}
|
||||||
if err := rc.Validate(); err != nil {
|
if err := rc.Validate(); err != nil {
|
||||||
return rc, errors.Wrap(err, "cannot build receiver config")
|
return rc, errors.Wrap(err, "cannot build receiver config")
|
||||||
|
|||||||
@@ -119,14 +119,6 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
t.Errorf("glob failed: %+v", err)
|
t.Errorf("glob failed: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
type additionalCheck struct {
|
|
||||||
state int
|
|
||||||
test func(t *testing.T, jobs []Job)
|
|
||||||
}
|
|
||||||
additionalChecks := map[string]*additionalCheck{
|
|
||||||
"bandwidth_limit.yml": {test: testSampleConfig_BandwidthLimit},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range paths {
|
for _, p := range paths {
|
||||||
|
|
||||||
if path.Ext(p) != ".yml" {
|
if path.Ext(p) != ".yml" {
|
||||||
@@ -134,20 +126,10 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
filename := path.Base(p)
|
|
||||||
t.Logf("checking for presence additonal checks for file %q", filename)
|
|
||||||
additionalCheck := additionalChecks[filename]
|
|
||||||
if additionalCheck == nil {
|
|
||||||
t.Logf("no additional checks")
|
|
||||||
} else {
|
|
||||||
t.Logf("additional check present")
|
|
||||||
additionalCheck.state = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run(p, func(t *testing.T) {
|
t.Run(p, func(t *testing.T) {
|
||||||
c, err := config.ParseConfig(p)
|
c, err := config.ParseConfig(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error parsing %s:\n%+v", p, err)
|
t.Errorf("error parsing %s:\n%+v", p, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("file: %s", p)
|
t.Logf("file: %s", p)
|
||||||
@@ -156,57 +138,11 @@ func TestSampleConfigsAreBuiltWithoutErrors(t *testing.T) {
|
|||||||
tls.FakeCertificateLoading(t)
|
tls.FakeCertificateLoading(t)
|
||||||
jobs, err := JobsFromConfig(c)
|
jobs, err := JobsFromConfig(c)
|
||||||
t.Logf("jobs: %#v", jobs)
|
t.Logf("jobs: %#v", jobs)
|
||||||
require.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
if additionalCheck != nil {
|
|
||||||
additionalCheck.test(t, jobs)
|
|
||||||
additionalCheck.state = 2
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for basename, c := range additionalChecks {
|
|
||||||
if c.state == 0 {
|
|
||||||
panic("univisited additional check " + basename)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSampleConfig_BandwidthLimit(t *testing.T, jobs []Job) {
|
|
||||||
require.Len(t, jobs, 3)
|
|
||||||
|
|
||||||
{
|
|
||||||
limitedSink, ok := jobs[0].(*PassiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[0])
|
|
||||||
limitedSinkMode, ok := limitedSink.mode.(*modeSink)
|
|
||||||
require.True(t, ok, "%T", limitedSink)
|
|
||||||
|
|
||||||
assert.Equal(t, int64(12345), limitedSinkMode.receiverConfig.BandwidthLimit.Max)
|
|
||||||
assert.Equal(t, int64(1<<17), limitedSinkMode.receiverConfig.BandwidthLimit.BucketCapacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
limitedPush, ok := jobs[1].(*ActiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[1])
|
|
||||||
limitedPushMode, ok := limitedPush.mode.(*modePush)
|
|
||||||
require.True(t, ok, "%T", limitedPush)
|
|
||||||
|
|
||||||
assert.Equal(t, int64(54321), limitedPushMode.senderConfig.BandwidthLimit.Max)
|
|
||||||
assert.Equal(t, int64(1024), limitedPushMode.senderConfig.BandwidthLimit.BucketCapacity)
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
unlimitedSink, ok := jobs[2].(*PassiveSide)
|
|
||||||
require.True(t, ok, "%T", jobs[2])
|
|
||||||
unlimitedSinkMode, ok := unlimitedSink.mode.(*modeSink)
|
|
||||||
require.True(t, ok, "%T", unlimitedSink)
|
|
||||||
|
|
||||||
max := unlimitedSinkMode.receiverConfig.BandwidthLimit.Max
|
|
||||||
assert.Less(t, max, int64(0), max, "unlimited mode <=> negative value for .Max, see bandwidthlimit.Config")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReplicationOptions(t *testing.T) {
|
func TestReplicationOptions(t *testing.T) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func GetLogger(ctx context.Context) Logger {
|
|||||||
return logging.GetLogger(ctx, logging.SubsysJob)
|
return logging.GetLogger(ctx, logging.SubsysJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type Job interface {
|
type Job interface {
|
||||||
Name() string
|
Name() string
|
||||||
Run(ctx context.Context)
|
Run(ctx context.Context)
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package reset
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const contextKeyReset contextKey = iota
|
|
||||||
|
|
||||||
func Wait(ctx context.Context) <-chan struct{} {
|
|
||||||
wc, ok := ctx.Value(contextKeyReset).(chan struct{})
|
|
||||||
if !ok {
|
|
||||||
wc = make(chan struct{})
|
|
||||||
}
|
|
||||||
return wc
|
|
||||||
}
|
|
||||||
|
|
||||||
type Func func() error
|
|
||||||
|
|
||||||
var AlreadyReset = errors.New("already reset")
|
|
||||||
|
|
||||||
func Context(ctx context.Context) (context.Context, Func) {
|
|
||||||
wc := make(chan struct{})
|
|
||||||
wuf := func() error {
|
|
||||||
select {
|
|
||||||
case wc <- struct{}{}:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return AlreadyReset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return context.WithValue(ctx, contextKeyReset, wc), wuf
|
|
||||||
}
|
|
||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
"github.com/zrepl/zrepl/daemon/filters"
|
"github.com/zrepl/zrepl/daemon/filters"
|
||||||
"github.com/zrepl/zrepl/daemon/job/wakeup"
|
|
||||||
"github.com/zrepl/zrepl/daemon/pruner"
|
"github.com/zrepl/zrepl/daemon/pruner"
|
||||||
"github.com/zrepl/zrepl/daemon/snapper"
|
"github.com/zrepl/zrepl/daemon/snapper"
|
||||||
"github.com/zrepl/zrepl/endpoint"
|
"github.com/zrepl/zrepl/endpoint"
|
||||||
@@ -112,13 +111,13 @@ func (j *SnapJob) Run(ctx context.Context) {
|
|||||||
invocationCount := 0
|
invocationCount := 0
|
||||||
outer:
|
outer:
|
||||||
for {
|
for {
|
||||||
log.Info("wait for wakeups")
|
log.Info("wait for replications")
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
log.WithError(ctx.Err()).Info("context")
|
log.WithError(ctx.Err()).Info("context")
|
||||||
break outer
|
break outer
|
||||||
|
|
||||||
case <-wakeup.Wait(ctx):
|
// case <-doreplication.Wait(ctx):
|
||||||
case <-periodicDone:
|
case <-periodicDone:
|
||||||
}
|
}
|
||||||
invocationCount++
|
invocationCount++
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
//
|
||||||
|
//
|
||||||
|
// Alternative Design (in "RustGo")
|
||||||
|
//
|
||||||
|
// enum InternalMsg {
|
||||||
|
// Trigger((), chan (TriggerResponse, error)),
|
||||||
|
// Poll(PollRequest, chan PollResponse),
|
||||||
|
// Reset(ResetRequest, chan (ResetResponse, error)),
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// enum State {
|
||||||
|
// Running{
|
||||||
|
// invocationId: u32,
|
||||||
|
// cancelCurrentInvocation: context.CancelFunc
|
||||||
|
// }
|
||||||
|
// Waiting{
|
||||||
|
// nextInvocationId: u32,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// for msg := <- t.internalMsgs {
|
||||||
|
// match (msg, state) {
|
||||||
|
// ...
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
package trigger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type T struct {
|
||||||
|
mtx sync.Mutex
|
||||||
|
cv sync.Cond
|
||||||
|
|
||||||
|
nextInvocationId uint64
|
||||||
|
activeInvocationId uint64 // 0 <=> inactive
|
||||||
|
triggerPending bool
|
||||||
|
contextDone bool
|
||||||
|
reset chan uint64
|
||||||
|
stopWaitForReset chan struct{}
|
||||||
|
cancelCurrentInvocation context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *T {
|
||||||
|
t := &T{
|
||||||
|
activeInvocationId: math.MaxUint64,
|
||||||
|
nextInvocationId: 1,
|
||||||
|
}
|
||||||
|
t.cv.L = &t.mtx
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) WaitForTrigger(ctx context.Context) (rctx context.Context, err error) {
|
||||||
|
t.mtx.Lock()
|
||||||
|
defer t.mtx.Unlock()
|
||||||
|
|
||||||
|
if t.activeInvocationId == 0 {
|
||||||
|
return nil, fmt.Errorf("must be running when calling this function")
|
||||||
|
}
|
||||||
|
t.activeInvocationId = 0
|
||||||
|
t.cancelCurrentInvocation = nil
|
||||||
|
|
||||||
|
if t.contextDone == true {
|
||||||
|
panic("implementation error: this variable is only true while in WaitForTrigger, and that's a mutually exclusive function")
|
||||||
|
}
|
||||||
|
stopWaitingForDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-stopWaitingForDone:
|
||||||
|
case <-ctx.Done():
|
||||||
|
t.mtx.Lock()
|
||||||
|
t.contextDone = true
|
||||||
|
t.cv.Broadcast()
|
||||||
|
t.mtx.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
t.triggerPending = false
|
||||||
|
t.contextDone = false
|
||||||
|
}()
|
||||||
|
for !t.triggerPending && !t.contextDone {
|
||||||
|
t.cv.Wait()
|
||||||
|
}
|
||||||
|
close(stopWaitingForDone)
|
||||||
|
if t.contextDone {
|
||||||
|
if ctx.Err() == nil {
|
||||||
|
panic("implementation error: contextDone <=> ctx.Err() != nil")
|
||||||
|
}
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.activeInvocationId = t.nextInvocationId
|
||||||
|
t.nextInvocationId++
|
||||||
|
rctx, t.cancelCurrentInvocation = context.WithCancel(ctx)
|
||||||
|
|
||||||
|
return rctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type TriggerResponse struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) Trigger() (TriggerResponse, error) {
|
||||||
|
t.mtx.Lock()
|
||||||
|
defer t.mtx.Unlock()
|
||||||
|
var invocationId uint64
|
||||||
|
if t.activeInvocationId != 0 {
|
||||||
|
invocationId = t.activeInvocationId
|
||||||
|
} else {
|
||||||
|
invocationId = t.nextInvocationId
|
||||||
|
}
|
||||||
|
// non-blocking send (.Run() must not hold mutex while waiting for signals)
|
||||||
|
t.triggerPending = true
|
||||||
|
t.cv.Broadcast()
|
||||||
|
return TriggerResponse{InvocationId: invocationId}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollRequest struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type PollResponse struct {
|
||||||
|
Done bool
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) Poll(req PollRequest) (res PollResponse) {
|
||||||
|
t.mtx.Lock()
|
||||||
|
defer t.mtx.Unlock()
|
||||||
|
|
||||||
|
waitForId := req.InvocationId
|
||||||
|
if req.InvocationId == 0 {
|
||||||
|
// handle the case where the client doesn't know what the current invocation id is
|
||||||
|
if t.activeInvocationId != 0 {
|
||||||
|
waitForId = t.activeInvocationId
|
||||||
|
} else {
|
||||||
|
waitForId = t.nextInvocationId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var done bool
|
||||||
|
if t.activeInvocationId == 0 {
|
||||||
|
done = waitForId < t.nextInvocationId
|
||||||
|
} else {
|
||||||
|
done = waitForId < t.activeInvocationId
|
||||||
|
}
|
||||||
|
return PollResponse{Done: done, InvocationId: waitForId}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetRequest struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetResponse struct {
|
||||||
|
InvocationId uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *T) Reset(req ResetRequest) (*ResetResponse, error) {
|
||||||
|
t.mtx.Lock()
|
||||||
|
defer t.mtx.Unlock()
|
||||||
|
|
||||||
|
resetId := req.InvocationId
|
||||||
|
if req.InvocationId == 0 {
|
||||||
|
// handle the case where the client doesn't know what the current invocation id is
|
||||||
|
resetId = t.activeInvocationId
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetId == 0 {
|
||||||
|
return nil, fmt.Errorf("no active invocation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if resetId != t.activeInvocationId {
|
||||||
|
return nil, fmt.Errorf("active invocation (%d) is not the invocation requested for reset (%d); (active invocation '0' indicates no active invocation)", t.activeInvocationId, resetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.cancelCurrentInvocation()
|
||||||
|
|
||||||
|
return &ResetResponse{InvocationId: resetId}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
package trigger_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/zrepl/zrepl/daemon/job/trigger"
|
||||||
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
"github.com/zrepl/zrepl/replication"
|
||||||
|
"github.com/zrepl/zrepl/replication/driver"
|
||||||
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBasics(t *testing.T) {
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
defer wg.Wait()
|
||||||
|
|
||||||
|
tr := trigger.New()
|
||||||
|
|
||||||
|
triggered := make(chan int)
|
||||||
|
waitForTriggerError := make(chan error)
|
||||||
|
waitForResetCallToBeMadeByMainGoroutine := make(chan struct{})
|
||||||
|
postResetAssertionsDone := make(chan struct{})
|
||||||
|
|
||||||
|
taskCtx := context.Background()
|
||||||
|
taskCtx, cancelTaskCtx := context.WithCancel(taskCtx)
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
taskCtx := context.WithValue(taskCtx, "mykey", "myvalue")
|
||||||
|
|
||||||
|
triggers := 0
|
||||||
|
|
||||||
|
outer:
|
||||||
|
for {
|
||||||
|
invocationCtx, err := tr.WaitForTrigger(taskCtx)
|
||||||
|
if err != nil {
|
||||||
|
waitForTriggerError <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.Equal(t, invocationCtx.Value("mykey"), "myvalue")
|
||||||
|
|
||||||
|
triggers++
|
||||||
|
triggered <- triggers
|
||||||
|
|
||||||
|
switch triggers {
|
||||||
|
case 1:
|
||||||
|
continue outer
|
||||||
|
case 2:
|
||||||
|
<-waitForResetCallToBeMadeByMainGoroutine
|
||||||
|
require.Equal(t, context.Canceled, invocationCtx.Err(), "Reset() cancels invocation context")
|
||||||
|
require.Nil(t, taskCtx.Err(), "Reset() does not cancel task context")
|
||||||
|
close(postResetAssertionsDone)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}()
|
||||||
|
|
||||||
|
t.Logf("trigger 1")
|
||||||
|
_, err := tr.Trigger()
|
||||||
|
require.NoError(t, err)
|
||||||
|
v := <-triggered
|
||||||
|
require.Equal(t, 1, v)
|
||||||
|
|
||||||
|
t.Logf("trigger 2")
|
||||||
|
triggerResponse, err := tr.Trigger()
|
||||||
|
require.NoError(t, err)
|
||||||
|
v = <-triggered
|
||||||
|
require.Equal(t, 2, v)
|
||||||
|
|
||||||
|
t.Logf("reset")
|
||||||
|
resetResponse, err := tr.Reset(trigger.ResetRequest{InvocationId: triggerResponse.InvocationId})
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Logf("reset response: %#v", resetResponse)
|
||||||
|
close(waitForResetCallToBeMadeByMainGoroutine)
|
||||||
|
<-postResetAssertionsDone
|
||||||
|
|
||||||
|
t.Logf("cancel the context")
|
||||||
|
cancelTaskCtx()
|
||||||
|
wfte := <-waitForTriggerError
|
||||||
|
require.Equal(t, taskCtx.Err(), wfte)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type PushJob struct {
|
||||||
|
snap *Snapshotter
|
||||||
|
repl *ReplicationAndTriggerRemotePruningSequence
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PushJob) Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PushJob) HandleTrigger(w http.ResponseWriter, r *http.Request) {
|
||||||
|
panic("unimplemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *PushJob) Run(ctx context.Context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type SnapshotSequence struct {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReplicationAndTriggerRemotePruningSequence struct {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s ReplicationAndTriggerRemotePruningSequence) Run(ctx context.Context) {
|
||||||
|
|
||||||
|
j.mode.ConnectEndpoints(ctx, j.connecter)
|
||||||
|
defer j.mode.DisconnectEndpoints()
|
||||||
|
|
||||||
|
sender, receiver := j.mode.SenderReceiver()
|
||||||
|
|
||||||
|
{
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "replication")
|
||||||
|
ctx, repCancel := context.WithCancel(ctx)
|
||||||
|
var repWait driver.WaitFunc
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
// reset it
|
||||||
|
*tasks = activeSideTasks{}
|
||||||
|
tasks.replicationCancel = func() { repCancel(); endSpan() }
|
||||||
|
tasks.replicationReport, repWait = replication.Do(
|
||||||
|
ctx, j.replicationDriverConfig, logic.NewPlanner(j.promRepStateSecs, j.promBytesReplicated, sender, receiver, j.mode.PlannerPolicy()),
|
||||||
|
)
|
||||||
|
tasks.state = ActiveSideReplicating
|
||||||
|
})
|
||||||
|
GetLogger(ctx).Info("start replication")
|
||||||
|
repWait(true) // wait blocking
|
||||||
|
repCancel() // always cancel to free up context resources
|
||||||
|
replicationReport := j.tasks.replicationReport()
|
||||||
|
j.promReplicationErrors.Set(float64(replicationReport.GetFailedFilesystemsCountInLatestAttempt()))
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.replicationDone = replicationReport
|
||||||
|
})
|
||||||
|
endSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "prune_sender")
|
||||||
|
ctx, senderCancel := context.WithCancel(ctx)
|
||||||
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.prunerSender = j.prunerFactory.BuildSenderPruner(ctx, sender, sender)
|
||||||
|
tasks.prunerSenderCancel = func() { senderCancel(); endSpan() }
|
||||||
|
tasks.state = ActiveSidePruneSender
|
||||||
|
})
|
||||||
|
GetLogger(ctx).Info("start pruning sender")
|
||||||
|
tasks.prunerSender.Prune()
|
||||||
|
GetLogger(ctx).Info("finished pruning sender")
|
||||||
|
senderCancel()
|
||||||
|
endSpan()
|
||||||
|
}
|
||||||
|
{
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
ctx, endSpan := trace.WithSpan(ctx, "prune_recever")
|
||||||
|
ctx, receiverCancel := context.WithCancel(ctx)
|
||||||
|
tasks := j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.prunerReceiver = j.prunerFactory.BuildReceiverPruner(ctx, receiver, sender)
|
||||||
|
tasks.prunerReceiverCancel = func() { receiverCancel(); endSpan() }
|
||||||
|
tasks.state = ActiveSidePruneReceiver
|
||||||
|
})
|
||||||
|
GetLogger(ctx).Info("start pruning receiver")
|
||||||
|
tasks.prunerReceiver.Prune()
|
||||||
|
GetLogger(ctx).Info("finished pruning receiver")
|
||||||
|
receiverCancel()
|
||||||
|
endSpan()
|
||||||
|
}
|
||||||
|
|
||||||
|
j.updateTasks(func(tasks *activeSideTasks) {
|
||||||
|
tasks.state = ActiveSideDone
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func TestUseCase(t *testing.T) {
|
||||||
|
|
||||||
|
var as ActiveSide
|
||||||
|
|
||||||
|
as.
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package wakeup
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
type contextKey int
|
|
||||||
|
|
||||||
const contextKeyWakeup contextKey = iota
|
|
||||||
|
|
||||||
func Wait(ctx context.Context) <-chan struct{} {
|
|
||||||
wc, ok := ctx.Value(contextKeyWakeup).(chan struct{})
|
|
||||||
if !ok {
|
|
||||||
wc = make(chan struct{})
|
|
||||||
}
|
|
||||||
return wc
|
|
||||||
}
|
|
||||||
|
|
||||||
type Func func() error
|
|
||||||
|
|
||||||
var AlreadyWokenUp = errors.New("already woken up")
|
|
||||||
|
|
||||||
func Context(ctx context.Context) (context.Context, Func) {
|
|
||||||
wc := make(chan struct{})
|
|
||||||
wuf := func() error {
|
|
||||||
select {
|
|
||||||
case wc <- struct{}{}:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return AlreadyWokenUp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return context.WithValue(ctx, contextKeyWakeup, wc), wuf
|
|
||||||
}
|
|
||||||
@@ -210,6 +210,10 @@ func syncUp(a args, u updater) state {
|
|||||||
return u(func(s *Snapper) {
|
return u(func(s *Snapper) {
|
||||||
s.state = Planning
|
s.state = Planning
|
||||||
}).sf()
|
}).sf()
|
||||||
|
// case <-dosnapshot.Wait(a.ctx):
|
||||||
|
// return u(func(s *Snapper) {
|
||||||
|
// s.state = Planning
|
||||||
|
// }).sf()
|
||||||
case <-a.ctx.Done():
|
case <-a.ctx.Done():
|
||||||
return onMainCtxDone(a.ctx, u)
|
return onMainCtxDone(a.ctx, u)
|
||||||
}
|
}
|
||||||
@@ -378,6 +382,10 @@ func wait(a args, u updater) state {
|
|||||||
return u(func(snapper *Snapper) {
|
return u(func(snapper *Snapper) {
|
||||||
snapper.state = Planning
|
snapper.state = Planning
|
||||||
}).sf()
|
}).sf()
|
||||||
|
// case <-dosnapshot.Wait(a.ctx):
|
||||||
|
// return u(func(snapper *Snapper) {
|
||||||
|
// snapper.state = Planning
|
||||||
|
// }).sf()
|
||||||
case <-a.ctx.Done():
|
case <-a.ctx.Done():
|
||||||
return onMainCtxDone(a.ctx, u)
|
return onMainCtxDone(a.ctx, u)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,9 +18,9 @@ type PeriodicOrManual struct {
|
|||||||
s *Snapper
|
s *Snapper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PeriodicOrManual) Run(ctx context.Context, wakeUpCommon chan<- struct{}) {
|
func (s *PeriodicOrManual) Run(ctx context.Context, replicationCommon chan<- struct{}) {
|
||||||
if s.s != nil {
|
if s.s != nil {
|
||||||
s.s.Run(ctx, wakeUpCommon)
|
s.s.Run(ctx, replicationCommon)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ We use the following annotations for classifying changes:
|
|||||||
0.4.0
|
0.4.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
* |break| Change syntax to trigger a job replication, rename ``zrepl signal wakeup JOB`` to ``zrepl signal replication JOB``
|
||||||
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
* |feature| support setting zfs send / recv flags in the config (send: ``-wLcepbS`` , recv: ``-ox`` ).
|
||||||
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
Config docs :ref:`here <job-send-options>` and :ref:`here <job-recv-options>` .
|
||||||
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
* |feature| parallel replication is now configurable (disabled by default, :ref:`config docs here <replication-option-concurrency>` ).
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ Job Type ``pull``
|
|||||||
``$root_fs/$source_path``
|
``$root_fs/$source_path``
|
||||||
* - ``interval``
|
* - ``interval``
|
||||||
- | Interval at which to pull from the source job (e.g. ``10m``).
|
- | Interval at which to pull from the source job (e.g. ``10m``).
|
||||||
| ``manual`` disables periodic pulling, replication then only happens on :ref:`wakeup <cli-signal-wakeup>`.
|
| ``manual`` disables periodic pulling, replication then only happens on :ref:`replication <cli-signal-replication>`.
|
||||||
* - ``pruning``
|
* - ``pruning``
|
||||||
- |pruning-spec|
|
- |pruning-spec|
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Prometheus & Grafana
|
|||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
zrepl can expose `Prometheus metrics <https://prometheus.io/docs/instrumenting/exposition_formats/>`_ via HTTP.
|
||||||
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9811`` or ``127.0.0.1:9811`` (port 9811 was reserved to zrepl `on the official list <https://github.com/prometheus/prometheus/wiki/Default-port-allocations/_compare/43e495dd251ee328ac0d08b58084665b5c0f7a7e...459195059b55b414193ebeb80c5ba463d2606951>`_).
|
The ``listen`` attribute is a `net.Listen <https://golang.org/pkg/net/#Listen>`_ string for tcp, e.g. ``:9091`` or ``127.0.0.1:9091``.
|
||||||
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
The ``listen_freebind`` attribute is :ref:`explained here <listen-freebind-explanation>`.
|
||||||
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
The Prometheus monitoring job appears in the ``zrepl control`` job list and may be specified **at most once**.
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ The dashboard also contains some advice on which metrics are important to monito
|
|||||||
global:
|
global:
|
||||||
monitoring:
|
monitoring:
|
||||||
- type: prometheus
|
- type: prometheus
|
||||||
listen: ':9811'
|
listen: ':9091'
|
||||||
listen_freebind: true # optional, default false
|
listen_freebind: true # optional, default false
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -124,51 +124,43 @@ The syntax to describe the bucket list is as follows:
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
Assume the following grid spec:
|
This grid spec produces the following list of adjacent buckets. For the sake of simplicity,
|
||||||
|
we subject all snapshots to the grid pruning policy by settings `regex: .*`.
|
||||||
|
|
||||||
|
`
|
||||||
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
grid: 1x1h(keep=all) | 2x2h | 1x3h
|
||||||
|
regex: .*
|
||||||
This grid spec produces the following constellation of buckets:
|
`
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
||||||
| | | | | | | | | |
|
| | | | | | | | | |
|
||||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
|-Bucket1-|-----Bucket 2------|------Bucket 3-----|-----------Bucket 4----------|
|
||||||
| keep=all| keep=1 | keep=1 | keep=1 |
|
| keep=all| keep=1 | keep=1 | keep=1 |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Now assume that we have a set of snapshots @a, @b, ..., @D.
|
Let us consider the following set of snapshots @a-zA-C:
|
||||||
Snapshot @a is the most recent snapshot.
|
|
||||||
Snapshot @D is the oldest snapshot, it is almost 9 hours older than snapshot @a.
|
|
||||||
We place the snapshots on the same timeline as the buckets:
|
|
||||||
|
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
| a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D |
|
||||||
| | | | | | | | | |
|
|
||||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
|
||||||
| keep=all| keep=1 | keep=1 | keep=1 |
|
|
||||||
| | | | |
|
|
||||||
| a b c| d e f g h i j k l m n o p |q r s t u v w x y z |A B C D
|
|
||||||
|
|
||||||
The result is the following mapping of snapshots to buckets:
|
The `grid` algorithm maps them to their respective buckets:
|
||||||
|
|
||||||
Bucket1: a, b, c
|
Bucket 1: a, b, c
|
||||||
Bucket2: d,e,f,g,h,i,j
|
Bucket 2: d,e,f,g,h,i,j
|
||||||
Bucket3: k,l,m,n,o,p
|
Bucket 3: k,l,m,n,o,p
|
||||||
Bucket4: q,r,s,t,u,v,w,x,y,z
|
Bucket 4: q,r, q,r,s,t,u,v,w,x,y,z
|
||||||
No bucket: A,B,C,D
|
None: A,B,C,D
|
||||||
|
|
||||||
For each bucket, we now prune snapshots until it only contains `keep` snapshots.
|
It then applies the per-bucket pruning logic described above which resulting in the
|
||||||
Newer snapshots are destroyed first.
|
following list of remaining snapshots.
|
||||||
Snapshots that do not fall into a bucket are always destroyed.
|
|
||||||
|
|
||||||
Result after pruning:
|
| a b c j p z |
|
||||||
|
|
||||||
|
Note that it only makes sense to grow (not shorten) the interval duration for buckets
|
||||||
|
further in the past since each bucket acts like a low-pass filter for incoming snapshots
|
||||||
|
and adding a less-low-pass-filter after a low-pass one has no effect.
|
||||||
|
|
||||||
0h 1h 2h 3h 4h 5h 6h 7h 8h 9h
|
|
||||||
| | | | | | | | | |
|
|
||||||
|-Bucket1-|-----Bucket2-------|------Bucket3------|-----------Bucket4-----------|
|
|
||||||
| | | | |
|
|
||||||
| a b c| j p | z |
|
|
||||||
|
|
||||||
.. _prune-keep-last-n:
|
.. _prune-keep-last-n:
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,6 @@ See the `upstream man page <https://openzfs.github.io/openzfs-docs/man/8/zfs-sen
|
|||||||
* - ``encrypted``
|
* - ``encrypted``
|
||||||
-
|
-
|
||||||
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
- Specific to zrepl, :ref:`see below <job-send-options-encrypted>`.
|
||||||
* - ``bandwidth_limit``
|
|
||||||
-
|
|
||||||
- Specific to zrepl, :ref:`see below <job-send-recv-options-bandwidth-limit>`.
|
|
||||||
* - ``raw``
|
* - ``raw``
|
||||||
- ``-w``
|
- ``-w``
|
||||||
- Use ``encrypted`` to only allow encrypted sends.
|
- Use ``encrypted`` to only allow encrypted sends.
|
||||||
@@ -141,7 +138,6 @@ Recv Options
|
|||||||
override: {
|
override: {
|
||||||
"org.openzfs.systemd:ignore": "on"
|
"org.openzfs.systemd:ignore": "on"
|
||||||
}
|
}
|
||||||
bandwidth_limit: ... # see below
|
|
||||||
...
|
...
|
||||||
|
|
||||||
.. _job-recv-options--inherit-and-override:
|
.. _job-recv-options--inherit-and-override:
|
||||||
@@ -216,25 +212,3 @@ and property replication is enabled, the receiver must :ref:`inherit the followi
|
|||||||
* ``keylocation``
|
* ``keylocation``
|
||||||
* ``keyformat``
|
* ``keyformat``
|
||||||
* ``encryption``
|
* ``encryption``
|
||||||
|
|
||||||
Common Options
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. _job-send-recv-options-bandwidth-limit:
|
|
||||||
|
|
||||||
Bandwidth Limit (send & recv)
|
|
||||||
-----------------------------
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
bandwidth_limit:
|
|
||||||
max: 23.5 MiB # -1 is the default and disabled rate limiting
|
|
||||||
bucket_capacity: # token bucket capacity in bytes; defaults to 128KiB
|
|
||||||
|
|
||||||
Both ``send`` and ``recv`` can be limited to a maximum bandwidth through ``bandwidth_limit``.
|
|
||||||
For most users, it should be sufficient to just set ``bandwidth_limit.max``.
|
|
||||||
The ``bandwidth_limit.bucket_capacity`` refers to the `token bucket size <https://github.com/juju/ratelimit>`_.
|
|
||||||
|
|
||||||
The bandwidth limit only applies to the payload data, i.e., the ZFS send stream.
|
|
||||||
It does not account for transport protocol overheads.
|
|
||||||
The scope is the job level, i.e., all :ref:`concurrent <replication-option-concurrency>` sends or incoming receives of a job share the bandwidth limit.
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ A filesystem that does not have snapshots by the snapshotter has lower priority
|
|||||||
|
|
||||||
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
For ``push`` jobs, replication is automatically triggered after all filesystems have been snapshotted.
|
||||||
|
|
||||||
Note that the ``zrepl signal wakeup JOB`` subcommand does not trigger snapshotting.
|
Note that the ``zrepl signal replication JOB`` subcommand does not trigger snapshotting.
|
||||||
|
|
||||||
|
|
||||||
::
|
::
|
||||||
@@ -38,7 +38,7 @@ There is also a ``manual`` snapshotting type, which covers the following use cas
|
|||||||
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
* Existing infrastructure for automatic snapshots: you only want to use this zrepl job for replication.
|
||||||
* Handling snapshotting through a separate ``snap`` job.
|
* Handling snapshotting through a separate ``snap`` job.
|
||||||
|
|
||||||
Note that you will have to trigger replication manually using the ``zrepl signal wakeup JOB`` subcommand in that case.
|
Note that you will have to trigger replication manually using the ``zrepl signal replication JOB`` subcommand in that case.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ The following snippet configure the repository for your Debian or Ubuntu release
|
|||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
sudo apt update && sudo apt install curl gnupg lsb-release; \
|
apt update && apt install curl gnupg lsb-release; \
|
||||||
ARCH="$(dpkg --print-architecture)"; \
|
ARCH="$(dpkg --print-architecture)"; \
|
||||||
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
CODENAME="$(lsb_release -i -s | tr '[:upper:]' '[:lower:]') $(lsb_release -c -s | tr '[:upper:]' '[:lower:]')"; \
|
||||||
echo "Using Distro and Codename: $CODENAME"; \
|
echo "Using Distro and Codename: $CODENAME"; \
|
||||||
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | sudo apt-key add -) && \
|
(curl https://zrepl.cschwarz.com/apt/apt-key.asc | apt-key add -) && \
|
||||||
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" | sudo tee /etc/apt/sources.list.d/zrepl.list) && \
|
(echo "deb [arch=$ARCH] https://zrepl.cschwarz.com/apt/$CODENAME main" > /etc/apt/sources.list.d/zrepl.list) && \
|
||||||
sudo apt update
|
apt update
|
||||||
|
|
||||||
|
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
|
|||||||
+9
-12
@@ -1,5 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -euo pipefail
|
set -eo pipefail
|
||||||
|
|
||||||
|
|
||||||
NON_INTERACTIVE=false
|
NON_INTERACTIVE=false
|
||||||
DO_CLONE=false
|
DO_CLONE=false
|
||||||
@@ -12,7 +13,7 @@ while getopts "ca" arg; do
|
|||||||
DO_CLONE=true
|
DO_CLONE=true
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "invalid option '-$arg'"
|
echo invalid option
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -26,6 +27,11 @@ checkout_repo_msg() {
|
|||||||
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
echo "clone ${GHPAGESREPO} to ${PUBLICDIR}:"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exit_msg() {
|
||||||
|
echo "error, exiting..."
|
||||||
|
}
|
||||||
|
trap exit_msg EXIT
|
||||||
|
|
||||||
if ! type sphinx-versioning >/dev/null; then
|
if ! type sphinx-versioning >/dev/null; then
|
||||||
echo "install sphinx-versioning and come back"
|
echo "install sphinx-versioning and come back"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -65,9 +71,6 @@ git reset --hard origin/master
|
|||||||
|
|
||||||
echo "cleaning GitHub pages repo"
|
echo "cleaning GitHub pages repo"
|
||||||
git rm -rf .
|
git rm -rf .
|
||||||
cat > .gitignore <<EOF
|
|
||||||
**/.doctrees
|
|
||||||
EOF
|
|
||||||
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
@@ -93,13 +96,7 @@ COMMIT_MSG="sphinx-versioning render from publish.sh - $(date -u) - ${CURRENT_CO
|
|||||||
pushd "$PUBLICDIR"
|
pushd "$PUBLICDIR"
|
||||||
|
|
||||||
echo "adding and commiting all changes in GitHub pages repo"
|
echo "adding and commiting all changes in GitHub pages repo"
|
||||||
git add .gitignore
|
|
||||||
git add -A
|
git add -A
|
||||||
if [ "$(git status --porcelain)" != "" ]; then
|
git commit -m "$COMMIT_MSG"
|
||||||
git commit -m "$COMMIT_MSG"
|
|
||||||
else
|
|
||||||
echo "nothing to commit"
|
|
||||||
fi
|
|
||||||
echo "pushing to GitHub pages repo"
|
|
||||||
git push origin master
|
git push origin master
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -55,8 +55,8 @@ Watch it Work
|
|||||||
=============
|
=============
|
||||||
|
|
||||||
Run ``zrepl status`` on the active side of the replication setup to monitor snaphotting, replication and pruning activity.
|
Run ``zrepl status`` on the active side of the replication setup to monitor snaphotting, replication and pruning activity.
|
||||||
To re-trigger replication (snapshots are separate!), use ``zrepl signal wakeup JOBNAME``.
|
To re-trigger replication (snapshots are separate!), use ``zrepl signal replication JOBNAME``.
|
||||||
(refer to the example use case document if you are uncertain which job you want to wake up).
|
(refer to the example use case document if you are uncertain which job you want to start replication).
|
||||||
|
|
||||||
You can also use basic UNIX tools to inspect see what's going on.
|
You can also use basic UNIX tools to inspect see what's going on.
|
||||||
If you like tmux, here is a handy script that works on FreeBSD: ::
|
If you like tmux, here is a handy script that works on FreeBSD: ::
|
||||||
|
|||||||
@@ -32,8 +32,6 @@ We would like to thank the following people and organizations for supporting zre
|
|||||||
|
|
||||||
<div class="fa fa-code" style="width: 1em;"></div>
|
<div class="fa fa-code" style="width: 1em;"></div>
|
||||||
|
|
||||||
* |supporter-gold| Prominic.NET, Inc.
|
|
||||||
* |supporter-std| Torsten Blum
|
|
||||||
* |supporter-gold| Cyberiada GmbH
|
* |supporter-gold| Cyberiada GmbH
|
||||||
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
* |supporter-std| `Gordon Schulz <https://github.com/azmodude>`_
|
||||||
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
* |supporter-std| `@jwittlincohen <https://github.com/jwittlincohen>`_
|
||||||
|
|||||||
+2
-2
@@ -13,7 +13,7 @@ CLI Overview
|
|||||||
The zrepl binary is self-documenting:
|
The zrepl binary is self-documenting:
|
||||||
run ``zrepl help`` for an overview of the available subcommands or ``zrepl SUBCOMMAND --help`` for information on available flags, etc.
|
run ``zrepl help`` for an overview of the available subcommands or ``zrepl SUBCOMMAND --help`` for information on available flags, etc.
|
||||||
|
|
||||||
.. _cli-signal-wakeup:
|
.. _cli-signal-replication:
|
||||||
|
|
||||||
.. list-table::
|
.. list-table::
|
||||||
:widths: 30 70
|
:widths: 30 70
|
||||||
@@ -29,7 +29,7 @@ CLI Overview
|
|||||||
- show job activity, or with ``--raw`` for JSON output
|
- show job activity, or with ``--raw`` for JSON output
|
||||||
* - ``zrepl stdinserver``
|
* - ``zrepl stdinserver``
|
||||||
- see :ref:`transport-ssh+stdinserver`
|
- see :ref:`transport-ssh+stdinserver`
|
||||||
* - ``zrepl signal wakeup JOB``
|
* - ``zrepl signal replication JOB``
|
||||||
- manually trigger replication + pruning of JOB
|
- manually trigger replication + pruning of JOB
|
||||||
* - ``zrepl signal reset JOB``
|
* - ``zrepl signal reset JOB``
|
||||||
- manually abort current replication + pruning of JOB
|
- manually abort current replication + pruning of JOB
|
||||||
|
|||||||
+1
-29
@@ -14,7 +14,6 @@ import (
|
|||||||
"github.com/zrepl/zrepl/daemon/logging/trace"
|
"github.com/zrepl/zrepl/daemon/logging/trace"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
|
||||||
"github.com/zrepl/zrepl/util/chainedio"
|
"github.com/zrepl/zrepl/util/chainedio"
|
||||||
"github.com/zrepl/zrepl/util/chainlock"
|
"github.com/zrepl/zrepl/util/chainlock"
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
"github.com/zrepl/zrepl/util/envconst"
|
||||||
@@ -35,8 +34,6 @@ type SenderConfig struct {
|
|||||||
SendCompressed bool
|
SendCompressed bool
|
||||||
SendEmbeddedData bool
|
SendEmbeddedData bool
|
||||||
SendSaved bool
|
SendSaved bool
|
||||||
|
|
||||||
BandwidthLimit bandwidthlimit.Config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SenderConfig) Validate() error {
|
func (c *SenderConfig) Validate() error {
|
||||||
@@ -47,9 +44,6 @@ func (c *SenderConfig) Validate() error {
|
|||||||
if _, err := StepHoldTag(c.JobID); err != nil {
|
if _, err := StepHoldTag(c.JobID); err != nil {
|
||||||
return fmt.Errorf("JobID cannot be used for hold tag: %s", err)
|
return fmt.Errorf("JobID cannot be used for hold tag: %s", err)
|
||||||
}
|
}
|
||||||
if err := bandwidthlimit.ValidateConfig(c.BandwidthLimit); err != nil {
|
|
||||||
return errors.Wrap(err, "`BandwidthLimit` field invalid")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,21 +54,16 @@ type Sender struct {
|
|||||||
FSFilter zfs.DatasetFilter
|
FSFilter zfs.DatasetFilter
|
||||||
jobId JobID
|
jobId JobID
|
||||||
config SenderConfig
|
config SenderConfig
|
||||||
bwLimit bandwidthlimit.Wrapper
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSender(conf SenderConfig) *Sender {
|
func NewSender(conf SenderConfig) *Sender {
|
||||||
if err := conf.Validate(); err != nil {
|
if err := conf.Validate(); err != nil {
|
||||||
panic("invalid config" + err.Error())
|
panic("invalid config" + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
ratelimiter := bandwidthlimit.WrapperFromConfig(conf.BandwidthLimit)
|
|
||||||
|
|
||||||
return &Sender{
|
return &Sender{
|
||||||
FSFilter: conf.FSF,
|
FSFilter: conf.FSF,
|
||||||
jobId: conf.JobID,
|
jobId: conf.JobID,
|
||||||
config: conf,
|
config: conf,
|
||||||
bwLimit: ratelimiter,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -312,16 +301,12 @@ func (s *Sender) Send(ctx context.Context, r *pdu.SendReq) (*pdu.SendRes, io.Rea
|
|||||||
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
abstractionsCacheSingleton.TryBatchDestroy(ctx, s.jobId, sendArgs.FS, destroyTypes, keep, check)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var sendStream io.ReadCloser
|
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||||
sendStream, err = zfs.ZFSSend(ctx, sendArgs)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
// it's ok to not destroy the abstractions we just created here, a new send attempt will take care of it
|
||||||
return nil, nil, errors.Wrap(err, "zfs send failed")
|
return nil, nil, errors.Wrap(err, "zfs send failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply rate limit
|
|
||||||
sendStream = s.bwLimit.WrapReadCloser(sendStream)
|
|
||||||
|
|
||||||
return res, sendStream, nil
|
return res, sendStream, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,8 +439,6 @@ type ReceiverConfig struct {
|
|||||||
|
|
||||||
InheritProperties []zfsprop.Property
|
InheritProperties []zfsprop.Property
|
||||||
OverrideProperties map[zfsprop.Property]string
|
OverrideProperties map[zfsprop.Property]string
|
||||||
|
|
||||||
BandwidthLimit bandwidthlimit.Config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ReceiverConfig) copyIn() {
|
func (c *ReceiverConfig) copyIn() {
|
||||||
@@ -492,11 +475,6 @@ func (c *ReceiverConfig) Validate() error {
|
|||||||
if c.RootWithoutClientComponent.Length() <= 0 {
|
if c.RootWithoutClientComponent.Length() <= 0 {
|
||||||
return errors.New("RootWithoutClientComponent must not be an empty dataset path")
|
return errors.New("RootWithoutClientComponent must not be an empty dataset path")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := bandwidthlimit.ValidateConfig(c.BandwidthLimit); err != nil {
|
|
||||||
return errors.Wrap(err, "`BandwidthLimit` field invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -506,8 +484,6 @@ type Receiver struct {
|
|||||||
|
|
||||||
conf ReceiverConfig // validated
|
conf ReceiverConfig // validated
|
||||||
|
|
||||||
bwLimit bandwidthlimit.Wrapper
|
|
||||||
|
|
||||||
recvParentCreationMtx *chainlock.L
|
recvParentCreationMtx *chainlock.L
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,7 +495,6 @@ func NewReceiver(config ReceiverConfig) *Receiver {
|
|||||||
return &Receiver{
|
return &Receiver{
|
||||||
conf: config,
|
conf: config,
|
||||||
recvParentCreationMtx: chainlock.New(),
|
recvParentCreationMtx: chainlock.New(),
|
||||||
bwLimit: bandwidthlimit.WrapperFromConfig(config.BandwidthLimit),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,9 +787,6 @@ func (s *Receiver) Receive(ctx context.Context, req *pdu.ReceiveReq, receive io.
|
|||||||
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
|
return nil, errors.Wrap(err, "cannot determine whether we can use resumable send & recv")
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply rate limit
|
|
||||||
receive = s.bwLimit.WrapReadCloser(receive)
|
|
||||||
|
|
||||||
var peek bytes.Buffer
|
var peek bytes.Buffer
|
||||||
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
var MaxPeek = envconst.Int64("ZREPL_ENDPOINT_RECV_PEEK_SIZE", 1<<20)
|
||||||
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
log.WithField("max_peek_bytes", MaxPeek).Info("peeking incoming stream")
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ require (
|
|||||||
github.com/golang/protobuf v1.4.3
|
github.com/golang/protobuf v1.4.3
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8
|
||||||
github.com/juju/ratelimit v1.0.1
|
|
||||||
github.com/kisielk/gotool v1.0.0 // indirect
|
github.com/kisielk/gotool v1.0.0 // indirect
|
||||||
github.com/kr/pretty v0.1.0
|
github.com/kr/pretty v0.1.0
|
||||||
github.com/leodido/go-urn v1.2.1 // indirect
|
github.com/leodido/go-urn v1.2.1 // indirect
|
||||||
|
|||||||
@@ -164,8 +164,6 @@ github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8 h1:+dKzeuiDYbD/Cfi/s
|
|||||||
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
github.com/jinzhu/copier v0.0.0-20170922082739-db4671f3a9b8/go.mod h1:yL958EeXv8Ylng6IfnvG4oflryUi3vgA3xPs9hmII1s=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||||
github.com/juju/ratelimit v1.0.1 h1:+7AIFJVQ0EQgq/K9+0Krm7m530Du7tIz0METWzN0RgY=
|
|
||||||
github.com/juju/ratelimit v1.0.1/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||||
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
|
||||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM=
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import (
|
|||||||
func init() {
|
func init() {
|
||||||
cli.AddSubcommand(daemon.DaemonCmd)
|
cli.AddSubcommand(daemon.DaemonCmd)
|
||||||
cli.AddSubcommand(status.Subcommand)
|
cli.AddSubcommand(status.Subcommand)
|
||||||
cli.AddSubcommand(client.SignalCmd)
|
cli.AddSubcommand(client.TriggerCmd)
|
||||||
|
cli.AddSubcommand(client.ResetCmd)
|
||||||
|
cli.AddSubcommand(client.WaitCmd)
|
||||||
cli.AddSubcommand(client.StdinserverCmd)
|
cli.AddSubcommand(client.StdinserverCmd)
|
||||||
cli.AddSubcommand(client.ConfigcheckCmd)
|
cli.AddSubcommand(client.ConfigcheckCmd)
|
||||||
cli.AddSubcommand(client.VersionCmd)
|
cli.AddSubcommand(client.VersionCmd)
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
FROM debian:latest
|
FROM debian:latest
|
||||||
|
|
||||||
# binutils are for cross-compilation to work in bullseye
|
|
||||||
RUN apt-get update && apt-get install -y \
|
RUN apt-get update && apt-get install -y \
|
||||||
build-essential \
|
build-essential \
|
||||||
devscripts \
|
devscripts \
|
||||||
dh-exec \
|
dh-exec
|
||||||
binutils-aarch64-linux-gnu \
|
|
||||||
binutils-arm-linux-gnueabihf \
|
|
||||||
binutils-i686-linux-gnu
|
|
||||||
|
|
||||||
RUN mkdir -p /build/src && chmod -R 0777 /build
|
RUN mkdir -p /build/src && chmod -R 0777 /build
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ func ReceiveForceRollbackWorksUnencrypted(ctx *platformtest.Context) {
|
|||||||
|
|
||||||
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
sendStream, err := zfs.ZFSSend(ctx, sendArgs)
|
||||||
require.NoError(ctx, err)
|
require.NoError(ctx, err)
|
||||||
defer sendStream.Close()
|
|
||||||
|
|
||||||
recvOpts := zfs.RecvOptions{
|
recvOpts := zfs.RecvOptions{
|
||||||
RollbackAndForceRecv: true,
|
RollbackAndForceRecv: true,
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
"github.com/kr/pretty"
|
||||||
@@ -21,7 +20,6 @@ import (
|
|||||||
"github.com/zrepl/zrepl/replication/logic"
|
"github.com/zrepl/zrepl/replication/logic"
|
||||||
"github.com/zrepl/zrepl/replication/logic/pdu"
|
"github.com/zrepl/zrepl/replication/logic/pdu"
|
||||||
"github.com/zrepl/zrepl/replication/report"
|
"github.com/zrepl/zrepl/replication/report"
|
||||||
"github.com/zrepl/zrepl/util/bandwidthlimit"
|
|
||||||
"github.com/zrepl/zrepl/util/limitio"
|
"github.com/zrepl/zrepl/util/limitio"
|
||||||
"github.com/zrepl/zrepl/util/nodefault"
|
"github.com/zrepl/zrepl/util/nodefault"
|
||||||
"github.com/zrepl/zrepl/zfs"
|
"github.com/zrepl/zrepl/zfs"
|
||||||
@@ -68,7 +66,6 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
FSF: i.sfilter.AsFilter(),
|
FSF: i.sfilter.AsFilter(),
|
||||||
Encrypt: &nodefault.Bool{B: false},
|
Encrypt: &nodefault.Bool{B: false},
|
||||||
JobID: i.sjid,
|
JobID: i.sjid,
|
||||||
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
|
||||||
}
|
}
|
||||||
if i.senderConfigHook != nil {
|
if i.senderConfigHook != nil {
|
||||||
i.senderConfigHook(&senderConfig)
|
i.senderConfigHook(&senderConfig)
|
||||||
@@ -78,7 +75,6 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
JobID: i.rjid,
|
JobID: i.rjid,
|
||||||
AppendClientIdentity: false,
|
AppendClientIdentity: false,
|
||||||
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
RootWithoutClientComponent: mustDatasetPath(i.rfsRoot),
|
||||||
BandwidthLimit: bandwidthlimit.NoLimitConfig(),
|
|
||||||
}
|
}
|
||||||
if i.receiverConfigHook != nil {
|
if i.receiverConfigHook != nil {
|
||||||
i.receiverConfigHook(&receiverConfig)
|
i.receiverConfigHook(&receiverConfig)
|
||||||
@@ -94,7 +90,6 @@ func (i replicationInvocation) Do(ctx *platformtest.Context) *report.Report {
|
|||||||
ReplicationConfig: &pdu.ReplicationConfig{
|
ReplicationConfig: &pdu.ReplicationConfig{
|
||||||
Protection: i.guarantee,
|
Protection: i.guarantee,
|
||||||
},
|
},
|
||||||
SizeEstimationConcurrency: 1,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
report, wait := replication.Do(
|
report, wait := replication.Do(
|
||||||
@@ -1059,9 +1054,6 @@ func ReplicationPropertyReplicationWorks(ctx *platformtest.Context) {
|
|||||||
rep := fsByName[fs]
|
rep := fsByName[fs]
|
||||||
require.Len(ctx, rep.Steps, 1)
|
require.Len(ctx, rep.Steps, 1)
|
||||||
require.Nil(ctx, rep.PlanError)
|
require.Nil(ctx, rep.PlanError)
|
||||||
if rep.StepError != nil && strings.Contains(rep.StepError.Error(), "invalid option 'x'") {
|
|
||||||
ctx.SkipNow() // XXX feature detection
|
|
||||||
}
|
|
||||||
require.Nil(ctx, rep.StepError)
|
require.Nil(ctx, rep.StepError)
|
||||||
require.Len(ctx, rep.Steps, 1)
|
require.Len(ctx, rep.Steps, 1)
|
||||||
require.Equal(ctx, 1, rep.CurrentStep)
|
require.Equal(ctx, 1, rep.CurrentStep)
|
||||||
|
|||||||
@@ -57,11 +57,6 @@ func (k KeepLastN) KeepRule(snaps []Snapshot) (destroyList []Snapshot) {
|
|||||||
// then lexicographically descending (e.g. b, a)
|
// then lexicographically descending (e.g. b, a)
|
||||||
return strings.Compare(matching[i].Name(), matching[j].Name()) == 1
|
return strings.Compare(matching[i].Name(), matching[j].Name()) == 1
|
||||||
})
|
})
|
||||||
|
destroyList = append(destroyList, matching[k.n:]...)
|
||||||
n := k.n
|
|
||||||
if n > len(matching) {
|
|
||||||
n = len(matching)
|
|
||||||
}
|
|
||||||
destroyList = append(destroyList, matching[n:]...)
|
|
||||||
return destroyList
|
return destroyList
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,19 +83,6 @@ func TestKeepLastN(t *testing.T) {
|
|||||||
"b1": true,
|
"b1": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"keep_more_than_matching": {
|
|
||||||
inputs: []Snapshot{
|
|
||||||
stubSnap{"a1", false, o(10)},
|
|
||||||
stubSnap{"b1", false, o(11)},
|
|
||||||
stubSnap{"a2", false, o(12)},
|
|
||||||
},
|
|
||||||
rules: []KeepRule{
|
|
||||||
MustKeepLastN(3, "a"),
|
|
||||||
},
|
|
||||||
expDestroy: map[string]bool{
|
|
||||||
"b1": true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testTable(tcs, t)
|
testTable(tcs, t)
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ func (s *Server) serveConnRequest(ctx context.Context, endpoint string, c *strea
|
|||||||
err := c.SendStream(ctx, sendStream, ZFSStream)
|
err := c.SendStream(ctx, sendStream, ZFSStream)
|
||||||
closeErr := sendStream.Close()
|
closeErr := sendStream.Close()
|
||||||
if closeErr != nil {
|
if closeErr != nil {
|
||||||
s.log.WithError(closeErr).Error("cannot close send stream")
|
s.log.WithError(err).Error("cannot close send stream")
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.WithError(err).Error("cannot write send stream")
|
s.log.WithError(err).Error("cannot write send stream")
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ package transportmux
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
@@ -16,7 +15,6 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/zrepl/zrepl/daemon/logging"
|
"github.com/zrepl/zrepl/daemon/logging"
|
||||||
"github.com/zrepl/zrepl/logger"
|
"github.com/zrepl/zrepl/logger"
|
||||||
"github.com/zrepl/zrepl/transport"
|
"github.com/zrepl/zrepl/transport"
|
||||||
@@ -153,7 +151,6 @@ func Demux(ctx context.Context, rawListener transport.AuthenticatedListener, lab
|
|||||||
|
|
||||||
rawConn, err := rawListener.Accept(ctx)
|
rawConn, err := rawListener.Accept(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "transportmux.Demux: rawListener.Accept() returned error: %T %s\n%s\n", err, err, pretty.Sprint(err))
|
|
||||||
if ctx.Err() != nil {
|
if ctx.Err() != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type HandshakeMessage struct {
|
type HandshakeMessage struct {
|
||||||
@@ -96,7 +93,6 @@ func (m *HandshakeMessage) Encode() ([]byte, error) {
|
|||||||
func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
|
func (m *HandshakeMessage) DecodeReader(r io.Reader, maxLen int) error {
|
||||||
var lenAndSpace [11]byte
|
var lenAndSpace [11]byte
|
||||||
if _, err := io.ReadFull(r, lenAndSpace[:]); err != nil {
|
if _, err := io.ReadFull(r, lenAndSpace[:]); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "HandshakeMessage.DecodeReader error: %T\n%s", err, pretty.Sprint(err))
|
|
||||||
return hsIOErr(err, "error reading protocol banner length: %s", err)
|
return hsIOErr(err, "error reading protocol banner length: %s", err)
|
||||||
}
|
}
|
||||||
if !utf8.Valid(lenAndSpace[:]) {
|
if !utf8.Valid(lenAndSpace[:]) {
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package versionhandshake
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/transport"
|
"github.com/zrepl/zrepl/transport"
|
||||||
@@ -25,7 +23,6 @@ func (c HandshakeConnecter) Connect(ctx context.Context) (transport.Wire, error)
|
|||||||
dl = time.Now().Add(c.timeout)
|
dl = time.Now().Add(c.timeout)
|
||||||
}
|
}
|
||||||
if err := DoHandshakeCurrentVersion(conn, dl); err != nil {
|
if err := DoHandshakeCurrentVersion(conn, dl); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "HandshakeConnecter error: %T\n\t%s\n\t%v\n\t%#v\n\n", err, err, err, err)
|
|
||||||
conn.Close()
|
conn.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,10 +93,13 @@ func TestIPMap(t *testing.T) {
|
|||||||
expect: map[string]testCaseExpect{
|
expect: map[string]testCaseExpect{
|
||||||
"10.1.2.3": {expectNoMapping: true},
|
"10.1.2.3": {expectNoMapping: true},
|
||||||
"192.168.23.1": {expectIdent: "db-192.168.23.1"},
|
"192.168.23.1": {expectIdent: "db-192.168.23.1"},
|
||||||
"192.168.42.1": {expectIdent: "web-192.168.42.1"},
|
|
||||||
"192.168.23.23": {expectIdent: "db-twentythree"},
|
"192.168.23.23": {expectIdent: "db-twentythree"},
|
||||||
|
"192.168.023.001": {expectIdent: "db-192.168.23.1"},
|
||||||
"10.1.4.5": {expectIdent: "my-10.1.4.5-server"},
|
"10.1.4.5": {expectIdent: "my-10.1.4.5-server"},
|
||||||
|
|
||||||
|
// normalization
|
||||||
|
"192.168.42.1": {expectIdent: "web-192.168.42.1"},
|
||||||
|
"192.168.042.001": {expectIdent: "web-192.168.42.1"},
|
||||||
// v6 matching
|
// v6 matching
|
||||||
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
|
"fe80::23:42%eth1": {expectIdent: "san-fe80::23:42-eth1"},
|
||||||
"fe80::23:42%eth2": {expectNoMapping: true},
|
"fe80::23:42%eth2": {expectNoMapping: true},
|
||||||
@@ -176,8 +179,7 @@ func TestIPMap(t *testing.T) {
|
|||||||
for input, expect := range c.expect {
|
for input, expect := range c.expect {
|
||||||
// reuse newIPMapEntry to parse test case input
|
// reuse newIPMapEntry to parse test case input
|
||||||
// "test" is not used during testing but must not be empty.
|
// "test" is not used during testing but must not be empty.
|
||||||
ipMapEntry, err := newIPMapEntry(input, "test")
|
ipMapEntry, _ := newIPMapEntry(input, "test")
|
||||||
require.NoError(t, err)
|
|
||||||
ones, bits := ipMapEntry.subnet.Mask.Size()
|
ones, bits := ipMapEntry.subnet.Mask.Size()
|
||||||
require.Equal(t, bits, net.IPv6len*8, "and we know ipMapEntry always expands its IPs to 16bytes")
|
require.Equal(t, bits, net.IPv6len*8, "and we know ipMapEntry always expands its IPs to 16bytes")
|
||||||
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
|
require.Equal(t, ones, net.IPv6len*8, "test case addresses must be fully specified")
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package tls
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
@@ -50,7 +48,6 @@ func TLSConnecterFromConfig(in *config.TLSConnect) (*TLSConnecter, error) {
|
|||||||
func (c *TLSConnecter) Connect(dialCtx context.Context) (transport.Wire, error) {
|
func (c *TLSConnecter) Connect(dialCtx context.Context) (transport.Wire, error) {
|
||||||
conn, err := c.dialer.DialContext(dialCtx, "tcp", c.Address)
|
conn, err := c.dialer.DialContext(dialCtx, "tcp", c.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "tls connecter error %T\n\t%s\n\t%v\n\t%#v\n\n", err, err, err, err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
tcpConn := conn.(*net.TCPConn)
|
tcpConn := conn.(*net.TCPConn)
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"flag"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
|
||||||
"github.com/zrepl/zrepl/transport/tls"
|
|
||||||
)
|
|
||||||
|
|
||||||
var servConf = config.TLSServe{
|
|
||||||
ServeCommon: config.ServeCommon{
|
|
||||||
Type: "tls",
|
|
||||||
},
|
|
||||||
HandshakeTimeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
var clientConf = config.TLSConnect{
|
|
||||||
ConnectCommon: config.ConnectCommon{
|
|
||||||
Type: "",
|
|
||||||
},
|
|
||||||
Address: "",
|
|
||||||
Ca: "",
|
|
||||||
Cert: "",
|
|
||||||
Key: "",
|
|
||||||
ServerCN: "",
|
|
||||||
DialTimeout: 10 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
var ca string
|
|
||||||
var mode string
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
|
|
||||||
flag.StringVar(&mode, "mode", "", "server|client")
|
|
||||||
|
|
||||||
flag.StringVar(&ca, "ca", "", "path")
|
|
||||||
|
|
||||||
flag.StringVar(&servConf.Listen, "serve.listen", "", "")
|
|
||||||
flag.StringVar(&servConf.Cert, "serve.cert", "", "path")
|
|
||||||
flag.StringVar(&servConf.Key, "serve.key", "", "path")
|
|
||||||
var clientCN string
|
|
||||||
flag.StringVar(&clientCN, "serve.client_cn", "", "")
|
|
||||||
|
|
||||||
flag.StringVar(&clientConf.Address, "client.address", "", "")
|
|
||||||
flag.StringVar(&clientConf.Cert, "client.cert", "", "path")
|
|
||||||
flag.StringVar(&clientConf.Key, "client.key", "", "path")
|
|
||||||
flag.StringVar(&clientConf.ServerCN, "client.server_cn", "", "")
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
servConf.ClientCNs = append(servConf.ClientCNs, clientCN)
|
|
||||||
|
|
||||||
servConf.Ca = ca
|
|
||||||
clientConf.Ca = ca
|
|
||||||
|
|
||||||
switch mode {
|
|
||||||
case "server":
|
|
||||||
server()
|
|
||||||
case "client":
|
|
||||||
client()
|
|
||||||
default:
|
|
||||||
panic(mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func server() {
|
|
||||||
|
|
||||||
servFactory, err := tls.TLSListenerFactoryFromConfig(nil, &servConf)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
listener, err := servFactory()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
for {
|
|
||||||
conn, err := listener.Accept(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("accept error: %s", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
log.Printf("handling connection %s", conn.RemoteAddr())
|
|
||||||
_, err = io.Copy(conn, strings.NewReader("here is the server\n"))
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s: respond to client error: %s", conn.RemoteAddr(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = conn.CloseWrite()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s: failed to close write connection: err", conn.RemoteAddr(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("%s: waiting for client to close connection", conn.RemoteAddr())
|
|
||||||
|
|
||||||
_, err = io.Copy(io.Discard, conn)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("%s: error draining client connection: %s", conn.RemoteAddr(), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("%s: done", conn.RemoteAddr())
|
|
||||||
return
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func client() {
|
|
||||||
connecter, err := tls.TLSConnecterFromConfig(&clientConf)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
conn, err := connecter.Connect(ctx)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(os.Stdout, conn)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,10 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kr/pretty"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/config"
|
"github.com/zrepl/zrepl/config"
|
||||||
@@ -70,7 +68,6 @@ type tlsAuthListener struct {
|
|||||||
func (l tlsAuthListener) Accept(ctx context.Context) (*transport.AuthConn, error) {
|
func (l tlsAuthListener) Accept(ctx context.Context) (*transport.AuthConn, error) {
|
||||||
tcpConn, tlsConn, cn, err := l.ClientAuthListener.Accept()
|
tcpConn, tlsConn, cn, err := l.ClientAuthListener.Accept()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "tlsAuthListener.Accept: l.ClientAuthListener.Accept returned error %T %s\n%s\n", err, err, pretty.Sprint(err))
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, ok := l.clientCNs[cn]; !ok {
|
if _, ok := l.clientCNs[cn]; !ok {
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
package bandwidthlimit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/juju/ratelimit"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Wrapper interface {
|
|
||||||
WrapReadCloser(io.ReadCloser) io.ReadCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
|
||||||
// Units in this struct are in _bytes_.
|
|
||||||
|
|
||||||
Max int64 // < 0 means no limit, BucketCapacity is irrelevant then
|
|
||||||
BucketCapacity int64
|
|
||||||
}
|
|
||||||
|
|
||||||
func NoLimitConfig() Config {
|
|
||||||
return Config{
|
|
||||||
Max: -1,
|
|
||||||
BucketCapacity: -1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValidateConfig(conf Config) error {
|
|
||||||
if conf.BucketCapacity == 0 {
|
|
||||||
return errors.New("BucketCapacity must not be zero")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func WrapperFromConfig(conf Config) Wrapper {
|
|
||||||
if err := ValidateConfig(conf); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conf.Max < 0 {
|
|
||||||
return noLimit{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &withLimit{
|
|
||||||
bucket: ratelimit.NewBucketWithRate(float64(conf.Max), conf.BucketCapacity),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type noLimit struct{}
|
|
||||||
|
|
||||||
func (_ noLimit) WrapReadCloser(rc io.ReadCloser) io.ReadCloser { return rc }
|
|
||||||
|
|
||||||
type withLimit struct {
|
|
||||||
bucket *ratelimit.Bucket
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *withLimit) WrapReadCloser(rc io.ReadCloser) io.ReadCloser {
|
|
||||||
return WrapReadCloser(rc, l.bucket)
|
|
||||||
}
|
|
||||||
|
|
||||||
type withLimitReadCloser struct {
|
|
||||||
orig io.Closer
|
|
||||||
limited io.Reader
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *withLimitReadCloser) Read(buf []byte) (int, error) {
|
|
||||||
return r.limited.Read(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *withLimitReadCloser) Close() error {
|
|
||||||
return r.orig.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func WrapReadCloser(rc io.ReadCloser, bucket *ratelimit.Bucket) io.ReadCloser {
|
|
||||||
return &withLimitReadCloser{
|
|
||||||
limited: ratelimit.Reader(rc, bucket),
|
|
||||||
orig: rc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package bandwidthlimit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNoLimitConfig(t *testing.T) {
|
|
||||||
|
|
||||||
conf := NoLimitConfig()
|
|
||||||
|
|
||||||
err := ValidateConfig(conf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
require.NotPanics(t, func() {
|
|
||||||
_ = WrapperFromConfig(conf)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
package datasizeunit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Bits struct {
|
|
||||||
bits float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b Bits) ToBits() float64 { return b.bits }
|
|
||||||
func (b Bits) ToBytes() float64 { return b.bits / 8 }
|
|
||||||
func FromBytesInt64(i int64) Bits { return Bits{float64(i) * 8} }
|
|
||||||
|
|
||||||
var datarateRegex = regexp.MustCompile(`^([-0-9\.]*)\s*(bit|(|K|Ki|M|Mi|G|Gi|T|Ti)([bB]))$`)
|
|
||||||
|
|
||||||
func (r *Bits) UnmarshalYAML(u func(interface{}, bool) error) (_ error) {
|
|
||||||
|
|
||||||
var s string
|
|
||||||
err := u(&s, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
genericErr := func(err error) error {
|
|
||||||
var buf strings.Builder
|
|
||||||
fmt.Fprintf(&buf, "cannot parse %q using regex %s", s, datarateRegex)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(&buf, ": %s", err)
|
|
||||||
}
|
|
||||||
return errors.New(buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
match := datarateRegex.FindStringSubmatch(s)
|
|
||||||
if match == nil {
|
|
||||||
return genericErr(nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
bps, err := strconv.ParseFloat(match[1], 64)
|
|
||||||
if err != nil {
|
|
||||||
return genericErr(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if match[2] == "bit" {
|
|
||||||
if math.Round(bps) != bps {
|
|
||||||
return genericErr(fmt.Errorf("unit bit must be an integer value"))
|
|
||||||
}
|
|
||||||
r.bits = bps
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
factorMap := map[string]uint64{
|
|
||||||
"": 1,
|
|
||||||
|
|
||||||
"K": 1e3,
|
|
||||||
"M": 1e6,
|
|
||||||
"G": 1e9,
|
|
||||||
"T": 1e12,
|
|
||||||
|
|
||||||
"Ki": 1 << 10,
|
|
||||||
"Mi": 1 << 20,
|
|
||||||
"Gi": 1 << 30,
|
|
||||||
"Ti": 1 << 40,
|
|
||||||
}
|
|
||||||
factor, ok := factorMap[match[3]]
|
|
||||||
if !ok {
|
|
||||||
panic(match)
|
|
||||||
}
|
|
||||||
|
|
||||||
baseUnitFactorMap := map[string]uint64{
|
|
||||||
"b": 1,
|
|
||||||
"B": 8,
|
|
||||||
}
|
|
||||||
baseUnitFactor, ok := baseUnitFactorMap[match[4]]
|
|
||||||
if !ok {
|
|
||||||
panic(match)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.bits = bps * float64(factor) * float64(baseUnitFactor)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package datasizeunit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"github.com/zrepl/yaml-config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBits(t *testing.T) {
|
|
||||||
|
|
||||||
tcs := []struct {
|
|
||||||
input string
|
|
||||||
expectRate float64
|
|
||||||
expectErr string
|
|
||||||
}{
|
|
||||||
{`23 bit`, 23, ""}, // bit special case works
|
|
||||||
{`23bit`, 23, ""}, // also without space
|
|
||||||
|
|
||||||
{`10MiB`, 10 * (1 << 20) * 8, ""}, // integer unit without space
|
|
||||||
{`10 MiB`, 8 * 10 * (1 << 20), ""}, // integer unit with space
|
|
||||||
|
|
||||||
{`10.5 Kib`, 10.5 * (1 << 10), ""}, // floating point with bit unit works with space
|
|
||||||
{`10.5Kib`, 10.5 * (1 << 10), ""}, // floating point with bit unit works without space
|
|
||||||
|
|
||||||
// unit checks
|
|
||||||
{`1 bit`, 1, ""},
|
|
||||||
{`1 B`, 1 * 8, ""},
|
|
||||||
{`1 Kb`, 1e3, ""},
|
|
||||||
{`1 Kib`, 1 << 10, ""},
|
|
||||||
{`1 Mb`, 1e6, ""},
|
|
||||||
{`1 Mib`, 1 << 20, ""},
|
|
||||||
{`1 Gb`, 1e9, ""},
|
|
||||||
{`1 Gib`, 1 << 30, ""},
|
|
||||||
{`1 Tb`, 1e12, ""},
|
|
||||||
{`1 Tib`, 1 << 40, ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tcs {
|
|
||||||
t.Run(tc.input, func(t *testing.T) {
|
|
||||||
|
|
||||||
var bits Bits
|
|
||||||
err := yaml.Unmarshal([]byte(tc.input), &bits)
|
|
||||||
if tc.expectErr != "" {
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.Regexp(t, tc.expectErr, err.Error())
|
|
||||||
assert.Zero(t, bits.bits)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.expectRate, bits.bits)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+9
-4
@@ -26,6 +26,11 @@ import (
|
|||||||
"github.com/zrepl/zrepl/zfs/zfscmd"
|
"github.com/zrepl/zrepl/zfs/zfscmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ZFSSendPipeCapacityHint = int(envconst.Int64("ZFS_SEND_PIPE_CAPACITY_HINT", 1<<25))
|
||||||
|
ZFSRecvPipeCapacityHint = int(envconst.Int64("ZFS_RECV_PIPE_CAPACITY_HINT", 1<<25))
|
||||||
|
)
|
||||||
|
|
||||||
type DatasetPath struct {
|
type DatasetPath struct {
|
||||||
comps []string
|
comps []string
|
||||||
}
|
}
|
||||||
@@ -323,8 +328,8 @@ func absVersion(fs string, v *ZFSSendArgVersion) (full string, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
|
func pipeWithCapacityHint(capacity int) (r, w *os.File, err error) {
|
||||||
if capacity < 0 {
|
if capacity <= 0 {
|
||||||
panic(fmt.Sprintf("capacity must be non-negative, got %v", capacity))
|
panic(fmt.Sprintf("capacity must be positive %v", capacity))
|
||||||
}
|
}
|
||||||
stdoutReader, stdoutWriter, err := os.Pipe()
|
stdoutReader, stdoutWriter, err := os.Pipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -853,7 +858,7 @@ func ZFSSend(ctx context.Context, sendArgs ZFSSendArgsValidated) (*SendStream, e
|
|||||||
ctx, cancel := context.WithCancel(ctx)
|
ctx, cancel := context.WithCancel(ctx)
|
||||||
|
|
||||||
// setup stdout with an os.Pipe to control pipe buffer size
|
// setup stdout with an os.Pipe to control pipe buffer size
|
||||||
stdoutReader, stdoutWriter, err := pipeWithCapacityHint(getPipeCapacityHint("ZFS_SEND_PIPE_CAPACITY_HINT"))
|
stdoutReader, stdoutWriter, err := pipeWithCapacityHint(ZFSSendPipeCapacityHint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cancel()
|
cancel()
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1155,7 +1160,7 @@ func ZFSRecv(ctx context.Context, fs string, v *ZFSSendArgVersion, stream io.Rea
|
|||||||
|
|
||||||
stderr := bytes.NewBuffer(make([]byte, 0, RecvStderrBufSiz))
|
stderr := bytes.NewBuffer(make([]byte, 0, RecvStderrBufSiz))
|
||||||
|
|
||||||
stdin, stdinWriter, err := pipeWithCapacityHint(getPipeCapacityHint("ZFS_RECV_PIPE_CAPACITY_HINT"))
|
stdin, stdinWriter, err := pipeWithCapacityHint(ZFSRecvPipeCapacityHint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -7,14 +7,10 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPipeCapacityHint(envvar string) int {
|
|
||||||
return 0 // not supported
|
|
||||||
}
|
|
||||||
|
|
||||||
var zfsPipeCapacityNotSupported sync.Once
|
var zfsPipeCapacityNotSupported sync.Once
|
||||||
|
|
||||||
func trySetPipeCapacity(p *os.File, capacity int) {
|
func trySetPipeCapacity(p *os.File, capacity int) {
|
||||||
if debugEnabled && capacity != 0 {
|
if debugEnabled {
|
||||||
zfsPipeCapacityNotSupported.Do(func() {
|
zfsPipeCapacityNotSupported.Do(func() {
|
||||||
debug("trySetPipeCapacity error: OS does not support setting pipe capacity")
|
debug("trySetPipeCapacity error: OS does not support setting pipe capacity")
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,33 +3,11 @@ package zfs
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/zrepl/zrepl/util/envconst"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func getPipeCapacityHint(envvar string) int {
|
|
||||||
var capacity int64 = 1 << 25
|
|
||||||
|
|
||||||
// Work around a race condition in Linux >= 5.8 related to pipe resizing.
|
|
||||||
// https://github.com/zrepl/zrepl/issues/424#issuecomment-800370928
|
|
||||||
// https://bugzilla.kernel.org/show_bug.cgi?id=212295
|
|
||||||
if _, err := os.Stat("/proc/sys/fs/pipe-max-size"); err == nil {
|
|
||||||
if dat, err := ioutil.ReadFile("/proc/sys/fs/pipe-max-size"); err == nil {
|
|
||||||
if capacity, err = strconv.ParseInt(strings.TrimSpace(string(dat)), 10, 64); err != nil {
|
|
||||||
capacity = 1 << 25
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return int(envconst.Int64(envvar, capacity))
|
|
||||||
}
|
|
||||||
|
|
||||||
func trySetPipeCapacity(p *os.File, capacity int) {
|
func trySetPipeCapacity(p *os.File, capacity int) {
|
||||||
res, err := unix.FcntlInt(p.Fd(), unix.F_SETPIPE_SZ, capacity)
|
res, err := unix.FcntlInt(p.Fd(), unix.F_SETPIPE_SZ, capacity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user