cmd/satellite: add graceful exit reports command to satellite CLI (#3300)

* update lock file and add comment

* add created at and bytes transferred

* cleanup

* rename db func to GetGracefulExitNodesByTimeFrame

* fix flag

* split into two overlay functions

* := to =

* fix test

* add node not found error class

* fix overlay test

* suggested test changes

* review suggestions

* get exit status from overlay.Get()

* check rows.Err

* fix panic when ExitFinishedAt is nil

* fix comments in cmdGracefulExit
This commit is contained in:
Maximillian von Briesen 2019-10-22 21:06:01 -04:00 committed by GitHub
parent 51d5d8656a
commit abb567f6ae
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 339 additions and 6 deletions

View File

@ -0,0 +1,100 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"context"
"encoding/csv"
"fmt"
"io"
"os"
"strconv"
"time"
"github.com/zeebo/errs"
"go.uber.org/zap"
"storj.io/storj/pkg/storj"
"storj.io/storj/satellite/gracefulexit"
"storj.io/storj/satellite/satellitedb"
)
// generateGracefulExitCSV creates a report with graceful exit data for exiting or exited nodes in a given period
func generateGracefulExitCSV(ctx context.Context, completed bool, start time.Time, end time.Time, output io.Writer) error {
db, err := satellitedb.New(zap.L().Named("db"), gracefulExitCfg.Database)
if err != nil {
return errs.New("error connecting to master database on satellite: %+v", err)
}
defer func() {
err = errs.Combine(err, db.Close())
}()
var nodeIDs storj.NodeIDList
if completed {
nodeIDs, err = db.OverlayCache().GetGracefulExitCompletedByTimeFrame(ctx, start, end)
if err != nil {
return err
}
} else {
nodeIDs, err = db.OverlayCache().GetGracefulExitIncompleteByTimeFrame(ctx, start, end)
if err != nil {
return err
}
}
w := csv.NewWriter(output)
headers := []string{
"nodeID",
"walletAddress",
"nodeCreationDate",
"initiatedGracefulExit",
"completedGracefulExit",
"transferredGB",
}
if err := w.Write(headers); err != nil {
return err
}
for _, id := range nodeIDs {
node, err := db.OverlayCache().Get(ctx, id)
if err != nil {
return err
}
exitProgress, err := db.GracefulExit().GetProgress(ctx, id)
if gracefulexit.ErrNodeNotFound.Has(err) {
exitProgress = &gracefulexit.Progress{}
} else if err != nil {
return err
}
exitStatus := node.ExitStatus
exitFinished := ""
if exitStatus.ExitFinishedAt != nil {
exitFinished = exitStatus.ExitFinishedAt.Format("2006-01-02")
}
nextRow := []string{
node.Id.String(),
node.Operator.Wallet,
node.CreatedAt.Format("2006-01-02"),
exitStatus.ExitInitiatedAt.Format("2006-01-02"),
exitFinished,
strconv.FormatInt(exitProgress.BytesTransferred, 10),
}
if err := w.Write(nextRow); err != nil {
return err
}
}
if err := w.Error(); err != nil {
return err
}
w.Flush()
if output != os.Stdout {
if completed {
fmt.Println("Generated report for nodes that have completed graceful exit.")
} else {
fmt.Println("Generated report for nodes that are in the process of graceful exiting.")
}
}
return err
}

View File

@ -79,6 +79,13 @@ var (
Args: cobra.MinimumNArgs(3),
RunE: cmdValueAttribution,
}
gracefulExitCmd = &cobra.Command{
Use: "graceful-exit [start] [end]",
Short: "Generate a graceful exit report",
Long: "Generate a node usage report for a given period to use for payments. Format dates using YYYY-MM-DD",
Args: cobra.MinimumNArgs(2),
RunE: cmdGracefulExit,
}
runCfg Satellite
setupCfg Satellite
@ -95,6 +102,11 @@ var (
Database string `help:"satellite database connection string" releaseDefault:"postgres://" devDefault:"postgres://"`
Output string `help:"destination of report output" default:""`
}
gracefulExitCfg struct {
Database string `help:"satellite database connection string" releaseDefault:"postgres://" devDefault:"sqlite3://$CONFDIR/master.db"`
Output string `help:"destination of report output" default:""`
Completed bool `help:"whether to output (initiated and completed) or (initiated and not completed)" default:"false"`
}
confDir string
identityDir string
)
@ -112,11 +124,13 @@ func init() {
rootCmd.AddCommand(reportsCmd)
reportsCmd.AddCommand(nodeUsageCmd)
reportsCmd.AddCommand(partnerAttributionCmd)
reportsCmd.AddCommand(gracefulExitCmd)
process.Bind(runCmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
process.Bind(runAPICmd, &runCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
process.Bind(setupCmd, &setupCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir), cfgstruct.SetupMode())
process.Bind(qdiagCmd, &qdiagCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
process.Bind(nodeUsageCmd, &nodeUsageCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
process.Bind(gracefulExitCmd, &gracefulExitCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
process.Bind(partnerAttributionCmd, &partnerAttribtionCfg, defaults, cfgstruct.ConfDir(confDir), cfgstruct.IdentityDir(identityDir))
}
@ -301,6 +315,45 @@ func cmdQDiag(cmd *cobra.Command, args []string) (err error) {
return w.Flush()
}
func cmdGracefulExit(cmd *cobra.Command, args []string) (err error) {
ctx, _ := process.Ctx(cmd)
layout := "2006-01-02"
start, err := time.Parse(layout, args[0])
if err != nil {
return errs.New("Invalid date format. Please use YYYY-MM-DD")
}
end, err := time.Parse(layout, args[1])
if err != nil {
return errs.New("Invalid date format. Please use YYYY-MM-DD")
}
// adding one day to properly account for the entire end day
end = end.AddDate(0, 0, 1)
// ensure that start date is not after end date
if start.After(end) {
return errs.New("Invalid time period (%v) - (%v)", start, end)
}
// send output to stdout
if gracefulExitCfg.Output == "" {
return generateGracefulExitCSV(ctx, gracefulExitCfg.Completed, start, end, os.Stdout)
}
// send output to file
file, err := os.Create(gracefulExitCfg.Output)
if err != nil {
return err
}
defer func() {
err = errs.Combine(err, file.Close())
}()
return generateGracefulExitCSV(ctx, gracefulExitCfg.Completed, start, end, file)
}
func cmdNodeUsage(cmd *cobra.Command, args []string) (err error) {
ctx, _ := process.Ctx(cmd)
@ -324,7 +377,7 @@ func cmdNodeUsage(cmd *cobra.Command, args []string) (err error) {
// send output to stdout
if nodeUsageCfg.Output == "" {
return generateCSV(ctx, start, end, os.Stdout)
return generateNodeUsageCSV(ctx, start, end, os.Stdout)
}
// send output to file
@ -337,7 +390,7 @@ func cmdNodeUsage(cmd *cobra.Command, args []string) (err error) {
err = errs.Combine(err, file.Close())
}()
return generateCSV(ctx, start, end, file)
return generateNodeUsageCSV(ctx, start, end, file)
}
func cmdValueAttribution(cmd *cobra.Command, args []string) (err error) {

View File

@ -19,8 +19,8 @@ import (
"storj.io/storj/satellite/satellitedb"
)
// generateCSV creates a report with node usage data for all nodes in a given period which can be used for payments
func generateCSV(ctx context.Context, start time.Time, end time.Time, output io.Writer) error {
// generateNodeUsageCSV creates a report with node usage data for all nodes in a given period which can be used for payments
func generateNodeUsageCSV(ctx context.Context, start time.Time, end time.Time, output io.Writer) error {
db, err := satellitedb.New(zap.L().Named("db"), nodeUsageCfg.Database)
if err != nil {
return errs.New("error connecting to master database on satellite: %+v", err)

View File

@ -14,6 +14,9 @@ var (
// Error is the default error class for graceful exit package.
Error = errs.Class("gracefulexit")
// ErrNodeNotFound is returned if a graceful exit entry for a node does not exist in database
ErrNodeNotFound = errs.Class("graceful exit node not found")
mon = monkit.Package()
)

View File

@ -82,3 +82,107 @@ func TestGetExitingNodes(t *testing.T) {
}
})
}
func TestGetGracefulExitNodesByTimeframe(t *testing.T) {
satellitedbtest.Run(t, func(t *testing.T, db satellite.DB) {
ctx := testcontext.New(t)
defer ctx.Cleanup()
cache := db.OverlayCache()
exitingToday := make(map[storj.NodeID]bool)
exitingLastWeek := make(map[storj.NodeID]bool)
exitedToday := make(map[storj.NodeID]bool)
exitedLastWeek := make(map[storj.NodeID]bool)
now := time.Now()
lastWeek := time.Now().AddDate(0, 0, -7)
testData := []struct {
nodeID storj.NodeID
initiatedAt time.Time
completedAt time.Time
finishedAt time.Time
}{
// exited today
{testrand.NodeID(), now, now, now},
// exited last week
{testrand.NodeID(), lastWeek, lastWeek, lastWeek},
// exiting today
{testrand.NodeID(), now, now, time.Time{}},
// exiting last week
{testrand.NodeID(), lastWeek, lastWeek, time.Time{}},
// not exiting
{testrand.NodeID(), time.Time{}, time.Time{}, time.Time{}},
}
for _, data := range testData {
err := cache.UpdateAddress(ctx, &pb.Node{Id: data.nodeID}, overlay.NodeSelectionConfig{})
require.NoError(t, err)
req := &overlay.ExitStatusRequest{
NodeID: data.nodeID,
ExitInitiatedAt: data.initiatedAt,
ExitLoopCompletedAt: data.completedAt,
ExitFinishedAt: data.finishedAt,
}
_, err = cache.UpdateExitStatus(ctx, req)
require.NoError(t, err)
if !data.finishedAt.IsZero() {
if data.finishedAt == now {
exitedToday[data.nodeID] = true
} else {
exitedLastWeek[data.nodeID] = true
}
} else if !data.initiatedAt.IsZero() {
if data.initiatedAt == now {
exitingToday[data.nodeID] = true
} else {
exitingLastWeek[data.nodeID] = true
}
}
}
// test GetGracefulExitIncompleteByTimeFrame
ids, err := cache.GetGracefulExitIncompleteByTimeFrame(ctx, lastWeek.Add(-24*time.Hour), lastWeek.Add(24*time.Hour))
require.NoError(t, err)
require.Len(t, ids, 1)
for _, id := range ids {
require.True(t, exitingLastWeek[id])
}
ids, err = cache.GetGracefulExitIncompleteByTimeFrame(ctx, now.Add(-24*time.Hour), now.Add(24*time.Hour))
require.NoError(t, err)
require.Len(t, ids, 1)
for _, id := range ids {
require.True(t, exitingToday[id])
}
ids, err = cache.GetGracefulExitIncompleteByTimeFrame(ctx, lastWeek.Add(-24*time.Hour), now.Add(24*time.Hour))
require.NoError(t, err)
require.Len(t, ids, 2)
for _, id := range ids {
require.True(t, exitingLastWeek[id] || exitingToday[id])
}
// test GetGracefulExitCompletedByTimeFrame
ids, err = cache.GetGracefulExitCompletedByTimeFrame(ctx, lastWeek.Add(-24*time.Hour), lastWeek.Add(24*time.Hour))
require.NoError(t, err)
require.Len(t, ids, 1)
for _, id := range ids {
require.True(t, exitedLastWeek[id])
}
ids, err = cache.GetGracefulExitCompletedByTimeFrame(ctx, now.Add(-24*time.Hour), now.Add(24*time.Hour))
require.NoError(t, err)
require.Len(t, ids, 1)
for _, id := range ids {
require.True(t, exitedToday[id])
}
ids, err = cache.GetGracefulExitCompletedByTimeFrame(ctx, lastWeek.Add(-24*time.Hour), now.Add(24*time.Hour))
require.NoError(t, err)
require.Len(t, ids, 2)
for _, id := range ids {
require.True(t, exitedLastWeek[id] || exitedToday[id])
}
})
}

View File

@ -77,7 +77,11 @@ type DB interface {
GetExitingNodes(ctx context.Context) (exitingNodes storj.NodeIDList, err error)
// GetExitingNodesLoopIncomplete returns exiting nodes who haven't completed the metainfo loop iteration.
GetExitingNodesLoopIncomplete(ctx context.Context) (exitingNodes storj.NodeIDList, err error)
// GetGracefulExitCompletedByTimeFrame returns nodes who have completed graceful exit within a time window (time window is around graceful exit completion).
GetGracefulExitCompletedByTimeFrame(ctx context.Context, begin, end time.Time) (exitedNodes storj.NodeIDList, err error)
// GetGracefulExitIncompleteByTimeFrame returns nodes who have initiated, but not completed graceful exit within a time window (time window is around graceful exit initiation).
GetGracefulExitIncompleteByTimeFrame(ctx context.Context, begin, end time.Time) (exitingNodes storj.NodeIDList, err error)
// GetExitStatus returns a node's graceful exit status.
GetExitStatus(ctx context.Context, nodeID storj.NodeID) (exitStatus *ExitStatus, err error)
}
@ -161,6 +165,7 @@ type NodeDossier struct {
Disqualified *time.Time
PieceCount int64
ExitStatus ExitStatus
CreatedAt time.Time
}
// NodeStats contains statistics about a node.

View File

@ -404,6 +404,7 @@ func TestUpdateCheckIn(t *testing.T) {
expectedNode.Reputation.LastContactSuccess = actualNode.Reputation.LastContactSuccess
expectedNode.Reputation.LastContactFailure = actualNode.Reputation.LastContactFailure
expectedNode.Version.Timestamp = actualNode.Version.Timestamp
expectedNode.CreatedAt = actualNode.CreatedAt
require.Equal(t, expectedNode, actualNode)
// confirm that we can update the address field

View File

@ -47,7 +47,10 @@ func (db *gracefulexitDB) IncrementProgress(ctx context.Context, nodeID storj.No
func (db *gracefulexitDB) GetProgress(ctx context.Context, nodeID storj.NodeID) (_ *gracefulexit.Progress, err error) {
defer mon.Task()(&ctx)(&err)
dbxProgress, err := db.db.Get_GracefulExitProgress_By_NodeId(ctx, dbx.GracefulExitProgress_NodeId(nodeID.Bytes()))
if err != nil {
if err == sql.ErrNoRows {
return nil, gracefulexit.ErrNodeNotFound.Wrap(err)
} else if err != nil {
return nil, Error.Wrap(err)
}
nID, err := storj.NodeIDFromBytes(dbxProgress.NodeId)

View File

@ -884,6 +884,8 @@ func (cache *overlaycache) GetExitingNodesLoopIncomplete(ctx context.Context) (e
}
return exitingNodes, nil
}
// GetExitStatus returns a node's graceful exit status.
func (cache *overlaycache) GetExitStatus(ctx context.Context, nodeID storj.NodeID) (_ *overlay.ExitStatus, err error) {
defer mon.Task()(&ctx)(&err)
@ -902,6 +904,67 @@ func (cache *overlaycache) GetExitStatus(ctx context.Context, nodeID storj.NodeI
return exitStatus, Error.Wrap(err)
}
// GetGracefulExitCompletedByTimeFrame returns nodes who have completed graceful exit within a time window (time window is around graceful exit completion).
func (cache *overlaycache) GetGracefulExitCompletedByTimeFrame(ctx context.Context, begin, end time.Time) (exitedNodes storj.NodeIDList, err error) {
defer mon.Task()(&ctx)(&err)
rows, err := cache.db.Query(cache.db.Rebind(`
SELECT id FROM nodes
WHERE exit_initiated_at IS NOT NULL
AND exit_finished_at IS NOT NULL
AND exit_finished_at >= ?
AND exit_finished_at < ?
`), begin, end,
)
if err != nil {
return nil, err
}
defer func() {
err = errs.Combine(err, rows.Close())
}()
for rows.Next() {
var id storj.NodeID
err = rows.Scan(&id)
if err != nil {
return nil, err
}
exitedNodes = append(exitedNodes, id)
}
return exitedNodes, rows.Err()
}
// GetGracefulExitIncompleteByTimeFrame returns nodes who have initiated, but not completed graceful exit within a time window (time window is around graceful exit initiation).
func (cache *overlaycache) GetGracefulExitIncompleteByTimeFrame(ctx context.Context, begin, end time.Time) (exitingNodes storj.NodeIDList, err error) {
defer mon.Task()(&ctx)(&err)
rows, err := cache.db.Query(cache.db.Rebind(`
SELECT id FROM nodes
WHERE exit_initiated_at IS NOT NULL
AND exit_finished_at IS NULL
AND exit_initiated_at >= ?
AND exit_initiated_at < ?
`), begin, end,
)
if err != nil {
return nil, err
}
defer func() {
err = errs.Combine(err, rows.Close())
}()
// TODO return more than just ID
for rows.Next() {
var id storj.NodeID
err = rows.Scan(&id)
if err != nil {
return nil, err
}
exitingNodes = append(exitingNodes, id)
}
return exitingNodes, rows.Err()
}
// UpdateExitStatus is used to update a node's graceful exit status.
func (cache *overlaycache) UpdateExitStatus(ctx context.Context, request *overlay.ExitStatusRequest) (stats *overlay.NodeStats, err error) {
defer mon.Task()(&ctx)(&err)
@ -993,6 +1056,7 @@ func convertDBNode(ctx context.Context, info *dbx.Node) (_ *overlay.NodeDossier,
Disqualified: info.Disqualified,
PieceCount: info.PieceCount,
ExitStatus: exitStatus,
CreatedAt: info.CreatedAt,
}
return node, nil