Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

storage/raft: Add retry_join_as_non_voter config option #18030

Merged
merged 1 commit into from Nov 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/18030.txt
@@ -0,0 +1,3 @@
```release-note:improvement
storage/raft: Add `retry_join_as_non_voter` config option.
```
32 changes: 32 additions & 0 deletions physical/raft/raft.go
Expand Up @@ -43,6 +43,10 @@ const (

// EnvVaultRaftPath is used to fetch the path where Raft data is stored from the environment.
EnvVaultRaftPath = "VAULT_RAFT_PATH"

// EnvVaultRaftNonVoter is used to override the non_voter config option, telling Vault to join as a non-voter (i.e. read replica).
EnvVaultRaftNonVoter = "VAULT_RAFT_RETRY_JOIN_AS_NON_VOTER"
raftNonVoterConfigKey = "retry_join_as_non_voter"
)

var getMmapFlags = func(string) int { return 0 }
Expand Down Expand Up @@ -172,6 +176,10 @@ type RaftBackend struct {
// redundancyZone specifies a redundancy zone for autopilot.
redundancyZone string

// nonVoter specifies whether the node should join the cluster as a non-voter. Non-voters get
// replicated to and can serve reads, but do not take part in leader elections.
nonVoter bool

effectiveSDKVersion string
}

Expand Down Expand Up @@ -473,6 +481,22 @@ func NewRaftBackend(conf map[string]string, logger log.Logger) (physical.Backend
}
}

var nonVoter bool
if v := os.Getenv(EnvVaultRaftNonVoter); v != "" {
// Consistent with handling of other raft boolean env vars
// VAULT_RAFT_AUTOPILOT_DISABLE and VAULT_RAFT_FREELIST_SYNC
nonVoter = true
} else if v, ok := conf[raftNonVoterConfigKey]; ok {
nonVoter, err = strconv.ParseBool(v)
if err != nil {
return nil, fmt.Errorf("failed to parse %s config value %q as a boolean: %w", raftNonVoterConfigKey, v, err)
}
}

if nonVoter && conf["retry_join"] == "" {
return nil, fmt.Errorf("setting %s to true is only valid if at least one retry_join stanza is specified", raftNonVoterConfigKey)
}

return &RaftBackend{
logger: logger,
fsm: fsm,
Expand All @@ -489,6 +513,7 @@ func NewRaftBackend(conf map[string]string, logger log.Logger) (physical.Backend
autopilotReconcileInterval: reconcileInterval,
autopilotUpdateInterval: updateInterval,
redundancyZone: conf["autopilot_redundancy_zone"],
nonVoter: nonVoter,
upgradeVersion: upgradeVersion,
}, nil
}
Expand Down Expand Up @@ -554,6 +579,13 @@ func (b *RaftBackend) RedundancyZone() string {
return b.redundancyZone
}

func (b *RaftBackend) NonVoter() bool {
b.l.RLock()
defer b.l.RUnlock()

return b.nonVoter
}

func (b *RaftBackend) EffectiveVersion() string {
b.l.RLock()
defer b.l.RUnlock()
Expand Down
66 changes: 66 additions & 0 deletions physical/raft/raft_test.go
Expand Up @@ -249,6 +249,72 @@ func TestRaft_ParseAutopilotUpgradeVersion(t *testing.T) {
}
}

func TestRaft_ParseNonVoter(t *testing.T) {
p := func(s string) *string {
return &s
}

for _, retryJoinConf := range []string{"", "not-empty"} {
t.Run(retryJoinConf, func(t *testing.T) {
for name, tc := range map[string]struct {
envValue *string
configValue *string
expectNonVoter bool
invalidNonVoterValue bool
}{
"valid false": {nil, p("false"), false, false},
"valid true": {nil, p("true"), true, false},
"invalid empty": {nil, p(""), false, true},
"invalid truthy": {nil, p("no"), false, true},
"invalid": {nil, p("totallywrong"), false, true},
"valid env false": {p("false"), nil, true, false},
"valid env true": {p("true"), nil, true, false},
"valid env not boolean": {p("anything"), nil, true, false},
"valid env empty": {p(""), nil, false, false},
"neither set, default false": {nil, nil, false, false},
"both set, env preferred": {p("true"), p("false"), true, false},
} {
t.Run(name, func(t *testing.T) {
if tc.envValue != nil {
t.Setenv(EnvVaultRaftNonVoter, *tc.envValue)
}
raftDir, err := ioutil.TempDir("", "vault-raft-")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(raftDir)

conf := map[string]string{
"path": raftDir,
"node_id": "abc123",
"retry_join": retryJoinConf,
}
if tc.configValue != nil {
conf[raftNonVoterConfigKey] = *tc.configValue
}

backend, err := NewRaftBackend(conf, hclog.NewNullLogger())
switch {
case tc.invalidNonVoterValue || (retryJoinConf == "" && tc.expectNonVoter):
if err == nil {
t.Fatal("expected an error but got none")
}
default:
if err != nil {
t.Fatalf("expected no error but got: %s", err)
}

raftBackend := backend.(*RaftBackend)
if tc.expectNonVoter != raftBackend.NonVoter() {
t.Fatalf("expected %s %v but got %v", raftNonVoterConfigKey, tc.expectNonVoter, raftBackend.NonVoter())
}
}
})
}
})
}
}

func TestRaft_Backend_LargeKey(t *testing.T) {
b, dir := getRaft(t, true, true)
defer os.RemoveAll(dir)
Expand Down
2 changes: 1 addition & 1 deletion vault/raft.go
Expand Up @@ -853,7 +853,7 @@ func (c *Core) InitiateRetryJoin(ctx context.Context) error {

c.logger.Info("raft retry join initiated")

if _, err = c.JoinRaftCluster(ctx, leaderInfos, false); err != nil {
if _, err = c.JoinRaftCluster(ctx, leaderInfos, raftBackend.NonVoter()); err != nil {
return err
}

Expand Down
2 changes: 2 additions & 0 deletions website/content/docs/commands/operator/raft.mdx
Expand Up @@ -79,6 +79,8 @@ The following flags are available for the `operator raft join` command.
server not participate in the Raft quorum, and have it only receive the data
replication stream. This can be used to add read scalability to a cluster in
cases where a high volume of reads to servers are needed. The default is false.
See [`retry_join_as_non_voter`](/docs/configuration/storage/raft#retry_join_as_non_voter)
for the equivalent config option when using `retry_join` stanzas instead.

- `-retry` `(bool: false)` - Continuously retry joining the Raft cluster upon
failures. The default is false.
Expand Down
10 changes: 10 additions & 0 deletions website/content/docs/configuration/storage/raft.mdx
Expand Up @@ -95,6 +95,16 @@ set [`disable_mlock`](/docs/configuration#disable_mlock) to `true`, and to disab
See [the section below](#retry_join-stanza) that describes the parameters
accepted by the [`retry_join`](#retry_join-stanza) stanza.

- `retry_join_as_non_voter` `(boolean: false)` - If set, causes any `retry_join`
config to join the Raft cluster as a non-voter. The node will not participate
in the Raft quorum but will still receive the data replication stream, adding
read scalability to a cluster. This option has the same effect as the
[`-non-voter`](/docs/commands/operator/raft#non-voter) flag for the
`vault operator raft join` command, but only affects voting status when joining
via `retry_join` config. This setting can be overridden to true by setting the
`VAULT_RAFT_RETRY_JOIN_AS_NON_VOTER` environment variable to any non-empty value.
Only valid if there is at least one `retry_join` stanza.

- `max_entry_size` `(integer: 1048576)` - This configures the maximum number of
bytes for a Raft entry. It applies to both Put operations and transactions.
Any put or transaction operation exceeding this configuration value will cause
Expand Down