2019-01-24 20:15:10 +00:00
// Copyright (C) 2019 Storj Labs, Inc.
2018-12-17 20:14:16 +00:00
// See LICENSE for copying information.
package satellitedb
import (
"context"
"database/sql"
2019-07-31 18:21:06 +01:00
"encoding/hex"
"fmt"
2019-09-11 22:38:58 +01:00
"sort"
2019-03-29 08:53:43 +00:00
"time"
2018-12-17 20:14:16 +00:00
2019-05-19 16:10:46 +01:00
"github.com/lib/pq"
2019-11-08 20:40:39 +00:00
"github.com/spacemonkeygo/monkit/v3"
2019-01-15 16:08:45 +00:00
"github.com/zeebo/errs"
2020-04-08 23:28:25 +01:00
"go.uber.org/zap"
2019-01-15 16:08:45 +00:00
2019-12-27 11:48:47 +00:00
"storj.io/common/pb"
"storj.io/common/storj"
2020-03-23 19:30:31 +00:00
"storj.io/private/version"
2019-07-28 06:55:36 +01:00
"storj.io/storj/satellite/overlay"
2020-01-15 02:29:51 +00:00
"storj.io/storj/satellite/satellitedb/dbx"
2018-12-17 20:14:16 +00:00
)
2019-03-25 22:25:09 +00:00
var (
2019-06-20 14:56:04 +01:00
mon = monkit . Package ( )
2019-03-25 22:25:09 +00:00
)
2019-01-15 16:08:45 +00:00
var _ overlay . DB = ( * overlaycache ) ( nil )
2018-12-17 20:14:16 +00:00
type overlaycache struct {
2019-12-14 02:29:54 +00:00
db * satelliteDB
2018-12-17 20:14:16 +00:00
}
2020-04-14 21:50:02 +01:00
// SelectAllStorageNodesUpload returns all nodes that qualify to store data, organized as reputable nodes and new nodes
func ( cache * overlaycache ) SelectAllStorageNodesUpload ( ctx context . Context , selectionCfg overlay . NodeSelectionConfig ) ( reputable , new [ ] * overlay . SelectedNode , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
query := `
SELECT id , address , last_net , last_ip_port , ( total_audit_count < $ 1 OR total_uptime_count < $ 2 ) as isnew
FROM nodes
WHERE disqualified IS NULL
AND suspended IS NULL
AND exit_initiated_at IS NULL
AND type = $ 3
AND free_disk >= $ 4
AND last_contact_success > $ 5
`
args := [ ] interface { } {
// $1, $2
selectionCfg . AuditCount , selectionCfg . UptimeCount ,
// $3
int ( pb . NodeType_STORAGE ) ,
// $4
selectionCfg . MinimumDiskSpace . Int64 ( ) ,
// $5
time . Now ( ) . Add ( - selectionCfg . OnlineWindow ) ,
}
if selectionCfg . MinimumVersion != "" {
version , err := version . NewSemVer ( selectionCfg . MinimumVersion )
if err != nil {
return nil , nil , err
}
query += ` AND (major > $6 OR (major = $7 AND (minor > $8 OR (minor = $9 AND patch >= $10)))) AND release `
args = append ( args ,
// $6 - $10
version . Major , version . Major , version . Minor , version . Minor , version . Patch ,
)
}
rows , err := cache . db . Query ( ctx , query , args ... )
if err != nil {
return nil , nil , err
}
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
var reputableNodes [ ] * overlay . SelectedNode
var newNodes [ ] * overlay . SelectedNode
for rows . Next ( ) {
var node overlay . SelectedNode
node . Address = & pb . NodeAddress { }
var lastIPPort sql . NullString
var isnew bool
err = rows . Scan ( & node . ID , & node . Address . Address , & node . LastNet , & lastIPPort , & isnew )
if err != nil {
return nil , nil , err
}
if lastIPPort . Valid {
node . LastIPPort = lastIPPort . String
}
if isnew {
newNodes = append ( newNodes , & node )
continue
}
reputableNodes = append ( reputableNodes , & node )
}
return reputableNodes , newNodes , Error . Wrap ( rows . Err ( ) )
}
2020-03-06 22:04:23 +00:00
// GetNodesNetwork returns the /24 subnet for each storage node, order is not guaranteed.
func ( cache * overlaycache ) GetNodesNetwork ( ctx context . Context , nodeIDs [ ] storj . NodeID ) ( nodeNets [ ] string , err error ) {
2019-11-06 21:38:52 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
var rows * sql . Rows
2020-01-17 20:07:00 +00:00
rows , err = cache . db . Query ( ctx , cache . db . Rebind ( `
2019-11-06 21:38:52 +00:00
SELECT last_net FROM nodes
WHERE id = any ( $ 1 : : bytea [ ] )
` ) , postgresNodeIDList ( nodeIDs ) ,
)
if err != nil {
return nil , err
}
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
for rows . Next ( ) {
var ip string
err = rows . Scan ( & ip )
if err != nil {
return nil , err
}
2020-03-06 22:04:23 +00:00
nodeNets = append ( nodeNets , ip )
2019-11-06 21:38:52 +00:00
}
2020-03-06 22:04:23 +00:00
return nodeNets , Error . Wrap ( rows . Err ( ) )
2019-11-06 21:38:52 +00:00
}
2019-01-15 16:08:45 +00:00
// Get looks up the node by nodeID
2019-06-04 12:55:38 +01:00
func ( cache * overlaycache ) Get ( ctx context . Context , id storj . NodeID ) ( _ * overlay . NodeDossier , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2019-01-15 16:08:45 +00:00
if id . IsZero ( ) {
return nil , overlay . ErrEmptyNode
2018-12-17 20:14:16 +00:00
}
2019-03-29 08:53:43 +00:00
node , err := cache . db . Get_Node_By_Id ( ctx , dbx . Node_Id ( id . Bytes ( ) ) )
2019-01-15 16:08:45 +00:00
if err == sql . ErrNoRows {
2019-08-21 17:30:29 +01:00
return nil , overlay . ErrNodeNotFound . New ( "%v" , id )
2019-01-15 16:08:45 +00:00
}
2018-12-17 20:14:16 +00:00
if err != nil {
2019-01-15 16:08:45 +00:00
return nil , err
2018-12-17 20:14:16 +00:00
}
2019-06-04 12:55:38 +01:00
return convertDBNode ( ctx , node )
2019-01-15 16:08:45 +00:00
}
2020-03-30 14:32:02 +01:00
// GetOnlineNodesForGetDelete returns a map of nodes for the supplied nodeIDs
func ( cache * overlaycache ) GetOnlineNodesForGetDelete ( ctx context . Context , nodeIDs [ ] storj . NodeID , onlineWindow time . Duration ) ( _ map [ storj . NodeID ] * overlay . SelectedNode , err error ) {
2020-03-13 18:01:48 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
var rows * sql . Rows
rows , err = cache . db . Query ( ctx , cache . db . Rebind ( `
2020-03-30 14:32:02 +01:00
SELECT last_net , id , address , last_ip_port
2020-03-13 18:01:48 +00:00
FROM nodes
WHERE id = any ( $ 1 : : bytea [ ] )
2020-03-30 14:32:02 +01:00
AND disqualified IS NULL
AND last_contact_success > $ 2
` ) , postgresNodeIDList ( nodeIDs ) , time . Now ( ) . Add ( - onlineWindow ) )
2020-03-13 18:01:48 +00:00
if err != nil {
return nil , err
}
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
2020-03-30 14:32:02 +01:00
nodes := make ( map [ storj . NodeID ] * overlay . SelectedNode )
2020-03-13 18:01:48 +00:00
for rows . Next ( ) {
2020-03-30 14:32:02 +01:00
var node overlay . SelectedNode
node . Address = & pb . NodeAddress { Transport : pb . NodeTransport_TCP_TLS_GRPC }
2020-03-13 18:01:48 +00:00
2020-04-03 19:22:24 +01:00
var lastIPPort sql . NullString
err = rows . Scan ( & node . LastNet , & node . ID , & node . Address . Address , & lastIPPort )
2020-03-13 18:01:48 +00:00
if err != nil {
return nil , err
}
2020-04-03 19:22:24 +01:00
if lastIPPort . Valid {
node . LastIPPort = lastIPPort . String
}
2020-03-30 14:32:02 +01:00
nodes [ node . ID ] = & node
2020-03-13 18:01:48 +00:00
}
return nodes , Error . Wrap ( rows . Err ( ) )
}
2019-06-18 23:22:14 +01:00
// KnownOffline filters a set of nodes to offline nodes
func ( cache * overlaycache ) KnownOffline ( ctx context . Context , criteria * overlay . NodeCriteria , nodeIds storj . NodeIDList ) ( offlineNodes storj . NodeIDList , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
if len ( nodeIds ) == 0 {
return nil , Error . New ( "no ids provided" )
}
// get offline nodes
var rows * sql . Rows
2020-01-17 20:07:00 +00:00
rows , err = cache . db . Query ( ctx , cache . db . Rebind ( `
2019-10-18 22:27:57 +01:00
SELECT id FROM nodes
WHERE id = any ( $ 1 : : bytea [ ] )
2020-01-16 14:27:24 +00:00
AND last_contact_success < $ 2
2019-10-18 22:27:57 +01:00
` ) , postgresNodeIDList ( nodeIds ) , time . Now ( ) . Add ( - criteria . OnlineWindow ) ,
)
2019-06-18 23:22:14 +01:00
if err != nil {
return nil , err
}
2019-10-18 22:27:57 +01:00
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
2019-06-18 23:22:14 +01:00
for rows . Next ( ) {
var id storj . NodeID
err = rows . Scan ( & id )
if err != nil {
return nil , err
}
offlineNodes = append ( offlineNodes , id )
}
2020-01-16 14:27:24 +00:00
return offlineNodes , Error . Wrap ( rows . Err ( ) )
2019-06-18 23:22:14 +01:00
}
2019-05-01 14:45:52 +01:00
// KnownUnreliableOrOffline filters a set of nodes to unreliable or offlines node, independent of new
2019-05-08 18:59:50 +01:00
func ( cache * overlaycache ) KnownUnreliableOrOffline ( ctx context . Context , criteria * overlay . NodeCriteria , nodeIds storj . NodeIDList ) ( badNodes storj . NodeIDList , err error ) {
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-05-01 14:45:52 +01:00
if len ( nodeIds ) == 0 {
return nil , Error . New ( "no ids provided" )
}
2019-05-08 18:59:50 +01:00
// get reliable and online nodes
2019-05-19 16:10:46 +01:00
var rows * sql . Rows
2020-01-17 20:07:00 +00:00
rows , err = cache . db . Query ( ctx , cache . db . Rebind ( `
2019-10-18 22:27:57 +01:00
SELECT id FROM nodes
WHERE id = any ( $ 1 : : bytea [ ] )
2019-06-18 10:14:31 +01:00
AND disqualified IS NULL
2020-03-11 21:11:46 +00:00
AND suspended IS NULL
2020-04-23 20:46:16 +01:00
AND exit_finished_at IS NULL
2019-11-15 22:43:06 +00:00
AND last_contact_success > $ 2
2019-10-18 22:27:57 +01:00
` ) , postgresNodeIDList ( nodeIds ) , time . Now ( ) . Add ( - criteria . OnlineWindow ) ,
)
2019-05-01 14:45:52 +01:00
if err != nil {
return nil , err
}
2019-10-18 22:27:57 +01:00
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
2019-05-08 18:59:50 +01:00
2019-05-19 16:10:46 +01:00
goodNodes := make ( map [ storj . NodeID ] struct { } , len ( nodeIds ) )
2019-05-01 14:45:52 +01:00
for rows . Next ( ) {
var id storj . NodeID
err = rows . Scan ( & id )
2018-12-17 20:14:16 +00:00
if err != nil {
2019-05-01 14:45:52 +01:00
return nil , err
2018-12-17 20:14:16 +00:00
}
2019-05-19 16:10:46 +01:00
goodNodes [ id ] = struct { } { }
2019-05-08 18:59:50 +01:00
}
for _ , id := range nodeIds {
2019-05-19 16:10:46 +01:00
if _ , ok := goodNodes [ id ] ; ! ok {
2019-05-08 18:59:50 +01:00
badNodes = append ( badNodes , id )
}
2018-12-17 20:14:16 +00:00
}
2020-01-16 14:27:24 +00:00
return badNodes , Error . Wrap ( rows . Err ( ) )
2018-12-17 20:14:16 +00:00
}
2019-12-16 13:45:13 +00:00
// KnownReliable filters a set of nodes to reliable (online and qualified) nodes.
func ( cache * overlaycache ) KnownReliable ( ctx context . Context , onlineWindow time . Duration , nodeIDs storj . NodeIDList ) ( nodes [ ] * pb . Node , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
if len ( nodeIDs ) == 0 {
return nil , Error . New ( "no ids provided" )
}
// get online nodes
2020-01-17 20:07:00 +00:00
rows , err := cache . db . Query ( ctx , cache . db . Rebind ( `
2020-03-06 22:04:23 +00:00
SELECT id , last_net , last_ip_port , address , protocol
FROM nodes
2019-12-16 13:45:13 +00:00
WHERE id = any ( $ 1 : : bytea [ ] )
AND disqualified IS NULL
2020-03-11 21:11:46 +00:00
AND suspended IS NULL
2020-04-23 20:46:16 +01:00
AND exit_finished_at IS NULL
2019-12-16 13:45:13 +00:00
AND last_contact_success > $ 2
` ) , postgresNodeIDList ( nodeIDs ) , time . Now ( ) . Add ( - onlineWindow ) ,
)
if err != nil {
return nil , err
}
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
for rows . Next ( ) {
2020-03-11 21:11:46 +00:00
row := & dbx . Node { }
2020-03-06 22:04:23 +00:00
err = rows . Scan ( & row . Id , & row . LastNet , & row . LastIpPort , & row . Address , & row . Protocol )
2019-12-16 13:45:13 +00:00
if err != nil {
return nil , err
}
2020-03-11 21:11:46 +00:00
node , err := convertDBNode ( ctx , row )
2019-12-16 13:45:13 +00:00
if err != nil {
return nil , err
}
2020-03-11 21:11:46 +00:00
nodes = append ( nodes , & node . Node )
2019-12-16 13:45:13 +00:00
}
2020-01-16 14:27:24 +00:00
return nodes , Error . Wrap ( rows . Err ( ) )
2019-12-16 13:45:13 +00:00
}
2019-07-08 23:04:35 +01:00
// Reliable returns all reliable nodes.
func ( cache * overlaycache ) Reliable ( ctx context . Context , criteria * overlay . NodeCriteria ) ( nodes storj . NodeIDList , err error ) {
// get reliable and online nodes
2020-01-17 20:07:00 +00:00
rows , err := cache . db . Query ( ctx , cache . db . Rebind ( `
2019-07-08 23:04:35 +01:00
SELECT id FROM nodes
WHERE disqualified IS NULL
2020-03-11 21:11:46 +00:00
AND suspended IS NULL
2020-04-23 20:46:16 +01:00
AND exit_finished_at IS NULL
2020-01-16 14:27:24 +00:00
AND last_contact_success > ?
` ) , time . Now ( ) . Add ( - criteria . OnlineWindow ) )
2019-07-08 23:04:35 +01:00
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
}
nodes = append ( nodes , id )
}
2020-01-16 14:27:24 +00:00
return nodes , Error . Wrap ( rows . Err ( ) )
2019-07-08 23:04:35 +01:00
}
2019-07-31 18:21:06 +01:00
// BatchUpdateStats updates multiple storagenode's stats in one transaction
func ( cache * overlaycache ) BatchUpdateStats ( ctx context . Context , updateRequests [ ] * overlay . UpdateRequest , batchSize int ) ( failed storj . NodeIDList , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
if len ( updateRequests ) == 0 {
return failed , nil
}
2019-11-01 17:07:23 +00:00
// ensure updates happen in-order
sort . Slice ( updateRequests , func ( i , k int ) bool {
return updateRequests [ i ] . NodeID . Less ( updateRequests [ k ] . NodeID )
} )
2019-07-31 18:21:06 +01:00
doUpdate := func ( updateSlice [ ] * overlay . UpdateRequest ) ( duf storj . NodeIDList , err error ) {
appendAll := func ( ) {
for _ , ur := range updateRequests {
duf = append ( duf , ur . NodeID )
}
}
2019-12-19 10:03:20 +00:00
doAppendAll := true
err = cache . db . WithTx ( ctx , func ( ctx context . Context , tx * dbx . Tx ) ( err error ) {
var allSQL string
for _ , updateReq := range updateSlice {
dbNode , err := tx . Get_Node_By_Id ( ctx , dbx . Node_Id ( updateReq . NodeID . Bytes ( ) ) )
if err != nil {
doAppendAll = false
return err
}
2019-07-31 18:21:06 +01:00
2019-12-19 10:03:20 +00:00
// do not update reputation if node is disqualified
if dbNode . Disqualified != nil {
continue
}
2020-04-23 20:46:16 +01:00
// do not update reputation if node has gracefully exited
if dbNode . ExitFinishedAt != nil {
continue
}
2019-07-31 18:21:06 +01:00
2020-04-08 23:28:25 +01:00
updateNodeStats := cache . populateUpdateNodeStats ( dbNode , updateReq )
2019-12-19 10:03:20 +00:00
sql := buildUpdateStatement ( updateNodeStats )
2019-07-31 18:21:06 +01:00
2019-12-19 10:03:20 +00:00
allSQL += sql
2019-07-31 18:21:06 +01:00
}
2019-12-19 10:03:20 +00:00
if allSQL != "" {
2020-01-17 20:07:00 +00:00
results , err := tx . Tx . Exec ( ctx , allSQL )
2019-12-19 10:03:20 +00:00
if err != nil {
return err
}
2019-07-31 18:21:06 +01:00
2019-12-19 10:03:20 +00:00
_ , err = results . RowsAffected ( )
if err != nil {
return err
}
2019-07-31 18:21:06 +01:00
}
2019-12-19 10:03:20 +00:00
return nil
} )
if err != nil {
if doAppendAll {
2019-07-31 18:21:06 +01:00
appendAll ( )
}
2019-12-19 10:03:20 +00:00
return duf , Error . Wrap ( err )
2019-07-31 18:21:06 +01:00
}
2019-12-19 10:03:20 +00:00
return duf , nil
2019-07-31 18:21:06 +01:00
}
var errlist errs . Group
length := len ( updateRequests )
for i := 0 ; i < length ; i += batchSize {
end := i + batchSize
if end > length {
end = length
}
failedBatch , err := doUpdate ( updateRequests [ i : end ] )
if err != nil && len ( failedBatch ) > 0 {
for _ , fb := range failedBatch {
errlist . Add ( err )
failed = append ( failed , fb )
}
}
}
return failed , errlist . Err ( )
}
2019-03-25 22:25:09 +00:00
// UpdateStats a single storagenode's stats in the db
func ( cache * overlaycache ) UpdateStats ( ctx context . Context , updateReq * overlay . UpdateRequest ) ( stats * overlay . NodeStats , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
nodeID := updateReq . NodeID
2019-12-19 10:03:20 +00:00
var dbNode * dbx . Node
err = cache . db . WithTx ( ctx , func ( ctx context . Context , tx * dbx . Tx ) ( err error ) {
dbNode , err = tx . Get_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) )
if err != nil {
return err
}
// do not update reputation if node is disqualified
if dbNode . Disqualified != nil {
return nil
}
2020-04-23 20:46:16 +01:00
// do not update reputation if node has gracefully exited
if dbNode . ExitFinishedAt != nil {
return nil
}
2019-03-25 22:25:09 +00:00
2020-04-08 23:28:25 +01:00
updateFields := cache . populateUpdateFields ( dbNode , updateReq )
2019-12-19 10:03:20 +00:00
dbNode , err = tx . Update_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) , updateFields )
if err != nil {
return err
}
2019-03-25 22:25:09 +00:00
2019-12-19 10:03:20 +00:00
// Cleanup containment table too
_ , err = tx . Delete_PendingAudits_By_NodeId ( ctx , dbx . PendingAudits_NodeId ( nodeID . Bytes ( ) ) )
return err
} )
2019-07-02 16:16:25 +01:00
if err != nil {
2019-12-19 10:03:20 +00:00
return nil , Error . Wrap ( err )
2019-07-02 16:16:25 +01:00
}
2019-04-08 18:52:53 +01:00
// TODO: Allegedly tx.Get_Node_By_Id and tx.Update_Node_By_Id should never return a nil value for dbNode,
// however we've seen from some crashes that it does. We need to track down the cause of these crashes
// but for now we're adding a nil check to prevent a panic.
if dbNode == nil {
2019-12-19 10:03:20 +00:00
return nil , Error . New ( "unable to get node by ID: %v" , nodeID )
2019-04-08 18:52:53 +01:00
}
2019-12-19 10:03:20 +00:00
return getNodeStats ( dbNode ) , nil
2019-03-25 22:25:09 +00:00
}
2019-09-10 17:05:07 +01:00
// UpdateNodeInfo updates the following fields for a given node ID:
2020-02-12 21:19:42 +00:00
// wallet, email for node operator, free disk, and version
2019-04-10 07:04:24 +01:00
func ( cache * overlaycache ) UpdateNodeInfo ( ctx context . Context , nodeID storj . NodeID , nodeInfo * pb . InfoResponse ) ( stats * overlay . NodeDossier , err error ) {
2019-03-25 22:25:09 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-04-10 07:04:24 +01:00
var updateFields dbx . Node_Update_Fields
if nodeInfo != nil {
2019-04-22 10:07:50 +01:00
if nodeInfo . GetType ( ) != pb . NodeType_INVALID {
updateFields . Type = dbx . Node_Type ( int ( nodeInfo . GetType ( ) ) )
}
2019-04-10 07:04:24 +01:00
if nodeInfo . GetOperator ( ) != nil {
updateFields . Wallet = dbx . Node_Wallet ( nodeInfo . GetOperator ( ) . GetWallet ( ) )
updateFields . Email = dbx . Node_Email ( nodeInfo . GetOperator ( ) . GetEmail ( ) )
}
if nodeInfo . GetCapacity ( ) != nil {
updateFields . FreeDisk = dbx . Node_FreeDisk ( nodeInfo . GetCapacity ( ) . GetFreeDisk ( ) )
}
if nodeInfo . GetVersion ( ) != nil {
semVer , err := version . NewSemVer ( nodeInfo . GetVersion ( ) . GetVersion ( ) )
if err != nil {
2019-04-22 10:07:50 +01:00
return nil , errs . New ( "unable to convert version to semVer" )
2019-04-10 07:04:24 +01:00
}
2019-10-21 11:50:59 +01:00
updateFields . Major = dbx . Node_Major ( int64 ( semVer . Major ) )
updateFields . Minor = dbx . Node_Minor ( int64 ( semVer . Minor ) )
updateFields . Patch = dbx . Node_Patch ( int64 ( semVer . Patch ) )
2019-04-10 07:04:24 +01:00
updateFields . Hash = dbx . Node_Hash ( nodeInfo . GetVersion ( ) . GetCommitHash ( ) )
2019-07-08 19:24:42 +01:00
updateFields . Timestamp = dbx . Node_Timestamp ( nodeInfo . GetVersion ( ) . Timestamp )
2019-04-10 07:04:24 +01:00
updateFields . Release = dbx . Node_Release ( nodeInfo . GetVersion ( ) . GetRelease ( ) )
}
2019-03-25 22:25:09 +00:00
}
2019-04-04 17:34:36 +01:00
updatedDBNode , err := cache . db . Update_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) , updateFields )
2019-03-25 22:25:09 +00:00
if err != nil {
2019-04-04 17:34:36 +01:00
return nil , Error . Wrap ( err )
2019-03-25 22:25:09 +00:00
}
2019-06-04 12:55:38 +01:00
return convertDBNode ( ctx , updatedDBNode )
2019-03-25 22:25:09 +00:00
}
// UpdateUptime updates a single storagenode's uptime stats in the db
2020-01-03 00:00:18 +00:00
func ( cache * overlaycache ) UpdateUptime ( ctx context . Context , nodeID storj . NodeID , isUp bool ) ( stats * overlay . NodeStats , err error ) {
2019-03-25 22:25:09 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-12-19 10:03:20 +00:00
var dbNode * dbx . Node
err = cache . db . WithTx ( ctx , func ( ctx context . Context , tx * dbx . Tx ) ( err error ) {
dbNode , err = tx . Get_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) )
if err != nil {
return err
}
// do not update reputation if node is disqualified
if dbNode . Disqualified != nil {
return nil
}
2020-04-23 20:46:16 +01:00
// do not update reputation if node has gracefully exited
if dbNode . ExitFinishedAt != nil {
return nil
}
2019-06-20 14:56:04 +01:00
2019-12-19 10:03:20 +00:00
updateFields := dbx . Node_Update_Fields { }
2020-01-03 00:00:18 +00:00
totalUptimeCount := dbNode . TotalUptimeCount
2019-03-25 22:25:09 +00:00
2019-12-19 10:03:20 +00:00
lastContactSuccess := dbNode . LastContactSuccess
lastContactFailure := dbNode . LastContactFailure
mon . Meter ( "uptime_updates" ) . Mark ( 1 )
if isUp {
2020-01-03 00:00:18 +00:00
totalUptimeCount ++
2019-12-19 10:03:20 +00:00
updateFields . UptimeSuccessCount = dbx . Node_UptimeSuccessCount ( dbNode . UptimeSuccessCount + 1 )
updateFields . LastContactSuccess = dbx . Node_LastContactSuccess ( time . Now ( ) )
2019-06-18 13:54:52 +01:00
2019-12-19 10:03:20 +00:00
mon . Meter ( "uptime_update_successes" ) . Mark ( 1 )
// we have seen this node in the past 24 hours
if time . Since ( lastContactFailure ) > time . Hour * 24 {
mon . Meter ( "uptime_seen_24h" ) . Mark ( 1 )
}
// we have seen this node in the past week
if time . Since ( lastContactFailure ) > time . Hour * 24 * 7 {
mon . Meter ( "uptime_seen_week" ) . Mark ( 1 )
}
} else {
updateFields . LastContactFailure = dbx . Node_LastContactFailure ( time . Now ( ) )
2019-06-20 14:56:04 +01:00
2019-12-19 10:03:20 +00:00
mon . Meter ( "uptime_update_failures" ) . Mark ( 1 )
// it's been over 24 hours since we've seen this node
if time . Since ( lastContactSuccess ) > time . Hour * 24 {
mon . Meter ( "uptime_not_seen_24h" ) . Mark ( 1 )
}
// it's been over a week since we've seen this node
if time . Since ( lastContactSuccess ) > time . Hour * 24 * 7 {
mon . Meter ( "uptime_not_seen_week" ) . Mark ( 1 )
}
2019-06-20 14:56:04 +01:00
}
2019-03-25 22:25:09 +00:00
2020-01-03 00:00:18 +00:00
updateFields . TotalUptimeCount = dbx . Node_TotalUptimeCount ( totalUptimeCount )
2019-12-19 10:03:20 +00:00
dbNode , err = tx . Update_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) , updateFields )
return err
} )
2019-03-25 22:25:09 +00:00
if err != nil {
2019-12-19 10:03:20 +00:00
return nil , Error . Wrap ( err )
2019-03-25 22:25:09 +00:00
}
2019-12-19 10:03:20 +00:00
2019-04-08 18:52:53 +01:00
// TODO: Allegedly tx.Get_Node_By_Id and tx.Update_Node_By_Id should never return a nil value for dbNode,
// however we've seen from some crashes that it does. We need to track down the cause of these crashes
// but for now we're adding a nil check to prevent a panic.
if dbNode == nil {
2019-12-19 10:03:20 +00:00
return nil , Error . New ( "unable to get node by ID: %v" , nodeID )
2019-04-08 18:52:53 +01:00
}
2019-03-25 22:25:09 +00:00
2019-12-19 10:03:20 +00:00
return getNodeStats ( dbNode ) , nil
2019-03-25 22:25:09 +00:00
}
2020-01-03 19:11:47 +00:00
// DisqualifyNode disqualifies a storage node.
func ( cache * overlaycache ) DisqualifyNode ( ctx context . Context , nodeID storj . NodeID ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
updateFields := dbx . Node_Update_Fields { }
updateFields . Disqualified = dbx . Node_Disqualified ( time . Now ( ) . UTC ( ) )
dbNode , err := cache . db . Update_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) , updateFields )
if err != nil {
return err
}
if dbNode == nil {
return errs . New ( "unable to get node by ID: %v" , nodeID )
}
return nil
}
2020-03-09 15:35:54 +00:00
// SuspendNode suspends a storage node.
func ( cache * overlaycache ) SuspendNode ( ctx context . Context , nodeID storj . NodeID , suspendedAt time . Time ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
updateFields := dbx . Node_Update_Fields { }
updateFields . Suspended = dbx . Node_Suspended ( suspendedAt . UTC ( ) )
dbNode , err := cache . db . Update_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) , updateFields )
if err != nil {
return err
}
if dbNode == nil {
return errs . New ( "unable to get node by ID: %v" , nodeID )
}
return nil
}
// UnsuspendNode unsuspends a storage node.
func ( cache * overlaycache ) UnsuspendNode ( ctx context . Context , nodeID storj . NodeID ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
updateFields := dbx . Node_Update_Fields { }
updateFields . Suspended = dbx . Node_Suspended_Null ( )
dbNode , err := cache . db . Update_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) , updateFields )
if err != nil {
return err
}
if dbNode == nil {
return errs . New ( "unable to get node by ID: %v" , nodeID )
}
return nil
}
2019-08-27 13:37:42 +01:00
// AllPieceCounts returns a map of node IDs to piece counts from the db.
// NB: a valid, partial piece map can be returned even if node ID parsing error(s) are returned.
func ( cache * overlaycache ) AllPieceCounts ( ctx context . Context ) ( _ map [ storj . NodeID ] int , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
// NB: `All_Node_Id_Node_PieceCount_By_PieceCount_Not_Number` selects node
// ID and piece count from the nodes table where piece count is not zero.
rows , err := cache . db . All_Node_Id_Node_PieceCount_By_PieceCount_Not_Number ( ctx )
if err != nil {
return nil , Error . Wrap ( err )
}
pieceCounts := make ( map [ storj . NodeID ] int )
nodeIDErrs := errs . Group { }
for _ , row := range rows {
nodeID , err := storj . NodeIDFromBytes ( row . Id )
if err != nil {
nodeIDErrs . Add ( err )
continue
}
pieceCounts [ nodeID ] = int ( row . PieceCount )
}
2019-09-11 22:38:58 +01:00
2019-08-27 13:37:42 +01:00
return pieceCounts , nodeIDErrs . Err ( )
}
func ( cache * overlaycache ) UpdatePieceCounts ( ctx context . Context , pieceCounts map [ storj . NodeID ] int ) ( err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
if len ( pieceCounts ) == 0 {
return nil
}
2019-09-11 22:38:58 +01:00
// TODO: pass in the apprioriate struct to database, rather than constructing it here
type NodeCount struct {
ID storj . NodeID
Count int64
2019-08-27 13:37:42 +01:00
}
2019-09-11 22:38:58 +01:00
var counts [ ] NodeCount
2019-08-27 13:37:42 +01:00
2019-09-11 22:38:58 +01:00
for nodeid , count := range pieceCounts {
counts = append ( counts , NodeCount {
ID : nodeid ,
Count : int64 ( count ) ,
} )
}
sort . Slice ( counts , func ( i , k int ) bool {
return counts [ i ] . ID . Less ( counts [ k ] . ID )
} )
2019-08-27 13:37:42 +01:00
2019-10-18 22:27:57 +01:00
var nodeIDs [ ] storj . NodeID
var countNumbers [ ] int64
for _ , count := range counts {
nodeIDs = append ( nodeIDs , count . ID )
countNumbers = append ( countNumbers , count . Count )
2019-08-27 13:37:42 +01:00
}
2019-09-11 22:38:58 +01:00
2019-10-18 22:27:57 +01:00
_ , err = cache . db . ExecContext ( ctx , `
UPDATE nodes
SET piece_count = update . count
FROM (
SELECT unnest ( $ 1 : : bytea [ ] ) as id , unnest ( $ 2 : : bigint [ ] ) as count
) as update
WHERE nodes . id = update . id
` , postgresNodeIDList ( nodeIDs ) , pq . Array ( countNumbers ) )
2019-09-11 22:38:58 +01:00
return Error . Wrap ( err )
2019-08-27 13:37:42 +01:00
}
2019-11-07 17:19:34 +00:00
// GetExitingNodes returns nodes who have initiated a graceful exit and is not disqualified, but have not completed it.
2019-10-24 17:24:42 +01:00
func ( cache * overlaycache ) GetExitingNodes ( ctx context . Context ) ( exitingNodes [ ] * overlay . ExitStatus , err error ) {
2019-10-01 23:18:21 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2020-01-17 20:07:00 +00:00
rows , err := cache . db . Query ( ctx , cache . db . Rebind ( `
2019-10-24 17:24:42 +01:00
SELECT id , exit_initiated_at , exit_loop_completed_at , exit_finished_at , exit_success FROM nodes
2019-10-01 23:18:21 +01:00
WHERE exit_initiated_at IS NOT NULL
AND exit_finished_at IS NULL
2019-11-07 17:19:34 +00:00
AND disqualified is NULL
2020-01-16 14:27:24 +00:00
` ) )
2019-10-01 23:18:21 +01:00
if err != nil {
return nil , err
}
2020-01-16 14:27:24 +00:00
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
2019-10-01 23:18:21 +01:00
for rows . Next ( ) {
2019-10-24 17:24:42 +01:00
var exitingNodeStatus overlay . ExitStatus
err = rows . Scan ( & exitingNodeStatus . NodeID , & exitingNodeStatus . ExitInitiatedAt , & exitingNodeStatus . ExitLoopCompletedAt , & exitingNodeStatus . ExitFinishedAt , & exitingNodeStatus . ExitSuccess )
2019-10-01 23:18:21 +01:00
if err != nil {
return nil , err
}
2019-10-24 17:24:42 +01:00
exitingNodes = append ( exitingNodes , & exitingNodeStatus )
2019-10-01 23:18:21 +01:00
}
2020-01-16 14:27:24 +00:00
return exitingNodes , Error . Wrap ( rows . Err ( ) )
2019-10-01 23:18:21 +01:00
}
2019-10-23 02:06:01 +01:00
// GetExitStatus returns a node's graceful exit status.
2019-10-11 22:18:05 +01:00
func ( cache * overlaycache ) GetExitStatus ( ctx context . Context , nodeID storj . NodeID ) ( _ * overlay . ExitStatus , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2020-01-17 20:07:00 +00:00
rows , err := cache . db . Query ( ctx , cache . db . Rebind ( `
2020-01-16 14:27:24 +00:00
SELECT id , exit_initiated_at , exit_loop_completed_at , exit_finished_at , exit_success
FROM nodes
WHERE id = ?
` ) , nodeID )
2019-10-11 22:18:05 +01:00
if err != nil {
return nil , Error . Wrap ( err )
}
2020-01-16 14:27:24 +00:00
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
2019-10-11 22:18:05 +01:00
exitStatus := & overlay . ExitStatus { }
if rows . Next ( ) {
2019-10-17 16:01:39 +01:00
err = rows . Scan ( & exitStatus . NodeID , & exitStatus . ExitInitiatedAt , & exitStatus . ExitLoopCompletedAt , & exitStatus . ExitFinishedAt , & exitStatus . ExitSuccess )
2020-01-16 14:27:24 +00:00
if err != nil {
return nil , err
}
2019-10-11 22:18:05 +01:00
}
2020-01-16 14:27:24 +00:00
return exitStatus , Error . Wrap ( rows . Err ( ) )
2019-10-11 22:18:05 +01:00
}
2019-10-01 23:18:21 +01:00
2019-10-23 02:06:01 +01:00
// 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 )
2020-01-17 20:07:00 +00:00
rows , err := cache . db . Query ( ctx , cache . db . Rebind ( `
2019-10-23 02:06:01 +01:00
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 < ?
2020-01-16 14:27:24 +00:00
` ) , begin , end )
2019-10-23 02:06:01 +01:00
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 )
}
2020-01-16 14:27:24 +00:00
return exitedNodes , Error . Wrap ( rows . Err ( ) )
2019-10-23 02:06:01 +01:00
}
// 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 )
2020-01-17 20:07:00 +00:00
rows , err := cache . db . Query ( ctx , cache . db . Rebind ( `
2019-10-23 02:06:01 +01:00
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 < ?
2020-01-16 14:27:24 +00:00
` ) , begin , end )
2019-10-23 02:06:01 +01:00
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 )
}
2020-01-16 14:27:24 +00:00
return exitingNodes , Error . Wrap ( rows . Err ( ) )
2019-10-23 02:06:01 +01:00
}
2019-10-01 23:18:21 +01:00
// UpdateExitStatus is used to update a node's graceful exit status.
2019-10-29 20:22:20 +00:00
func ( cache * overlaycache ) UpdateExitStatus ( ctx context . Context , request * overlay . ExitStatusRequest ) ( _ * overlay . NodeDossier , err error ) {
2019-10-01 23:18:21 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
nodeID := request . NodeID
updateFields := populateExitStatusFields ( request )
2019-12-20 15:59:47 +00:00
dbNode , err := cache . db . Update_Node_By_Id ( ctx , dbx . Node_Id ( nodeID . Bytes ( ) ) , updateFields )
2019-10-01 23:18:21 +01:00
if err != nil {
return nil , Error . Wrap ( err )
}
if dbNode == nil {
2019-12-20 15:59:47 +00:00
return nil , Error . Wrap ( errs . New ( "unable to get node by ID: %v" , nodeID ) )
2019-10-29 20:22:20 +00:00
}
return convertDBNode ( ctx , dbNode )
2019-10-01 23:18:21 +01:00
}
2020-01-02 20:41:18 +00:00
// GetSuccesfulNodesNotCheckedInSince returns all nodes that last check-in was successful, but haven't checked-in within a given duration.
func ( cache * overlaycache ) GetSuccesfulNodesNotCheckedInSince ( ctx context . Context , duration time . Duration ) ( nodeLastContacts [ ] overlay . NodeLastContact , err error ) {
// get successful nodes that have not checked-in with the hour
defer mon . Task ( ) ( & ctx ) ( & err )
2020-03-06 22:04:23 +00:00
dbxNodes , err := cache . db . DB . All_Node_Id_Node_Address_Node_LastIpPort_Node_LastContactSuccess_Node_LastContactFailure_By_LastContactSuccess_Less_And_LastContactSuccess_Greater_LastContactFailure_And_Disqualified_Is_Null_OrderBy_Asc_LastContactSuccess (
2020-01-02 20:41:18 +00:00
ctx , dbx . Node_LastContactSuccess ( time . Now ( ) . UTC ( ) . Add ( - duration ) ) )
if err != nil {
return nil , Error . Wrap ( err )
}
for _ , node := range dbxNodes {
nodeID , err := storj . NodeIDFromBytes ( node . Id )
if err != nil {
return nil , err
}
nodeLastContact := overlay . NodeLastContact {
2020-05-19 17:42:00 +01:00
URL : storj . NodeURL { ID : nodeID , Address : node . Address } ,
2020-01-02 20:41:18 +00:00
LastContactSuccess : node . LastContactSuccess . UTC ( ) ,
LastContactFailure : node . LastContactFailure . UTC ( ) ,
}
2020-03-06 22:04:23 +00:00
if node . LastIpPort != nil {
nodeLastContact . LastIPPort = * node . LastIpPort
}
2020-01-02 20:41:18 +00:00
nodeLastContacts = append ( nodeLastContacts , nodeLastContact )
}
return nodeLastContacts , nil
}
2019-10-01 23:18:21 +01:00
func populateExitStatusFields ( req * overlay . ExitStatusRequest ) dbx . Node_Update_Fields {
dbxUpdateFields := dbx . Node_Update_Fields { }
if ! req . ExitInitiatedAt . IsZero ( ) {
dbxUpdateFields . ExitInitiatedAt = dbx . Node_ExitInitiatedAt ( req . ExitInitiatedAt )
}
if ! req . ExitLoopCompletedAt . IsZero ( ) {
dbxUpdateFields . ExitLoopCompletedAt = dbx . Node_ExitLoopCompletedAt ( req . ExitLoopCompletedAt )
}
if ! req . ExitFinishedAt . IsZero ( ) {
dbxUpdateFields . ExitFinishedAt = dbx . Node_ExitFinishedAt ( req . ExitFinishedAt )
}
2019-10-17 16:01:39 +01:00
dbxUpdateFields . ExitSuccess = dbx . Node_ExitSuccess ( req . ExitSuccess )
2019-10-01 23:18:21 +01:00
return dbxUpdateFields
}
2019-12-30 17:10:24 +00:00
// GetOfflineNodesLimited returns a list of the first N offline nodes ordered by least recently contacted.
2020-01-06 20:06:05 +00:00
func ( cache * overlaycache ) GetOfflineNodesLimited ( ctx context . Context , limit int ) ( nodeLastContacts [ ] overlay . NodeLastContact , err error ) {
2019-12-30 17:10:24 +00:00
defer mon . Task ( ) ( & ctx ) ( & err )
2020-03-06 22:04:23 +00:00
dbxNodes , err := cache . db . DB . Limited_Node_Id_Node_Address_Node_LastIpPort_Node_LastContactSuccess_Node_LastContactFailure_By_LastContactSuccess_Less_LastContactFailure_And_Disqualified_Is_Null_OrderBy_Asc_LastContactFailure (
2020-01-06 20:06:05 +00:00
ctx , limit , 0 )
2019-12-30 17:10:24 +00:00
if err != nil {
return nil , Error . Wrap ( err )
}
2020-01-06 20:06:05 +00:00
for _ , node := range dbxNodes {
nodeID , err := storj . NodeIDFromBytes ( node . Id )
2019-12-30 17:10:24 +00:00
if err != nil {
2020-01-06 20:06:05 +00:00
return nil , err
}
nodeLastContact := overlay . NodeLastContact {
2020-05-19 17:42:00 +01:00
URL : storj . NodeURL { ID : nodeID , Address : node . Address } ,
2020-01-06 20:06:05 +00:00
LastContactSuccess : node . LastContactSuccess . UTC ( ) ,
LastContactFailure : node . LastContactFailure . UTC ( ) ,
2019-12-30 17:10:24 +00:00
}
2020-03-06 22:04:23 +00:00
if node . LastIpPort != nil {
nodeLastContact . LastIPPort = * node . LastIpPort
}
2020-01-06 20:06:05 +00:00
nodeLastContacts = append ( nodeLastContacts , nodeLastContact )
2019-12-30 17:10:24 +00:00
}
2020-01-06 20:06:05 +00:00
return nodeLastContacts , nil
2019-12-30 17:10:24 +00:00
}
2019-06-04 12:55:38 +01:00
func convertDBNode ( ctx context . Context , info * dbx . Node ) ( _ * overlay . NodeDossier , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2019-01-15 16:08:45 +00:00
if info == nil {
return nil , Error . New ( "missing info" )
}
2019-03-29 08:53:43 +00:00
id , err := storj . NodeIDFromBytes ( info . Id )
2019-01-15 16:08:45 +00:00
if err != nil {
return nil , err
}
2019-10-21 11:50:59 +01:00
ver , err := version . NewSemVer ( fmt . Sprintf ( "%d.%d.%d" , info . Major , info . Minor , info . Patch ) )
if err != nil {
return nil , err
2019-04-10 07:04:24 +01:00
}
2019-10-11 22:18:05 +01:00
exitStatus := overlay . ExitStatus { NodeID : id }
exitStatus . ExitInitiatedAt = info . ExitInitiatedAt
exitStatus . ExitLoopCompletedAt = info . ExitLoopCompletedAt
exitStatus . ExitFinishedAt = info . ExitFinishedAt
2020-03-10 20:42:11 +00:00
exitStatus . ExitSuccess = info . ExitSuccess
2019-10-11 22:18:05 +01:00
2019-04-04 17:34:36 +01:00
node := & overlay . NodeDossier {
Node : pb . Node {
2020-03-06 22:04:23 +00:00
Id : id ,
2019-04-04 17:34:36 +01:00
Address : & pb . NodeAddress {
Address : info . Address ,
Transport : pb . NodeTransport ( info . Protocol ) ,
} ,
2019-01-15 16:08:45 +00:00
} ,
2019-04-04 17:34:36 +01:00
Type : pb . NodeType ( info . Type ) ,
Operator : pb . NodeOperator {
2019-03-29 08:53:43 +00:00
Email : info . Email ,
Wallet : info . Wallet ,
2019-01-15 16:08:45 +00:00
} ,
2019-04-04 17:34:36 +01:00
Capacity : pb . NodeCapacity {
2020-02-12 21:19:42 +00:00
FreeDisk : info . FreeDisk ,
2019-01-15 16:08:45 +00:00
} ,
2019-06-20 14:56:04 +01:00
Reputation : * getNodeStats ( info ) ,
2019-04-10 07:04:24 +01:00
Version : pb . NodeVersion {
Version : ver . String ( ) ,
CommitHash : info . Hash ,
2019-07-08 19:24:42 +01:00
Timestamp : info . Timestamp ,
2019-04-10 07:04:24 +01:00
Release : info . Release ,
} ,
2019-05-30 22:38:23 +01:00
Contained : info . Contained ,
Disqualified : info . Disqualified ,
2020-03-09 15:35:54 +00:00
Suspended : info . Suspended ,
2019-08-19 11:58:13 +01:00
PieceCount : info . PieceCount ,
2019-10-11 22:18:05 +01:00
ExitStatus : exitStatus ,
2019-10-23 02:06:01 +01:00
CreatedAt : info . CreatedAt ,
2020-03-06 22:04:23 +00:00
LastNet : info . LastNet ,
}
if info . LastIpPort != nil {
node . LastIPPort = * info . LastIpPort
2019-01-15 16:08:45 +00:00
}
return node , nil
2018-12-17 20:14:16 +00:00
}
2019-03-25 22:25:09 +00:00
2019-04-08 18:52:53 +01:00
func getNodeStats ( dbNode * dbx . Node ) * overlay . NodeStats {
2019-03-25 22:25:09 +00:00
nodeStats := & overlay . NodeStats {
2020-03-09 15:35:54 +00:00
Latency90 : dbNode . Latency90 ,
2020-05-20 20:57:53 +01:00
VettedAt : dbNode . VettedAt ,
2020-03-09 15:35:54 +00:00
AuditCount : dbNode . TotalAuditCount ,
AuditSuccessCount : dbNode . AuditSuccessCount ,
UptimeCount : dbNode . TotalUptimeCount ,
UptimeSuccessCount : dbNode . UptimeSuccessCount ,
LastContactSuccess : dbNode . LastContactSuccess ,
LastContactFailure : dbNode . LastContactFailure ,
AuditReputationAlpha : dbNode . AuditReputationAlpha ,
AuditReputationBeta : dbNode . AuditReputationBeta ,
Disqualified : dbNode . Disqualified ,
UnknownAuditReputationAlpha : dbNode . UnknownAuditReputationAlpha ,
UnknownAuditReputationBeta : dbNode . UnknownAuditReputationBeta ,
Suspended : dbNode . Suspended ,
2019-03-25 22:25:09 +00:00
}
return nodeStats
}
2019-06-20 14:56:04 +01:00
// updateReputation uses the Beta distribution model to determine a node's reputation.
// lambda is the "forgetting factor" which determines how much past info is kept when determining current reputation score.
// w is the normalization weight that affects how severely new updates affect the current reputation distribution.
func updateReputation ( isSuccess bool , alpha , beta , lambda , w float64 , totalCount int64 ) ( newAlpha , newBeta float64 , updatedCount int64 ) {
// v is a single feedback value that allows us to update both alpha and beta
var v float64 = - 1
if isSuccess {
v = 1
}
newAlpha = lambda * alpha + w * ( 1 + v ) / 2
newBeta = lambda * beta + w * ( 1 - v ) / 2
return newAlpha , newBeta , totalCount + 1
2019-03-25 22:25:09 +00:00
}
2019-07-31 18:21:06 +01:00
2019-12-14 02:29:54 +00:00
func buildUpdateStatement ( update updateNodeStats ) string {
2019-07-31 18:21:06 +01:00
if update . NodeID . IsZero ( ) {
return ""
}
atLeastOne := false
sql := "UPDATE nodes SET "
2020-05-20 20:57:53 +01:00
if update . VettedAt . set {
atLeastOne = true
sql += fmt . Sprintf ( "vetted_at = '%v'" , update . VettedAt . value . Format ( time . RFC3339Nano ) )
}
2019-07-31 18:21:06 +01:00
if update . TotalAuditCount . set {
2020-05-20 20:57:53 +01:00
if atLeastOne {
sql += ","
}
2019-07-31 18:21:06 +01:00
atLeastOne = true
sql += fmt . Sprintf ( "total_audit_count = %v" , update . TotalAuditCount . value )
}
if update . TotalUptimeCount . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "total_uptime_count = %v" , update . TotalUptimeCount . value )
}
if update . AuditReputationAlpha . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "audit_reputation_alpha = %v" , update . AuditReputationAlpha . value )
}
if update . AuditReputationBeta . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "audit_reputation_beta = %v" , update . AuditReputationBeta . value )
}
2020-04-23 15:06:06 +01:00
if update . UnknownAuditReputationAlpha . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "unknown_audit_reputation_alpha = %v" , update . UnknownAuditReputationAlpha . value )
}
if update . UnknownAuditReputationBeta . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "unknown_audit_reputation_beta = %v" , update . UnknownAuditReputationBeta . value )
}
2019-07-31 18:21:06 +01:00
if update . Disqualified . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "disqualified = '%v'" , update . Disqualified . value . Format ( time . RFC3339Nano ) )
}
2020-03-09 15:35:54 +00:00
if update . Suspended . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
2020-03-11 21:11:46 +00:00
if update . Suspended . isNil {
sql += fmt . Sprintf ( "suspended = NULL" )
} else {
sql += fmt . Sprintf ( "suspended = '%v'" , update . Suspended . value . Format ( time . RFC3339Nano ) )
}
2020-03-09 15:35:54 +00:00
}
2019-07-31 18:21:06 +01:00
if update . UptimeSuccessCount . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "uptime_success_count = %v" , update . UptimeSuccessCount . value )
}
if update . LastContactSuccess . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "last_contact_success = '%v'" , update . LastContactSuccess . value . Format ( time . RFC3339Nano ) )
}
if update . LastContactFailure . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "last_contact_failure = '%v'" , update . LastContactFailure . value . Format ( time . RFC3339Nano ) )
}
if update . AuditSuccessCount . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "audit_success_count = %v" , update . AuditSuccessCount . value )
}
if update . Contained . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "contained = %v" , update . Contained . value )
}
if ! atLeastOne {
return ""
}
hexNodeID := hex . EncodeToString ( update . NodeID . Bytes ( ) )
2019-10-18 22:27:57 +01:00
sql += fmt . Sprintf ( " WHERE nodes.id = decode('%v', 'hex');\n" , hexNodeID )
sql += fmt . Sprintf ( "DELETE FROM pending_audits WHERE pending_audits.node_id = decode('%v', 'hex');\n" , hexNodeID )
2019-07-31 18:21:06 +01:00
return sql
}
type int64Field struct {
set bool
value int64
}
type float64Field struct {
set bool
value float64
}
type boolField struct {
set bool
value bool
}
type timeField struct {
set bool
2020-03-09 15:35:54 +00:00
isNil bool
2019-07-31 18:21:06 +01:00
value time . Time
}
type updateNodeStats struct {
2020-03-09 15:35:54 +00:00
NodeID storj . NodeID
2020-05-20 20:57:53 +01:00
VettedAt timeField
2020-03-09 15:35:54 +00:00
TotalAuditCount int64Field
TotalUptimeCount int64Field
AuditReputationAlpha float64Field
AuditReputationBeta float64Field
Disqualified timeField
UnknownAuditReputationAlpha float64Field
UnknownAuditReputationBeta float64Field
Suspended timeField
UptimeSuccessCount int64Field
LastContactSuccess timeField
LastContactFailure timeField
AuditSuccessCount int64Field
Contained boolField
2019-07-31 18:21:06 +01:00
}
2020-04-08 23:28:25 +01:00
func ( cache * overlaycache ) populateUpdateNodeStats ( dbNode * dbx . Node , updateReq * overlay . UpdateRequest ) updateNodeStats {
2020-03-09 15:35:54 +00:00
// there are three audit outcomes: success, failure, and unknown
// if a node fails enough audits, it gets disqualified
// if a node gets enough "unknown" audits, it gets put into suspension
// if a node gets enough successful audits, and is in suspension, it gets removed from suspension
auditAlpha := dbNode . AuditReputationAlpha
auditBeta := dbNode . AuditReputationBeta
unknownAuditAlpha := dbNode . UnknownAuditReputationAlpha
unknownAuditBeta := dbNode . UnknownAuditReputationBeta
totalAuditCount := dbNode . TotalAuditCount
2020-05-20 20:57:53 +01:00
vettedAt := dbNode . VettedAt
2020-03-09 15:35:54 +00:00
2020-03-11 21:11:46 +00:00
var updatedTotalAuditCount int64
2020-03-09 15:35:54 +00:00
switch updateReq . AuditOutcome {
case overlay . AuditSuccess :
// for a successful audit, increase reputation for normal *and* unknown audits
2020-03-11 21:11:46 +00:00
auditAlpha , auditBeta , updatedTotalAuditCount = updateReputation (
2020-03-09 15:35:54 +00:00
true ,
auditAlpha ,
auditBeta ,
updateReq . AuditLambda ,
updateReq . AuditWeight ,
totalAuditCount ,
)
2020-03-11 21:11:46 +00:00
// we will use updatedTotalAuditCount from the updateReputation call above
unknownAuditAlpha , unknownAuditBeta , _ = updateReputation (
2020-03-09 15:35:54 +00:00
true ,
unknownAuditAlpha ,
unknownAuditBeta ,
updateReq . AuditLambda ,
updateReq . AuditWeight ,
2020-03-11 21:11:46 +00:00
totalAuditCount ,
2020-03-09 15:35:54 +00:00
)
case overlay . AuditFailure :
// for audit failure, only update normal alpha/beta
2020-03-11 21:11:46 +00:00
auditAlpha , auditBeta , updatedTotalAuditCount = updateReputation (
2020-03-09 15:35:54 +00:00
false ,
auditAlpha ,
auditBeta ,
updateReq . AuditLambda ,
updateReq . AuditWeight ,
totalAuditCount ,
)
case overlay . AuditUnknown :
// for audit unknown, only update unknown alpha/beta
2020-03-11 21:11:46 +00:00
unknownAuditAlpha , unknownAuditBeta , updatedTotalAuditCount = updateReputation (
2020-03-09 15:35:54 +00:00
false ,
unknownAuditAlpha ,
unknownAuditBeta ,
updateReq . AuditLambda ,
updateReq . AuditWeight ,
totalAuditCount ,
)
}
mon . FloatVal ( "audit_reputation_alpha" ) . Observe ( auditAlpha ) //locked
mon . FloatVal ( "audit_reputation_beta" ) . Observe ( auditBeta ) //locked
mon . FloatVal ( "unknown_audit_reputation_alpha" ) . Observe ( unknownAuditAlpha ) //locked
mon . FloatVal ( "unknown_audit_reputation_beta" ) . Observe ( unknownAuditBeta ) //locked
2019-07-31 18:21:06 +01:00
2020-01-03 00:00:18 +00:00
totalUptimeCount := dbNode . TotalUptimeCount
if updateReq . IsUp {
totalUptimeCount ++
}
2019-07-31 18:21:06 +01:00
updateFields := updateNodeStats {
2020-03-09 15:35:54 +00:00
NodeID : updateReq . NodeID ,
2020-03-11 21:11:46 +00:00
TotalAuditCount : int64Field { set : true , value : updatedTotalAuditCount } ,
2020-03-09 15:35:54 +00:00
TotalUptimeCount : int64Field { set : true , value : totalUptimeCount } ,
AuditReputationAlpha : float64Field { set : true , value : auditAlpha } ,
AuditReputationBeta : float64Field { set : true , value : auditBeta } ,
UnknownAuditReputationAlpha : float64Field { set : true , value : unknownAuditAlpha } ,
UnknownAuditReputationBeta : float64Field { set : true , value : unknownAuditBeta } ,
2019-07-31 18:21:06 +01:00
}
2020-05-20 20:57:53 +01:00
if vettedAt == nil && updatedTotalAuditCount >= updateReq . AuditsRequiredForVetting && totalUptimeCount >= updateReq . UptimesRequiredForVetting {
updateFields . VettedAt = timeField { set : true , value : time . Now ( ) . UTC ( ) }
}
2020-04-14 17:49:45 +01:00
// disqualification case a
// a) Success/fail audit reputation falls below audit DQ threshold
2019-07-31 18:21:06 +01:00
auditRep := auditAlpha / ( auditAlpha + auditBeta )
if auditRep <= updateReq . AuditDQ {
2020-04-14 17:49:45 +01:00
cache . db . log . Info ( "Disqualified" , zap . String ( "DQ type" , "audit failure" ) , zap . String ( "Node ID" , updateReq . NodeID . String ( ) ) )
2019-07-31 18:21:06 +01:00
updateFields . Disqualified = timeField { set : true , value : time . Now ( ) . UTC ( ) }
}
2020-03-09 15:35:54 +00:00
// if unknown audit rep goes below threshold, suspend node. Otherwise unsuspend node.
unknownAuditRep := unknownAuditAlpha / ( unknownAuditAlpha + unknownAuditBeta )
if unknownAuditRep <= updateReq . AuditDQ {
2020-04-08 23:28:25 +01:00
if dbNode . Suspended == nil {
cache . db . log . Info ( "Suspended" , zap . String ( "Node ID" , updateFields . NodeID . String ( ) ) , zap . String ( "Category" , "Unknown Audits" ) )
updateFields . Suspended = timeField { set : true , value : time . Now ( ) . UTC ( ) }
}
2020-04-14 17:49:45 +01:00
// disqualification case b
// b) Node is suspended (success/unknown reputation below audit DQ threshold)
// AND the suspended grace period has elapsed
// AND audit outcome is unknown or failed
// if suspended grace period has elapsed and audit outcome was failed or unknown,
// disqualify node. Set suspended to nil if node is disqualified
// NOTE: if updateFields.Suspended is set, we just suspended the node so it will not be disqualified
if updateReq . AuditOutcome != overlay . AuditSuccess {
if dbNode . Suspended != nil && ! updateFields . Suspended . set &&
2020-05-04 17:32:06 +01:00
time . Since ( * dbNode . Suspended ) > updateReq . SuspensionGracePeriod &&
updateReq . SuspensionDQEnabled {
2020-04-14 17:49:45 +01:00
cache . db . log . Info ( "Disqualified" , zap . String ( "DQ type" , "suspension grace period expired" ) , zap . String ( "Node ID" , updateReq . NodeID . String ( ) ) )
updateFields . Disqualified = timeField { set : true , value : time . Now ( ) . UTC ( ) }
updateFields . Suspended = timeField { set : true , isNil : true }
}
2020-04-08 23:28:25 +01:00
}
2020-04-14 17:49:45 +01:00
} else if dbNode . Suspended != nil {
cache . db . log . Info ( "Suspension lifted" , zap . String ( "Category" , "Unknown Audits" ) , zap . String ( "Node ID" , updateFields . NodeID . String ( ) ) )
updateFields . Suspended = timeField { set : true , isNil : true }
2020-03-09 15:35:54 +00:00
}
2019-07-31 18:21:06 +01:00
if updateReq . IsUp {
updateFields . UptimeSuccessCount = int64Field { set : true , value : dbNode . UptimeSuccessCount + 1 }
updateFields . LastContactSuccess = timeField { set : true , value : time . Now ( ) }
} else {
updateFields . LastContactFailure = timeField { set : true , value : time . Now ( ) }
}
2020-03-09 15:35:54 +00:00
if updateReq . AuditOutcome == overlay . AuditSuccess {
2019-07-31 18:21:06 +01:00
updateFields . AuditSuccessCount = int64Field { set : true , value : dbNode . AuditSuccessCount + 1 }
}
// Updating node stats always exits it from containment mode
updateFields . Contained = boolField { set : true , value : false }
return updateFields
}
2020-04-08 23:28:25 +01:00
func ( cache * overlaycache ) populateUpdateFields ( dbNode * dbx . Node , updateReq * overlay . UpdateRequest ) dbx . Node_Update_Fields {
2019-07-31 18:21:06 +01:00
2020-04-08 23:28:25 +01:00
update := cache . populateUpdateNodeStats ( dbNode , updateReq )
2019-07-31 18:21:06 +01:00
updateFields := dbx . Node_Update_Fields { }
2020-05-20 20:57:53 +01:00
if update . VettedAt . set {
updateFields . VettedAt = dbx . Node_VettedAt ( update . VettedAt . value )
}
2019-07-31 18:21:06 +01:00
if update . TotalAuditCount . set {
updateFields . TotalAuditCount = dbx . Node_TotalAuditCount ( update . TotalAuditCount . value )
}
if update . TotalUptimeCount . set {
updateFields . TotalUptimeCount = dbx . Node_TotalUptimeCount ( update . TotalUptimeCount . value )
}
if update . AuditReputationAlpha . set {
updateFields . AuditReputationAlpha = dbx . Node_AuditReputationAlpha ( update . AuditReputationAlpha . value )
}
if update . AuditReputationBeta . set {
updateFields . AuditReputationBeta = dbx . Node_AuditReputationBeta ( update . AuditReputationBeta . value )
}
if update . Disqualified . set {
updateFields . Disqualified = dbx . Node_Disqualified ( update . Disqualified . value )
}
2020-03-09 15:35:54 +00:00
if update . UnknownAuditReputationAlpha . set {
updateFields . UnknownAuditReputationAlpha = dbx . Node_UnknownAuditReputationAlpha ( update . UnknownAuditReputationAlpha . value )
}
if update . UnknownAuditReputationBeta . set {
updateFields . UnknownAuditReputationBeta = dbx . Node_UnknownAuditReputationBeta ( update . UnknownAuditReputationBeta . value )
}
if update . Suspended . set {
if update . Suspended . isNil {
updateFields . Suspended = dbx . Node_Suspended_Null ( )
} else {
updateFields . Suspended = dbx . Node_Suspended ( update . Suspended . value )
}
}
2019-07-31 18:21:06 +01:00
if update . UptimeSuccessCount . set {
updateFields . UptimeSuccessCount = dbx . Node_UptimeSuccessCount ( update . UptimeSuccessCount . value )
}
if update . LastContactSuccess . set {
updateFields . LastContactSuccess = dbx . Node_LastContactSuccess ( update . LastContactSuccess . value )
}
if update . LastContactFailure . set {
updateFields . LastContactFailure = dbx . Node_LastContactFailure ( update . LastContactFailure . value )
}
if update . AuditSuccessCount . set {
updateFields . AuditSuccessCount = dbx . Node_AuditSuccessCount ( update . AuditSuccessCount . value )
}
if update . Contained . set {
updateFields . Contained = dbx . Node_Contained ( update . Contained . value )
}
2020-03-09 15:35:54 +00:00
if updateReq . AuditOutcome == overlay . AuditSuccess {
2019-07-31 18:21:06 +01:00
updateFields . AuditSuccessCount = dbx . Node_AuditSuccessCount ( dbNode . AuditSuccessCount + 1 )
}
return updateFields
}
2019-09-19 19:37:31 +01:00
// UpdateCheckIn updates a single storagenode with info from when the the node last checked in.
2019-11-15 22:43:06 +00:00
func ( cache * overlaycache ) UpdateCheckIn ( ctx context . Context , node overlay . NodeCheckInInfo , timestamp time . Time , config overlay . NodeSelectionConfig ) ( err error ) {
2019-09-19 19:37:31 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
if node . Address . GetAddress ( ) == "" {
return Error . New ( "error UpdateCheckIn: missing the storage node address" )
}
2019-10-18 22:27:57 +01:00
semVer , err := version . NewSemVer ( node . Version . GetVersion ( ) )
if err != nil {
return Error . New ( "unable to convert version to semVer" )
}
2019-09-19 19:37:31 +01:00
2019-10-18 22:27:57 +01:00
query := `
2019-09-19 19:37:31 +01:00
INSERT INTO nodes
(
id , address , last_net , protocol , type ,
2020-02-12 21:19:42 +00:00
email , wallet , free_disk ,
2020-01-03 00:00:18 +00:00
uptime_success_count , total_uptime_count ,
2019-09-19 19:37:31 +01:00
last_contact_success ,
last_contact_failure ,
2020-01-03 00:00:18 +00:00
audit_reputation_alpha , audit_reputation_beta ,
2020-02-11 21:32:50 +00:00
unknown_audit_reputation_alpha , unknown_audit_reputation_beta ,
2020-03-06 22:04:23 +00:00
major , minor , patch , hash , timestamp , release ,
last_ip_port
2019-09-19 19:37:31 +01:00
)
VALUES (
$ 1 , $ 2 , $ 3 , $ 4 , $ 5 ,
2020-02-12 21:19:42 +00:00
$ 6 , $ 7 , $ 8 ,
$ 9 : : bool : : int , 1 ,
CASE WHEN $ 9 : : bool IS TRUE THEN $ 18 : : timestamptz
2019-11-15 22:43:06 +00:00
ELSE ' 0001 - 01 - 01 00 : 00 : 00 + 00 ' : : timestamptz
2019-09-19 19:37:31 +01:00
END ,
2020-02-12 21:19:42 +00:00
CASE WHEN $ 9 : : bool IS FALSE THEN $ 18 : : timestamptz
2019-11-15 22:43:06 +00:00
ELSE ' 0001 - 01 - 01 00 : 00 : 00 + 00 ' : : timestamptz
2019-09-19 19:37:31 +01:00
END ,
2020-02-12 21:19:42 +00:00
$ 10 , $ 11 ,
$ 10 , $ 11 ,
2020-03-06 22:04:23 +00:00
$ 12 , $ 13 , $ 14 , $ 15 , $ 16 , $ 17 ,
$ 19
2019-09-19 19:37:31 +01:00
)
ON CONFLICT ( id )
DO UPDATE
SET
address = $ 2 ,
last_net = $ 3 ,
protocol = $ 4 ,
email = $ 6 ,
wallet = $ 7 ,
2020-02-12 21:19:42 +00:00
free_disk = $ 8 ,
major = $ 12 , minor = $ 13 , patch = $ 14 , hash = $ 15 , timestamp = $ 16 , release = $ 17 ,
2019-09-19 19:37:31 +01:00
total_uptime_count = nodes . total_uptime_count + 1 ,
2020-02-12 21:19:42 +00:00
uptime_success_count = nodes . uptime_success_count + $ 9 : : bool : : int ,
last_contact_success = CASE WHEN $ 9 : : bool IS TRUE
THEN $ 18 : : timestamptz
2019-09-19 19:37:31 +01:00
ELSE nodes . last_contact_success
END ,
2020-02-12 21:19:42 +00:00
last_contact_failure = CASE WHEN $ 9 : : bool IS FALSE
THEN $ 18 : : timestamptz
2019-09-19 19:37:31 +01:00
ELSE nodes . last_contact_failure
2020-03-06 22:04:23 +00:00
END ,
last_ip_port = $ 19 ;
2019-09-19 19:37:31 +01:00
`
2019-10-18 22:27:57 +01:00
_ , err = cache . db . ExecContext ( ctx , query ,
// args $1 - $5
2020-03-06 22:04:23 +00:00
node . NodeID . Bytes ( ) , node . Address . GetAddress ( ) , node . LastNet , node . Address . GetTransport ( ) , int ( pb . NodeType_STORAGE ) ,
2020-02-12 21:19:42 +00:00
// args $6 - $8
node . Operator . GetEmail ( ) , node . Operator . GetWallet ( ) , node . Capacity . GetFreeDisk ( ) ,
// args $9
2019-10-18 22:27:57 +01:00
node . IsUp ,
2020-02-12 21:19:42 +00:00
// args $10 - $11
2020-04-13 22:38:33 +01:00
1 , 0 ,
2020-02-12 21:19:42 +00:00
// args $12 - $17
2019-10-18 22:27:57 +01:00
semVer . Major , semVer . Minor , semVer . Patch , node . Version . GetCommitHash ( ) , node . Version . Timestamp , node . Version . GetRelease ( ) ,
2020-02-12 21:19:42 +00:00
// args $18
2019-11-15 22:43:06 +00:00
timestamp ,
2020-03-06 22:04:23 +00:00
// args $19
node . LastIPPort ,
2019-10-18 22:27:57 +01:00
)
if err != nil {
return Error . Wrap ( err )
2019-09-19 19:37:31 +01:00
}
return nil
}