storj/cmd/tools/node-cleanup/main.go
Egon Elbre 3146ad7f2e satellite/satellitedb: cleanup testing access
Previously we were exposing the testing facilities via interface casting
the necessary parts, however, when things are not part of the main
satellite.DB interface they need to be manually propagated. Rather than
relying on using hidden methods lets expose things as long as they don't
create a direct dependency to the database driver.

Change-Id: I2eb7d8b60f4b64de1320c2d32581f7be267c0f57
2023-02-06 14:36:11 +02:00

196 lines
5.0 KiB
Go

// Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"context"
"database/sql"
"encoding/hex"
"errors"
"math"
"time"
"github.com/spacemonkeygo/monkit/v3"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/common/storj"
"storj.io/private/process"
"storj.io/private/tagsql"
"storj.io/storj/satellite/satellitedb"
)
var mon = monkit.Package()
var (
rootCmd = &cobra.Command{
Use: "delete-uncontacted-nodes",
Short: "delete-uncontacted-nodes",
}
runCmd = &cobra.Command{
Use: "run",
Short: "run delete-uncontacted-nodes",
RunE: run,
}
config Config
)
func init() {
rootCmd.AddCommand(runCmd)
config.BindFlags(runCmd.Flags())
}
// Config defines configuration for deletion.
type Config struct {
SatelliteDB string
Limit int
CreatedAt string
MaxIterations int
}
// BindFlags adds bench flags to the flagset.
func (config *Config) BindFlags(flag *flag.FlagSet) {
flag.StringVar(&config.SatelliteDB, "satellitedb", "", "connection URL for satelliteDB")
flag.IntVar(&config.Limit, "limit", 1000, "number of deletes to perform at once")
flag.StringVar(&config.CreatedAt, "created-at", "", "latest node creation date for which to delete in iso8601 format YYYY-MM-DD")
flag.IntVar(&config.MaxIterations, "max-iterations", -1, "number of maximum iterations (negative is unlimited)")
}
// VerifyFlags verifies whether the values provided are valid.
func (config *Config) VerifyFlags() error {
var errlist errs.Group
if config.SatelliteDB == "" {
errlist.Add(errors.New("flag '--satellitedb' is not set"))
}
return errlist.Err()
}
func run(cmd *cobra.Command, args []string) error {
if err := config.VerifyFlags(); err != nil {
return err
}
ctx, _ := process.Ctx(cmd)
log := zap.L()
return Delete(ctx, log, config)
}
func main() {
process.Exec(rootCmd)
}
// Delete opens the database and starts the database.
func Delete(ctx context.Context, log *zap.Logger, config Config) (err error) {
defer mon.Task()(&ctx)(&err)
db, err := satellitedb.Open(ctx, log.Named("db"), config.SatelliteDB, satellitedb.Options{
ApplicationName: "node-cleanup",
})
if err != nil {
return errs.New("unable to connect %q: %w", config.SatelliteDB, err)
}
defer func() { err = errs.Combine(err, db.Close()) }()
if err := db.CheckVersion(ctx); err != nil {
return errs.New("database version not correct: %w", err)
}
return DeleteFromTables(ctx, log, db.Testing().RawDB(), config)
}
var maxNodeID = (func() storj.NodeID {
var x storj.NodeID
for i := range x {
x[i] = 0xff
}
return x
})()
// DeleteFromTables deletes nodes matching the query in batches.
func DeleteFromTables(ctx context.Context, log *zap.Logger, db tagsql.DB, config Config) (err error) {
var cursor storj.NodeID
progress := 0
if config.MaxIterations < 0 {
config.MaxIterations = math.MaxInt
}
more := true
for iteration := 0; more && iteration < config.MaxIterations; iteration++ {
var batchEnd storj.NodeID
err := db.QueryRowContext(ctx, `
SELECT id
FROM nodes
WHERE id > $1
ORDER BY id
OFFSET $2 LIMIT 1
`, cursor, config.Limit-1).Scan(&batchEnd)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
batchEnd = maxNodeID
more = false
} else {
return errs.New("batch end query failed: %w", err)
}
}
progress += config.Limit
log.Info("deleting batch",
zap.String("from", hex.EncodeToString(cursor[:])),
zap.String("to", hex.EncodeToString(batchEnd[:])),
zap.Int("progress", progress))
start := time.Now()
var deletedNodes, deletedPaystubs, deletedPeerIdentities, deletedNodeAPIVersions int64
err = db.QueryRowContext(ctx, `
WITH deleted_nodes AS (
DELETE FROM nodes
WHERE id > $1 AND id <= $2
AND last_contact_success = '0001-01-01 00:00:00+00'
AND created_at <= $3
RETURNING id
),
deleted_paystubs AS (
DELETE FROM storagenode_paystubs
WHERE node_id in (select deleted_nodes.id FROM deleted_nodes)
RETURNING 1
),
deleted_peer_identities AS (
DELETE FROM peer_identities
WHERE node_id in (select deleted_nodes.id FROM deleted_nodes)
RETURNING 1
),
deleted_node_api_versions AS (
DELETE FROM node_api_versions
WHERE id in (select deleted_nodes.id FROM deleted_nodes)
RETURNING 1
)
SELECT
(select count(*) from deleted_nodes),
(select count(*) from deleted_paystubs),
(select count(*) from deleted_peer_identities),
(select count(*) from deleted_node_api_versions)
`, cursor, batchEnd, config.CreatedAt).Scan(&deletedNodes, &deletedPaystubs, &deletedPeerIdentities, &deletedNodeAPIVersions)
if err != nil {
return errs.New("batch deletion failed: %w", err)
}
log.Info("delete batch",
zap.Duration("duration", time.Since(start)),
zap.Int64("nodes", deletedNodes),
zap.Int64("paystubs", deletedPaystubs),
zap.Int64("peer identities", deletedPeerIdentities),
zap.Int64("node api versions", deletedNodeAPIVersions),
)
cursor = batchEnd
}
return nil
}