72189330fd
Currently, graceful exit is a complicated subsystem that keeps a queue of all pieces expected to be on a node, and asks the node to transfer those pieces to other nodes one by one. The complexity of the system has, unfortunately, led to numerous bugs and unexpected behaviors. We have decided to remove this entire subsystem and restructure graceful exit as follows: * Nodes will signal their intent to exit gracefully * The satellite will not send any new pieces to gracefully exiting nodes * Pieces on gracefully exiting nodes will be considered by the repair subsystem as "retrievable but unhealthy". They will be repaired off of the exiting node as needed. * After one month (with an appropriately high online score), the node will be considered exited, and held amounts for the node will be released. The repair worker will continue to fetch pieces from the node as long as the node stays online. * If, at the end of the month, a node's online score is below a certain threshold, its graceful exit will fail. Refs: https://github.com/storj/storj/issues/6042 Change-Id: I52d4e07a4198e9cb2adf5e6cee2cb64d6f9f426b
181 lines
6.7 KiB
Go
181 lines
6.7 KiB
Go
// Copyright (C) 2023 Storj Labs, Inc.
|
|
// See LICENSE for copying information.
|
|
|
|
package repair
|
|
|
|
import (
|
|
"golang.org/x/exp/maps"
|
|
|
|
"storj.io/common/storj"
|
|
"storj.io/common/storj/location"
|
|
"storj.io/storj/satellite/metabase"
|
|
"storj.io/storj/satellite/nodeselection"
|
|
)
|
|
|
|
// PiecesCheckResult contains all necessary aggregate information about the state of pieces in a
|
|
// segment. The node that should be holding each piece is evaluated to see if it is online and
|
|
// whether it is in a clumped IP network, in an excluded country, or out of placement for the
|
|
// segment.
|
|
type PiecesCheckResult struct {
|
|
// ExcludeNodeIDs is a list of all node IDs holding pieces of this segment.
|
|
ExcludeNodeIDs []storj.NodeID
|
|
|
|
// Missing is a set of Piece Numbers which are to be considered as lost and irretrievable.
|
|
// (They reside on offline/disqualified/unknown nodes.)
|
|
Missing map[uint16]struct{}
|
|
// Retrievable contains all Piece Numbers that are retrievable; that is, all piece numbers
|
|
// from the segment that are NOT in Missing.
|
|
Retrievable map[uint16]struct{}
|
|
|
|
// Suspended is a set of Piece Numbers which reside on nodes which are suspended.
|
|
Suspended map[uint16]struct{}
|
|
// Clumped is a set of Piece Numbers which are to be considered unhealthy because of IP
|
|
// clumping. (If DoDeclumping is disabled, this set will be empty.)
|
|
Clumped map[uint16]struct{}
|
|
// Exiting is a set of Piece Numbers which are considered unhealthy because the node on
|
|
// which they reside has initiated graceful exit.
|
|
Exiting map[uint16]struct{}
|
|
// OutOfPlacement is a set of Piece Numbers which are unhealthy because of placement rules.
|
|
// (If DoPlacementCheck is disabled, this set will be empty.)
|
|
OutOfPlacement map[uint16]struct{}
|
|
// InExcludedCountry is a set of Piece Numbers which are unhealthy because they are in
|
|
// Excluded countries.
|
|
InExcludedCountry map[uint16]struct{}
|
|
|
|
// ForcingRepair is the set of pieces which force a repair operation for this segment (that
|
|
// includes, currently, only pieces in OutOfPlacement).
|
|
ForcingRepair map[uint16]struct{}
|
|
// Unhealthy contains all Piece Numbers which are in Missing OR Suspended OR Clumped OR
|
|
// Exiting OR OutOfPlacement OR InExcludedCountry.
|
|
Unhealthy map[uint16]struct{}
|
|
// UnhealthyRetrievable is the set of pieces that are "unhealthy-but-retrievable". That is,
|
|
// pieces that are in Unhealthy AND Retrievable.
|
|
UnhealthyRetrievable map[uint16]struct{}
|
|
// Healthy contains all Piece Numbers from the segment which are not in Unhealthy.
|
|
// (Equivalently: all Piece Numbers from the segment which are NOT in Missing OR
|
|
// Suspended OR Clumped OR Exiting OR OutOfPlacement OR InExcludedCountry).
|
|
Healthy map[uint16]struct{}
|
|
}
|
|
|
|
// ClassifySegmentPieces classifies the pieces of a segment into the categories
|
|
// represented by a PiecesCheckResult. Pieces may be put into multiple
|
|
// categories.
|
|
func ClassifySegmentPieces(pieces metabase.Pieces, nodes []nodeselection.SelectedNode, excludedCountryCodes map[location.CountryCode]struct{}, doPlacementCheck, doDeclumping bool, filter nodeselection.NodeFilter) (result PiecesCheckResult) {
|
|
result.ExcludeNodeIDs = make([]storj.NodeID, len(pieces))
|
|
for i, p := range pieces {
|
|
result.ExcludeNodeIDs[i] = p.StorageNode
|
|
}
|
|
|
|
// check excluded countries and remove online nodes from missing pieces
|
|
result.Missing = make(map[uint16]struct{})
|
|
result.Suspended = make(map[uint16]struct{})
|
|
result.Exiting = make(map[uint16]struct{})
|
|
result.Retrievable = make(map[uint16]struct{})
|
|
result.InExcludedCountry = make(map[uint16]struct{})
|
|
for index, nodeRecord := range nodes {
|
|
pieceNum := pieces[index].Number
|
|
|
|
if !nodeRecord.ID.IsZero() && pieces[index].StorageNode != nodeRecord.ID {
|
|
panic("wrong order")
|
|
}
|
|
|
|
if nodeRecord.ID.IsZero() || !nodeRecord.Online {
|
|
// node ID was not found, or the node is disqualified or exited,
|
|
// or it is offline
|
|
result.Missing[pieceNum] = struct{}{}
|
|
} else {
|
|
// node is expected to be online and receiving requests.
|
|
result.Retrievable[pieceNum] = struct{}{}
|
|
}
|
|
|
|
if nodeRecord.Suspended {
|
|
result.Suspended[pieceNum] = struct{}{}
|
|
}
|
|
if nodeRecord.Exiting {
|
|
result.Exiting[pieceNum] = struct{}{}
|
|
}
|
|
|
|
if _, excluded := excludedCountryCodes[nodeRecord.CountryCode]; excluded {
|
|
result.InExcludedCountry[pieceNum] = struct{}{}
|
|
}
|
|
}
|
|
|
|
if doDeclumping && nodeselection.GetAnnotation(filter, nodeselection.AutoExcludeSubnet) != nodeselection.AutoExcludeSubnetOFF {
|
|
// if multiple pieces are on the same last_net, keep only the first one. The rest are
|
|
// to be considered retrievable but unhealthy.
|
|
|
|
lastNets := make(map[string]struct{}, len(pieces))
|
|
result.Clumped = make(map[uint16]struct{})
|
|
|
|
collectClumpedPieces := func(onlineness bool) {
|
|
for index, nodeRecord := range nodes {
|
|
if nodeRecord.Online != onlineness {
|
|
continue
|
|
}
|
|
if nodeRecord.LastNet == "" {
|
|
continue
|
|
}
|
|
pieceNum := pieces[index].Number
|
|
_, ok := lastNets[nodeRecord.LastNet]
|
|
if ok {
|
|
// this LastNet was already seen
|
|
result.Clumped[pieceNum] = struct{}{}
|
|
} else {
|
|
// add to the list of seen LastNets
|
|
lastNets[nodeRecord.LastNet] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
// go over online nodes first, so that if we have to remove clumped pieces, we prefer
|
|
// to remove offline ones over online ones.
|
|
collectClumpedPieces(true)
|
|
collectClumpedPieces(false)
|
|
}
|
|
|
|
if doPlacementCheck {
|
|
// mark all pieces that are out of placement.
|
|
|
|
result.OutOfPlacement = make(map[uint16]struct{})
|
|
for index, nodeRecord := range nodes {
|
|
if nodeRecord.ID.IsZero() {
|
|
continue
|
|
}
|
|
if filter.Match(&nodeRecord) {
|
|
continue
|
|
}
|
|
pieceNum := pieces[index].Number
|
|
result.OutOfPlacement[pieceNum] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// ForcingRepair = OutOfPlacement only, for now
|
|
result.ForcingRepair = make(map[uint16]struct{})
|
|
maps.Copy(result.ForcingRepair, result.OutOfPlacement)
|
|
|
|
// Unhealthy = Missing OR Suspended OR Clumped OR OutOfPlacement OR InExcludedCountry
|
|
result.Unhealthy = make(map[uint16]struct{})
|
|
maps.Copy(result.Unhealthy, result.Missing)
|
|
maps.Copy(result.Unhealthy, result.Suspended)
|
|
maps.Copy(result.Unhealthy, result.Clumped)
|
|
maps.Copy(result.Unhealthy, result.Exiting)
|
|
maps.Copy(result.Unhealthy, result.OutOfPlacement)
|
|
maps.Copy(result.Unhealthy, result.InExcludedCountry)
|
|
|
|
// UnhealthyRetrievable = Unhealthy AND Retrievable
|
|
result.UnhealthyRetrievable = make(map[uint16]struct{})
|
|
for pieceNum := range result.Unhealthy {
|
|
if _, isRetrievable := result.Retrievable[pieceNum]; isRetrievable {
|
|
result.UnhealthyRetrievable[pieceNum] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Healthy = NOT Unhealthy
|
|
result.Healthy = make(map[uint16]struct{})
|
|
for _, piece := range pieces {
|
|
if _, found := result.Unhealthy[piece.Number]; !found {
|
|
result.Healthy[piece.Number] = struct{}{}
|
|
}
|
|
}
|
|
return result
|
|
}
|