Skip to content

Commit e3a7424

Browse files
mandeserooleg-jukovec
authored andcommitted
tt replicaset: add subcommand downgrade
Closes #968 @TarantoolBot document Title: `tt replicaset downgrade` downgrades database schema. The `tt replicaset downgrade` command allows for a automate downgrade of each replicaset in a Tarantool cluster. The process is performed sequentially on the master instance and its replicas to ensure data consistency. Below are the steps involved: For Each Replicaset: - **On the Master Instance**: 1. Run the following commands in sequence to downgrade the schema and take a snapshot: ```lua box.schema.downgrade(<..version..>) box.snapshot() ``` - **On Each Replica**: 1. Wait for the replica to apply all transactions produced by the `box.schema.downgrade` command executed on the master. This is done by monitoring the vector clocks (vclock) to ensure synchronization. 2. Once the repica has caught up, run the following command to take a snapshot: ```lua box.snapshot() ``` > **Error Handling**: If any errors occur during the downgrade process, the operation will halt, and an error report will be generated. --- - Specify the schema version for downgrade The `tt replicaset downgrade` command requires specifying the target version for the schema downgrade. This version should be provided using the `--version` (or `-v`) option. The version must follow the `x.x.x` format, where `x` represents a numerical value. To view the list of available downgrade versions, execute the following command in Tarantool: ```lua box.schema.downgrade_versions() ``` **Example:** ```bash $ tt replicaset downgrade [<APP_NAME> | <URI>] --version 3.0.0 ``` - Timeout for Synchronization Replicas will wait for synchronization for a maximum of `Timeout` seconds. The default timeout is set to 5 seconds, but this can be adjusted manually using the `--timeout` option. **Example:** ```bash $ tt replicaset downgrade [<APP_NAME> | <URI>] -v 3.0.0 --timeout 10 ``` - Selecting Replicasets for Downgrade You can specify which replicaset(s) to downgrade by using the `--replicaset` or `-r` option to target specific replicaset names. **Example:** ```bash $ tt replicaset downgrade [<APP_NAME> | <URI>] -v 3.0.0 replicaset <RS_NAME_1> -r <RS_NAME_2> ... ``` This provides flexibility in downgrading only the desired parts of the cluster without affecting the entire system.
1 parent d730e92 commit e3a7424

File tree

5 files changed

+535
-0
lines changed

5 files changed

+535
-0
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99

1010
### Added
1111

12+
- `tt replicaset downgrade`: command to downgrade the schema on a Tarantool
13+
cluster.
14+
* `-v (--version)`: (Required) specify schema version to downgrade to.
15+
* `-r (--replicaset)`: specify the replicaset name(s) to upgrade.
16+
* `-t (--timeout)`: timeout for waiting the LSN synchronization (in seconds)
17+
(default 5).
1218
- `tt replicaset upgrade`: command to upgrade the schema on a Tarantool
1319
cluster.
1420
* `-r (--replicaset)`: specify the replicaset name(s) to upgrade.

cli/cmd/replicaset.go

+83
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package cmd
22

33
import (
4+
"errors"
45
"fmt"
6+
"os"
7+
"regexp"
58
"strings"
69

710
"github.com/spf13/cobra"
@@ -48,6 +51,7 @@ var (
4851

4952
chosenReplicasetAliases []string
5053
lsnTimeout int
54+
downgradeVersion string
5155

5256
replicasetUriHelp = " The URI can be specified in the following formats:\n" +
5357
" * [tcp://][username:password@][host:port]\n" +
@@ -84,6 +88,51 @@ func newUpgradeCmd() *cobra.Command {
8488
return cmd
8589
}
8690

91+
// newDowngradeCmd creates a "replicaset downgrade" command.
92+
func newDowngradeCmd() *cobra.Command {
93+
cmd := &cobra.Command{
94+
Use: "downgrade (<APP_NAME> | <URI>) [flags]\n\n" +
95+
replicasetUriHelp,
96+
DisableFlagsInUseLine: true,
97+
Short: "Downgrade tarantool cluster",
98+
Long: "Downgrade tarantool cluster.\n\n" +
99+
libconnect.EnvCredentialsHelp + "\n\n",
100+
Run: func(cmd *cobra.Command, args []string) {
101+
var versionPattern = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
102+
if downgradeVersion == "" {
103+
err := errors.New("need to specify the version to downgrade " +
104+
"use --version (-v) option")
105+
util.HandleCmdErr(cmd, err)
106+
os.Exit(1)
107+
} else if !versionPattern.MatchString(downgradeVersion) {
108+
err := errors.New("--version (-v) must be in the format " +
109+
"'x.x.x', where x is a number")
110+
util.HandleCmdErr(cmd, err)
111+
os.Exit(1)
112+
}
113+
114+
cmdCtx.CommandName = cmd.Name()
115+
err := modules.RunCmd(&cmdCtx, cmd.CommandPath(), &modulesInfo,
116+
internalReplicasetDowngradeModule, args)
117+
util.HandleCmdErr(cmd, err)
118+
},
119+
Args: cobra.ExactArgs(1),
120+
}
121+
122+
cmd.Flags().StringArrayVarP(&chosenReplicasetAliases, "replicaset", "r",
123+
[]string{}, "specify the replicaset name(s) to downgrade")
124+
125+
cmd.Flags().IntVarP(&lsnTimeout, "timeout", "t", 5,
126+
"timeout for waiting the LSN synchronization (in seconds)")
127+
128+
cmd.Flags().StringVarP(&downgradeVersion, "version", "v", "",
129+
"version to downgrade the schema to")
130+
131+
addOrchestratorFlags(cmd)
132+
addTarantoolConnectFlags(cmd)
133+
return cmd
134+
}
135+
87136
// newStatusCmd creates a "replicaset status" command.
88137
func newStatusCmd() *cobra.Command {
89138
cmd := &cobra.Command{
@@ -374,6 +423,7 @@ func NewReplicasetCmd() *cobra.Command {
374423
}
375424

376425
cmd.AddCommand(newUpgradeCmd())
426+
cmd.AddCommand(newDowngradeCmd())
377427
cmd.AddCommand(newStatusCmd())
378428
cmd.AddCommand(newPromoteCmd())
379429
cmd.AddCommand(newDemoteCmd())
@@ -555,6 +605,39 @@ func internalReplicasetUpgradeModule(cmdCtx *cmdcontext.CmdCtx, args []string) e
555605
}, connOpts)
556606
}
557607

608+
// internalReplicasetDowngradeModule is a "upgrade" command for the replicaset module.
609+
func internalReplicasetDowngradeModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
610+
var ctx replicasetCtx
611+
if err := replicasetFillCtx(cmdCtx, &ctx, args, false); err != nil {
612+
return err
613+
}
614+
if ctx.IsInstanceConnect {
615+
defer ctx.Conn.Close()
616+
}
617+
618+
connectCtx := connect.ConnectCtx{
619+
Username: replicasetUser,
620+
Password: replicasetPassword,
621+
SslKeyFile: replicasetSslKeyFile,
622+
SslCertFile: replicasetSslCertFile,
623+
SslCaFile: replicasetSslCaFile,
624+
SslCiphers: replicasetSslCiphers,
625+
}
626+
var connOpts connector.ConnectOpts
627+
connOpts, _, _ = resolveConnectOpts(cmdCtx, cliOpts, &connectCtx, args)
628+
629+
return replicasetcmd.Downgrade(replicasetcmd.DiscoveryCtx{
630+
IsApplication: ctx.IsApplication,
631+
RunningCtx: ctx.RunningCtx,
632+
Conn: ctx.Conn,
633+
Orchestrator: ctx.Orchestrator,
634+
}, replicasetcmd.DowngradeOpts{
635+
ChosenReplicasetAliases: chosenReplicasetAliases,
636+
Timeout: lsnTimeout,
637+
DowngradeVersion: downgradeVersion,
638+
}, connOpts)
639+
}
640+
558641
// internalReplicasetPromoteModule is a "promote" command for the replicaset module.
559642
func internalReplicasetPromoteModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
560643
var ctx replicasetCtx

cli/replicaset/cmd/downgrade.go

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package replicasetcmd
2+
3+
import (
4+
_ "embed"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/mitchellh/mapstructure"
9+
"github.com/tarantool/tt/cli/connector"
10+
"github.com/tarantool/tt/cli/replicaset"
11+
"github.com/tarantool/tt/cli/running"
12+
)
13+
14+
// DowngradeOpts contains options used for the downgrade process.
15+
type DowngradeOpts struct {
16+
// ChosenReplicasetAliases is a list of replicaset names specified by
17+
// the user for the downgrade.
18+
ChosenReplicasetAliases []string
19+
// Timeout period (in seconds) for waiting on LSN synchronization.
20+
Timeout int
21+
// DowngradeVersion is a version to downgrade the schema to.
22+
DowngradeVersion string
23+
}
24+
25+
//go:embed lua/downgrade.lua
26+
var downgradeMasterLua string
27+
28+
func filterComments(script string) string {
29+
var filteredLines []string
30+
lines := strings.Split(script, "\n")
31+
for _, line := range lines {
32+
trimmedLine := strings.TrimSpace(line)
33+
if !strings.HasPrefix(trimmedLine, "--") {
34+
filteredLines = append(filteredLines, line)
35+
}
36+
}
37+
return strings.Join(filteredLines, "\n")
38+
}
39+
40+
// Downgrade downgrades tarantool schema.
41+
func Downgrade(discoveryCtx DiscoveryCtx, opts DowngradeOpts,
42+
connOpts connector.ConnectOpts) error {
43+
replicasets, err := getReplicasets(discoveryCtx)
44+
if err != nil {
45+
return err
46+
}
47+
48+
replicasets = fillAliases(replicasets)
49+
replicasetsToDowngrade, err := filterReplicasetsByAliases(replicasets,
50+
opts.ChosenReplicasetAliases)
51+
if err != nil {
52+
return err
53+
}
54+
55+
return internalDowngrade(replicasetsToDowngrade, opts.Timeout,
56+
opts.DowngradeVersion, connOpts)
57+
}
58+
59+
func internalDowngrade(replicasets []replicaset.Replicaset, lsnTimeout int, version string,
60+
connOpts connector.ConnectOpts) error {
61+
for _, replicaset := range replicasets {
62+
err := downgradeReplicaset(replicaset, lsnTimeout, version, connOpts)
63+
if err != nil {
64+
fmt.Printf("• %s: error\n", replicaset.Alias)
65+
return fmt.Errorf("replicaset %s: %w", replicaset.Alias, err)
66+
}
67+
fmt.Printf("• %s: ok\n", replicaset.Alias)
68+
}
69+
return nil
70+
}
71+
72+
func downgradeMaster(master *instanceMeta, version string) (syncInfo, error) {
73+
var downgradeInfo syncInfo
74+
fullMasterName := running.GetAppInstanceName(master.run)
75+
res, err := master.conn.Eval(filterComments(downgradeMasterLua),
76+
[]interface{}{version}, connector.RequestOpts{})
77+
if err != nil {
78+
return downgradeInfo, fmt.Errorf(
79+
"failed to execute downgrade script on master instance - %s: %w",
80+
fullMasterName, err)
81+
}
82+
83+
if err := mapstructure.Decode(res[0], &downgradeInfo); err != nil {
84+
return downgradeInfo, fmt.Errorf(
85+
"failed to decode response from master instance - %s: %w",
86+
fullMasterName, err)
87+
}
88+
89+
if downgradeInfo.Err != nil {
90+
return downgradeInfo, fmt.Errorf(
91+
"master instance downgrade failed - %s: %s",
92+
fullMasterName, *downgradeInfo.Err)
93+
}
94+
return downgradeInfo, nil
95+
}
96+
97+
func downgradeReplicaset(replicaset replicaset.Replicaset, lsnTimeout int, version string,
98+
connOpts connector.ConnectOpts) error {
99+
master, replicas, err := collectRWROInfo(replicaset, connOpts)
100+
if err != nil {
101+
return err
102+
}
103+
104+
defer closeConnectors(master, replicas)
105+
106+
// Downgrade master instance, collect LSN and IID from master instance.
107+
downgradeInfo, err := downgradeMaster(master, version)
108+
if err != nil {
109+
return err
110+
}
111+
112+
// Downgrade replica instances.
113+
masterLSN := downgradeInfo.LSN
114+
masterIID := downgradeInfo.IID
115+
116+
for _, replica := range replicas {
117+
fullReplicaName := running.GetAppInstanceName(replica.run)
118+
err := waitLSN(replica.conn, masterIID, masterLSN, lsnTimeout)
119+
if err != nil {
120+
return fmt.Errorf("can't ensure that downgrade operations performed on "+
121+
"%s are replicated to %s to perform snapshotting on it: error "+
122+
"waiting LSN %d in vclock component %d: %w",
123+
running.GetAppInstanceName(master.run), fullReplicaName,
124+
masterLSN, masterIID, err)
125+
}
126+
err = snapshot(&replica)
127+
if err != nil {
128+
return err
129+
}
130+
}
131+
return nil
132+
}

cli/replicaset/cmd/lua/downgrade.lua

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
local version = ...
2+
local allowed_versions = box.schema.downgrade_versions()
3+
4+
local function is_version_allowed(version, allowed_versions)
5+
for _, allowed_version in ipairs(allowed_versions) do
6+
if allowed_version == version then
7+
return true
8+
end
9+
end
10+
return false
11+
end
12+
13+
local function format_allowed_versions(versions)
14+
return "[" .. table.concat(versions, ", ") .. "]"
15+
end
16+
17+
local function downgrade_schema(version)
18+
if not is_version_allowed(version, allowed_versions) then
19+
local err = ("Version '%s' is not allowed.\nAllowed versions: %s"):format(
20+
version, format_allowed_versions(allowed_versions)
21+
)
22+
return {
23+
lsn = box.info.lsn,
24+
iid = box.info.id,
25+
err = err,
26+
}
27+
end
28+
29+
local ok, err = pcall(function()
30+
box.schema.downgrade(version)
31+
box.snapshot()
32+
end)
33+
34+
return {
35+
lsn = box.info.lsn,
36+
iid = box.info.id,
37+
err = not ok and tostring(err) or nil,
38+
}
39+
end
40+
41+
return downgrade_schema(version)

0 commit comments

Comments
 (0)