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-01-29 19:42:43 +00:00
"strings"
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"
2019-12-27 11:48:47 +00:00
"storj.io/common/pb"
"storj.io/common/storj"
2019-11-14 19:46:15 +00:00
"storj.io/storj/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
)
2020-01-22 19:00:46 +00:00
const (
// OverlayPaginateLimit defines how many nodes can be paginated at the same time.
OverlayPaginateLimit = 1000
)
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
}
2019-05-22 21:06:27 +01:00
func ( cache * overlaycache ) SelectStorageNodes ( ctx context . Context , count int , criteria * overlay . NodeCriteria ) ( nodes [ ] * pb . Node , err error ) {
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-02-11 16:35:28 +00:00
nodeType := int ( pb . NodeType_STORAGE )
2019-04-10 07:04:24 +01:00
safeQuery := `
2019-06-18 10:14:31 +01:00
WHERE disqualified IS NULL
2019-10-08 20:03:38 +01:00
AND exit_initiated_at IS NULL
2019-06-06 01:21:32 +01:00
AND type = ?
AND free_disk >= ?
AND total_audit_count >= ?
AND total_uptime_count >= ?
2019-11-15 22:43:06 +00:00
AND last_contact_success > ? `
2019-04-10 07:04:24 +01:00
args := append ( make ( [ ] interface { } , 0 , 13 ) ,
2020-02-12 21:19:42 +00:00
nodeType , criteria . FreeDisk , criteria . AuditCount ,
2019-06-06 01:21:32 +01:00
criteria . UptimeCount , time . Now ( ) . Add ( - criteria . OnlineWindow ) )
2019-04-10 07:04:24 +01:00
if criteria . MinimumVersion != "" {
v , err := version . NewSemVer ( criteria . MinimumVersion )
if err != nil {
return nil , Error . New ( "invalid node selection criteria version: %v" , err )
}
safeQuery += `
2019-05-14 00:55:51 +01:00
AND ( major > ? OR ( major = ? AND ( minor > ? OR ( minor = ? AND patch >= ? ) ) ) )
2019-04-10 07:04:24 +01:00
AND release `
args = append ( args , v . Major , v . Major , v . Minor , v . Minor , v . Patch )
}
2019-05-22 21:06:27 +01:00
if ! criteria . DistinctIP {
nodes , err = cache . queryNodes ( ctx , criteria . ExcludedNodes , count , safeQuery , args ... )
if err != nil {
return nil , err
}
return nodes , nil
}
// query for distinct IPs
for i := 0 ; i < 3 ; i ++ {
moreNodes , err := cache . queryNodesDistinct ( ctx , criteria . ExcludedNodes , criteria . ExcludedIPs , count - len ( nodes ) , safeQuery , criteria . DistinctIP , args ... )
if err != nil {
return nil , err
}
for _ , n := range moreNodes {
nodes = append ( nodes , n )
criteria . ExcludedNodes = append ( criteria . ExcludedNodes , n . Id )
criteria . ExcludedIPs = append ( criteria . ExcludedIPs , n . LastIp )
}
if len ( nodes ) == count {
break
}
}
return nodes , nil
2019-01-29 19:42:43 +00:00
}
2019-05-22 21:06:27 +01:00
func ( cache * overlaycache ) SelectNewStorageNodes ( ctx context . Context , count int , criteria * overlay . NodeCriteria ) ( nodes [ ] * pb . Node , err error ) {
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-02-11 16:35:28 +00:00
nodeType := int ( pb . NodeType_STORAGE )
2019-04-10 07:04:24 +01:00
safeQuery := `
2019-06-18 10:14:31 +01:00
WHERE disqualified IS NULL
2019-10-08 20:03:38 +01:00
AND exit_initiated_at IS NULL
2019-06-06 01:21:32 +01:00
AND type = ?
AND free_disk >= ?
2019-06-18 18:40:28 +01:00
AND ( total_audit_count < ? OR total_uptime_count < ? )
2019-11-15 22:43:06 +00:00
AND last_contact_success > ? `
2019-04-10 07:04:24 +01:00
args := append ( make ( [ ] interface { } , 0 , 10 ) ,
2020-02-12 21:19:42 +00:00
nodeType , criteria . FreeDisk , criteria . AuditCount , criteria . UptimeCount , time . Now ( ) . Add ( - criteria . OnlineWindow ) )
2019-04-10 07:04:24 +01:00
if criteria . MinimumVersion != "" {
v , err := version . NewSemVer ( criteria . MinimumVersion )
if err != nil {
return nil , Error . New ( "invalid node selection criteria version: %v" , err )
}
safeQuery += `
2019-05-14 00:55:51 +01:00
AND ( major > ? OR ( major = ? AND ( minor > ? OR ( minor = ? AND patch >= ? ) ) ) )
2019-04-10 07:04:24 +01:00
AND release `
args = append ( args , v . Major , v . Major , v . Minor , v . Minor , v . Patch )
}
2019-05-22 21:06:27 +01:00
if ! criteria . DistinctIP {
nodes , err = cache . queryNodes ( ctx , criteria . ExcludedNodes , count , safeQuery , args ... )
if err != nil {
return nil , err
}
return nodes , nil
}
// query for distinct IPs
for i := 0 ; i < 3 ; i ++ {
moreNodes , err := cache . queryNodesDistinct ( ctx , criteria . ExcludedNodes , criteria . ExcludedIPs , count - len ( nodes ) , safeQuery , criteria . DistinctIP , args ... )
if err != nil {
return nil , err
}
for _ , n := range moreNodes {
nodes = append ( nodes , n )
criteria . ExcludedNodes = append ( criteria . ExcludedNodes , n . Id )
criteria . ExcludedIPs = append ( criteria . ExcludedIPs , n . LastIp )
}
if len ( nodes ) == count {
break
}
}
return nodes , nil
2019-01-29 19:42:43 +00:00
}
2019-11-06 21:38:52 +00:00
// GetNodeIPs returns a list of node IP addresses. Warning: these node IP addresses might be returned out of order.
func ( cache * overlaycache ) GetNodeIPs ( ctx context . Context , nodeIDs [ ] storj . NodeID ) ( nodeIPs [ ] string , err error ) {
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
}
nodeIPs = append ( nodeIPs , ip )
}
2020-01-16 14:27:24 +00:00
return nodeIPs , Error . Wrap ( rows . Err ( ) )
2019-11-06 21:38:52 +00:00
}
2019-05-22 21:06:27 +01:00
func ( cache * overlaycache ) queryNodes ( ctx context . Context , excludedNodes [ ] storj . NodeID , count int , safeQuery string , args ... interface { } ) ( _ [ ] * pb . Node , err error ) {
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-01-31 18:49:00 +00:00
if count == 0 {
return nil , nil
2019-01-29 19:42:43 +00:00
}
2019-01-31 18:49:00 +00:00
safeExcludeNodes := ""
2019-05-22 21:06:27 +01:00
if len ( excludedNodes ) > 0 {
safeExcludeNodes = ` AND id NOT IN (? ` + strings . Repeat ( ", ?" , len ( excludedNodes ) - 1 ) + ` ) `
for _ , id := range excludedNodes {
args = append ( args , id . Bytes ( ) )
}
2019-01-29 19:42:43 +00:00
}
2019-05-22 21:06:27 +01:00
args = append ( args , count )
var rows * sql . Rows
2020-01-17 20:07:00 +00:00
rows , err = cache . db . Query ( ctx , cache . db . Rebind ( ` SELECT id , type , address , last_net ,
2020-02-12 21:19:42 +00:00
free_disk , total_audit_count , audit_success_count ,
2020-01-16 14:27:24 +00:00
total_uptime_count , uptime_success_count , disqualified , audit_reputation_alpha ,
audit_reputation_beta
FROM nodes
` +safeQuery+safeExcludeNodes+ `
ORDER BY RANDOM ( )
LIMIT ? ` ) , args ... )
2019-05-22 21:06:27 +01:00
if err != nil {
return nil , err
}
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
2020-01-16 14:27:24 +00:00
2019-05-22 21:06:27 +01:00
var nodes [ ] * pb . Node
for rows . Next ( ) {
dbNode := & dbx . Node { }
err = rows . Scan ( & dbNode . Id , & dbNode . Type ,
2020-02-12 21:19:42 +00:00
& dbNode . Address , & dbNode . LastNet , & dbNode . FreeDisk ,
2019-05-22 21:06:27 +01:00
& dbNode . TotalAuditCount , & dbNode . AuditSuccessCount ,
2019-06-18 14:45:02 +01:00
& dbNode . TotalUptimeCount , & dbNode . UptimeSuccessCount , & dbNode . Disqualified ,
& dbNode . AuditReputationAlpha , & dbNode . AuditReputationBeta ,
)
2019-05-22 21:06:27 +01:00
if err != nil {
return nil , err
}
2019-06-04 12:55:38 +01:00
dossier , err := convertDBNode ( ctx , dbNode )
2019-05-22 21:06:27 +01:00
if err != nil {
return nil , err
}
nodes = append ( nodes , & dossier . Node )
2019-01-29 19:42:43 +00:00
}
2019-05-22 21:06:27 +01:00
2020-01-16 14:27:24 +00:00
return nodes , Error . Wrap ( rows . Err ( ) )
2019-05-22 21:06:27 +01:00
}
func ( cache * overlaycache ) queryNodesDistinct ( ctx context . Context , excludedNodes [ ] storj . NodeID , excludedIPs [ ] string , count int , safeQuery string , distinctIP bool , args ... interface { } ) ( _ [ ] * pb . Node , err error ) {
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-05-22 21:06:27 +01:00
if count == 0 {
return nil , nil
}
safeExcludeNodes := ""
if len ( excludedNodes ) > 0 {
safeExcludeNodes = ` AND id NOT IN (? ` + strings . Repeat ( ", ?" , len ( excludedNodes ) - 1 ) + ` ) `
for _ , id := range excludedNodes {
args = append ( args , id . Bytes ( ) )
}
}
safeExcludeIPs := ""
if len ( excludedIPs ) > 0 {
2019-06-24 16:33:18 +01:00
safeExcludeIPs = ` AND last_net NOT IN (? ` + strings . Repeat ( ", ?" , len ( excludedIPs ) - 1 ) + ` ) `
2019-05-22 21:06:27 +01:00
for _ , ip := range excludedIPs {
args = append ( args , ip )
}
}
args = append ( args , count )
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 *
FROM (
SELECT DISTINCT ON ( last_net ) last_net , -- choose at max 1 node from this IP or network
2020-02-12 21:19:42 +00:00
id , type , address , free_disk , total_audit_count ,
2020-01-16 14:27:24 +00:00
audit_success_count , total_uptime_count , uptime_success_count ,
audit_reputation_alpha , audit_reputation_beta
FROM nodes
` +safeQuery+safeExcludeNodes+safeExcludeIPs+ `
AND last_net < > ' ' -- don ' t try to IP - filter nodes with no known IP yet
ORDER BY last_net , RANDOM ( ) -- equal chance of choosing any qualified node at this IP or network
) filteredcandidates
ORDER BY RANDOM ( ) -- do the actual node selection from filtered pool
LIMIT ? ` ) , args ... )
2019-01-29 19:42:43 +00:00
if err != nil {
return nil , err
}
2019-01-31 18:49:00 +00:00
defer func ( ) { err = errs . Combine ( err , rows . Close ( ) ) } ( )
2020-01-16 14:27:24 +00:00
2019-01-31 18:49:00 +00:00
var nodes [ ] * pb . Node
2019-01-29 19:42:43 +00:00
for rows . Next ( ) {
2019-03-29 08:53:43 +00:00
dbNode := & dbx . Node { }
2019-07-29 13:32:43 +01:00
err = rows . Scan ( & dbNode . LastNet , & dbNode . Id , & dbNode . Type ,
2020-02-12 21:19:42 +00:00
& dbNode . Address , & dbNode . FreeDisk ,
2019-03-29 08:53:43 +00:00
& dbNode . TotalAuditCount , & dbNode . AuditSuccessCount ,
2019-06-18 14:45:02 +01:00
& dbNode . TotalUptimeCount , & dbNode . UptimeSuccessCount ,
& dbNode . AuditReputationAlpha , & dbNode . AuditReputationBeta ,
)
2019-01-29 19:42:43 +00:00
if err != nil {
return nil , err
}
2019-06-04 12:55:38 +01:00
dossier , err := convertDBNode ( ctx , dbNode )
2019-01-29 19:42:43 +00:00
if err != nil {
return nil , err
}
2019-04-04 17:34:36 +01:00
nodes = append ( nodes , & dossier . Node )
2019-01-29 19:42:43 +00:00
}
2020-01-16 14:27:24 +00:00
return nodes , Error . Wrap ( rows . Err ( ) )
2019-01-29 19:42:43 +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
}
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
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 ( `
2019-12-16 13:45:13 +00:00
SELECT id , last_net , address , protocol FROM nodes
WHERE id = any ( $ 1 : : bytea [ ] )
AND disqualified IS NULL
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 ( ) {
row := & dbx . Id_LastNet_Address_Protocol_Row { }
err = rows . Scan ( & row . Id , & row . LastNet , & row . Address , & row . Protocol )
if err != nil {
return nil , err
}
node , err := convertDBNodeToPBNode ( ctx , row )
if err != nil {
return nil , err
}
nodes = append ( nodes , node )
}
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-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-01-30 16:29:18 +00:00
// Paginate will run through
2019-06-04 12:55:38 +01:00
func ( cache * overlaycache ) Paginate ( ctx context . Context , offset int64 , limit int ) ( _ [ ] * overlay . NodeDossier , _ bool , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
2019-01-30 16:29:18 +00:00
cursor := storj . NodeID { }
// more represents end of table. If there are more rows in the database, more will be true.
more := true
2020-01-22 19:00:46 +00:00
if limit <= 0 || limit > OverlayPaginateLimit {
limit = OverlayPaginateLimit
2019-01-30 16:29:18 +00:00
}
2019-03-29 08:53:43 +00:00
dbxInfos , err := cache . db . Limited_Node_By_Id_GreaterOrEqual_OrderBy_Asc_Id ( ctx , dbx . Node_Id ( cursor . Bytes ( ) ) , limit , offset )
2019-01-30 16:29:18 +00:00
if err != nil {
return nil , false , err
}
if len ( dbxInfos ) < limit {
more = false
}
2019-04-04 17:34:36 +01:00
infos := make ( [ ] * overlay . NodeDossier , len ( dbxInfos ) )
2019-01-30 16:29:18 +00:00
for i , dbxInfo := range dbxInfos {
2019-06-04 12:55:38 +01:00
infos [ i ] , err = convertDBNode ( ctx , dbxInfo )
2019-01-30 16:29:18 +00:00
if err != nil {
return nil , false , err
}
}
return infos , more , nil
}
2019-07-12 15:35:48 +01:00
// PaginateQualified will retrieve all qualified nodes
func ( cache * overlaycache ) PaginateQualified ( ctx context . Context , offset int64 , limit int ) ( _ [ ] * pb . Node , _ bool , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
cursor := storj . NodeID { }
// more represents end of table. If there are more rows in the database, more will be true.
more := true
2020-01-22 19:00:46 +00:00
if limit <= 0 || limit > OverlayPaginateLimit {
limit = OverlayPaginateLimit
2019-07-12 15:35:48 +01:00
}
dbxInfos , err := cache . db . Limited_Node_Id_Node_LastNet_Node_Address_Node_Protocol_By_Id_GreaterOrEqual_And_Disqualified_Is_Null_OrderBy_Asc_Id ( ctx , dbx . Node_Id ( cursor . Bytes ( ) ) , limit , offset )
if err != nil {
return nil , false , err
}
if len ( dbxInfos ) < limit {
more = false
}
infos := make ( [ ] * pb . Node , len ( dbxInfos ) )
for i , dbxInfo := range dbxInfos {
infos [ i ] , err = convertDBNodeToPBNode ( ctx , dbxInfo )
if err != nil {
return nil , false , err
}
}
return infos , more , nil
}
2019-04-22 10:07:50 +01:00
// Update updates node address
2019-06-20 14:56:04 +01:00
func ( cache * overlaycache ) UpdateAddress ( ctx context . Context , info * pb . Node , defaults overlay . NodeSelectionConfig ) ( err error ) {
2019-06-04 12:55:38 +01:00
defer mon . Task ( ) ( & ctx ) ( & err )
2019-01-15 16:08:45 +00:00
if info == nil || info . Id . IsZero ( ) {
return overlay . ErrEmptyNode
}
2018-12-17 20:14:16 +00:00
2020-02-12 21:19:42 +00:00
address := info . Address
if address == nil {
address = & pb . NodeAddress { }
}
query := `
INSERT INTO nodes
(
id , address , last_net , protocol , type ,
email , wallet , free_disk ,
uptime_success_count , total_uptime_count ,
last_contact_success ,
last_contact_failure ,
audit_reputation_alpha , audit_reputation_beta ,
major , minor , patch , hash , timestamp , release
2019-12-19 10:03:20 +00:00
)
2020-02-12 21:19:42 +00:00
VALUES (
$ 1 , $ 2 , $ 3 , $ 4 , $ 5 ,
' ' , ' ' , - 1 ,
0 , 0 ,
$ 8 : : timestamptz ,
' 0001 - 01 - 01 00 : 00 : 00 + 00 ' : : timestamptz ,
$ 6 , $ 7 ,
0 , 0 , 0 , ' ' , ' 0001 - 01 - 01 00 : 00 : 00 + 00 ' : : timestamptz , false
)
ON CONFLICT ( id )
DO UPDATE
SET
address = $ 2 ,
last_net = $ 3 ,
protocol = $ 4
`
_ , err = cache . db . ExecContext ( ctx , query ,
// args $1 - $5
info . Id . Bytes ( ) , address . Address , info . LastIp , int ( address . Transport ) , int ( pb . NodeType_INVALID ) ,
// args $6 - $7
defaults . AuditReputationAlpha0 , defaults . AuditReputationBeta0 ,
// args $8
time . Now ( ) ,
)
2019-12-19 10:03:20 +00:00
return Error . Wrap ( err )
2018-12-17 20:14:16 +00: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
}
2019-07-31 18:21:06 +01:00
2019-12-19 10:03:20 +00:00
updateNodeStats := populateUpdateNodeStats ( dbNode , updateReq )
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
}
2019-03-25 22:25:09 +00:00
2019-12-19 10:03:20 +00:00
updateFields := populateUpdateFields ( dbNode , updateReq )
2019-07-02 16:16:25 +01:00
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
}
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
}
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 )
dbxNodes , err := cache . db . DB . All_Node_Id_Node_Address_Node_LastContactSuccess_Node_LastContactFailure_By_LastContactSuccess_Less_And_LastContactSuccess_Greater_LastContactFailure_And_Disqualified_Is_Null_OrderBy_Asc_LastContactSuccess (
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 {
ID : nodeID ,
Address : node . Address ,
LastContactSuccess : node . LastContactSuccess . UTC ( ) ,
LastContactFailure : node . LastContactFailure . UTC ( ) ,
}
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-01-06 20:06:05 +00:00
dbxNodes , err := cache . db . DB . Limited_Node_Id_Node_Address_Node_LastContactSuccess_Node_LastContactFailure_By_LastContactSuccess_Less_LastContactFailure_And_Disqualified_Is_Null_OrderBy_Asc_LastContactFailure (
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 {
ID : nodeID ,
Address : node . Address ,
LastContactSuccess : node . LastContactSuccess . UTC ( ) ,
LastContactFailure : node . LastContactFailure . UTC ( ) ,
2019-12-30 17:10:24 +00:00
}
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
2019-04-04 17:34:36 +01:00
node := & overlay . NodeDossier {
Node : pb . Node {
2019-05-22 21:06:27 +01:00
Id : id ,
2019-06-24 16:33:18 +01:00
LastIp : info . LastNet ,
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 ,
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 ,
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-07-12 15:35:48 +01:00
func convertDBNodeToPBNode ( ctx context . Context , info * dbx . Id_LastNet_Address_Protocol_Row ) ( _ * pb . Node , err error ) {
defer mon . Task ( ) ( & ctx ) ( & err )
if info == nil {
return nil , Error . New ( "missing info" )
}
id , err := storj . NodeIDFromBytes ( info . Id )
if err != nil {
return nil , err
}
return & pb . Node {
Id : id ,
LastIp : info . LastNet ,
Address : & pb . NodeAddress {
Address : info . Address ,
Transport : pb . NodeTransport ( info . Protocol ) ,
} ,
} , nil
}
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-01-03 00:00:18 +00:00
Latency90 : dbNode . Latency90 ,
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 ,
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 "
if update . TotalAuditCount . set {
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 )
}
if update . Disqualified . set {
if atLeastOne {
sql += ","
}
atLeastOne = true
sql += fmt . Sprintf ( "disqualified = '%v'" , update . Disqualified . value . Format ( time . RFC3339Nano ) )
}
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
value time . Time
}
type updateNodeStats struct {
2020-01-03 00:00:18 +00:00
NodeID storj . NodeID
TotalAuditCount int64Field
TotalUptimeCount int64Field
AuditReputationAlpha float64Field
AuditReputationBeta float64Field
Disqualified timeField
UptimeSuccessCount int64Field
LastContactSuccess timeField
LastContactFailure timeField
AuditSuccessCount int64Field
Contained boolField
2019-07-31 18:21:06 +01:00
}
func populateUpdateNodeStats ( dbNode * dbx . Node , updateReq * overlay . UpdateRequest ) updateNodeStats {
auditAlpha , auditBeta , totalAuditCount := updateReputation (
updateReq . AuditSuccess ,
dbNode . AuditReputationAlpha ,
dbNode . AuditReputationBeta ,
updateReq . AuditLambda ,
updateReq . AuditWeight ,
dbNode . TotalAuditCount ,
)
2019-10-15 18:00:14 +01:00
mon . FloatVal ( "audit_reputation_alpha" ) . Observe ( auditAlpha ) //locked
mon . FloatVal ( "audit_reputation_beta" ) . Observe ( auditBeta ) //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-01-03 00:00:18 +00:00
NodeID : updateReq . NodeID ,
TotalAuditCount : int64Field { set : true , value : totalAuditCount } ,
TotalUptimeCount : int64Field { set : true , value : totalUptimeCount } ,
AuditReputationAlpha : float64Field { set : true , value : auditAlpha } ,
AuditReputationBeta : float64Field { set : true , value : auditBeta } ,
2019-07-31 18:21:06 +01:00
}
auditRep := auditAlpha / ( auditAlpha + auditBeta )
if auditRep <= updateReq . AuditDQ {
updateFields . Disqualified = timeField { set : true , value : time . Now ( ) . UTC ( ) }
}
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 ( ) }
}
if updateReq . AuditSuccess {
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
}
func populateUpdateFields ( dbNode * dbx . Node , updateReq * overlay . UpdateRequest ) dbx . Node_Update_Fields {
update := populateUpdateNodeStats ( dbNode , updateReq )
updateFields := dbx . Node_Update_Fields { }
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 )
}
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 )
}
if updateReq . AuditSuccess {
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 ,
2019-09-19 19:37:31 +01:00
major , minor , patch , hash , timestamp , release
)
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 ,
$ 12 , $ 13 , $ 14 , $ 15 , $ 16 , $ 17
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
END ;
`
2019-10-18 22:27:57 +01:00
_ , err = cache . db . ExecContext ( ctx , query ,
// args $1 - $5
node . NodeID . Bytes ( ) , node . Address . GetAddress ( ) , node . LastIP , 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-01-03 00:00:18 +00:00
config . AuditReputationAlpha0 , config . AuditReputationBeta0 ,
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 ,
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
}