|  | 
|  | 1 | +package lib | 
|  | 2 | + | 
|  | 3 | +import ( | 
|  | 4 | +	"context" | 
|  | 5 | +	"fmt" | 
|  | 6 | +	"sync" | 
|  | 7 | +	"time" | 
|  | 8 | + | 
|  | 9 | +	"github.com/filecoin-project/dagstore/indexbs" | 
|  | 10 | +	"github.com/filecoin-project/dagstore/shard" | 
|  | 11 | +	"github.com/filecoin-project/go-fil-markets/piecestore" | 
|  | 12 | +	"github.com/filecoin-project/go-fil-markets/retrievalmarket" | 
|  | 13 | +	"github.com/filecoin-project/go-state-types/abi" | 
|  | 14 | +	lru "github.com/hnlq715/golang-lru" | 
|  | 15 | +	"github.com/ipfs/go-cid" | 
|  | 16 | +	logging "github.com/ipfs/go-log/v2" | 
|  | 17 | +) | 
|  | 18 | + | 
|  | 19 | +var sslog = logging.Logger("shardselect") | 
|  | 20 | + | 
|  | 21 | +// ShardSelector is used by the dagstore's index-backed blockstore to select | 
|  | 22 | +// the best shard from which to retrieve a particular cid. | 
|  | 23 | +// It chooses the first shard that is unsealed and free (zero cost). | 
|  | 24 | +// It caches the results per-shard. | 
|  | 25 | +type ShardSelector struct { | 
|  | 26 | +	ctx context.Context | 
|  | 27 | +	ps  piecestore.PieceStore | 
|  | 28 | +	sa  retrievalmarket.SectorAccessor | 
|  | 29 | +	rp  retrievalmarket.RetrievalProvider | 
|  | 30 | + | 
|  | 31 | +	// The striped lock protects against multiple threads doing a lookup | 
|  | 32 | +	// against the sealing subsystem / retrieval ask for the same shard | 
|  | 33 | +	stripedLock [256]sync.Mutex | 
|  | 34 | +	cache       *lru.Cache | 
|  | 35 | +} | 
|  | 36 | + | 
|  | 37 | +func NewShardSelector(ctx context.Context, ps piecestore.PieceStore, sa retrievalmarket.SectorAccessor, rp retrievalmarket.RetrievalProvider) (*ShardSelector, error) { | 
|  | 38 | +	cache, err := lru.New(2048) | 
|  | 39 | +	if err != nil { | 
|  | 40 | +		return nil, fmt.Errorf("creating shard selector cache: %w", err) | 
|  | 41 | +	} | 
|  | 42 | +	return &ShardSelector{ctx: ctx, ps: ps, sa: sa, rp: rp, cache: cache}, nil | 
|  | 43 | +} | 
|  | 44 | + | 
|  | 45 | +var selectorCacheDuration = 10 * time.Minute | 
|  | 46 | +var selectorCacheErrorDuration = time.Minute | 
|  | 47 | + | 
|  | 48 | +type shardSelectResult struct { | 
|  | 49 | +	available bool | 
|  | 50 | +	err       error | 
|  | 51 | +} | 
|  | 52 | + | 
|  | 53 | +// ShardSelectorF chooses the first shard that is unsealed and free (zero cost) | 
|  | 54 | +func (s *ShardSelector) ShardSelectorF(c cid.Cid, shards []shard.Key) (shard.Key, error) { | 
|  | 55 | +	// If no shards are selected, return ErrNoShardSelected | 
|  | 56 | +	lastErr := indexbs.ErrNoShardSelected | 
|  | 57 | + | 
|  | 58 | +	sslog.Debugw("shard selection", "shards", shards) | 
|  | 59 | +	for _, sk := range shards { | 
|  | 60 | +		lkidx := s.stripedLockIndex(sk) | 
|  | 61 | +		s.stripedLock[lkidx].Lock() | 
|  | 62 | +		available, err := s.isAvailable(sk) | 
|  | 63 | +		s.stripedLock[lkidx].Unlock() | 
|  | 64 | + | 
|  | 65 | +		if available { | 
|  | 66 | +			// We found an available shard, return it | 
|  | 67 | +			sslog.Debugw("shard selected", "shard", sk) | 
|  | 68 | +			return sk, nil | 
|  | 69 | +		} | 
|  | 70 | +		if err != nil { | 
|  | 71 | +			sslog.Debugw("shard error", "shard", sk, "err", err) | 
|  | 72 | +			lastErr = err | 
|  | 73 | +		} | 
|  | 74 | +	} | 
|  | 75 | + | 
|  | 76 | +	// None of the shards are available | 
|  | 77 | +	sslog.Debugw("no shard selected", "shards", shards, "err", lastErr) | 
|  | 78 | +	return shard.Key{}, lastErr | 
|  | 79 | +} | 
|  | 80 | + | 
|  | 81 | +func (s *ShardSelector) isAvailable(sk shard.Key) (bool, error) { | 
|  | 82 | +	// Check if the shard key is in the cache | 
|  | 83 | +	var res *shardSelectResult | 
|  | 84 | +	resi, cached := s.cache.Get(sk) | 
|  | 85 | +	if cached { | 
|  | 86 | +		res = resi.(*shardSelectResult) | 
|  | 87 | +		sslog.Debugw("shard cache hit", "shard", sk) | 
|  | 88 | +		return res.available, res.err | 
|  | 89 | +	} | 
|  | 90 | +	sslog.Debugw("shard cache miss", "shard", sk) | 
|  | 91 | + | 
|  | 92 | +	// Check if the shard is available | 
|  | 93 | +	res = &shardSelectResult{} | 
|  | 94 | +	res.available, res.err = s.checkIsAvailable(sk) | 
|  | 95 | +	expireIn := selectorCacheDuration | 
|  | 96 | +	if res.err != nil { | 
|  | 97 | +		// If there's an error, cache for a short duration so that we | 
|  | 98 | +		// don't wait too long to try again. | 
|  | 99 | +		expireIn = selectorCacheErrorDuration | 
|  | 100 | +		res.available = false | 
|  | 101 | +		res.err = fmt.Errorf("running shard selection for shard %s: %w", sk, res.err) | 
|  | 102 | +		sslog.Warnw("checking shard availability", "shard", sk, "err", res.err) | 
|  | 103 | +	} | 
|  | 104 | +	// Add the result to the cache | 
|  | 105 | +	s.cache.AddEx(sk, res, expireIn) | 
|  | 106 | + | 
|  | 107 | +	return res.available, res.err | 
|  | 108 | +} | 
|  | 109 | + | 
|  | 110 | +func (s *ShardSelector) checkIsAvailable(sk shard.Key) (bool, error) { | 
|  | 111 | +	// Parse piece CID | 
|  | 112 | +	pieceCid, err := cid.Parse(sk.String()) | 
|  | 113 | +	if err != nil { | 
|  | 114 | +		return false, fmt.Errorf("parsing shard key as cid: %w", err) | 
|  | 115 | +	} | 
|  | 116 | + | 
|  | 117 | +	// Read piece info from piece store | 
|  | 118 | +	sslog.Debugw("getting piece info", "shard", sk) | 
|  | 119 | +	pieceInfo, err := s.ps.GetPieceInfo(pieceCid) | 
|  | 120 | +	if err != nil { | 
|  | 121 | +		return false, fmt.Errorf("get piece info: %w", err) | 
|  | 122 | +	} | 
|  | 123 | + | 
|  | 124 | +	// Filter for deals that are unsealed | 
|  | 125 | +	sslog.Debugw("filtering for unsealed deals", "shard", sk, "deals", len(pieceInfo.Deals)) | 
|  | 126 | +	unsealedDeals := make([]piecestore.DealInfo, 0, len(pieceInfo.Deals)) | 
|  | 127 | +	var lastErr error | 
|  | 128 | +	for _, di := range pieceInfo.Deals { | 
|  | 129 | +		isUnsealed, err := s.sa.IsUnsealed(s.ctx, di.SectorID, di.Offset.Unpadded(), di.Length.Unpadded()) | 
|  | 130 | +		if err != nil { | 
|  | 131 | +			sslog.Warnf("checking if sector is unsealed", "shard", "sector", di.SectorID, sk, "err", err) | 
|  | 132 | +			lastErr = err | 
|  | 133 | +			continue | 
|  | 134 | +		} | 
|  | 135 | + | 
|  | 136 | +		if isUnsealed { | 
|  | 137 | +			sslog.Debugw("sector is unsealed", "shard", "sector", di.SectorID) | 
|  | 138 | +			unsealedDeals = append(unsealedDeals, di) | 
|  | 139 | +		} else { | 
|  | 140 | +			sslog.Debugw("sector is sealed", "shard", "sector", di.SectorID) | 
|  | 141 | +		} | 
|  | 142 | +	} | 
|  | 143 | + | 
|  | 144 | +	if len(unsealedDeals) == 0 { | 
|  | 145 | +		// It wasn't possible to find an unsealed sector | 
|  | 146 | +		sslog.Debugw("no unsealed deals found", "shard", sk) | 
|  | 147 | +		return false, lastErr | 
|  | 148 | +	} | 
|  | 149 | + | 
|  | 150 | +	// Check if the piece is available for free (zero-cost) retrieval | 
|  | 151 | +	input := retrievalmarket.PricingInput{ | 
|  | 152 | +		// Piece from which the payload will be retrieved | 
|  | 153 | +		PieceCID: pieceInfo.PieceCID, | 
|  | 154 | +		Unsealed: true, | 
|  | 155 | +	} | 
|  | 156 | + | 
|  | 157 | +	var dealsIds []abi.DealID | 
|  | 158 | +	for _, d := range unsealedDeals { | 
|  | 159 | +		dealsIds = append(dealsIds, d.DealID) | 
|  | 160 | +	} | 
|  | 161 | + | 
|  | 162 | +	sslog.Debugw("getting dynamic asking price for unsealed deals", "shard", sk, "deals", len(unsealedDeals)) | 
|  | 163 | +	ask, err := s.rp.GetDynamicAsk(s.ctx, input, dealsIds) | 
|  | 164 | +	if err != nil { | 
|  | 165 | +		return false, fmt.Errorf("getting retrieval ask: %w", err) | 
|  | 166 | +	} | 
|  | 167 | + | 
|  | 168 | +	// The piece is available for free retrieval | 
|  | 169 | +	if ask.PricePerByte.NilOrZero() { | 
|  | 170 | +		sslog.Debugw("asking price for unsealed deals is zero", "shard", sk) | 
|  | 171 | +		return true, nil | 
|  | 172 | +	} | 
|  | 173 | + | 
|  | 174 | +	sslog.Debugw("asking price-per-byte for unsealed deals is non-zero", "shard", sk, "price", ask.PricePerByte.String()) | 
|  | 175 | +	return false, nil | 
|  | 176 | +} | 
|  | 177 | + | 
|  | 178 | +func (s *ShardSelector) stripedLockIndex(sk shard.Key) int { | 
|  | 179 | +	skstr := sk.String() | 
|  | 180 | +	return int(skstr[len(skstr)-1]) | 
|  | 181 | +} | 
0 commit comments