Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/source/backup/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,13 @@ Selecting tables and nodes to back up
| All currently down nodes are ignored for the backup procedure.
| In case table should be backed up, but some of its token ranges are not replicated on any currently live node in the cluster, the backup will fail.

| Moreover, `Materialized Views <https://docs.scylladb.com/manual/stable/features/materialized-views.html>`_ and `Secondary Indexes <https://docs.scylladb.com/manual/stable/features/secondary-indexes.html>`_
| `Materialized Views <https://docs.scylladb.com/manual/stable/features/materialized-views.html>`_ and `Secondary Indexes <https://docs.scylladb.com/manual/stable/features/secondary-indexes.html>`_
won't be backed up, as they should be restored by recreating them on the restored base table (see `ScyllaDB docs <https://docs.scylladb.com/manual/stable/operating-scylla/procedures/backup-restore/restore.html#repeat-the-following-steps-for-each-node-in-the-cluster>`_).
| In order to ensure that data residing in View table is preserved, make sure to backup its base table.

| `LWT state tables <https://docs.scylladb.com/manual/stable/features/lwt.html#paxos-state-tables>`_ won't be backed up,
as they only store the state of ongoing LWT queries and do not user data. Restoring these tables is not supported either.

Process
=======

Expand Down
2 changes: 0 additions & 2 deletions docs/source/restore/restore-schema.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ Restore schema for ScyllaDB 6.0/2024.2 or newer

.. note:: Currently, ScyllaDB Manager supports only entire schema restoration, so ``--keyspace`` flag is not allowed.

.. note:: Currently, restoring schema containing `alternator tables <https://docs.scylladb.com/manual/stable/using-scylla/alternator/>`_ is not supported.

| In order to restore ScyllaDB cluster schema use :ref:`sctool restore <sctool-restore>` with ``--restore-schema`` flag.
| Please note that the term *schema* specifically refers to the data residing in the ``system_schema keyspace``, such as keyspace and table definitions. All other data stored in keyspaces managed by ScyllaDB is restored as part of the :doc:`restore tables procedure <restore-tables>`.
| The restore schema procedure works with any cluster size, so the backed-up cluster can have a different number of nodes than the restore destination cluster.
Expand Down
2 changes: 1 addition & 1 deletion docs/source/restore/restore-tables.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Restore tables
==============

.. note:: Currently, ScyllaDB Manager does not support restoring content of `CDC log tables <https://docs.scylladb.com/manual/stable/features/cdc/cdc-log-table.html>`_.
.. note:: ScyllaDB Manager does not support restoring content of `CDC log tables <https://docs.scylladb.com/manual/stable/features/cdc/cdc-log-table.html>`_ nor `LWT state tables <https://docs.scylladb.com/manual/stable/features/lwt.html#paxos-state-tables>`_.

.. warning:: Data related to *authentication* and *service levels* is a part of the backed up CQL schema file, but it is not automatically restored as a part of the restore tables procedure. To restore it, it needs to be fetched from the backup location and applied manually via CQL.

Expand Down
14 changes: 13 additions & 1 deletion pkg/service/backup/model.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (C) 2017 ScyllaDB
// Copyright (C) 2025 ScyllaDB

package backup

Expand All @@ -17,6 +17,7 @@ import (
"github.com/scylladb/go-set/strset"
"github.com/scylladb/gocqlx/v2"
"github.com/scylladb/scylla-manager/backupspec"
"github.com/scylladb/scylla-manager/v3/pkg/table"
"github.com/scylladb/scylla-manager/v3/pkg/util/inexlist/ksfilter"
"github.com/scylladb/scylla-manager/v3/pkg/util2/maps"

Expand Down Expand Up @@ -515,6 +516,17 @@ func (f viewFilter) filter(ks, tab string, _ scyllaclient.Ring) bool {
return !f.views.Has(ks + "." + tab)
}

// Filter out LWT state tables (#4732).
type lwtFilter struct{}

func (lf lwtFilter) filter(ks, tab string, _ scyllaclient.Ring) bool {
t := table.CQLTable{
Keyspace: ks,
Name: tab,
}
return t != table.LWTSystemTable && !strings.HasSuffix(tab, table.LWTStateTableSuffix)
}

// tableValidator checks if it's safe to back up table.
type tabValidator interface {
validate(ks, tab string, ring scyllaclient.Ring) error
Expand Down
1 change: 1 addition & 0 deletions pkg/service/backup/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ func (s *Service) targetFromProperties(ctx context.Context, clusterID uuid.UUID,
patternFilter{pattern: f},
dcFilter{dcs: strset.New(dcs...)},
localDataFilter{keyspaces: ks},
lwtFilter{},
}

// Try to add view filter - possible only when credentials are set
Expand Down
140 changes: 138 additions & 2 deletions pkg/service/backup/service_backup_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2268,7 +2268,7 @@ func TestBackupAlternatorIntegration(t *testing.T) {

accessKeyID, secretAccessKey := GetAlternatorCreds(t, clusterSession, "")
client := CreateAlternatorClient(t, h.Client, ManagedClusterHost(), accessKeyID, secretAccessKey)
CreateAlternatorTable(t, client, ni, false, 0, 0, testTable)
CreateAlternatorTable(t, client, ni, "", 0, 0, testTable)
InsertAlternatorTableData(t, client, rowCnt, testTable)

Print("When: validate data insertion")
Expand Down Expand Up @@ -2447,6 +2447,142 @@ func TestBackupViewsIntegration(t *testing.T) {
}
}

func TestBackupLWTIntegration(t *testing.T) {
location := s3Location("backuptest-lwt")
config := defaultConfig()

var (
session = CreateScyllaManagerDBSession(t)
h = newBackupTestHelper(t, session, config, location, nil)
ctx = context.Background()
clusterSession = CreateSessionAndDropAllKeyspaces(t, h.Client)
)

ni, err := h.Client.AnyNodeInfo(ctx)
if err != nil {
t.Fatal(err)
}
if CheckConstraint(t, ni.ScyllaVersion, "< 2025.1") {
t.Skip("Test expects that it's possible to create table with tablets")
}

Print("Given: CQl vnode table with LWT")
const (
cqlVnodeKs = "cql_vnode_ks"
cqlVnodeTab = "cql_vnode_tab"
)
ExecStmt(t, clusterSession, fmt.Sprintf("CREATE KEYSPACE IF NOT EXISTS %q WITH "+
"replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 3} AND "+
"tablets = {'enabled': 'false'}", cqlVnodeKs))
WriteData(t, clusterSession, cqlVnodeKs, 1, cqlVnodeTab)
ExecuteLWTCQLQuery(t, clusterSession, cqlVnodeKs, cqlVnodeTab)

Print("And: CQl tablet table with LWT")
const (
cqlTabletKs = "cql_tablet_ks"
cqlTabletTab = "cql_tablet_tab"
)
ExecStmt(t, clusterSession, fmt.Sprintf("CREATE KEYSPACE IF NOT EXISTS %q WITH "+
"replication = {'class': 'NetworkTopologyStrategy', 'replication_factor': 3} AND "+
"tablets = {'enabled': 'true'}", cqlTabletKs))
WriteData(t, clusterSession, cqlTabletKs, 1, cqlTabletTab)
// LWT on tablets is enabled starting from 2025.4
if CheckConstraint(t, ni.ScyllaVersion, ">= 2025.4") {
ExecuteLWTCQLQuery(t, clusterSession, cqlTabletKs, cqlTabletTab)
}

Print("And: Alternator vnode table with LWT")
accessKeyID, secretAccessKey := GetAlternatorCreds(t, clusterSession, "")
altClient := CreateAlternatorClient(t, h.Client, ManagedClusterHost(), accessKeyID, secretAccessKey)
const (
altVnodeTab = "alt_vnode_tab"
altVnodeKs = "alternator_" + altVnodeTab
)
CreateAlternatorTable(t, altClient, ni, "none", 0, 0, altVnodeTab)
InsertAlternatorTableData(t, altClient, 100, altVnodeTab)
ExecuteLWTAlternatorQuery(t, altClient, altVnodeTab)

Print("And: Alternator tablet table with LWT")
const (
altTabletTab = "alt_tablet_tab"
altTabletKs = "alternator_" + altTabletTab
)
CreateAlternatorTable(t, altClient, ni, "8", 0, 0, altTabletTab)
InsertAlternatorTableData(t, altClient, 100, altTabletTab)
if CheckConstraint(t, ni.ScyllaVersion, ">= 2025.4") {
ExecuteLWTAlternatorQuery(t, altClient, altTabletTab)
}

Print("When: create backup target")
props := defaultTestProperties(location, "")
props["keyspace"] = []string{cqlVnodeKs, cqlTabletKs, altVnodeKs, altTabletKs}
rawProps, err := json.Marshal(props)
if err != nil {
t.Fatal(err)
}
target, err := h.service.GetTarget(ctx, h.ClusterID, rawProps)
if err != nil {
t.Fatal(err)
}

Print("Then: target contains just the base table and system_schema")
expected := []backup.Unit{
{
Keyspace: cqlVnodeKs,
Tables: []string{cqlVnodeTab},
},
{
Keyspace: cqlTabletKs,
Tables: []string{cqlTabletTab},
},
{
Keyspace: altVnodeKs,
Tables: []string{altVnodeTab},
},
{
Keyspace: altTabletKs,
Tables: []string{altTabletTab},
},
}
if diff := cmp.Diff(target.Units, expected,
cmpopts.IgnoreFields(backup.Unit{}, "AllTables"),
cmpopts.SortSlices(func(a, b backup.Unit) bool { return a.Keyspace < b.Keyspace }),
cmpopts.SortSlices(func(a, b string) bool { return a < b }),
cmpopts.IgnoreSliceElements(func(u backup.Unit) bool { return u.Keyspace == "system_schema" }),
); diff != "" {
t.Fatal(diff)
}

Print("When: run backup with generated target")
if err = h.service.Backup(ctx, h.ClusterID, h.TaskID, h.RunID, target); err != nil {
t.Fatal(err)
}

Print("Then: backup files contains just the base table and system_schema")
filesInfo, err := h.service.ListFiles(ctx, h.ClusterID, []backupspec.Location{location}, backup.ListFilter{ClusterID: h.ClusterID})
if err != nil {
t.Fatal(err)
}
for _, fi := range filesInfo {
for _, fm := range fi.Files {
if fm.Keyspace == "system_schema" {
continue
}
ok := false
for _, u := range expected {
for _, tab := range u.Tables {
if fm.Keyspace == u.Keyspace && fm.Table == tab {
ok = true
}
}
}
if !ok {
t.Fatalf("Unexpected table %q.%q found in backed up files", fm.Keyspace, fm.Table)
}
}
}
}

func TestBackupSkipSchemaIntegration(t *testing.T) {
const (
testBucket = "backuptest-skip-schema"
Expand Down Expand Up @@ -2725,7 +2861,7 @@ func TestGetDescribeSchemaIntegration(t *testing.T) {
tabAltTag = "tab_alt_tag"
tabAltTTLAttr = "tab_alt_ttl_attr"
)
CreateAlternatorTable(t, client, ni, true, 1, 0, tabAlt)
CreateAlternatorTable(t, client, ni, "none", 1, 0, tabAlt)
CreateAlternatorGSI(t, client, tabAlt, tabAltGSI)
TagAlternatorTable(t, client, tabAlt, tabAltTag)
UpdateAlternatorTableTTL(t, client, tabAlt, tabAltTTLAttr, true)
Expand Down
8 changes: 8 additions & 0 deletions pkg/service/backup/worker_alternator.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ package backup

import (
"context"
"strings"

"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/pkg/errors"
"github.com/scylladb/scylla-manager/backupspec"
"github.com/scylladb/scylla-manager/v3/pkg/service/cluster"
"github.com/scylladb/scylla-manager/v3/pkg/table"
)

func (w *worker) DumpAlternatorSchema(ctx context.Context, his []hostInfo, alternatorFunc cluster.AlternatorClientFunc) error {
Expand Down Expand Up @@ -94,6 +96,12 @@ func GetAlternatorSchema(ctx context.Context, client *dynamodb.Client) (backupsp

var tableSchema []backupspec.AlternatorTableSchema
for _, name := range tableNames {
// LWT state tables shouldn't be listed with alternator API,
// but this bug exists in early 2025.4 versions (#4732).
if strings.HasSuffix(name, table.LWTStateTableSuffix) {
continue
}

describeOut, err := client.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: &name,
})
Expand Down
2 changes: 1 addition & 1 deletion pkg/service/repair/service_repair_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1874,7 +1874,7 @@ func TestServiceRepairIntegration(t *testing.T) {
Print("When: create alternator table with 1 row")
accessKeyID, secretAccessKey := GetAlternatorCreds(t, clusterSession, "")
client := CreateAlternatorClient(t, h.Client, ManagedClusterHost(), accessKeyID, secretAccessKey)
CreateAlternatorTable(t, client, globalNodeInfo, false, 0, 0, testTable)
CreateAlternatorTable(t, client, globalNodeInfo, "", 0, 0, testTable)
InsertAlternatorTableData(t, client, 100, testTable)
defer dropKeyspace(t, clusterSession, testKeyspace)

Expand Down
2 changes: 1 addition & 1 deletion pkg/service/repair/tablet/service_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func TestTabletRepairIntegration(t *testing.T) {
// Note that alternator tables on tablets are not supported in all scylla versions
// which support tablet repair. Because of that, they might or might not be included
// in the tablet repair task.
CreateAlternatorTable(t, h.alternatorClient, ni, false, 0, 0, altTab1, altTab2, altTab3)
CreateAlternatorTable(t, h.alternatorClient, ni, "", 0, 0, altTab1, altTab2, altTab3)

// Get all tables from scylla so that we don't need to handle alternator and cdc table names manually
allTables := make(map[fullTab]struct{})
Expand Down
6 changes: 6 additions & 0 deletions pkg/service/restore/alternator.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/scylladb/scylla-manager/backupspec"
"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
"github.com/scylladb/scylla-manager/v3/pkg/service/backup"
"github.com/scylladb/scylla-manager/v3/pkg/table"
"github.com/scylladb/scylla-manager/v3/pkg/util/query"
slices2 "github.com/scylladb/scylla-manager/v3/pkg/util2/slices"
)
Expand Down Expand Up @@ -43,6 +44,11 @@ func newAlternatorSchemaWorker(client *dynamodb.Client, schema backupspec.Altern
if t.Describe == nil || t.Describe.TableName == nil {
continue
}
// LWT state tables shouldn't be listed with alternator API,
// but this bug exists in early 2025.4 versions (#4732).
if strings.HasSuffix(*t.Describe.TableName, table.LWTStateTableSuffix) {
continue
}
ksSchema[w.cqlKeyspaceName(*t.Describe.TableName)] = t
}
return &alternatorSchemaWorker{
Expand Down
Loading
Loading