Merge pull request #9183 from filecoin-project/feat/sectornum-mgmt
feat: sealing: Use bitfields to manage sector numbers
This commit is contained in:
commit
45d1bd61ce
@ -979,6 +979,11 @@ workflows:
|
|||||||
suite: itest-sector_miner_collateral
|
suite: itest-sector_miner_collateral
|
||||||
target: "./itests/sector_miner_collateral_test.go"
|
target: "./itests/sector_miner_collateral_test.go"
|
||||||
|
|
||||||
|
- test:
|
||||||
|
name: test-itest-sector_numassign
|
||||||
|
suite: itest-sector_numassign
|
||||||
|
target: "./itests/sector_numassign_test.go"
|
||||||
|
|
||||||
- test:
|
- test:
|
||||||
name: test-itest-sector_pledge
|
name: test-itest-sector_pledge
|
||||||
suite: itest-sector_pledge
|
suite: itest-sector_pledge
|
||||||
|
@ -540,6 +540,8 @@ type FullNode interface {
|
|||||||
StateChangedActors(context.Context, cid.Cid, cid.Cid) (map[string]types.Actor, error) //perm:read
|
StateChangedActors(context.Context, cid.Cid, cid.Cid) (map[string]types.Actor, error) //perm:read
|
||||||
// StateMinerSectorCount returns the number of sectors in a miner's sector set and proving set
|
// StateMinerSectorCount returns the number of sectors in a miner's sector set and proving set
|
||||||
StateMinerSectorCount(context.Context, address.Address, types.TipSetKey) (MinerSectors, error) //perm:read
|
StateMinerSectorCount(context.Context, address.Address, types.TipSetKey) (MinerSectors, error) //perm:read
|
||||||
|
// StateMinerAllocated returns a bitfield containing all sector numbers marked as allocated in miner state
|
||||||
|
StateMinerAllocated(context.Context, address.Address, types.TipSetKey) (*bitfield.BitField, error) //perm:read
|
||||||
// StateCompute is a flexible command that applies the given messages on the given tipset.
|
// StateCompute is a flexible command that applies the given messages on the given tipset.
|
||||||
// The messages are run as though the VM were at the provided height.
|
// The messages are run as though the VM were at the provided height.
|
||||||
//
|
//
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/libp2p/go-libp2p/core/peer"
|
"github.com/libp2p/go-libp2p/core/peer"
|
||||||
|
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
|
"github.com/filecoin-project/go-bitfield"
|
||||||
datatransfer "github.com/filecoin-project/go-data-transfer"
|
datatransfer "github.com/filecoin-project/go-data-transfer"
|
||||||
"github.com/filecoin-project/go-fil-markets/piecestore"
|
"github.com/filecoin-project/go-fil-markets/piecestore"
|
||||||
"github.com/filecoin-project/go-fil-markets/retrievalmarket"
|
"github.com/filecoin-project/go-fil-markets/retrievalmarket"
|
||||||
@ -123,6 +124,21 @@ type StorageMiner interface {
|
|||||||
// SectorAbortUpgrade can be called on sectors that are in the process of being upgraded to abort it
|
// SectorAbortUpgrade can be called on sectors that are in the process of being upgraded to abort it
|
||||||
SectorAbortUpgrade(context.Context, abi.SectorNumber) error //perm:admin
|
SectorAbortUpgrade(context.Context, abi.SectorNumber) error //perm:admin
|
||||||
|
|
||||||
|
// SectorNumAssignerMeta returns sector number assigner metadata - reserved/allocated
|
||||||
|
SectorNumAssignerMeta(ctx context.Context) (NumAssignerMeta, error) //perm:read
|
||||||
|
// SectorNumReservations returns a list of sector number reservations
|
||||||
|
SectorNumReservations(ctx context.Context) (map[string]bitfield.BitField, error) //perm:read
|
||||||
|
// SectorNumReserve creates a new sector number reservation. Will fail if any other reservation has colliding
|
||||||
|
// numbers or name. Set force to true to override safety checks.
|
||||||
|
// Valid characters for name: a-z, A-Z, 0-9, _, -
|
||||||
|
SectorNumReserve(ctx context.Context, name string, sectors bitfield.BitField, force bool) error //perm:admin
|
||||||
|
// SectorNumReserveCount creates a new sector number reservation for `count` sector numbers.
|
||||||
|
// by default lotus will allocate lowest-available sector numbers to the reservation.
|
||||||
|
// For restrictions on `name` see SectorNumReserve
|
||||||
|
SectorNumReserveCount(ctx context.Context, name string, count uint64) (bitfield.BitField, error) //perm:admin
|
||||||
|
// SectorNumFree drops a sector reservation
|
||||||
|
SectorNumFree(ctx context.Context, name string) error //perm:admin
|
||||||
|
|
||||||
// WorkerConnect tells the node to connect to workers RPC
|
// WorkerConnect tells the node to connect to workers RPC
|
||||||
WorkerConnect(context.Context, string) error //perm:admin retry:true
|
WorkerConnect(context.Context, string) error //perm:admin retry:true
|
||||||
WorkerStats(context.Context) (map[uuid.UUID]storiface.WorkerStats, error) //perm:admin
|
WorkerStats(context.Context) (map[uuid.UUID]storiface.WorkerStats, error) //perm:admin
|
||||||
@ -468,3 +484,13 @@ type DagstoreInitializeAllEvent struct {
|
|||||||
Total int
|
Total int
|
||||||
Current int
|
Current int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NumAssignerMeta struct {
|
||||||
|
Reserved bitfield.BitField
|
||||||
|
Allocated bitfield.BitField
|
||||||
|
|
||||||
|
// ChainAllocated+Reserved+Allocated
|
||||||
|
InUse bitfield.BitField
|
||||||
|
|
||||||
|
Next abi.SectorNumber
|
||||||
|
}
|
||||||
|
@ -336,6 +336,9 @@ func init() {
|
|||||||
Conns: 4,
|
Conns: 4,
|
||||||
FD: 5,
|
FD: 5,
|
||||||
})
|
})
|
||||||
|
addExample(map[string]bitfield.BitField{
|
||||||
|
"": bitfield.NewFromSet([]uint64{5, 6, 7, 10}),
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2677,6 +2677,21 @@ func (mr *MockFullNodeMockRecorder) StateMinerActiveSectors(arg0, arg1, arg2 int
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMinerActiveSectors", reflect.TypeOf((*MockFullNode)(nil).StateMinerActiveSectors), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMinerActiveSectors", reflect.TypeOf((*MockFullNode)(nil).StateMinerActiveSectors), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StateMinerAllocated mocks base method.
|
||||||
|
func (m *MockFullNode) StateMinerAllocated(arg0 context.Context, arg1 address.Address, arg2 types.TipSetKey) (*bitfield.BitField, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "StateMinerAllocated", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(*bitfield.BitField)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateMinerAllocated indicates an expected call of StateMinerAllocated.
|
||||||
|
func (mr *MockFullNodeMockRecorder) StateMinerAllocated(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMinerAllocated", reflect.TypeOf((*MockFullNode)(nil).StateMinerAllocated), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
// StateMinerAvailableBalance mocks base method.
|
// StateMinerAvailableBalance mocks base method.
|
||||||
func (m *MockFullNode) StateMinerAvailableBalance(arg0 context.Context, arg1 address.Address, arg2 types.TipSetKey) (big.Int, error) {
|
func (m *MockFullNode) StateMinerAvailableBalance(arg0 context.Context, arg1 address.Address, arg2 types.TipSetKey) (big.Int, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
@ -393,6 +393,8 @@ type FullNodeStruct struct {
|
|||||||
|
|
||||||
StateMinerActiveSectors func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) ([]*miner.SectorOnChainInfo, error) `perm:"read"`
|
StateMinerActiveSectors func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) ([]*miner.SectorOnChainInfo, error) `perm:"read"`
|
||||||
|
|
||||||
|
StateMinerAllocated func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*bitfield.BitField, error) `perm:"read"`
|
||||||
|
|
||||||
StateMinerAvailableBalance func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (types.BigInt, error) `perm:"read"`
|
StateMinerAvailableBalance func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (types.BigInt, error) `perm:"read"`
|
||||||
|
|
||||||
StateMinerDeadlines func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) ([]Deadline, error) `perm:"read"`
|
StateMinerDeadlines func(p0 context.Context, p1 address.Address, p2 types.TipSetKey) ([]Deadline, error) `perm:"read"`
|
||||||
@ -826,6 +828,16 @@ type StorageMinerStruct struct {
|
|||||||
|
|
||||||
SectorMatchPendingPiecesToOpenSectors func(p0 context.Context) error `perm:"admin"`
|
SectorMatchPendingPiecesToOpenSectors func(p0 context.Context) error `perm:"admin"`
|
||||||
|
|
||||||
|
SectorNumAssignerMeta func(p0 context.Context) (NumAssignerMeta, error) `perm:"read"`
|
||||||
|
|
||||||
|
SectorNumFree func(p0 context.Context, p1 string) error `perm:"admin"`
|
||||||
|
|
||||||
|
SectorNumReservations func(p0 context.Context) (map[string]bitfield.BitField, error) `perm:"read"`
|
||||||
|
|
||||||
|
SectorNumReserve func(p0 context.Context, p1 string, p2 bitfield.BitField, p3 bool) error `perm:"admin"`
|
||||||
|
|
||||||
|
SectorNumReserveCount func(p0 context.Context, p1 string, p2 uint64) (bitfield.BitField, error) `perm:"admin"`
|
||||||
|
|
||||||
SectorPreCommitFlush func(p0 context.Context) ([]sealiface.PreCommitBatchRes, error) `perm:"admin"`
|
SectorPreCommitFlush func(p0 context.Context) ([]sealiface.PreCommitBatchRes, error) `perm:"admin"`
|
||||||
|
|
||||||
SectorPreCommitPending func(p0 context.Context) ([]abi.SectorID, error) `perm:"admin"`
|
SectorPreCommitPending func(p0 context.Context) ([]abi.SectorID, error) `perm:"admin"`
|
||||||
@ -2724,6 +2736,17 @@ func (s *FullNodeStub) StateMinerActiveSectors(p0 context.Context, p1 address.Ad
|
|||||||
return *new([]*miner.SectorOnChainInfo), ErrNotSupported
|
return *new([]*miner.SectorOnChainInfo), ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *FullNodeStruct) StateMinerAllocated(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*bitfield.BitField, error) {
|
||||||
|
if s.Internal.StateMinerAllocated == nil {
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
return s.Internal.StateMinerAllocated(p0, p1, p2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *FullNodeStub) StateMinerAllocated(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (*bitfield.BitField, error) {
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
func (s *FullNodeStruct) StateMinerAvailableBalance(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (types.BigInt, error) {
|
func (s *FullNodeStruct) StateMinerAvailableBalance(p0 context.Context, p1 address.Address, p2 types.TipSetKey) (types.BigInt, error) {
|
||||||
if s.Internal.StateMinerAvailableBalance == nil {
|
if s.Internal.StateMinerAvailableBalance == nil {
|
||||||
return *new(types.BigInt), ErrNotSupported
|
return *new(types.BigInt), ErrNotSupported
|
||||||
@ -4913,6 +4936,61 @@ func (s *StorageMinerStub) SectorMatchPendingPiecesToOpenSectors(p0 context.Cont
|
|||||||
return ErrNotSupported
|
return ErrNotSupported
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStruct) SectorNumAssignerMeta(p0 context.Context) (NumAssignerMeta, error) {
|
||||||
|
if s.Internal.SectorNumAssignerMeta == nil {
|
||||||
|
return *new(NumAssignerMeta), ErrNotSupported
|
||||||
|
}
|
||||||
|
return s.Internal.SectorNumAssignerMeta(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStub) SectorNumAssignerMeta(p0 context.Context) (NumAssignerMeta, error) {
|
||||||
|
return *new(NumAssignerMeta), ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStruct) SectorNumFree(p0 context.Context, p1 string) error {
|
||||||
|
if s.Internal.SectorNumFree == nil {
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
||||||
|
return s.Internal.SectorNumFree(p0, p1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStub) SectorNumFree(p0 context.Context, p1 string) error {
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStruct) SectorNumReservations(p0 context.Context) (map[string]bitfield.BitField, error) {
|
||||||
|
if s.Internal.SectorNumReservations == nil {
|
||||||
|
return *new(map[string]bitfield.BitField), ErrNotSupported
|
||||||
|
}
|
||||||
|
return s.Internal.SectorNumReservations(p0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStub) SectorNumReservations(p0 context.Context) (map[string]bitfield.BitField, error) {
|
||||||
|
return *new(map[string]bitfield.BitField), ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStruct) SectorNumReserve(p0 context.Context, p1 string, p2 bitfield.BitField, p3 bool) error {
|
||||||
|
if s.Internal.SectorNumReserve == nil {
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
||||||
|
return s.Internal.SectorNumReserve(p0, p1, p2, p3)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStub) SectorNumReserve(p0 context.Context, p1 string, p2 bitfield.BitField, p3 bool) error {
|
||||||
|
return ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStruct) SectorNumReserveCount(p0 context.Context, p1 string, p2 uint64) (bitfield.BitField, error) {
|
||||||
|
if s.Internal.SectorNumReserveCount == nil {
|
||||||
|
return *new(bitfield.BitField), ErrNotSupported
|
||||||
|
}
|
||||||
|
return s.Internal.SectorNumReserveCount(p0, p1, p2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StorageMinerStub) SectorNumReserveCount(p0 context.Context, p1 string, p2 uint64) (bitfield.BitField, error) {
|
||||||
|
return *new(bitfield.BitField), ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
func (s *StorageMinerStruct) SectorPreCommitFlush(p0 context.Context) ([]sealiface.PreCommitBatchRes, error) {
|
func (s *StorageMinerStruct) SectorPreCommitFlush(p0 context.Context) ([]sealiface.PreCommitBatchRes, error) {
|
||||||
if s.Internal.SectorPreCommitFlush == nil {
|
if s.Internal.SectorPreCommitFlush == nil {
|
||||||
return *new([]sealiface.PreCommitBatchRes), ErrNotSupported
|
return *new([]sealiface.PreCommitBatchRes), ErrNotSupported
|
||||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -387,7 +387,7 @@ func migratePreSealMeta(ctx context.Context, api v1api.FullNode, metadata string
|
|||||||
|
|
||||||
buf := make([]byte, binary.MaxVarintLen64)
|
buf := make([]byte, binary.MaxVarintLen64)
|
||||||
size := binary.PutUvarint(buf, uint64(maxSectorID))
|
size := binary.PutUvarint(buf, uint64(maxSectorID))
|
||||||
return mds.Put(ctx, datastore.NewKey(modules.StorageCounterDSPrefix), buf[:size])
|
return mds.Put(ctx, datastore.NewKey(pipeline.StorageCounterDSPrefix), buf[:size])
|
||||||
}
|
}
|
||||||
|
|
||||||
func findMarketDealID(ctx context.Context, api v1api.FullNode, deal market8.DealProposal) (abi.DealID, error) {
|
func findMarketDealID(ctx context.Context, api v1api.FullNode, deal market8.DealProposal) (abi.DealID, error) {
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
"github.com/filecoin-project/lotus/chain/actors/policy"
|
"github.com/filecoin-project/lotus/chain/actors/policy"
|
||||||
"github.com/filecoin-project/lotus/chain/types"
|
"github.com/filecoin-project/lotus/chain/types"
|
||||||
lcli "github.com/filecoin-project/lotus/cli"
|
lcli "github.com/filecoin-project/lotus/cli"
|
||||||
|
"github.com/filecoin-project/lotus/lib/strle"
|
||||||
"github.com/filecoin-project/lotus/lib/tablewriter"
|
"github.com/filecoin-project/lotus/lib/tablewriter"
|
||||||
sealing "github.com/filecoin-project/lotus/storage/pipeline"
|
sealing "github.com/filecoin-project/lotus/storage/pipeline"
|
||||||
)
|
)
|
||||||
@ -45,6 +46,7 @@ var sectorsCmd = &cli.Command{
|
|||||||
sectorsRefsCmd,
|
sectorsRefsCmd,
|
||||||
sectorsUpdateCmd,
|
sectorsUpdateCmd,
|
||||||
sectorsPledgeCmd,
|
sectorsPledgeCmd,
|
||||||
|
sectorsNumbersCmd,
|
||||||
sectorPreCommitsCmd,
|
sectorPreCommitsCmd,
|
||||||
sectorsCheckExpireCmd,
|
sectorsCheckExpireCmd,
|
||||||
sectorsExpiredCmd,
|
sectorsExpiredCmd,
|
||||||
@ -2201,3 +2203,142 @@ var sectorsCompactPartitionsCmd = &cli.Command{
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var sectorsNumbersCmd = &cli.Command{
|
||||||
|
Name: "numbers",
|
||||||
|
Usage: "manage sector number assignments",
|
||||||
|
Subcommands: []*cli.Command{
|
||||||
|
sectorsNumbersInfoCmd,
|
||||||
|
sectorsNumbersReservationsCmd,
|
||||||
|
sectorsNumbersReserveCmd,
|
||||||
|
sectorsNumbersFreeCmd,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sectorsNumbersInfoCmd = &cli.Command{
|
||||||
|
Name: "info",
|
||||||
|
Usage: "view sector assigner state",
|
||||||
|
Action: func(cctx *cli.Context) error {
|
||||||
|
api, closer, err := lcli.GetStorageMinerAPI(cctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer closer()
|
||||||
|
ctx := lcli.ReqContext(cctx)
|
||||||
|
|
||||||
|
am, err := api.SectorNumAssignerMeta(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
alloc, err := strle.BitfieldToHumanRanges(am.Allocated)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
reserved, err := strle.BitfieldToHumanRanges(am.Reserved)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Next free: %s\n", am.Next)
|
||||||
|
fmt.Printf("Allocated: %s\n", alloc)
|
||||||
|
fmt.Printf("Reserved: %s\n", reserved)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sectorsNumbersReservationsCmd = &cli.Command{
|
||||||
|
Name: "reservations",
|
||||||
|
Usage: "list sector number reservations",
|
||||||
|
Action: func(cctx *cli.Context) error {
|
||||||
|
api, closer, err := lcli.GetStorageMinerAPI(cctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer closer()
|
||||||
|
ctx := lcli.ReqContext(cctx)
|
||||||
|
|
||||||
|
rs, err := api.SectorNumReservations(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var out []string
|
||||||
|
|
||||||
|
for name, field := range rs {
|
||||||
|
hr, err := strle.BitfieldToHumanRanges(field)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
count, err := field.Count()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, fmt.Sprintf("%s: count=%d %s", name, count, hr))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("reservations: %d\n", len(out))
|
||||||
|
|
||||||
|
sort.Strings(out)
|
||||||
|
|
||||||
|
for _, s := range out {
|
||||||
|
fmt.Println(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sectorsNumbersReserveCmd = &cli.Command{
|
||||||
|
Name: "reserve",
|
||||||
|
Usage: "create sector number reservations",
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.BoolFlag{
|
||||||
|
Name: "force",
|
||||||
|
Usage: "skip duplicate reservation checks (note: can lead to damaging other reservations on free)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ArgsUsage: "[reservation name] [reserved ranges]",
|
||||||
|
Action: func(cctx *cli.Context) error {
|
||||||
|
api, closer, err := lcli.GetStorageMinerAPI(cctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer closer()
|
||||||
|
ctx := lcli.ReqContext(cctx)
|
||||||
|
|
||||||
|
if cctx.Args().Len() != 2 {
|
||||||
|
return xerrors.Errorf("expected 2 arguments: [reservation name] [reserved ranges]")
|
||||||
|
}
|
||||||
|
|
||||||
|
bf, err := strle.HumanRangesToBitField(cctx.Args().Get(1))
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("parsing ranges: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.SectorNumReserve(ctx, cctx.Args().First(), bf, cctx.Bool("force"))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var sectorsNumbersFreeCmd = &cli.Command{
|
||||||
|
Name: "free",
|
||||||
|
Usage: "remove sector number reservations",
|
||||||
|
ArgsUsage: "[reservation name]",
|
||||||
|
Action: func(cctx *cli.Context) error {
|
||||||
|
api, closer, err := lcli.GetStorageMinerAPI(cctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer closer()
|
||||||
|
ctx := lcli.ReqContext(cctx)
|
||||||
|
|
||||||
|
if cctx.Args().Len() != 1 {
|
||||||
|
return xerrors.Errorf("expected 1 argument: [reservation name]")
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.SectorNumFree(ctx, cctx.Args().First())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
@ -139,6 +139,11 @@
|
|||||||
* [SectorGetSealDelay](#SectorGetSealDelay)
|
* [SectorGetSealDelay](#SectorGetSealDelay)
|
||||||
* [SectorMarkForUpgrade](#SectorMarkForUpgrade)
|
* [SectorMarkForUpgrade](#SectorMarkForUpgrade)
|
||||||
* [SectorMatchPendingPiecesToOpenSectors](#SectorMatchPendingPiecesToOpenSectors)
|
* [SectorMatchPendingPiecesToOpenSectors](#SectorMatchPendingPiecesToOpenSectors)
|
||||||
|
* [SectorNumAssignerMeta](#SectorNumAssignerMeta)
|
||||||
|
* [SectorNumFree](#SectorNumFree)
|
||||||
|
* [SectorNumReservations](#SectorNumReservations)
|
||||||
|
* [SectorNumReserve](#SectorNumReserve)
|
||||||
|
* [SectorNumReserveCount](#SectorNumReserveCount)
|
||||||
* [SectorPreCommitFlush](#SectorPreCommitFlush)
|
* [SectorPreCommitFlush](#SectorPreCommitFlush)
|
||||||
* [SectorPreCommitPending](#SectorPreCommitPending)
|
* [SectorPreCommitPending](#SectorPreCommitPending)
|
||||||
* [SectorRemove](#SectorRemove)
|
* [SectorRemove](#SectorRemove)
|
||||||
@ -2938,6 +2943,114 @@ Inputs: `null`
|
|||||||
|
|
||||||
Response: `{}`
|
Response: `{}`
|
||||||
|
|
||||||
|
### SectorNumAssignerMeta
|
||||||
|
SectorNumAssignerMeta returns sector number assigner metadata - reserved/allocated
|
||||||
|
|
||||||
|
|
||||||
|
Perms: read
|
||||||
|
|
||||||
|
Inputs: `null`
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Reserved": [
|
||||||
|
5,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"Allocated": [
|
||||||
|
5,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"InUse": [
|
||||||
|
5,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"Next": 9
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SectorNumFree
|
||||||
|
SectorNumFree drops a sector reservation
|
||||||
|
|
||||||
|
|
||||||
|
Perms: admin
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"string value"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response: `{}`
|
||||||
|
|
||||||
|
### SectorNumReservations
|
||||||
|
SectorNumReservations returns a list of sector number reservations
|
||||||
|
|
||||||
|
|
||||||
|
Perms: read
|
||||||
|
|
||||||
|
Inputs: `null`
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"": [
|
||||||
|
5,
|
||||||
|
3,
|
||||||
|
2,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### SectorNumReserve
|
||||||
|
SectorNumReserve creates a new sector number reservation. Will fail if any other reservation has colliding
|
||||||
|
numbers or name. Set force to true to override safety checks.
|
||||||
|
Valid characters for name: a-z, A-Z, 0-9, _, -
|
||||||
|
|
||||||
|
|
||||||
|
Perms: admin
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"string value",
|
||||||
|
[
|
||||||
|
5,
|
||||||
|
1
|
||||||
|
],
|
||||||
|
true
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response: `{}`
|
||||||
|
|
||||||
|
### SectorNumReserveCount
|
||||||
|
SectorNumReserveCount creates a new sector number reservation for `count` sector numbers.
|
||||||
|
by default lotus will allocate lowest-available sector numbers to the reservation.
|
||||||
|
For restrictions on `name` see SectorNumReserve
|
||||||
|
|
||||||
|
|
||||||
|
Perms: admin
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"string value",
|
||||||
|
42
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
5,
|
||||||
|
1
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### SectorPreCommitFlush
|
### SectorPreCommitFlush
|
||||||
SectorPreCommitFlush immediately sends a PreCommit message with sectors batched for PreCommit.
|
SectorPreCommitFlush immediately sends a PreCommit message with sectors batched for PreCommit.
|
||||||
Returns null if message wasn't sent
|
Returns null if message wasn't sent
|
||||||
|
@ -192,6 +192,7 @@
|
|||||||
* [StateMarketParticipants](#StateMarketParticipants)
|
* [StateMarketParticipants](#StateMarketParticipants)
|
||||||
* [StateMarketStorageDeal](#StateMarketStorageDeal)
|
* [StateMarketStorageDeal](#StateMarketStorageDeal)
|
||||||
* [StateMinerActiveSectors](#StateMinerActiveSectors)
|
* [StateMinerActiveSectors](#StateMinerActiveSectors)
|
||||||
|
* [StateMinerAllocated](#StateMinerAllocated)
|
||||||
* [StateMinerAvailableBalance](#StateMinerAvailableBalance)
|
* [StateMinerAvailableBalance](#StateMinerAvailableBalance)
|
||||||
* [StateMinerDeadlines](#StateMinerDeadlines)
|
* [StateMinerDeadlines](#StateMinerDeadlines)
|
||||||
* [StateMinerFaults](#StateMinerFaults)
|
* [StateMinerFaults](#StateMinerFaults)
|
||||||
@ -6175,6 +6176,34 @@ Response:
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### StateMinerAllocated
|
||||||
|
StateMinerAllocated returns a bitfield containing all sector numbers marked as allocated in miner state
|
||||||
|
|
||||||
|
|
||||||
|
Perms: read
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
"f01234",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"/": "bafy2bzacea3wsdh6y3a36tb3skempjoxqpuyompjbmfeyf34fi3uy6uue42v4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"/": "bafy2bzacebp3shtrn43k7g3unredz7fxn4gj533d3o43tqn2p2ipxxhrvchve"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
0
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### StateMinerAvailableBalance
|
### StateMinerAvailableBalance
|
||||||
StateMinerAvailableBalance returns the portion of a miner's balance that can be withdrawn or spent
|
StateMinerAvailableBalance returns the portion of a miner's balance that can be withdrawn or spent
|
||||||
|
|
||||||
|
@ -1637,6 +1637,7 @@ COMMANDS:
|
|||||||
refs List References to sectors
|
refs List References to sectors
|
||||||
update-state ADVANCED: manually update the state of a sector, this may aid in error recovery
|
update-state ADVANCED: manually update the state of a sector, this may aid in error recovery
|
||||||
pledge store random data in a sector
|
pledge store random data in a sector
|
||||||
|
numbers manage sector number assignments
|
||||||
precommits Print on-chain precommit info
|
precommits Print on-chain precommit info
|
||||||
check-expire Inspect expiring sectors
|
check-expire Inspect expiring sectors
|
||||||
expired Get or cleanup expired sectors
|
expired Get or cleanup expired sectors
|
||||||
@ -1734,6 +1735,78 @@ OPTIONS:
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### lotus-miner sectors numbers
|
||||||
|
```
|
||||||
|
NAME:
|
||||||
|
lotus-miner sectors numbers - manage sector number assignments
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
lotus-miner sectors numbers command [command options] [arguments...]
|
||||||
|
|
||||||
|
COMMANDS:
|
||||||
|
info view sector assigner state
|
||||||
|
reservations list sector number reservations
|
||||||
|
reserve create sector number reservations
|
||||||
|
free remove sector number reservations
|
||||||
|
help, h Shows a list of commands or help for one command
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--help, -h show help (default: false)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### lotus-miner sectors numbers info
|
||||||
|
```
|
||||||
|
NAME:
|
||||||
|
lotus-miner sectors numbers info - view sector assigner state
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
lotus-miner sectors numbers info [command options] [arguments...]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--help, -h show help (default: false)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### lotus-miner sectors numbers reservations
|
||||||
|
```
|
||||||
|
NAME:
|
||||||
|
lotus-miner sectors numbers reservations - list sector number reservations
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
lotus-miner sectors numbers reservations [command options] [arguments...]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--help, -h show help (default: false)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### lotus-miner sectors numbers reserve
|
||||||
|
```
|
||||||
|
NAME:
|
||||||
|
lotus-miner sectors numbers reserve - create sector number reservations
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
lotus-miner sectors numbers reserve [command options] [reservation name] [reserved ranges]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--force skip duplicate reservation checks (note: can lead to damaging other reservations on free) (default: false)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
#### lotus-miner sectors numbers free
|
||||||
|
```
|
||||||
|
NAME:
|
||||||
|
lotus-miner sectors numbers free - remove sector number reservations
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
lotus-miner sectors numbers free [command options] [reservation name]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--help, -h show help (default: false)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
### lotus-miner sectors precommits
|
### lotus-miner sectors precommits
|
||||||
```
|
```
|
||||||
NAME:
|
NAME:
|
||||||
|
@ -992,5 +992,5 @@ func importPreSealMeta(ctx context.Context, meta genesis.Miner, mds dtypes.Metad
|
|||||||
|
|
||||||
buf := make([]byte, binary.MaxVarintLen64)
|
buf := make([]byte, binary.MaxVarintLen64)
|
||||||
size := binary.PutUvarint(buf, uint64(maxSectorID))
|
size := binary.PutUvarint(buf, uint64(maxSectorID))
|
||||||
return mds.Put(ctx, datastore.NewKey(modules.StorageCounterDSPrefix), buf[:size])
|
return mds.Put(ctx, datastore.NewKey(pipeline.StorageCounterDSPrefix), buf[:size])
|
||||||
}
|
}
|
||||||
|
177
itests/sector_numassign_test.go
Normal file
177
itests/sector_numassign_test.go
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
package itests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/go-bitfield"
|
||||||
|
rlepluslazy "github.com/filecoin-project/go-bitfield/rle"
|
||||||
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/lotus/itests/kit"
|
||||||
|
"github.com/filecoin-project/lotus/lib/strle"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAssignBasic(t *testing.T) {
|
||||||
|
kit.QuietMiningLogs()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
blockTime := 1 * time.Millisecond
|
||||||
|
|
||||||
|
_, miner, ens := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.MockProofs())
|
||||||
|
ens.InterconnectAll().BeginMiningMustPost(blockTime)
|
||||||
|
|
||||||
|
nSectors := 2
|
||||||
|
|
||||||
|
{
|
||||||
|
nam, err := miner.SectorNumAssignerMeta(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// genesis sectors start at 1, so if there are 2, we expect the Next to be 3
|
||||||
|
require.Equal(t, abi.SectorNumber(miner.PresealSectors+1), nam.Next)
|
||||||
|
}
|
||||||
|
|
||||||
|
miner.PledgeSectors(ctx, nSectors, 0, nil)
|
||||||
|
|
||||||
|
sl, err := miner.SectorsListNonGenesis(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, sl, nSectors)
|
||||||
|
require.Equal(t, abi.SectorNumber(miner.PresealSectors+1), sl[0])
|
||||||
|
require.Equal(t, abi.SectorNumber(miner.PresealSectors+2), sl[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func rangeBitField(from, to uint64) bitfield.BitField {
|
||||||
|
var runs []rlepluslazy.Run
|
||||||
|
if from > 0 {
|
||||||
|
runs = append(runs, rlepluslazy.Run{
|
||||||
|
Val: false,
|
||||||
|
Len: from,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
runs = append(runs, rlepluslazy.Run{
|
||||||
|
Val: true,
|
||||||
|
Len: to - from + 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
r, err := bitfield.NewFromIter(&rlepluslazy.RunSliceIterator{Runs: runs})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssignReservation(t *testing.T) {
|
||||||
|
kit.QuietMiningLogs()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
blockTime := 1 * time.Millisecond
|
||||||
|
|
||||||
|
_, miner, ens := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.MockProofs())
|
||||||
|
ens.InterconnectAll().BeginMiningMustPost(blockTime)
|
||||||
|
|
||||||
|
err := miner.SectorNumReserve(ctx, "test-reservation", rangeBitField(3, 10), false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// colliding name fails
|
||||||
|
err = miner.SectorNumReserve(ctx, "test-reservation", rangeBitField(30, 33), false)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// colliding range
|
||||||
|
err = miner.SectorNumReserve(ctx, "test-reservation2", rangeBitField(7, 12), false)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
// illegal characters in the name
|
||||||
|
err = miner.SectorNumReserve(ctx, "test/reservation", rangeBitField(99, 100), false)
|
||||||
|
require.Error(t, err)
|
||||||
|
|
||||||
|
nSectors := 2
|
||||||
|
|
||||||
|
{
|
||||||
|
nam, err := miner.SectorNumAssignerMeta(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// reservation to 10, so we expect 11 to be first free
|
||||||
|
require.Equal(t, abi.SectorNumber(11), nam.Next)
|
||||||
|
}
|
||||||
|
|
||||||
|
miner.PledgeSectors(ctx, nSectors, 0, nil)
|
||||||
|
|
||||||
|
sl, err := miner.SectorsListNonGenesis(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, sl, nSectors)
|
||||||
|
require.Equal(t, abi.SectorNumber(11), sl[0])
|
||||||
|
require.Equal(t, abi.SectorNumber(12), sl[1])
|
||||||
|
|
||||||
|
// drop the reservation and see if we use the unused numbers
|
||||||
|
err = miner.SectorNumFree(ctx, "test-reservation")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
{
|
||||||
|
nam, err := miner.SectorNumAssignerMeta(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// first post-genesis sector is 3
|
||||||
|
require.Equal(t, abi.SectorNumber(3), nam.Next)
|
||||||
|
}
|
||||||
|
|
||||||
|
miner.PledgeSectors(ctx, 1, nSectors, nil)
|
||||||
|
|
||||||
|
sl, err = miner.SectorsListNonGenesis(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, sl, nSectors+1)
|
||||||
|
require.Equal(t, abi.SectorNumber(3), sl[0])
|
||||||
|
require.Equal(t, abi.SectorNumber(11), sl[1])
|
||||||
|
require.Equal(t, abi.SectorNumber(12), sl[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReserveCount(t *testing.T) {
|
||||||
|
kit.QuietMiningLogs()
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
_, miner, _ := kit.EnsembleMinimal(t, kit.ThroughRPC(), kit.MockProofs())
|
||||||
|
|
||||||
|
// with no reservations higher
|
||||||
|
r1, err := miner.SectorNumReserveCount(ctx, "r1", 2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
requireBitField(t, "3-4", r1)
|
||||||
|
|
||||||
|
// reserve some higher numbers
|
||||||
|
err = miner.SectorNumReserve(ctx, "test-reservation", rangeBitField(10, 15), false)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// reserve a few below an existing reservation
|
||||||
|
r2, err := miner.SectorNumReserveCount(ctx, "r2", 2)
|
||||||
|
require.NoError(t, err)
|
||||||
|
requireBitField(t, "5-6", r2)
|
||||||
|
|
||||||
|
// reserve a few through an existing reservation
|
||||||
|
r3, err := miner.SectorNumReserveCount(ctx, "r3", 6)
|
||||||
|
require.NoError(t, err)
|
||||||
|
requireBitField(t, "7-9,16-18", r3)
|
||||||
|
|
||||||
|
// do one more
|
||||||
|
r4, err := miner.SectorNumReserveCount(ctx, "r4", 4)
|
||||||
|
require.NoError(t, err)
|
||||||
|
requireBitField(t, "19-22", r4)
|
||||||
|
|
||||||
|
resvs, err := miner.SectorNumReservations(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
requireBitField(t, "3-4", resvs["r1"])
|
||||||
|
requireBitField(t, "5-6", resvs["r2"])
|
||||||
|
requireBitField(t, "7-9,16-18", resvs["r3"])
|
||||||
|
requireBitField(t, "19-22", resvs["r4"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireBitField(t *testing.T, expect string, bf bitfield.BitField) {
|
||||||
|
s, err := strle.BitfieldToHumanRanges(bf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, expect, s)
|
||||||
|
}
|
96
lib/strle/human.go
Normal file
96
lib/strle/human.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package strle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/go-bitfield"
|
||||||
|
rlepluslazy "github.com/filecoin-project/go-bitfield/rle"
|
||||||
|
)
|
||||||
|
|
||||||
|
func HumanRangesToBitField(h string) (bitfield.BitField, error) {
|
||||||
|
var runs []rlepluslazy.Run
|
||||||
|
var last uint64
|
||||||
|
|
||||||
|
strRanges := strings.Split(h, ",")
|
||||||
|
for i, strRange := range strRanges {
|
||||||
|
lr := strings.Split(strRange, "-")
|
||||||
|
|
||||||
|
var start, end uint64
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch len(lr) {
|
||||||
|
case 1: // one number
|
||||||
|
start, err = strconv.ParseUint(lr[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return bitfield.BitField{}, xerrors.Errorf("parsing left side of run %d: %w", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
end = start
|
||||||
|
case 2: // x-y
|
||||||
|
start, err = strconv.ParseUint(lr[0], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return bitfield.BitField{}, xerrors.Errorf("parsing left side of run %d: %w", i, err)
|
||||||
|
}
|
||||||
|
end, err = strconv.ParseUint(lr[1], 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return bitfield.BitField{}, xerrors.Errorf("parsing right side of run %d: %w", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if start <= last && last > 0 {
|
||||||
|
return bitfield.BitField{}, xerrors.Errorf("run %d start(%d) was equal to last run end(%d)", i, start, last)
|
||||||
|
}
|
||||||
|
|
||||||
|
if start > end {
|
||||||
|
return bitfield.BitField{}, xerrors.Errorf("run start(%d) can't be greater than run end(%d) (run %d)", start, end, i)
|
||||||
|
}
|
||||||
|
|
||||||
|
if start > last {
|
||||||
|
runs = append(runs, rlepluslazy.Run{Val: false, Len: start - last})
|
||||||
|
}
|
||||||
|
|
||||||
|
runs = append(runs, rlepluslazy.Run{Val: true, Len: end - start + 1})
|
||||||
|
last = end + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitfield.NewFromIter(&rlepluslazy.RunSliceIterator{Runs: runs})
|
||||||
|
}
|
||||||
|
|
||||||
|
func BitfieldToHumanRanges(bf bitfield.BitField) (string, error) {
|
||||||
|
bj, err := bf.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var bints []int64
|
||||||
|
if err := json.Unmarshal(bj, &bints); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var at int64
|
||||||
|
var out string
|
||||||
|
|
||||||
|
for i, bi := range bints {
|
||||||
|
at += bi
|
||||||
|
|
||||||
|
if i%2 == 0 {
|
||||||
|
if i > 0 {
|
||||||
|
out += ","
|
||||||
|
}
|
||||||
|
out += fmt.Sprint(at)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if bi > 1 {
|
||||||
|
out += "-"
|
||||||
|
out += fmt.Sprint(at - 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, err
|
||||||
|
}
|
57
lib/strle/human_test.go
Normal file
57
lib/strle/human_test.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package strle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/go-bitfield"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHumanBitfield(t *testing.T) {
|
||||||
|
check := func(ints []uint64, out string) {
|
||||||
|
bf := bitfield.NewFromSet(ints)
|
||||||
|
h, err := BitfieldToHumanRanges(bf)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, out, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
check([]uint64{2, 3, 4}, "2-4")
|
||||||
|
check([]uint64{2, 3, 4, 8, 9, 10, 11}, "2-4,8-11")
|
||||||
|
check([]uint64{2}, "2")
|
||||||
|
check([]uint64{0}, "0")
|
||||||
|
check([]uint64{0, 1, 2}, "0-2")
|
||||||
|
check([]uint64{0, 1, 5, 9, 11, 13, 14, 19}, "0-1,5,9,11,13-14,19")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHumanBitfieldRoundtrip(t *testing.T) {
|
||||||
|
check := func(ints []uint64, out string) {
|
||||||
|
parsed, err := HumanRangesToBitField(out)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
h, err := BitfieldToHumanRanges(parsed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, out, h)
|
||||||
|
|
||||||
|
bf := bitfield.NewFromSet(ints)
|
||||||
|
ins, err := bitfield.IntersectBitField(bf, parsed)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// if intersected bitfield has the same length as both bitfields they are the same
|
||||||
|
ic, err := ins.Count()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
pc, err := parsed.Count()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, uint64(len(ints)), ic)
|
||||||
|
require.Equal(t, ic, pc)
|
||||||
|
}
|
||||||
|
|
||||||
|
check([]uint64{2, 3, 4}, "2-4")
|
||||||
|
check([]uint64{2, 3, 4, 8, 9, 10, 11}, "2-4,8-11")
|
||||||
|
check([]uint64{2}, "2")
|
||||||
|
check([]uint64{0}, "0")
|
||||||
|
check([]uint64{0, 1, 2}, "0-2")
|
||||||
|
check([]uint64{0, 1, 5, 9, 11, 13, 14, 19}, "0-1,5,9,11,13-14,19")
|
||||||
|
}
|
@ -108,7 +108,6 @@ func ConfigStorageMiner(c interface{}) Option {
|
|||||||
Override(new(storiface.ProverPoSt), From(new(sectorstorage.SectorManager))),
|
Override(new(storiface.ProverPoSt), From(new(sectorstorage.SectorManager))),
|
||||||
|
|
||||||
// Sealing (todo should be under EnableSealing, but storagefsm is currently bundled with storage.Miner)
|
// Sealing (todo should be under EnableSealing, but storagefsm is currently bundled with storage.Miner)
|
||||||
Override(new(sealing.SectorIDCounter), modules.SectorIDCounter),
|
|
||||||
Override(GetParamsKey, modules.GetParams),
|
Override(GetParamsKey, modules.GetParams),
|
||||||
|
|
||||||
Override(new(dtypes.SetSealingConfigFunc), modules.NewSetSealConfigFunc),
|
Override(new(dtypes.SetSealingConfigFunc), modules.NewSetSealConfigFunc),
|
||||||
|
@ -880,6 +880,18 @@ func (a *StateAPI) StateMinerSectorCount(ctx context.Context, addr address.Addre
|
|||||||
return api.MinerSectors{Live: liveCount, Active: activeCount, Faulty: faultyCount}, nil
|
return api.MinerSectors{Live: liveCount, Active: activeCount, Faulty: faultyCount}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *StateAPI) StateMinerAllocated(ctx context.Context, addr address.Address, tsk types.TipSetKey) (*bitfield.BitField, error) {
|
||||||
|
act, err := a.StateManager.LoadActorTsk(ctx, addr, tsk)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mas, err := miner.Load(a.Chain.ActorStore(ctx), act)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mas.GetAllocatedSectors()
|
||||||
|
}
|
||||||
|
|
||||||
func (a *StateAPI) StateSectorPreCommitInfo(ctx context.Context, maddr address.Address, n abi.SectorNumber, tsk types.TipSetKey) (*minertypes.SectorPreCommitOnChainInfo, error) {
|
func (a *StateAPI) StateSectorPreCommitInfo(ctx context.Context, maddr address.Address, n abi.SectorNumber, tsk types.TipSetKey) (*minertypes.SectorPreCommitOnChainInfo, error) {
|
||||||
ts, err := a.Chain.GetTipSetFromKey(ctx, tsk)
|
ts, err := a.Chain.GetTipSetFromKey(ctx, tsk)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/filecoin-project/dagstore"
|
"github.com/filecoin-project/dagstore"
|
||||||
"github.com/filecoin-project/dagstore/shard"
|
"github.com/filecoin-project/dagstore/shard"
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
|
"github.com/filecoin-project/go-bitfield"
|
||||||
datatransfer "github.com/filecoin-project/go-data-transfer"
|
datatransfer "github.com/filecoin-project/go-data-transfer"
|
||||||
gst "github.com/filecoin-project/go-data-transfer/transport/graphsync"
|
gst "github.com/filecoin-project/go-data-transfer/transport/graphsync"
|
||||||
"github.com/filecoin-project/go-fil-markets/piecestore"
|
"github.com/filecoin-project/go-fil-markets/piecestore"
|
||||||
@ -427,6 +428,26 @@ func (sm *StorageMinerAPI) SectorMatchPendingPiecesToOpenSectors(ctx context.Con
|
|||||||
return sm.Miner.SectorMatchPendingPiecesToOpenSectors(ctx)
|
return sm.Miner.SectorMatchPendingPiecesToOpenSectors(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sm *StorageMinerAPI) SectorNumAssignerMeta(ctx context.Context) (api.NumAssignerMeta, error) {
|
||||||
|
return sm.Miner.NumAssignerMeta(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *StorageMinerAPI) SectorNumReservations(ctx context.Context) (map[string]bitfield.BitField, error) {
|
||||||
|
return sm.Miner.NumReservations(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *StorageMinerAPI) SectorNumReserve(ctx context.Context, name string, field bitfield.BitField, force bool) error {
|
||||||
|
return sm.Miner.NumReserve(ctx, name, field, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *StorageMinerAPI) SectorNumReserveCount(ctx context.Context, name string, count uint64) (bitfield.BitField, error) {
|
||||||
|
return sm.Miner.NumReserveCount(ctx, name, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sm *StorageMinerAPI) SectorNumFree(ctx context.Context, name string) error {
|
||||||
|
return sm.Miner.NumFree(ctx, name)
|
||||||
|
}
|
||||||
|
|
||||||
func (sm *StorageMinerAPI) ComputeWindowPoSt(ctx context.Context, dlIdx uint64, tsk types.TipSetKey) ([]minertypes.SubmitWindowedPoStParams, error) {
|
func (sm *StorageMinerAPI) ComputeWindowPoSt(ctx context.Context, dlIdx uint64, tsk types.TipSetKey) ([]minertypes.SubmitWindowedPoStParams, error) {
|
||||||
var ts *types.TipSet
|
var ts *types.TipSet
|
||||||
var err error
|
var err error
|
||||||
|
@ -42,7 +42,6 @@ import (
|
|||||||
"github.com/filecoin-project/go-state-types/abi"
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
"github.com/filecoin-project/go-state-types/big"
|
"github.com/filecoin-project/go-state-types/big"
|
||||||
"github.com/filecoin-project/go-statestore"
|
"github.com/filecoin-project/go-statestore"
|
||||||
"github.com/filecoin-project/go-storedcounter"
|
|
||||||
provider "github.com/filecoin-project/index-provider"
|
provider "github.com/filecoin-project/index-provider"
|
||||||
|
|
||||||
"github.com/filecoin-project/lotus/api"
|
"github.com/filecoin-project/lotus/api"
|
||||||
@ -76,7 +75,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
StorageCounterDSPrefix = "/storage/nextid"
|
|
||||||
StagingAreaDirName = "deal-staging"
|
StagingAreaDirName = "deal-staging"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -155,20 +153,6 @@ func SealProofType(maddr dtypes.MinerAddress, fnapi v1api.FullNode) (abi.Registe
|
|||||||
return miner.PreferredSealProofTypeFromWindowPoStType(networkVersion, mi.WindowPoStProofType)
|
return miner.PreferredSealProofTypeFromWindowPoStType(networkVersion, mi.WindowPoStProofType)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sidsc struct {
|
|
||||||
sc *storedcounter.StoredCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *sidsc) Next() (abi.SectorNumber, error) {
|
|
||||||
i, err := s.sc.Next()
|
|
||||||
return abi.SectorNumber(i), err
|
|
||||||
}
|
|
||||||
|
|
||||||
func SectorIDCounter(ds dtypes.MetadataDS) sealing.SectorIDCounter {
|
|
||||||
sc := storedcounter.New(ds, datastore.NewKey(StorageCounterDSPrefix))
|
|
||||||
return &sidsc{sc}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AddressSelector(addrConf *config.MinerAddressConfig) func() (*ctladdr.AddressSelector, error) {
|
func AddressSelector(addrConf *config.MinerAddressConfig) func() (*ctladdr.AddressSelector, error) {
|
||||||
return func() (*ctladdr.AddressSelector, error) {
|
return func() (*ctladdr.AddressSelector, error) {
|
||||||
as := &ctladdr.AddressSelector{}
|
as := &ctladdr.AddressSelector{}
|
||||||
@ -257,7 +241,6 @@ type SealingPipelineParams struct {
|
|||||||
API v1api.FullNode
|
API v1api.FullNode
|
||||||
MetadataDS dtypes.MetadataDS
|
MetadataDS dtypes.MetadataDS
|
||||||
Sealer sealer.SectorManager
|
Sealer sealer.SectorManager
|
||||||
SectorIDCounter sealing.SectorIDCounter
|
|
||||||
Verifier storiface.Verifier
|
Verifier storiface.Verifier
|
||||||
Prover storiface.Prover
|
Prover storiface.Prover
|
||||||
GetSealingConfigFn dtypes.GetSealingConfigFunc
|
GetSealingConfigFn dtypes.GetSealingConfigFunc
|
||||||
@ -274,7 +257,6 @@ func SealingPipeline(fc config.MinerFeeConfig) func(params SealingPipelineParams
|
|||||||
lc = params.Lifecycle
|
lc = params.Lifecycle
|
||||||
api = params.API
|
api = params.API
|
||||||
sealer = params.Sealer
|
sealer = params.Sealer
|
||||||
sc = params.SectorIDCounter
|
|
||||||
verif = params.Verifier
|
verif = params.Verifier
|
||||||
prover = params.Prover
|
prover = params.Prover
|
||||||
gsd = params.GetSealingConfigFn
|
gsd = params.GetSealingConfigFn
|
||||||
@ -297,7 +279,7 @@ func SealingPipeline(fc config.MinerFeeConfig) func(params SealingPipelineParams
|
|||||||
provingBuffer := md.WPoStProvingPeriod * 2
|
provingBuffer := md.WPoStProvingPeriod * 2
|
||||||
pcp := sealing.NewBasicPreCommitPolicy(api, gsd, provingBuffer)
|
pcp := sealing.NewBasicPreCommitPolicy(api, gsd, provingBuffer)
|
||||||
|
|
||||||
pipeline := sealing.New(ctx, api, fc, evts, maddr, ds, sealer, sc, verif, prover, &pcp, gsd, j, as)
|
pipeline := sealing.New(ctx, api, fc, evts, maddr, ds, sealer, verif, prover, &pcp, gsd, j, as)
|
||||||
|
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStart: func(context.Context) error {
|
OnStart: func(context.Context) error {
|
||||||
|
@ -626,7 +626,7 @@ func (m *Sealing) maybeUpgradeSector(ctx context.Context, sp abi.RegisteredSealP
|
|||||||
|
|
||||||
// call with m.inputLk
|
// call with m.inputLk
|
||||||
func (m *Sealing) createSector(ctx context.Context, cfg sealiface.Config, sp abi.RegisteredSealProof) (abi.SectorNumber, error) {
|
func (m *Sealing) createSector(ctx context.Context, cfg sealiface.Config, sp abi.RegisteredSealProof) (abi.SectorNumber, error) {
|
||||||
sid, err := m.sc.Next()
|
sid, err := m.NextSectorNumber(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, xerrors.Errorf("getting sector number: %w", err)
|
return 0, xerrors.Errorf("getting sector number: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@ import (
|
|||||||
cid "github.com/ipfs/go-cid"
|
cid "github.com/ipfs/go-cid"
|
||||||
|
|
||||||
address "github.com/filecoin-project/go-address"
|
address "github.com/filecoin-project/go-address"
|
||||||
|
bitfield "github.com/filecoin-project/go-bitfield"
|
||||||
abi "github.com/filecoin-project/go-state-types/abi"
|
abi "github.com/filecoin-project/go-state-types/abi"
|
||||||
big "github.com/filecoin-project/go-state-types/big"
|
big "github.com/filecoin-project/go-state-types/big"
|
||||||
miner "github.com/filecoin-project/go-state-types/builtin/v8/miner"
|
miner "github.com/filecoin-project/go-state-types/builtin/v8/miner"
|
||||||
@ -197,6 +198,21 @@ func (mr *MockSealingAPIMockRecorder) StateMarketStorageDeal(arg0, arg1, arg2 in
|
|||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMarketStorageDeal", reflect.TypeOf((*MockSealingAPI)(nil).StateMarketStorageDeal), arg0, arg1, arg2)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMarketStorageDeal", reflect.TypeOf((*MockSealingAPI)(nil).StateMarketStorageDeal), arg0, arg1, arg2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StateMinerAllocated mocks base method.
|
||||||
|
func (m *MockSealingAPI) StateMinerAllocated(arg0 context.Context, arg1 address.Address, arg2 types.TipSetKey) (*bitfield.BitField, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "StateMinerAllocated", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(*bitfield.BitField)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateMinerAllocated indicates an expected call of StateMinerAllocated.
|
||||||
|
func (mr *MockSealingAPIMockRecorder) StateMinerAllocated(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateMinerAllocated", reflect.TypeOf((*MockSealingAPI)(nil).StateMinerAllocated), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
// StateMinerAvailableBalance mocks base method.
|
// StateMinerAvailableBalance mocks base method.
|
||||||
func (m *MockSealingAPI) StateMinerAvailableBalance(arg0 context.Context, arg1 address.Address, arg2 types.TipSetKey) (big.Int, error) {
|
func (m *MockSealingAPI) StateMinerAvailableBalance(arg0 context.Context, arg1 address.Address, arg2 types.TipSetKey) (big.Int, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
398
storage/pipeline/numassign.go
Normal file
398
storage/pipeline/numassign.go
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
package sealing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/ipfs/go-datastore"
|
||||||
|
dsq "github.com/ipfs/go-datastore/query"
|
||||||
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/go-bitfield"
|
||||||
|
rlepluslazy "github.com/filecoin-project/go-bitfield/rle"
|
||||||
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
|
|
||||||
|
"github.com/filecoin-project/lotus/api"
|
||||||
|
"github.com/filecoin-project/lotus/chain/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var StorageCounterDSPrefix = "/storage/nextid"
|
||||||
|
var SectorBitfieldsDSPrefix = "/storage/sectorsids/"
|
||||||
|
var SectorReservationsDSPrefix = "/storage/sectorsids/reserved/"
|
||||||
|
|
||||||
|
var allocatedSectorsKey = datastore.NewKey(SectorBitfieldsDSPrefix + "allocated")
|
||||||
|
var reservedSectorsKey = datastore.NewKey(SectorBitfieldsDSPrefix + "reserved")
|
||||||
|
|
||||||
|
func (m *Sealing) loadBitField(ctx context.Context, name datastore.Key) (*bitfield.BitField, error) {
|
||||||
|
raw, err := m.ds.Get(ctx, name)
|
||||||
|
if err == datastore.ErrNotFound {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var bf bitfield.BitField
|
||||||
|
|
||||||
|
if err := bf.UnmarshalCBOR(bytes.NewBuffer(raw)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &bf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Sealing) saveBitField(ctx context.Context, name datastore.Key, bf bitfield.BitField) error {
|
||||||
|
var bb bytes.Buffer
|
||||||
|
err := bf.MarshalCBOR(&bb)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return m.ds.Put(ctx, name, bb.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Sealing) NextSectorNumber(ctx context.Context) (abi.SectorNumber, error) {
|
||||||
|
m.sclk.Lock()
|
||||||
|
defer m.sclk.Unlock()
|
||||||
|
|
||||||
|
am, err := m.numAssignerMetaLocked(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
allocated := am.Allocated
|
||||||
|
|
||||||
|
allocated.Set(uint64(am.Next))
|
||||||
|
|
||||||
|
if err := m.saveBitField(ctx, allocatedSectorsKey, allocated); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// save legacy counter so that in case of a miner downgrade things keep working
|
||||||
|
{
|
||||||
|
buf := make([]byte, binary.MaxVarintLen64)
|
||||||
|
size := binary.PutUvarint(buf, uint64(am.Next))
|
||||||
|
|
||||||
|
if err := m.ds.Put(ctx, datastore.NewKey(StorageCounterDSPrefix), buf[:size]); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return am.Next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Sealing) NumAssignerMeta(ctx context.Context) (api.NumAssignerMeta, error) {
|
||||||
|
m.sclk.Lock()
|
||||||
|
defer m.sclk.Unlock()
|
||||||
|
|
||||||
|
return m.numAssignerMetaLocked(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Sealing) numAssignerMetaLocked(ctx context.Context) (api.NumAssignerMeta, error) {
|
||||||
|
// load user-reserved and allocated bitfields
|
||||||
|
reserved, err := m.loadBitField(ctx, reservedSectorsKey)
|
||||||
|
if err != nil {
|
||||||
|
return api.NumAssignerMeta{}, xerrors.Errorf("loading reserved sectors bitfield: %w", err)
|
||||||
|
}
|
||||||
|
allocated, err := m.loadBitField(ctx, allocatedSectorsKey)
|
||||||
|
if err != nil {
|
||||||
|
return api.NumAssignerMeta{}, xerrors.Errorf("loading allocated sectors bitfield: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the allocated bitfield doesn't exist, create it from the legacy sector counter
|
||||||
|
if allocated == nil {
|
||||||
|
var i uint64
|
||||||
|
{
|
||||||
|
curBytes, err := m.ds.Get(ctx, datastore.NewKey(StorageCounterDSPrefix))
|
||||||
|
if err != nil && err != datastore.ErrNotFound {
|
||||||
|
return api.NumAssignerMeta{}, err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
cur, _ := binary.Uvarint(curBytes)
|
||||||
|
i = cur + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rl := &rlepluslazy.RunSliceIterator{Runs: []rlepluslazy.Run{
|
||||||
|
{
|
||||||
|
Val: true,
|
||||||
|
Len: i,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
bf, err := bitfield.NewFromIter(rl)
|
||||||
|
if err != nil {
|
||||||
|
return api.NumAssignerMeta{}, xerrors.Errorf("bitfield from iter: %w", err)
|
||||||
|
}
|
||||||
|
allocated = &bf
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there are no user reservations, use an empty bitfield
|
||||||
|
if reserved == nil {
|
||||||
|
emptyBf := bitfield.New()
|
||||||
|
reserved = &emptyBf
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo union with miner allocated nums
|
||||||
|
inuse, err := bitfield.MergeBitFields(*reserved, *allocated)
|
||||||
|
if err != nil {
|
||||||
|
return api.NumAssignerMeta{}, xerrors.Errorf("merge reserved/allocated: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stateAllocated, err := m.Api.StateMinerAllocated(ctx, m.maddr, types.EmptyTSK)
|
||||||
|
if err != nil || stateAllocated == nil {
|
||||||
|
return api.NumAssignerMeta{}, xerrors.Errorf("getting state-allocated sector numbers: %w", err)
|
||||||
|
}
|
||||||
|
inuse, err = bitfield.MergeBitFields(inuse, *stateAllocated)
|
||||||
|
if err != nil {
|
||||||
|
return api.NumAssignerMeta{}, xerrors.Errorf("merge inuse/stateAllocated: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// find first available sector number
|
||||||
|
iri, err := inuse.RunIterator()
|
||||||
|
if err != nil {
|
||||||
|
return api.NumAssignerMeta{}, xerrors.Errorf("inuse run iter: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var firstFree abi.SectorNumber
|
||||||
|
for iri.HasNext() {
|
||||||
|
r, err := iri.NextRun()
|
||||||
|
if err != nil {
|
||||||
|
return api.NumAssignerMeta{}, xerrors.Errorf("next run: %w", err)
|
||||||
|
}
|
||||||
|
if !r.Val {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
firstFree += abi.SectorNumber(r.Len)
|
||||||
|
}
|
||||||
|
|
||||||
|
return api.NumAssignerMeta{
|
||||||
|
Reserved: *reserved,
|
||||||
|
Allocated: *allocated,
|
||||||
|
InUse: inuse,
|
||||||
|
Next: firstFree,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumReservations returns a list of named sector reservations
|
||||||
|
func (m *Sealing) NumReservations(ctx context.Context) (map[string]bitfield.BitField, error) {
|
||||||
|
res, err := m.ds.Query(ctx, dsq.Query{Prefix: SectorReservationsDSPrefix})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerrors.Errorf("query reservations: %w", err)
|
||||||
|
}
|
||||||
|
defer res.Close() //nolint:errcheck
|
||||||
|
|
||||||
|
out := map[string]bitfield.BitField{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
res, ok := res.NextSync()
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.Error != nil {
|
||||||
|
return nil, xerrors.Errorf("res error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b := bitfield.New()
|
||||||
|
if err := b.UnmarshalCBOR(bytes.NewReader(res.Value)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out[datastore.NewKey(res.Key).BaseNamespace()] = b
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumReserve creates a new sector reservation
|
||||||
|
func (m *Sealing) NumReserve(ctx context.Context, name string, reserving bitfield.BitField, force bool) error {
|
||||||
|
m.sclk.Lock()
|
||||||
|
defer m.sclk.Unlock()
|
||||||
|
|
||||||
|
return m.numReserveLocked(ctx, name, reserving, force)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumReserve creates a new sector reservation
|
||||||
|
func (m *Sealing) numReserveLocked(ctx context.Context, name string, reserving bitfield.BitField, force bool) error {
|
||||||
|
rk, err := reservationKey(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if there's an existing reservation with this name
|
||||||
|
cur := bitfield.New()
|
||||||
|
|
||||||
|
curRes, err := m.ds.Get(ctx, rk)
|
||||||
|
if err == nil {
|
||||||
|
log.Warnw("reservation with this name already exists", "name", name)
|
||||||
|
if !force {
|
||||||
|
return xerrors.Errorf("reservation with this name already exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cur.UnmarshalCBOR(bytes.NewReader(curRes)); err != nil {
|
||||||
|
return xerrors.Errorf("unmarshaling existing reservation: %w", err)
|
||||||
|
}
|
||||||
|
} else if err != datastore.ErrNotFound {
|
||||||
|
return xerrors.Errorf("checking if reservation exists: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the global reserved bitfield and subtract current reservation if we're overwriting
|
||||||
|
|
||||||
|
nm, err := m.numAssignerMetaLocked(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
allReserved := nm.Reserved
|
||||||
|
|
||||||
|
allReserved, err = bitfield.SubtractBitField(allReserved, cur)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("allReserved - cur: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the reservation is colliding with any other reservation
|
||||||
|
colliding, err := bitfield.IntersectBitField(allReserved, reserving)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("intersect all / reserving: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
empty, err := colliding.IsEmpty()
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("colliding.empty: %w", err)
|
||||||
|
}
|
||||||
|
if !empty {
|
||||||
|
log.Warnw("new reservation is colliding with another existing reservation", "name", name)
|
||||||
|
if !force {
|
||||||
|
return xerrors.Errorf("new reservation is colliding with another existing reservation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the reservation is colliding with allocated sectors
|
||||||
|
colliding, err = bitfield.IntersectBitField(nm.Allocated, reserving)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("intersect all / reserving: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
empty, err = colliding.IsEmpty()
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("colliding.empty: %w", err)
|
||||||
|
}
|
||||||
|
if !empty {
|
||||||
|
log.Warnw("new reservation is colliding with allocated sector numbers", "name", name)
|
||||||
|
if !force {
|
||||||
|
return xerrors.Errorf("new reservation is colliding with allocated sector numbers")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the reservation
|
||||||
|
newReserved, err := bitfield.MergeBitFields(allReserved, reserving)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("merge allReserved+reserving: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.saveBitField(ctx, rk, reserving); err != nil {
|
||||||
|
return xerrors.Errorf("saving reservation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.saveBitField(ctx, reservedSectorsKey, newReserved); err != nil {
|
||||||
|
return xerrors.Errorf("save reserved nums: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Sealing) NumReserveCount(ctx context.Context, name string, count uint64) (bitfield.BitField, error) {
|
||||||
|
m.sclk.Lock()
|
||||||
|
defer m.sclk.Unlock()
|
||||||
|
|
||||||
|
nm, err := m.numAssignerMetaLocked(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return bitfield.BitField{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// figure out `count` unused sectors at lowest possible numbers
|
||||||
|
|
||||||
|
usedCount, err := nm.InUse.Count()
|
||||||
|
if err != nil {
|
||||||
|
return bitfield.BitField{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// get a bitfield mask which has at least `count` bits more set than the nm.InUse field
|
||||||
|
mask, err := bitfield.NewFromIter(&rlepluslazy.RunSliceIterator{Runs: []rlepluslazy.Run{
|
||||||
|
{
|
||||||
|
Val: true,
|
||||||
|
Len: count + usedCount,
|
||||||
|
},
|
||||||
|
}})
|
||||||
|
if err != nil {
|
||||||
|
return bitfield.BitField{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
free, err := bitfield.SubtractBitField(mask, nm.InUse)
|
||||||
|
if err != nil {
|
||||||
|
return bitfield.BitField{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// free now has at least 'count' bits set - it's possible that InUse had some bits set outside the count+usedCount range
|
||||||
|
|
||||||
|
free, err = free.Slice(0, count)
|
||||||
|
if err != nil {
|
||||||
|
return bitfield.BitField{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.numReserveLocked(ctx, name, free, false); err != nil {
|
||||||
|
return bitfield.BitField{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return free, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumFree removes a named sector reservation
|
||||||
|
func (m *Sealing) NumFree(ctx context.Context, name string) error {
|
||||||
|
rk, err := reservationKey(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := m.loadBitField(ctx, rk)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("loading reservation: %w", err)
|
||||||
|
}
|
||||||
|
if res == nil {
|
||||||
|
return xerrors.Errorf("reservation with this name doesn't exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
allRes, err := m.loadBitField(ctx, reservedSectorsKey)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("leading all reservations: %w", err)
|
||||||
|
}
|
||||||
|
if allRes == nil {
|
||||||
|
return xerrors.Errorf("all reservations bitfield not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
newAll, err := bitfield.SubtractBitField(*allRes, *res)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("allRes-res: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.saveBitField(ctx, reservedSectorsKey, newAll); err != nil {
|
||||||
|
return xerrors.Errorf("saving reservations bitfield: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := m.ds.Delete(ctx, rk); err != nil {
|
||||||
|
return xerrors.Errorf("deleting reservation: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func reservationKey(name string) (datastore.Key, error) {
|
||||||
|
ok, err := regexp.Match("^[a-zA-Z0-9_\\-]+$", []byte(name))
|
||||||
|
if err != nil {
|
||||||
|
return datastore.Key{}, err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return datastore.Key{}, xerrors.Errorf("reservation name contained disallowed characters (allowed: a-z, A-Z, 0-9, '-', '_')")
|
||||||
|
}
|
||||||
|
|
||||||
|
return datastore.KeyWithNamespaces([]string{SectorReservationsDSPrefix, name}), nil
|
||||||
|
}
|
@ -12,6 +12,7 @@ import (
|
|||||||
"golang.org/x/xerrors"
|
"golang.org/x/xerrors"
|
||||||
|
|
||||||
"github.com/filecoin-project/go-address"
|
"github.com/filecoin-project/go-address"
|
||||||
|
"github.com/filecoin-project/go-bitfield"
|
||||||
"github.com/filecoin-project/go-state-types/abi"
|
"github.com/filecoin-project/go-state-types/abi"
|
||||||
"github.com/filecoin-project/go-state-types/big"
|
"github.com/filecoin-project/go-state-types/big"
|
||||||
"github.com/filecoin-project/go-state-types/builtin/v8/miner"
|
"github.com/filecoin-project/go-state-types/builtin/v8/miner"
|
||||||
@ -19,6 +20,7 @@ import (
|
|||||||
"github.com/filecoin-project/go-state-types/dline"
|
"github.com/filecoin-project/go-state-types/dline"
|
||||||
"github.com/filecoin-project/go-state-types/network"
|
"github.com/filecoin-project/go-state-types/network"
|
||||||
"github.com/filecoin-project/go-statemachine"
|
"github.com/filecoin-project/go-statemachine"
|
||||||
|
"github.com/filecoin-project/go-storedcounter"
|
||||||
|
|
||||||
"github.com/filecoin-project/lotus/api"
|
"github.com/filecoin-project/lotus/api"
|
||||||
lminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner"
|
lminer "github.com/filecoin-project/lotus/chain/actors/builtin/miner"
|
||||||
@ -66,6 +68,7 @@ type SealingAPI interface {
|
|||||||
StateGetRandomnessFromBeacon(ctx context.Context, personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte, tsk types.TipSetKey) (abi.Randomness, error)
|
StateGetRandomnessFromBeacon(ctx context.Context, personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte, tsk types.TipSetKey) (abi.Randomness, error)
|
||||||
StateGetRandomnessFromTickets(ctx context.Context, personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte, tsk types.TipSetKey) (abi.Randomness, error)
|
StateGetRandomnessFromTickets(ctx context.Context, personalization crypto.DomainSeparationTag, randEpoch abi.ChainEpoch, entropy []byte, tsk types.TipSetKey) (abi.Randomness, error)
|
||||||
ChainReadObj(context.Context, cid.Cid) ([]byte, error)
|
ChainReadObj(context.Context, cid.Cid) ([]byte, error)
|
||||||
|
StateMinerAllocated(context.Context, address.Address, types.TipSetKey) (*bitfield.BitField, error)
|
||||||
|
|
||||||
// Address selector
|
// Address selector
|
||||||
WalletBalance(context.Context, address.Address) (types.BigInt, error)
|
WalletBalance(context.Context, address.Address) (types.BigInt, error)
|
||||||
@ -87,6 +90,8 @@ type Sealing struct {
|
|||||||
Api SealingAPI
|
Api SealingAPI
|
||||||
DealInfo *CurrentDealInfoManager
|
DealInfo *CurrentDealInfoManager
|
||||||
|
|
||||||
|
ds datastore.Batching
|
||||||
|
|
||||||
feeCfg config.MinerFeeConfig
|
feeCfg config.MinerFeeConfig
|
||||||
events Events
|
events Events
|
||||||
|
|
||||||
@ -96,7 +101,6 @@ type Sealing struct {
|
|||||||
|
|
||||||
sealer sealer.SectorManager
|
sealer sealer.SectorManager
|
||||||
sectors *statemachine.StateGroup
|
sectors *statemachine.StateGroup
|
||||||
sc SectorIDCounter
|
|
||||||
verif storiface.Verifier
|
verif storiface.Verifier
|
||||||
pcp PreCommitPolicy
|
pcp PreCommitPolicy
|
||||||
|
|
||||||
@ -120,6 +124,9 @@ type Sealing struct {
|
|||||||
precommiter *PreCommitBatcher
|
precommiter *PreCommitBatcher
|
||||||
commiter *CommitBatcher
|
commiter *CommitBatcher
|
||||||
|
|
||||||
|
sclk sync.Mutex
|
||||||
|
legacySc *storedcounter.StoredCounter
|
||||||
|
|
||||||
getConfig dtypes.GetSealingConfigFunc
|
getConfig dtypes.GetSealingConfigFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,17 +168,18 @@ type pendingPiece struct {
|
|||||||
accepted func(abi.SectorNumber, abi.UnpaddedPieceSize, error)
|
accepted func(abi.SectorNumber, abi.UnpaddedPieceSize, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(mctx context.Context, api SealingAPI, fc config.MinerFeeConfig, events Events, maddr address.Address, ds datastore.Batching, sealer sealer.SectorManager, sc SectorIDCounter, verif storiface.Verifier, prov storiface.Prover, pcp PreCommitPolicy, gc dtypes.GetSealingConfigFunc, journal journal.Journal, addrSel AddressSelector) *Sealing {
|
func New(mctx context.Context, api SealingAPI, fc config.MinerFeeConfig, events Events, maddr address.Address, ds datastore.Batching, sealer sealer.SectorManager, verif storiface.Verifier, prov storiface.Prover, pcp PreCommitPolicy, gc dtypes.GetSealingConfigFunc, journal journal.Journal, addrSel AddressSelector) *Sealing {
|
||||||
s := &Sealing{
|
s := &Sealing{
|
||||||
Api: api,
|
Api: api,
|
||||||
DealInfo: &CurrentDealInfoManager{api},
|
DealInfo: &CurrentDealInfoManager{api},
|
||||||
|
|
||||||
|
ds: ds,
|
||||||
|
|
||||||
feeCfg: fc,
|
feeCfg: fc,
|
||||||
events: events,
|
events: events,
|
||||||
|
|
||||||
maddr: maddr,
|
maddr: maddr,
|
||||||
sealer: sealer,
|
sealer: sealer,
|
||||||
sc: sc,
|
|
||||||
verif: verif,
|
verif: verif,
|
||||||
pcp: pcp,
|
pcp: pcp,
|
||||||
|
|
||||||
@ -193,6 +201,8 @@ func New(mctx context.Context, api SealingAPI, fc config.MinerFeeConfig, events
|
|||||||
|
|
||||||
getConfig: gc,
|
getConfig: gc,
|
||||||
|
|
||||||
|
legacySc: storedcounter.New(ds, datastore.NewKey(StorageCounterDSPrefix)),
|
||||||
|
|
||||||
stats: SectorStats{
|
stats: SectorStats{
|
||||||
bySector: map[abi.SectorID]SectorState{},
|
bySector: map[abi.SectorID]SectorState{},
|
||||||
byState: map[SectorState]int64{},
|
byState: map[SectorState]int64{},
|
||||||
|
@ -190,10 +190,6 @@ func (t *SectorInfo) keepUnsealedRanges(pieces []Piece, invert, alwaysKeep bool)
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
type SectorIDCounter interface {
|
|
||||||
Next() (abi.SectorNumber, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SealingStateEvt is a journal event that records a sector state transition.
|
// SealingStateEvt is a journal event that records a sector state transition.
|
||||||
type SealingStateEvt struct {
|
type SealingStateEvt struct {
|
||||||
SectorNumber abi.SectorNumber
|
SectorNumber abi.SectorNumber
|
||||||
|
@ -39,9 +39,9 @@ import (
|
|||||||
"github.com/filecoin-project/lotus/node"
|
"github.com/filecoin-project/lotus/node"
|
||||||
"github.com/filecoin-project/lotus/node/config"
|
"github.com/filecoin-project/lotus/node/config"
|
||||||
"github.com/filecoin-project/lotus/node/impl"
|
"github.com/filecoin-project/lotus/node/impl"
|
||||||
"github.com/filecoin-project/lotus/node/modules"
|
|
||||||
"github.com/filecoin-project/lotus/node/repo"
|
"github.com/filecoin-project/lotus/node/repo"
|
||||||
"github.com/filecoin-project/lotus/storage/paths"
|
"github.com/filecoin-project/lotus/storage/paths"
|
||||||
|
sealing "github.com/filecoin-project/lotus/storage/pipeline"
|
||||||
"github.com/filecoin-project/lotus/storage/sealer/storiface"
|
"github.com/filecoin-project/lotus/storage/sealer/storiface"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -190,7 +190,7 @@ func PrepareMiner(t *TestEnvironment) (*LotusMiner, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
nic := storedcounter.New(ds, datastore.NewKey(modules.StorageCounterDSPrefix))
|
nic := storedcounter.New(ds, datastore.NewKey(sealing.StorageCounterDSPrefix))
|
||||||
for i := 0; i < (sectors + 1); i++ {
|
for i := 0; i < (sectors + 1); i++ {
|
||||||
_, err = nic.Next()
|
_, err = nic.Next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
Reference in New Issue
Block a user