From 1577740bc443fc4a21cbadcb1433795c162c4c61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Fri, 28 Oct 2022 12:03:39 +0100 Subject: [PATCH 01/13] bstore: Network blockstore --- blockstore/cbor_gen.go | 441 ++++++++++++++++++++++++++++++++++++++ blockstore/net.go | 462 ++++++++++++++++++++++++++++++++++++++++ blockstore/net_serve.go | 236 ++++++++++++++++++++ blockstore/net_test.go | 63 ++++++ blockstore/net_ws.go | 100 +++++++++ gen/main.go | 10 + 6 files changed, 1312 insertions(+) create mode 100644 blockstore/cbor_gen.go create mode 100644 blockstore/net.go create mode 100644 blockstore/net_serve.go create mode 100644 blockstore/net_test.go create mode 100644 blockstore/net_ws.go diff --git a/blockstore/cbor_gen.go b/blockstore/cbor_gen.go new file mode 100644 index 00000000000..b8ebdb474bc --- /dev/null +++ b/blockstore/cbor_gen.go @@ -0,0 +1,441 @@ +// Code generated by github.com/whyrusleeping/cbor-gen. DO NOT EDIT. + +package blockstore + +import ( + "fmt" + "io" + "math" + "sort" + + cid "github.com/ipfs/go-cid" + cbg "github.com/whyrusleeping/cbor-gen" + xerrors "golang.org/x/xerrors" +) + +var _ = xerrors.Errorf +var _ = cid.Undef +var _ = math.E +var _ = sort.Sort + +var lengthBufNetRpcReq = []byte{132} + +func (t *NetRpcReq) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufNetRpcReq); err != nil { + return err + } + + // t.Type (blockstore.NetRPCReqType) (uint8) + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Type)); err != nil { + return err + } + + // t.ID (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.ID)); err != nil { + return err + } + + // t.Cid ([]cid.Cid) (slice) + if len(t.Cid) > cbg.MaxLength { + return xerrors.Errorf("Slice value in field t.Cid was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Cid))); err != nil { + return err + } + for _, v := range t.Cid { + if err := cbg.WriteCid(w, v); err != nil { + return xerrors.Errorf("failed writing cid field t.Cid: %w", err) + } + } + + // t.Data ([][]uint8) (slice) + if len(t.Data) > cbg.MaxLength { + return xerrors.Errorf("Slice value in field t.Data was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajArray, uint64(len(t.Data))); err != nil { + return err + } + for _, v := range t.Data { + if len(v) > cbg.ByteArrayMaxLen { + return xerrors.Errorf("Byte array in field v was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(v))); err != nil { + return err + } + + if _, err := cw.Write(v[:]); err != nil { + return err + } + } + return nil +} + +func (t *NetRpcReq) UnmarshalCBOR(r io.Reader) (err error) { + *t = NetRpcReq{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 4 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Type (blockstore.NetRPCReqType) (uint8) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint8 field") + } + if extra > math.MaxUint8 { + return fmt.Errorf("integer in input was too large for uint8 field") + } + t.Type = NetRPCReqType(extra) + // t.ID (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.ID = uint64(extra) + + } + // t.Cid ([]cid.Cid) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > cbg.MaxLength { + return fmt.Errorf("t.Cid: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Cid = make([]cid.Cid, extra) + } + + for i := 0; i < int(extra); i++ { + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("reading cid field t.Cid failed: %w", err) + } + t.Cid[i] = c + } + + // t.Data ([][]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > cbg.MaxLength { + return fmt.Errorf("t.Data: array too large (%d)", extra) + } + + if maj != cbg.MajArray { + return fmt.Errorf("expected cbor array") + } + + if extra > 0 { + t.Data = make([][]uint8, extra) + } + + for i := 0; i < int(extra); i++ { + { + var maj byte + var extra uint64 + var err error + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > cbg.ByteArrayMaxLen { + return fmt.Errorf("t.Data[i]: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Data[i] = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Data[i][:]); err != nil { + return err + } + } + } + + return nil +} + +var lengthBufNetRpcResp = []byte{131} + +func (t *NetRpcResp) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufNetRpcResp); err != nil { + return err + } + + // t.Type (blockstore.NetRPCRespType) (uint8) + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Type)); err != nil { + return err + } + + // t.ID (uint64) (uint64) + + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.ID)); err != nil { + return err + } + + // t.Data ([]uint8) (slice) + if len(t.Data) > cbg.ByteArrayMaxLen { + return xerrors.Errorf("Byte array in field t.Data was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(t.Data))); err != nil { + return err + } + + if _, err := cw.Write(t.Data[:]); err != nil { + return err + } + return nil +} + +func (t *NetRpcResp) UnmarshalCBOR(r io.Reader) (err error) { + *t = NetRpcResp{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 3 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Type (blockstore.NetRPCRespType) (uint8) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint8 field") + } + if extra > math.MaxUint8 { + return fmt.Errorf("integer in input was too large for uint8 field") + } + t.Type = NetRPCRespType(extra) + // t.ID (uint64) (uint64) + + { + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint64 field") + } + t.ID = uint64(extra) + + } + // t.Data ([]uint8) (slice) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + + if extra > cbg.ByteArrayMaxLen { + return fmt.Errorf("t.Data: byte array too large (%d)", extra) + } + if maj != cbg.MajByteString { + return fmt.Errorf("expected byte array") + } + + if extra > 0 { + t.Data = make([]uint8, extra) + } + + if _, err := io.ReadFull(cr, t.Data[:]); err != nil { + return err + } + return nil +} + +var lengthBufNetRpcErr = []byte{131} + +func (t *NetRpcErr) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write(lengthBufNetRpcErr); err != nil { + return err + } + + // t.Type (blockstore.NetRPCErrType) (uint8) + if err := cw.WriteMajorTypeHeader(cbg.MajUnsignedInt, uint64(t.Type)); err != nil { + return err + } + + // t.Msg (string) (string) + if len(t.Msg) > cbg.MaxLength { + return xerrors.Errorf("Value in field t.Msg was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Msg))); err != nil { + return err + } + if _, err := io.WriteString(w, string(t.Msg)); err != nil { + return err + } + + // t.Cid (cid.Cid) (struct) + + if t.Cid == nil { + if _, err := cw.Write(cbg.CborNull); err != nil { + return err + } + } else { + if err := cbg.WriteCid(cw, *t.Cid); err != nil { + return xerrors.Errorf("failed to write cid field t.Cid: %w", err) + } + } + + return nil +} + +func (t *NetRpcErr) UnmarshalCBOR(r io.Reader) (err error) { + *t = NetRpcErr{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajArray { + return fmt.Errorf("cbor input should be of type array") + } + + if extra != 3 { + return fmt.Errorf("cbor input had wrong number of fields") + } + + // t.Type (blockstore.NetRPCErrType) (uint8) + + maj, extra, err = cr.ReadHeader() + if err != nil { + return err + } + if maj != cbg.MajUnsignedInt { + return fmt.Errorf("wrong type for uint8 field") + } + if extra > math.MaxUint8 { + return fmt.Errorf("integer in input was too large for uint8 field") + } + t.Type = NetRPCErrType(extra) + // t.Msg (string) (string) + + { + sval, err := cbg.ReadString(cr) + if err != nil { + return err + } + + t.Msg = string(sval) + } + // t.Cid (cid.Cid) (struct) + + { + + b, err := cr.ReadByte() + if err != nil { + return err + } + if b != cbg.CborNull[0] { + if err := cr.UnreadByte(); err != nil { + return err + } + + c, err := cbg.ReadCid(cr) + if err != nil { + return xerrors.Errorf("failed to read cid field t.Cid: %w", err) + } + + t.Cid = &c + } + + } + return nil +} diff --git a/blockstore/net.go b/blockstore/net.go new file mode 100644 index 00000000000..fa8b24591fb --- /dev/null +++ b/blockstore/net.go @@ -0,0 +1,462 @@ +package blockstore + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "sync" + "sync/atomic" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + ipld "github.com/ipfs/go-ipld-format" + "github.com/libp2p/go-msgio" + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/xerrors" +) + +type NetRPCReqType byte + +const ( + NRpcHas NetRPCReqType = iota + NRpcGet NetRPCReqType = iota + NRpcGetSize NetRPCReqType = iota + NRpcPut NetRPCReqType = iota + NRpcDelete NetRPCReqType = iota + NRpcList NetRPCReqType = iota + + // todo cancel req +) + +type NetRPCRespType byte + +const ( + NRpcOK NetRPCRespType = iota + NRpcErr NetRPCRespType = iota + NRpcMore NetRPCRespType = iota +) + +type NetRPCErrType byte + +const ( + NRpcErrGeneric NetRPCErrType = iota + NRpcErrNotFound NetRPCErrType = iota +) + +type NetRpcReq struct { + Type NetRPCReqType + ID uint64 + + Cid []cid.Cid // todo maxsize? + Data [][]byte // todo maxsize? +} + +type NetRpcResp struct { + Type NetRPCRespType + ID uint64 + + // error or cids in allkeys + Data []byte // todo maxsize? + + next <-chan NetRpcResp +} + +type NetRpcErr struct { + Type NetRPCErrType + + Msg string + + // in case of NRpcErrNotFound + Cid *cid.Cid +} + +type NetworkStore struct { + // note: writer is thread-safe + msgStream msgio.ReadWriteCloser + + // atomic + reqCount uint64 + + respLk sync.Mutex + + // respMap is nil after store closes + respMap map[uint64]chan<- NetRpcResp + + closing chan struct{} + closed chan struct{} + + closeLk sync.Mutex + onClose func() +} + +func NewNetworkStore(mss msgio.ReadWriteCloser) *NetworkStore { + ns := &NetworkStore{ + msgStream: mss, + + respMap: map[uint64]chan<- NetRpcResp{}, + + closing: make(chan struct{}), + closed: make(chan struct{}), + } + + go ns.receive() + + return ns +} + +func (n *NetworkStore) shutdown(msg string) { + if err := n.msgStream.Close(); err != nil { + log.Errorw("closing netstore msg stream", "error", err) + } + + nerr := NetRpcErr{ + Type: NRpcErrGeneric, + Msg: msg, + Cid: nil, + } + + var errb bytes.Buffer + if err := nerr.MarshalCBOR(&errb); err != nil { + log.Errorw("netstore shutdown: error marshaling error", "err", err) + } + + n.respLk.Lock() + for id, resps := range n.respMap { + resps <- NetRpcResp{ + Type: NRpcErr, + ID: id, + Data: errb.Bytes(), + } + } + + n.respMap = nil + + n.respLk.Unlock() +} + +func (n *NetworkStore) OnClose(cb func()) { + n.closeLk.Lock() + defer n.closeLk.Unlock() + + select { + case <-n.closed: + cb() + default: + n.onClose = cb + } +} + +func (n *NetworkStore) receive() { + defer func() { + n.closeLk.Lock() + defer n.closeLk.Unlock() + + close(n.closed) + if n.onClose != nil { + n.onClose() + } + }() + + for { + select { + case <-n.closing: + n.shutdown("netstore stopping") + return + default: + } + + msg, err := n.msgStream.ReadMsg() + if err != nil { + n.shutdown(fmt.Sprintf("netstore ReadMsg: %s", err)) + return + } + + var resp NetRpcResp + if err := resp.UnmarshalCBOR(bytes.NewReader(msg)); err != nil { + n.shutdown(fmt.Sprintf("unmarshaling netstore response: %s", err)) + return + } + + n.msgStream.ReleaseMsg(msg) + + n.respLk.Lock() + if ch, ok := n.respMap[resp.ID]; ok { + if resp.Type == NRpcMore { + nch := make(chan NetRpcResp, 1) + resp.next = nch + n.respMap[resp.ID] = nch + } else { + delete(n.respMap, resp.ID) + } + + ch <- resp + } + n.respLk.Unlock() + } +} + +func (n *NetworkStore) sendRpc(rt NetRPCReqType, cids []cid.Cid, data [][]byte) (uint64, <-chan NetRpcResp, error) { + rid := atomic.AddUint64(&n.reqCount, 1) + + respCh := make(chan NetRpcResp, 1) // todo pool? + + n.respLk.Lock() + if n.respMap == nil { + return 0, nil, xerrors.Errorf("netstore closed") + } + n.respMap[rid] = respCh + n.respLk.Unlock() + + req := NetRpcReq{ + Type: rt, + ID: rid, + Cid: cids, + Data: data, + } + + var rbuf bytes.Buffer // todo buffer pool + if err := req.MarshalCBOR(&rbuf); err != nil { + n.respLk.Lock() + if n.respMap == nil { + return 0, nil, xerrors.Errorf("netstore closed") + } + delete(n.respMap, rid) + n.respLk.Unlock() + + return 0, nil, err + } + + if err := n.msgStream.WriteMsg(rbuf.Bytes()); err != nil { + n.respLk.Lock() + if n.respMap == nil { + return 0, nil, xerrors.Errorf("netstore closed") + } + delete(n.respMap, rid) + n.respLk.Unlock() + + return 0, nil, err + } + + return rid, respCh, nil +} + +func (n *NetworkStore) waitResp(ctx context.Context, rch <-chan NetRpcResp, rid uint64) (NetRpcResp, error) { + select { + case resp := <-rch: + if resp.Type == NRpcErr { + var e NetRpcErr + if err := e.UnmarshalCBOR(bytes.NewReader(resp.Data)); err != nil { + return NetRpcResp{}, xerrors.Errorf("unmarshaling error data: %w", err) + } + + var err error + switch e.Type { + case NRpcErrNotFound: + if e.Cid != nil { + err = ipld.ErrNotFound{ + Cid: *e.Cid, + } + } else { + err = xerrors.Errorf("block not found, but cid was null") + } + default: + err = xerrors.Errorf("unknown error type") + case NRpcErrGeneric: + err = xerrors.Errorf("generic error") + } + + return NetRpcResp{}, xerrors.Errorf("netstore error response: %s (%w)", e.Msg, err) + } + + return resp, nil + case <-ctx.Done(): + // todo send cancel req + + n.respLk.Lock() + if n.respMap != nil { + delete(n.respMap, rid) + } + n.respLk.Unlock() + + return NetRpcResp{}, ctx.Err() + } +} + +func (n *NetworkStore) Has(ctx context.Context, c cid.Cid) (bool, error) { + req, rch, err := n.sendRpc(NRpcHas, []cid.Cid{c}, nil) + if err != nil { + return false, err + } + + resp, err := n.waitResp(ctx, rch, req) + if err != nil { + return false, err + } + + if len(resp.Data) != 1 { + return false, xerrors.Errorf("expected reposnse length to be 1 byte") + } + switch resp.Data[0] { + case cbg.CborBoolTrue[0]: + return true, nil + case cbg.CborBoolFalse[0]: + return false, nil + default: + return false, xerrors.Errorf("has: bad response: %x", resp.Data[0]) + } +} + +func (n *NetworkStore) Get(ctx context.Context, c cid.Cid) (blocks.Block, error) { + req, rch, err := n.sendRpc(NRpcGet, []cid.Cid{c}, nil) + if err != nil { + return nil, err + } + + resp, err := n.waitResp(ctx, rch, req) + if err != nil { + return nil, err + } + + return blocks.NewBlockWithCid(resp.Data, c) +} + +func (n *NetworkStore) View(ctx context.Context, c cid.Cid, callback func([]byte) error) error { + req, rch, err := n.sendRpc(NRpcGet, []cid.Cid{c}, nil) + if err != nil { + return err + } + + resp, err := n.waitResp(ctx, rch, req) + if err != nil { + return err + } + + return callback(resp.Data) // todo return buf to pool +} + +func (n *NetworkStore) GetSize(ctx context.Context, c cid.Cid) (int, error) { + req, rch, err := n.sendRpc(NRpcGetSize, []cid.Cid{c}, nil) + if err != nil { + return 0, err + } + + resp, err := n.waitResp(ctx, rch, req) + if err != nil { + return 0, err + } + + if len(resp.Data) != 4 { + return 0, xerrors.Errorf("expected getsize response to be 4 bytes, was %d", resp.Data) + } + + return int(binary.LittleEndian.Uint32(resp.Data)), nil +} + +func (n *NetworkStore) Put(ctx context.Context, block blocks.Block) error { + return n.PutMany(ctx, []blocks.Block{block}) +} + +func (n *NetworkStore) PutMany(ctx context.Context, blocks []blocks.Block) error { + // todo pool + cids := make([]cid.Cid, len(blocks)) + blkDatas := make([][]byte, len(blocks)) + for i, block := range blocks { + cids[i] = block.Cid() + blkDatas[i] = block.RawData() + } + + req, rch, err := n.sendRpc(NRpcPut, cids, blkDatas) + if err != nil { + return err + } + + _, err = n.waitResp(ctx, rch, req) + if err != nil { + return err + } + + return nil +} + +func (n *NetworkStore) DeleteBlock(ctx context.Context, c cid.Cid) error { + return n.DeleteMany(ctx, []cid.Cid{c}) +} + +func (n *NetworkStore) DeleteMany(ctx context.Context, cids []cid.Cid) error { + req, rch, err := n.sendRpc(NRpcDelete, cids, nil) + if err != nil { + return err + } + + _, err = n.waitResp(ctx, rch, req) + if err != nil { + return err + } + + return nil +} + +func (n *NetworkStore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) { + req, rch, err := n.sendRpc(NRpcList, nil, nil) + if err != nil { + return nil, err + } + + outCh := make(chan cid.Cid, 16) + + go func() { + defer close(outCh) + // todo defer cancel request + + for { + if rch == nil { + return + } + + resp, err := n.waitResp(ctx, rch, req) + if err != nil { + return + } + + switch resp.Type { + case NRpcOK, NRpcMore: + c, err := cid.Cast(resp.Data) + if err != nil { + return + } + + // todo propagate backpressure + select { + case outCh <- c: + case <-ctx.Done(): + return + } + + rch = resp.next + default: + return + } + } + }() + + return outCh, err +} + +func (n *NetworkStore) HashOnRead(enabled bool) { + // todo + return +} + +func (n *NetworkStore) Stop(ctx context.Context) error { + close(n.closing) + + select { + case <-n.closed: + return nil + case <-ctx.Done(): + return ctx.Err() + } +} + +var _ Blockstore = &NetworkStore{} diff --git a/blockstore/net_serve.go b/blockstore/net_serve.go new file mode 100644 index 00000000000..25226c5c503 --- /dev/null +++ b/blockstore/net_serve.go @@ -0,0 +1,236 @@ +package blockstore + +import ( + "bytes" + "context" + "encoding/binary" + + block "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + ipld "github.com/ipfs/go-ipld-format" + "github.com/libp2p/go-msgio" + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/xerrors" +) + +type NetworkStoreHandler struct { + msgStream msgio.ReadWriteCloser + + bs Blockstore +} + +func HandleNetBstoreStream(ctx context.Context, bs Blockstore, mss msgio.ReadWriteCloser) *NetworkStoreHandler { + ns := &NetworkStoreHandler{ + msgStream: mss, + bs: bs, + } + + go ns.handle(ctx) + + return ns +} + +func (h *NetworkStoreHandler) handle(ctx context.Context) { + defer func() { + if err := h.msgStream.Close(); err != nil { + log.Errorw("error closing blockstore stream", "error", err) + } + }() + + for { + var req NetRpcReq + + ms, err := h.msgStream.ReadMsg() + if err != nil { + log.Warnw("bstore stream err", "error", err) + return + } + + if err := req.UnmarshalCBOR(bytes.NewReader(ms)); err != nil { + return + } + + h.msgStream.ReleaseMsg(ms) + + switch req.Type { + case NRpcHas: + if len(req.Cid) != 1 { + if err := h.respondError(req.ID, xerrors.New("expected request for 1 cid"), cid.Undef); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + + res, err := h.bs.Has(ctx, req.Cid[0]) + if err != nil { + if err := h.respondError(req.ID, err, req.Cid[0]); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + + var resData [1]byte + if res { + resData[0] = cbg.CborBoolTrue[0] + } else { + resData[0] = cbg.CborBoolFalse[0] + } + + if err := h.respond(req.ID, NRpcOK, resData[:]); err != nil { + log.Warnw("writing response", "error", err) + return + } + + case NRpcGet: + if len(req.Cid) != 1 { + if err := h.respondError(req.ID, xerrors.New("expected request for 1 cid"), cid.Undef); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + + err := h.bs.View(ctx, req.Cid[0], func(bdata []byte) error { + return h.respond(req.ID, NRpcOK, bdata) + }) + if err != nil { + if err := h.respondError(req.ID, err, req.Cid[0]); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + + case NRpcGetSize: + if len(req.Cid) != 1 { + if err := h.respondError(req.ID, xerrors.New("expected request for 1 cid"), cid.Undef); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + + sz, err := h.bs.GetSize(ctx, req.Cid[0]) + if err != nil { + if err := h.respondError(req.ID, err, req.Cid[0]); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + + var resData [4]byte + binary.LittleEndian.PutUint32(resData[:], uint32(sz)) + + if err := h.respond(req.ID, NRpcOK, resData[:]); err != nil { + log.Warnw("writing response", "error", err) + return + } + + case NRpcPut: + blocks := make([]block.Block, len(req.Cid)) + + if len(req.Cid) != len(req.Data) { + if err := h.respondError(req.ID, xerrors.New("cid count didn't match data count"), cid.Undef); err != nil { + log.Warnw("writing error response", "error", err) + } + return + } + + for i := range req.Cid { + blocks[i], err = block.NewBlockWithCid(req.Data[i], req.Cid[i]) + if err != nil { + log.Warnw("make block", "error", err) + return + } + } + + err := h.bs.PutMany(ctx, blocks) + if err != nil { + if err := h.respondError(req.ID, err, cid.Undef); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + + if err := h.respond(req.ID, NRpcOK, []byte{}); err != nil { + log.Warnw("writing response", "error", err) + return + } + case NRpcDelete: + err := h.bs.DeleteMany(ctx, req.Cid) + if err != nil { + if err := h.respondError(req.ID, err, cid.Undef); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + + if err := h.respond(req.ID, NRpcOK, []byte{}); err != nil { + log.Warnw("writing response", "error", err) + return + } + case NRpcList: + if err := h.respondError(req.ID, xerrors.New("list todo"), cid.Undef); err != nil { + log.Warnw("writing error response", "error", err) + return + } + continue + } + } +} + +func (h *NetworkStoreHandler) respondError(req uint64, uerr error, c cid.Cid) error { + var resp NetRpcResp + resp.ID = req + resp.Type = NRpcErr + + nerr := NetRpcErr{ + Type: NRpcErrGeneric, + Msg: uerr.Error(), + } + if ipld.IsNotFound(uerr) { + nerr.Type = NRpcErrNotFound + nerr.Cid = &c + } + + var edata bytes.Buffer + if err := nerr.MarshalCBOR(&edata); err != nil { + return xerrors.Errorf("marshaling error data: %w", err) + } + + resp.Data = edata.Bytes() + + var msg bytes.Buffer + if err := resp.MarshalCBOR(&msg); err != nil { + return xerrors.Errorf("marshaling error response: %w", err) + } + + if err := h.msgStream.WriteMsg(msg.Bytes()); err != nil { + return xerrors.Errorf("write error response: %w", err) + } + + return nil +} + +func (h *NetworkStoreHandler) respond(req uint64, rt NetRPCRespType, data []byte) error { + var resp NetRpcResp + resp.ID = req + resp.Type = rt + resp.Data = data + + var msg bytes.Buffer + if err := resp.MarshalCBOR(&msg); err != nil { + return xerrors.Errorf("marshaling response: %w", err) + } + + if err := h.msgStream.WriteMsg(msg.Bytes()); err != nil { + return xerrors.Errorf("write response: %w", err) + } + + return nil +} diff --git a/blockstore/net_test.go b/blockstore/net_test.go new file mode 100644 index 00000000000..d8c33818eb3 --- /dev/null +++ b/blockstore/net_test.go @@ -0,0 +1,63 @@ +package blockstore + +import ( + "context" + "fmt" + "io" + "testing" + + block "github.com/ipfs/go-block-format" + ipld "github.com/ipfs/go-ipld-format" + "github.com/libp2p/go-msgio" + "github.com/stretchr/testify/require" +) + +func TestNetBstore(t *testing.T) { + ctx := context.Background() + + cr, sw := io.Pipe() + sr, cw := io.Pipe() + + cm := msgio.Combine(msgio.NewWriter(cw), msgio.NewReader(cr)) + sm := msgio.Combine(msgio.NewWriter(sw), msgio.NewReader(sr)) + + bbs := NewMemorySync() + _ = HandleNetBstoreStream(ctx, bbs, sm) + + nbs := NewNetworkStore(cm) + + tb1 := block.NewBlock([]byte("aoeu")) + + h, err := nbs.Has(ctx, tb1.Cid()) + require.NoError(t, err) + require.False(t, h) + + err = nbs.Put(ctx, tb1) + require.NoError(t, err) + + h, err = nbs.Has(ctx, tb1.Cid()) + require.NoError(t, err) + require.True(t, h) + + sz, err := nbs.GetSize(ctx, tb1.Cid()) + require.NoError(t, err) + require.Equal(t, 4, sz) + + err = nbs.DeleteBlock(ctx, tb1.Cid()) + require.NoError(t, err) + + h, err = nbs.Has(ctx, tb1.Cid()) + require.NoError(t, err) + require.False(t, h) + + _, err = nbs.Get(ctx, tb1.Cid()) + fmt.Println(err) + require.True(t, ipld.IsNotFound(err)) + + err = nbs.Put(ctx, tb1) + require.NoError(t, err) + + b, err := nbs.Get(ctx, tb1.Cid()) + require.NoError(t, err) + require.Equal(t, "aoeu", string(b.RawData())) +} diff --git a/blockstore/net_ws.go b/blockstore/net_ws.go new file mode 100644 index 00000000000..5c9a70d8435 --- /dev/null +++ b/blockstore/net_ws.go @@ -0,0 +1,100 @@ +package blockstore + +import ( + "bytes" + "context" + + "github.com/gorilla/websocket" + "github.com/libp2p/go-msgio" + "golang.org/x/xerrors" +) + +type wsWrapper struct { + wc *websocket.Conn + + nextMsg []byte +} + +func (w *wsWrapper) Read(b []byte) (int, error) { + return 0, xerrors.New("read unsupported") +} + +func (w *wsWrapper) ReadMsg() ([]byte, error) { + if w.nextMsg != nil { + nm := w.nextMsg + w.nextMsg = nil + return nm, nil + } + + mt, r, err := w.wc.NextReader() + if err != nil { + return nil, err + } + + switch mt { + case websocket.BinaryMessage, websocket.TextMessage: + default: + return nil, xerrors.Errorf("unexpected message type") + } + + // todo pool + // todo limit sizes + var mbuf bytes.Buffer + if _, err := mbuf.ReadFrom(r); err != nil { + return nil, err + } + + return mbuf.Bytes(), nil +} + +func (w *wsWrapper) ReleaseMsg(bytes []byte) { + // todo use a pool +} + +func (w *wsWrapper) NextMsgLen() (int, error) { + if w.nextMsg != nil { + return len(w.nextMsg), nil + } + + mt, msg, err := w.wc.ReadMessage() + if err != nil { + return 0, err + } + + switch mt { + case websocket.BinaryMessage, websocket.TextMessage: + default: + return 0, xerrors.Errorf("unexpected message type") + } + + w.nextMsg = msg + return len(w.nextMsg), nil +} + +func (w *wsWrapper) Write(bytes []byte) (int, error) { + return 0, xerrors.New("write unsupported") +} + +func (w *wsWrapper) WriteMsg(bytes []byte) error { + return w.wc.WriteMessage(websocket.BinaryMessage, bytes) +} + +func (w *wsWrapper) Close() error { + return w.wc.Close() +} + +var _ msgio.ReadWriteCloser = &wsWrapper{} + +func wsConnToMio(wc *websocket.Conn) msgio.ReadWriteCloser { + return &wsWrapper{ + wc: wc, + } +} + +func HandleNetBstoreWS(ctx context.Context, bs Blockstore, wc *websocket.Conn) *NetworkStoreHandler { + return HandleNetBstoreStream(ctx, bs, wsConnToMio(wc)) +} + +func NewNetworkStoreWS(wc *websocket.Conn) *NetworkStore { + return NewNetworkStore(wsConnToMio(wc)) +} diff --git a/gen/main.go b/gen/main.go index 02548e18f2d..439194ef48e 100644 --- a/gen/main.go +++ b/gen/main.go @@ -7,6 +7,7 @@ import ( gen "github.com/whyrusleeping/cbor-gen" "github.com/filecoin-project/lotus/api" + "github.com/filecoin-project/lotus/blockstore" "github.com/filecoin-project/lotus/chain/exchange" "github.com/filecoin-project/lotus/chain/market" "github.com/filecoin-project/lotus/chain/types" @@ -127,4 +128,13 @@ func main() { fmt.Println(err) os.Exit(1) } + err = gen.WriteTupleEncodersToFile("./blockstore/cbor_gen.go", "blockstore", + blockstore.NetRpcReq{}, + blockstore.NetRpcResp{}, + blockstore.NetRpcErr{}, + ) + if err != nil { + fmt.Println(err) + os.Exit(1) + } } From 2c89b3240f23176daab90d1d1aeb8754eccc7ea4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Fri, 28 Oct 2022 16:56:22 +0100 Subject: [PATCH 02/13] retrieval: Support retrievals into remote stores --- api/api_full.go | 5 ++ api/docgen/docgen.go | 1 + build/openrpc/full.json.gz | Bin 29041 -> 28456 bytes cli/client_retr.go | 24 ++++--- documentation/en/api-v1-unstable-methods.md | 3 +- markets/retrievaladapter/client_blockstore.go | 66 ++++++++++++++++++ markets/utils/selectors.go | 7 ++ node/builder_chain.go | 2 + node/impl/client/client.go | 33 ++++++++- node/modules/client.go | 4 +- node/rpc.go | 43 ++++++++++++ 11 files changed, 174 insertions(+), 14 deletions(-) diff --git a/api/api_full.go b/api/api_full.go index 320a206873f..d4ed14cc6b1 100644 --- a/api/api_full.go +++ b/api/api_full.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/google/uuid" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p/core/peer" @@ -1012,8 +1013,12 @@ type RetrievalOrder struct { Client address.Address Miner address.Address MinerPeer *retrievalmarket.RetrievalPeer + + RemoteStore *RemoteStoreID `json:"RemoteStore,omitempty"` } +type RemoteStoreID = uuid.UUID + type InvocResult struct { MsgCid cid.Cid Msg *types.Message diff --git a/api/docgen/docgen.go b/api/docgen/docgen.go index 5bddef2ec3e..28c4e8b8524 100644 --- a/api/docgen/docgen.go +++ b/api/docgen/docgen.go @@ -361,6 +361,7 @@ func init() { Headers: nil, }, }) + addExample(&uuid.UUID{}) } func GetAPIType(name, pkg string) (i interface{}, t reflect.Type, permStruct []reflect.Type) { diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index 978fbb730b6a85804d2e4b1e4c136ce1ef451a92..bd7b14d30d2535c7abdbe09a933db97bcea57e54 100644 GIT binary patch delta 25902 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 Vk?fy;0{{U3|FyEg#8QR=1ptGGkVyal delta 26546 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 Zf7K-z{dxre0Req~*GeE>w^xD#{SSb4mJ$E} diff --git a/cli/client_retr.go b/cli/client_retr.go index 23acf1f08bb..9aa893f3c93 100644 --- a/cli/client_retr.go +++ b/cli/client_retr.go @@ -337,23 +337,16 @@ Examples: }, } -func ClientExportStream(apiAddr string, apiAuth http.Header, eref lapi.ExportRef, car bool) (io.ReadCloser, error) { - rj, err := json.Marshal(eref) - if err != nil { - return nil, xerrors.Errorf("marshaling export ref: %w", err) - } - +func ApiAddrToUrl(apiAddr string) (*url.URL, error) { ma, err := multiaddr.NewMultiaddr(apiAddr) if err == nil { _, addr, err := manet.DialArgs(ma) if err != nil { return nil, err } - // todo: make cliutil helpers for this apiAddr = "http://" + addr } - aa, err := url.Parse(apiAddr) if err != nil { return nil, xerrors.Errorf("parsing api address: %w", err) @@ -365,6 +358,20 @@ func ClientExportStream(apiAddr string, apiAuth http.Header, eref lapi.ExportRef aa.Scheme = "https" } + return aa, nil +} + +func ClientExportStream(apiAddr string, apiAuth http.Header, eref lapi.ExportRef, car bool) (io.ReadCloser, error) { + rj, err := json.Marshal(eref) + if err != nil { + return nil, xerrors.Errorf("marshaling export ref: %w", err) + } + + aa, err := ApiAddrToUrl(apiAddr) + if err != nil { + return nil, err + } + aa.Path = path.Join(aa.Path, "rest/v0/export") req, err := http.NewRequest("GET", fmt.Sprintf("%s?car=%t&export=%s", aa, car, url.QueryEscape(string(rj))), nil) if err != nil { @@ -583,6 +590,7 @@ var clientRetrieveLsCmd = &cli.Command{ dserv, roots[0], sel, + nil, func(p traversal.Progress, n ipld.Node, r traversal.VisitReason) error { if r == traversal.VisitReason_SelectionMatch { fmt.Println(p.Path) diff --git a/documentation/en/api-v1-unstable-methods.md b/documentation/en/api-v1-unstable-methods.md index 5f96eddd459..6d7c43149de 100644 --- a/documentation/en/api-v1-unstable-methods.md +++ b/documentation/en/api-v1-unstable-methods.md @@ -1992,7 +1992,8 @@ Inputs: "Address": "f01234", "ID": "12D3KooWGzxzKZYveHXtpG6AsrUJBcWxHBFS2HsEoGTxrMLvKXtf", "PieceCID": null - } + }, + "RemoteStore": "00000000-0000-0000-0000-000000000000" } ] ``` diff --git a/markets/retrievaladapter/client_blockstore.go b/markets/retrievaladapter/client_blockstore.go index 84c75fdbde7..35cfa387b5f 100644 --- a/markets/retrievaladapter/client_blockstore.go +++ b/markets/retrievaladapter/client_blockstore.go @@ -8,8 +8,12 @@ import ( "github.com/ipfs/go-cid" bstore "github.com/ipfs/go-ipfs-blockstore" "github.com/ipld/go-car/v2/blockstore" + "golang.org/x/xerrors" "github.com/filecoin-project/go-fil-markets/retrievalmarket" + + "github.com/filecoin-project/lotus/api" + lbstore "github.com/filecoin-project/lotus/blockstore" ) // ProxyBlockstoreAccessor is an accessor that returns a fixed blockstore. @@ -32,6 +36,68 @@ func (p *ProxyBlockstoreAccessor) Done(_ retrievalmarket.DealID) error { return nil } +func NewAPIBlockstoreAdapter(sub retrievalmarket.BlockstoreAccessor) *APIBlockstoreAccessor { + return &APIBlockstoreAccessor{ + sub: sub, + retrStores: map[retrievalmarket.DealID]api.RemoteStoreID{}, + remoteStores: map[api.RemoteStoreID]bstore.Blockstore{}, + } +} + +// APIBlockstoreAccessor adds support to API-specified remote blockstores +type APIBlockstoreAccessor struct { + sub retrievalmarket.BlockstoreAccessor + + retrStores map[retrievalmarket.DealID]api.RemoteStoreID + remoteStores map[api.RemoteStoreID]bstore.Blockstore +} + +func (a *APIBlockstoreAccessor) Get(id retrievalmarket.DealID, payloadCID retrievalmarket.PayloadCID) (bstore.Blockstore, error) { + as, has := a.retrStores[id] + if !has { + return a.sub.Get(id, payloadCID) + } + + return a.remoteStores[as], nil +} + +func (a *APIBlockstoreAccessor) Done(id retrievalmarket.DealID) error { + if _, has := a.retrStores[id]; has { + delete(a.retrStores, id) + return nil + } + return a.sub.Done(id) +} + +func (a *APIBlockstoreAccessor) UseRetrievalStore(id retrievalmarket.DealID, sid api.RemoteStoreID) error { + if _, has := a.retrStores[id]; has { + return xerrors.Errorf("apistore for deal %d already registered", id) + } + if _, has := a.remoteStores[sid]; !has { + return xerrors.Errorf("remote store not found") + } + + a.retrStores[id] = sid + return nil +} + +func (a *APIBlockstoreAccessor) RegisterApiStore(sid api.RemoteStoreID, st *lbstore.NetworkStore) error { + if _, has := a.remoteStores[sid]; has { + return xerrors.Errorf("remote store already registered with this uuid") + } + + a.remoteStores[sid] = st + + st.OnClose(func() { + if _, has := a.remoteStores[sid]; has { + delete(a.remoteStores, sid) + } + }) + return nil +} + +var _ retrievalmarket.BlockstoreAccessor = &APIBlockstoreAccessor{} + type CARBlockstoreAccessor struct { rootdir string lk sync.Mutex diff --git a/markets/utils/selectors.go b/markets/utils/selectors.go index e1009d1ff90..1b8a62401dd 100644 --- a/markets/utils/selectors.go +++ b/markets/utils/selectors.go @@ -26,6 +26,7 @@ func TraverseDag( ds mdagipld.DAGService, startFrom cid.Cid, optionalSelector ipld.Node, + onOpen func(node mdagipld.Node) error, visitCallback traversal.AdvVisitFn, ) error { @@ -61,6 +62,12 @@ func TraverseDag( return nil, err } + if onOpen != nil { + if err := onOpen(node); err != nil { + return nil, err + } + } + return bytes.NewBuffer(node.RawData()), nil } unixfsnode.AddUnixFSReificationToLinkSystem(&linkSystem) diff --git a/node/builder_chain.go b/node/builder_chain.go index 7a96e163c20..6dbd542d672 100644 --- a/node/builder_chain.go +++ b/node/builder_chain.go @@ -29,6 +29,7 @@ import ( ledgerwallet "github.com/filecoin-project/lotus/chain/wallet/ledger" "github.com/filecoin-project/lotus/chain/wallet/remotewallet" "github.com/filecoin-project/lotus/lib/peermgr" + "github.com/filecoin-project/lotus/markets/retrievaladapter" "github.com/filecoin-project/lotus/markets/storageadapter" "github.com/filecoin-project/lotus/node/config" "github.com/filecoin-project/lotus/node/hello" @@ -129,6 +130,7 @@ var ChainNode = Options( Override(new(*market.FundManager), market.NewFundManager), Override(new(dtypes.ClientDatastore), modules.NewClientDatastore), Override(new(storagemarket.BlockstoreAccessor), modules.StorageBlockstoreAccessor), + Override(new(*retrievaladapter.APIBlockstoreAccessor), retrievaladapter.NewAPIBlockstoreAdapter), Override(new(storagemarket.StorageClient), modules.StorageClient), Override(new(storagemarket.StorageClientNode), storageadapter.NewClientNodeAdapter), Override(HandleMigrateClientFundsKey, modules.HandleMigrateClientFunds), diff --git a/node/impl/client/client.go b/node/impl/client/client.go index f69691e41aa..7bfa4149636 100644 --- a/node/impl/client/client.go +++ b/node/impl/client/client.go @@ -10,6 +10,7 @@ import ( "os" "sort" "strings" + "sync" "time" "github.com/ipfs/go-blockservice" @@ -97,6 +98,7 @@ type API struct { Imports dtypes.ClientImportMgr StorageBlockstoreAccessor storagemarket.BlockstoreAccessor RtvlBlockstoreAccessor rm.BlockstoreAccessor + ApiBlockstoreAccessor *retrievaladapter.APIBlockstoreAccessor DataTransfer dtypes.ClientDataTransfer Host host.Host @@ -845,6 +847,13 @@ func (a *API) doRetrieval(ctx context.Context, order api.RetrievalOrder, sel dat } id := a.Retrieval.NextID() + + if order.RemoteStore != nil { + if err := a.ApiBlockstoreAccessor.UseRetrievalStore(id, *order.RemoteStore); err != nil { + return 0, xerrors.Errorf("registering api store: %w", err) + } + } + id, err = a.Retrieval.Retrieve( ctx, id, @@ -999,6 +1008,8 @@ func (a *API) outputCAR(ctx context.Context, ds format.DAGService, bs bstore.Blo roots[i] = dag.root } + var lk sync.Mutex + return dest.doWrite(func(w io.Writer) error { if err := car.WriteHeader(&car.CarHeader{ @@ -1016,8 +1027,21 @@ func (a *API) outputCAR(ctx context.Context, ds format.DAGService, bs bstore.Blo ds, root, dagSpec.selector, + func(node format.Node) error { + if dagSpec.exportAll { + lk.Lock() + defer lk.Unlock() + if cs.Visit(node.Cid()) { + err := util.LdWrite(w, node.Cid().Bytes(), node.RawData()) + if err != nil { + return xerrors.Errorf("writing block data: %w", err) + } + } + } + return nil + }, func(p traversal.Progress, n ipld.Node, r traversal.VisitReason) error { - if r == traversal.VisitReason_SelectionMatch { + if !dagSpec.exportAll && r == traversal.VisitReason_SelectionMatch { var c cid.Cid if p.LastBlock.Link == nil { c = root @@ -1082,8 +1106,9 @@ func (a *API) outputUnixFS(ctx context.Context, root cid.Cid, ds format.DAGServi } type dagSpec struct { - root cid.Cid - selector ipld.Node + root cid.Cid + selector ipld.Node + exportAll bool } func parseDagSpec(ctx context.Context, root cid.Cid, dsp []api.DagSpec, ds format.DAGService, car bool) ([]dagSpec, error) { @@ -1098,6 +1123,7 @@ func parseDagSpec(ctx context.Context, root cid.Cid, dsp []api.DagSpec, ds forma out := make([]dagSpec, len(dsp)) for i, spec := range dsp { + out[i].exportAll = spec.ExportMerkleProof if spec.DataSelector == nil { return nil, xerrors.Errorf("invalid DagSpec at position %d: `DataSelector` can not be nil", i) @@ -1131,6 +1157,7 @@ func parseDagSpec(ctx context.Context, root cid.Cid, dsp []api.DagSpec, ds forma ds, root, rsn, + nil, func(p traversal.Progress, n ipld.Node, r traversal.VisitReason) error { if r == traversal.VisitReason_SelectionMatch { if !car && p.LastBlock.Path.String() != p.Path.String() { diff --git a/node/modules/client.go b/node/modules/client.go index 22fcbb00d5e..69f8db559bb 100644 --- a/node/modules/client.go +++ b/node/modules/client.go @@ -202,9 +202,9 @@ func StorageClient(lc fx.Lifecycle, h host.Host, dataTransfer dtypes.ClientDataT // RetrievalClient creates a new retrieval client attached to the client blockstore func RetrievalClient(forceOffChain bool) func(lc fx.Lifecycle, h host.Host, r repo.LockedRepo, dt dtypes.ClientDataTransfer, payAPI payapi.PaychAPI, resolver discovery.PeerResolver, - ds dtypes.MetadataDS, chainAPI full.ChainAPI, stateAPI full.StateAPI, accessor retrievalmarket.BlockstoreAccessor, j journal.Journal) (retrievalmarket.RetrievalClient, error) { + ds dtypes.MetadataDS, chainAPI full.ChainAPI, stateAPI full.StateAPI, accessor *retrievaladapter.APIBlockstoreAccessor, j journal.Journal) (retrievalmarket.RetrievalClient, error) { return func(lc fx.Lifecycle, h host.Host, r repo.LockedRepo, dt dtypes.ClientDataTransfer, payAPI payapi.PaychAPI, resolver discovery.PeerResolver, - ds dtypes.MetadataDS, chainAPI full.ChainAPI, stateAPI full.StateAPI, accessor retrievalmarket.BlockstoreAccessor, j journal.Journal) (retrievalmarket.RetrievalClient, error) { + ds dtypes.MetadataDS, chainAPI full.ChainAPI, stateAPI full.StateAPI, accessor *retrievaladapter.APIBlockstoreAccessor, j journal.Journal) (retrievalmarket.RetrievalClient, error) { adapter := retrievaladapter.NewRetrievalClientNode(forceOffChain, payAPI, chainAPI, stateAPI) network := rmnet.NewFromLibp2pHost(h) ds = namespace.Wrap(ds, datastore.NewKey("/retrievals/client")) diff --git a/node/rpc.go b/node/rpc.go index 2c85c71bea1..96a81a383b9 100644 --- a/node/rpc.go +++ b/node/rpc.go @@ -3,13 +3,16 @@ package node import ( "context" "encoding/json" + "fmt" "net" "net/http" _ "net/http/pprof" "runtime" "strconv" + "github.com/google/uuid" "github.com/gorilla/mux" + "github.com/gorilla/websocket" "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" "github.com/multiformats/go-multiaddr" @@ -23,6 +26,7 @@ import ( "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api/v0api" "github.com/filecoin-project/lotus/api/v1api" + bstore "github.com/filecoin-project/lotus/blockstore" "github.com/filecoin-project/lotus/lib/rpcenc" "github.com/filecoin-project/lotus/metrics" "github.com/filecoin-project/lotus/metrics/proxy" @@ -92,6 +96,7 @@ func FullNodeHandler(a v1api.FullNode, permissioned bool, opts ...jsonrpc.Server // Import handler handleImportFunc := handleImport(a.(*impl.FullNodeAPI)) handleExportFunc := handleExport(a.(*impl.FullNodeAPI)) + handleRemoteStoreFunc := handleRemoteStore(a.(*impl.FullNodeAPI)) if permissioned { importAH := &auth.Handler{ Verify: a.AuthVerify, @@ -104,9 +109,16 @@ func FullNodeHandler(a v1api.FullNode, permissioned bool, opts ...jsonrpc.Server Next: handleExportFunc, } m.Handle("/rest/v0/export", exportAH) + + storeAH := &auth.Handler{ + Verify: a.AuthVerify, + Next: handleRemoteStoreFunc, + } + m.Handle("/rest/v0/store/{uuid}", storeAH) } else { m.HandleFunc("/rest/v0/import", handleImportFunc) m.HandleFunc("/rest/v0/export", handleExportFunc) + m.HandleFunc("/rest/v0/store/{uuid}", handleRemoteStoreFunc) } // debugging @@ -256,3 +268,34 @@ func handleFractionOpt(name string, setter func(int)) http.HandlerFunc { setter(fr) } } + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + return true + }, +} + +func handleRemoteStore(a *impl.FullNodeAPI) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id, err := uuid.Parse(vars["uuid"]) + if err != nil { + http.Error(w, fmt.Sprintf("parse uuid: %s", err), http.StatusBadRequest) + return + } + + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Error(err) + w.WriteHeader(500) + return + } + + nstore := bstore.NewNetworkStoreWS(c) + if err := a.ApiBlockstoreAccessor.RegisterApiStore(id, nstore); err != nil { + log.Errorw("registering api bstore", "error", err) + _ = c.Close() + return + } + } +} From 8c1bc10aa41efe9f30ff5ec23ce63c8c90b06403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Sat, 29 Oct 2022 15:02:52 +0100 Subject: [PATCH 03/13] apistore: Lock accesses --- markets/retrievaladapter/client_blockstore.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/markets/retrievaladapter/client_blockstore.go b/markets/retrievaladapter/client_blockstore.go index 35cfa387b5f..b3122572693 100644 --- a/markets/retrievaladapter/client_blockstore.go +++ b/markets/retrievaladapter/client_blockstore.go @@ -50,9 +50,14 @@ type APIBlockstoreAccessor struct { retrStores map[retrievalmarket.DealID]api.RemoteStoreID remoteStores map[api.RemoteStoreID]bstore.Blockstore + + accessLk sync.Locker } func (a *APIBlockstoreAccessor) Get(id retrievalmarket.DealID, payloadCID retrievalmarket.PayloadCID) (bstore.Blockstore, error) { + a.accessLk.Lock() + defer a.accessLk.Unlock() + as, has := a.retrStores[id] if !has { return a.sub.Get(id, payloadCID) @@ -62,6 +67,9 @@ func (a *APIBlockstoreAccessor) Get(id retrievalmarket.DealID, payloadCID retrie } func (a *APIBlockstoreAccessor) Done(id retrievalmarket.DealID) error { + a.accessLk.Lock() + defer a.accessLk.Unlock() + if _, has := a.retrStores[id]; has { delete(a.retrStores, id) return nil @@ -70,6 +78,9 @@ func (a *APIBlockstoreAccessor) Done(id retrievalmarket.DealID) error { } func (a *APIBlockstoreAccessor) UseRetrievalStore(id retrievalmarket.DealID, sid api.RemoteStoreID) error { + a.accessLk.Lock() + defer a.accessLk.Unlock() + if _, has := a.retrStores[id]; has { return xerrors.Errorf("apistore for deal %d already registered", id) } @@ -82,6 +93,9 @@ func (a *APIBlockstoreAccessor) UseRetrievalStore(id retrievalmarket.DealID, sid } func (a *APIBlockstoreAccessor) RegisterApiStore(sid api.RemoteStoreID, st *lbstore.NetworkStore) error { + a.accessLk.Lock() + defer a.accessLk.Unlock() + if _, has := a.remoteStores[sid]; has { return xerrors.Errorf("remote store already registered with this uuid") } From 73a515b93f4ad0645a18b04d973375fe4d387f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Sat, 29 Oct 2022 15:11:09 +0100 Subject: [PATCH 04/13] make lint happy --- go.mod | 2 +- markets/retrievaladapter/client_blockstore.go | 2 +- node/impl/client/client.go | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index a39c70956a9..dbe41d95ebc 100644 --- a/go.mod +++ b/go.mod @@ -115,6 +115,7 @@ require ( github.com/libp2p/go-libp2p-record v0.2.0 github.com/libp2p/go-libp2p-routing-helpers v0.2.3 github.com/libp2p/go-maddr-filter v0.1.0 + github.com/libp2p/go-msgio v0.2.0 github.com/mattn/go-isatty v0.0.16 github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 github.com/mitchellh/go-homedir v1.1.0 @@ -254,7 +255,6 @@ require ( github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect github.com/libp2p/go-libp2p-noise v0.5.0 // indirect github.com/libp2p/go-libp2p-tls v0.5.0 // indirect - github.com/libp2p/go-msgio v0.2.0 // indirect github.com/libp2p/go-nat v0.1.0 // indirect github.com/libp2p/go-netroute v0.2.0 // indirect github.com/libp2p/go-openssl v0.1.0 // indirect diff --git a/markets/retrievaladapter/client_blockstore.go b/markets/retrievaladapter/client_blockstore.go index b3122572693..de56b36418c 100644 --- a/markets/retrievaladapter/client_blockstore.go +++ b/markets/retrievaladapter/client_blockstore.go @@ -51,7 +51,7 @@ type APIBlockstoreAccessor struct { retrStores map[retrievalmarket.DealID]api.RemoteStoreID remoteStores map[api.RemoteStoreID]bstore.Blockstore - accessLk sync.Locker + accessLk sync.Mutex } func (a *APIBlockstoreAccessor) Get(id retrievalmarket.DealID, payloadCID retrievalmarket.PayloadCID) (bstore.Blockstore, error) { diff --git a/node/impl/client/client.go b/node/impl/client/client.go index 7bfa4149636..86da6500d64 100644 --- a/node/impl/client/client.go +++ b/node/impl/client/client.go @@ -1022,6 +1022,8 @@ func (a *API) outputCAR(ctx context.Context, ds format.DAGService, bs bstore.Blo cs := cid.NewSet() for _, dagSpec := range dags { + dagSpec := dagSpec + if err := utils.TraverseDag( ctx, ds, From e66d5a05378a986a08b763f34b1e802e5aaff07f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 31 Oct 2022 12:32:00 +0000 Subject: [PATCH 05/13] cli: Move EpochTime to cliutil --- cli/state.go | 7 +++--- cli/util.go | 39 -------------------------------- cli/util/epoch.go | 46 ++++++++++++++++++++++++++++++++++++++ cmd/lotus-miner/info.go | 3 ++- cmd/lotus-miner/proving.go | 13 ++++++----- cmd/lotus-miner/sectors.go | 15 +++++++------ 6 files changed, 67 insertions(+), 56 deletions(-) create mode 100644 cli/util/epoch.go diff --git a/cli/state.go b/cli/state.go index 8fb7ddf61c0..cd134b49ddb 100644 --- a/cli/state.go +++ b/cli/state.go @@ -46,6 +46,7 @@ import ( "github.com/filecoin-project/lotus/chain/stmgr" "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" + cliutil "github.com/filecoin-project/lotus/cli/util" ) var StateCmd = &cli.Command{ @@ -230,7 +231,7 @@ var StateMinerInfo = &cli.Command{ return xerrors.Errorf("getting miner info: %w", err) } - fmt.Printf("Proving Period Start:\t%s\n", EpochTime(cd.CurrentEpoch, cd.PeriodStart)) + fmt.Printf("Proving Period Start:\t%s\n", cliutil.EpochTime(cd.CurrentEpoch, cd.PeriodStart)) return nil }, @@ -1816,8 +1817,8 @@ var StateSectorCmd = &cli.Command{ } fmt.Println("DealIDs: ", si.DealIDs) fmt.Println() - fmt.Println("Activation: ", EpochTimeTs(ts.Height(), si.Activation, ts)) - fmt.Println("Expiration: ", EpochTimeTs(ts.Height(), si.Expiration, ts)) + fmt.Println("Activation: ", cliutil.EpochTimeTs(ts.Height(), si.Activation, ts)) + fmt.Println("Expiration: ", cliutil.EpochTimeTs(ts.Height(), si.Expiration, ts)) fmt.Println() fmt.Println("DealWeight: ", si.DealWeight) fmt.Println("VerifiedDealWeight: ", si.VerifiedDealWeight) diff --git a/cli/util.go b/cli/util.go index 69c45b38237..03de817f9b1 100644 --- a/cli/util.go +++ b/cli/util.go @@ -2,19 +2,13 @@ package cli import ( "context" - "fmt" "os" - "time" "github.com/fatih/color" - "github.com/hako/durafmt" "github.com/ipfs/go-cid" "github.com/mattn/go-isatty" - "github.com/filecoin-project/go-state-types/abi" - "github.com/filecoin-project/lotus/api/v0api" - "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/types" ) @@ -43,36 +37,3 @@ func parseTipSet(ctx context.Context, api v0api.FullNode, vals []string) (*types return types.NewTipSet(headers) } - -func EpochTime(curr, e abi.ChainEpoch) string { - switch { - case curr > e: - return fmt.Sprintf("%d (%s ago)", e, durafmt.Parse(time.Second*time.Duration(int64(build.BlockDelaySecs)*int64(curr-e))).LimitFirstN(2)) - case curr == e: - return fmt.Sprintf("%d (now)", e) - case curr < e: - return fmt.Sprintf("%d (in %s)", e, durafmt.Parse(time.Second*time.Duration(int64(build.BlockDelaySecs)*int64(e-curr))).LimitFirstN(2)) - } - - panic("math broke") -} - -// EpochTimeTs is like EpochTime, but also outputs absolute time. `ts` is only -// used to provide a timestamp at some epoch to calculate time from. It can be -// a genesis tipset. -// -// Example output: `1944975 (01 Jul 22 08:07 CEST, 10 hours 29 minutes ago)` -func EpochTimeTs(curr, e abi.ChainEpoch, ts *types.TipSet) string { - timeStr := time.Unix(int64(ts.MinTimestamp()+(uint64(e-ts.Height())*build.BlockDelaySecs)), 0).Format(time.RFC822) - - switch { - case curr > e: - return fmt.Sprintf("%d (%s, %s ago)", e, timeStr, durafmt.Parse(time.Second*time.Duration(int64(build.BlockDelaySecs)*int64(curr-e))).LimitFirstN(2)) - case curr == e: - return fmt.Sprintf("%d (%s, now)", e, timeStr) - case curr < e: - return fmt.Sprintf("%d (%s, in %s)", e, timeStr, durafmt.Parse(time.Second*time.Duration(int64(build.BlockDelaySecs)*int64(e-curr))).LimitFirstN(2)) - } - - panic("math broke") -} diff --git a/cli/util/epoch.go b/cli/util/epoch.go new file mode 100644 index 00000000000..81c92a7e3ed --- /dev/null +++ b/cli/util/epoch.go @@ -0,0 +1,46 @@ +package cliutil + +import ( + "fmt" + "time" + + "github.com/hako/durafmt" + + "github.com/filecoin-project/go-state-types/abi" + + "github.com/filecoin-project/lotus/build" + "github.com/filecoin-project/lotus/chain/types" +) + +func EpochTime(curr, e abi.ChainEpoch) string { + switch { + case curr > e: + return fmt.Sprintf("%d (%s ago)", e, durafmt.Parse(time.Second*time.Duration(int64(build.BlockDelaySecs)*int64(curr-e))).LimitFirstN(2)) + case curr == e: + return fmt.Sprintf("%d (now)", e) + case curr < e: + return fmt.Sprintf("%d (in %s)", e, durafmt.Parse(time.Second*time.Duration(int64(build.BlockDelaySecs)*int64(e-curr))).LimitFirstN(2)) + } + + panic("math broke") +} + +// EpochTimeTs is like EpochTime, but also outputs absolute time. `ts` is only +// used to provide a timestamp at some epoch to calculate time from. It can be +// a genesis tipset. +// +// Example output: `1944975 (01 Jul 22 08:07 CEST, 10 hours 29 minutes ago)` +func EpochTimeTs(curr, e abi.ChainEpoch, ts *types.TipSet) string { + timeStr := time.Unix(int64(ts.MinTimestamp()+(uint64(e-ts.Height())*build.BlockDelaySecs)), 0).Format(time.RFC822) + + switch { + case curr > e: + return fmt.Sprintf("%d (%s, %s ago)", e, timeStr, durafmt.Parse(time.Second*time.Duration(int64(build.BlockDelaySecs)*int64(curr-e))).LimitFirstN(2)) + case curr == e: + return fmt.Sprintf("%d (%s, now)", e, timeStr) + case curr < e: + return fmt.Sprintf("%d (%s, in %s)", e, timeStr, durafmt.Parse(time.Second*time.Duration(int64(build.BlockDelaySecs)*int64(e-curr))).LimitFirstN(2)) + } + + panic("math broke") +} diff --git a/cmd/lotus-miner/info.go b/cmd/lotus-miner/info.go index 312d866004e..f175a4e243e 100644 --- a/cmd/lotus-miner/info.go +++ b/cmd/lotus-miner/info.go @@ -33,6 +33,7 @@ import ( "github.com/filecoin-project/lotus/chain/actors/builtin/reward" "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" + cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/filecoin-project/lotus/journal/alerting" sealing "github.com/filecoin-project/lotus/storage/pipeline" "github.com/filecoin-project/lotus/storage/sealer/sealtasks" @@ -664,7 +665,7 @@ func producedBlocks(ctx context.Context, count int, maddr address.Address, napi fmt.Printf("%8d | %s | %s\n", ts.Height(), bh.Cid(), types.FIL(minerReward)) count-- } else if tty && bh.Height%120 == 0 { - _, _ = fmt.Fprintf(os.Stderr, "\r\x1b[0KChecking epoch %s", lcli.EpochTime(head.Height(), bh.Height)) + _, _ = fmt.Fprintf(os.Stderr, "\r\x1b[0KChecking epoch %s", cliutil.EpochTime(head.Height(), bh.Height)) } } tsk = ts.Parents() diff --git a/cmd/lotus-miner/proving.go b/cmd/lotus-miner/proving.go index 6f6fd663580..c9a3133ee8b 100644 --- a/cmd/lotus-miner/proving.go +++ b/cmd/lotus-miner/proving.go @@ -26,6 +26,7 @@ import ( "github.com/filecoin-project/lotus/chain/store" "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" + cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -185,18 +186,18 @@ var provingInfoCmd = &cli.Command{ fmt.Printf("Current Epoch: %d\n", cd.CurrentEpoch) fmt.Printf("Proving Period Boundary: %d\n", cd.PeriodStart%cd.WPoStProvingPeriod) - fmt.Printf("Proving Period Start: %s\n", lcli.EpochTimeTs(cd.CurrentEpoch, cd.PeriodStart, head)) - fmt.Printf("Next Period Start: %s\n\n", lcli.EpochTimeTs(cd.CurrentEpoch, cd.PeriodStart+cd.WPoStProvingPeriod, head)) + fmt.Printf("Proving Period Start: %s\n", cliutil.EpochTimeTs(cd.CurrentEpoch, cd.PeriodStart, head)) + fmt.Printf("Next Period Start: %s\n\n", cliutil.EpochTimeTs(cd.CurrentEpoch, cd.PeriodStart+cd.WPoStProvingPeriod, head)) fmt.Printf("Faults: %d (%.2f%%)\n", faults, faultPerc) fmt.Printf("Recovering: %d\n", recovering) fmt.Printf("Deadline Index: %d\n", cd.Index) fmt.Printf("Deadline Sectors: %d\n", curDeadlineSectors) - fmt.Printf("Deadline Open: %s\n", lcli.EpochTime(cd.CurrentEpoch, cd.Open)) - fmt.Printf("Deadline Close: %s\n", lcli.EpochTime(cd.CurrentEpoch, cd.Close)) - fmt.Printf("Deadline Challenge: %s\n", lcli.EpochTime(cd.CurrentEpoch, cd.Challenge)) - fmt.Printf("Deadline FaultCutoff: %s\n", lcli.EpochTime(cd.CurrentEpoch, cd.FaultCutoff)) + fmt.Printf("Deadline Open: %s\n", cliutil.EpochTime(cd.CurrentEpoch, cd.Open)) + fmt.Printf("Deadline Close: %s\n", cliutil.EpochTime(cd.CurrentEpoch, cd.Close)) + fmt.Printf("Deadline Challenge: %s\n", cliutil.EpochTime(cd.CurrentEpoch, cd.Challenge)) + fmt.Printf("Deadline FaultCutoff: %s\n", cliutil.EpochTime(cd.CurrentEpoch, cd.FaultCutoff)) return nil }, } diff --git a/cmd/lotus-miner/sectors.go b/cmd/lotus-miner/sectors.go index 44bce55bc92..f17346821ef 100644 --- a/cmd/lotus-miner/sectors.go +++ b/cmd/lotus-miner/sectors.go @@ -32,6 +32,7 @@ import ( "github.com/filecoin-project/lotus/chain/actors/policy" "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" + cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/filecoin-project/lotus/lib/strle" "github.com/filecoin-project/lotus/lib/tablewriter" sealing "github.com/filecoin-project/lotus/storage/pipeline" @@ -485,9 +486,9 @@ var sectorsListCmd = &cli.Command{ if !inSSet { m["Expiration"] = "n/a" } else { - m["Expiration"] = lcli.EpochTime(head.Height(), exp) + m["Expiration"] = cliutil.EpochTime(head.Height(), exp) if st.Early > 0 { - m["RecoveryTimeout"] = color.YellowString(lcli.EpochTime(head.Height(), st.Early)) + m["RecoveryTimeout"] = color.YellowString(cliutil.EpochTime(head.Height(), st.Early)) } } if inSSet && cctx.Bool("initial-pledge") { @@ -666,10 +667,10 @@ var sectorsCheckExpireCmd = &cli.Command{ "ID": sector.SectorNumber, "SealProof": sector.SealProof, "InitialPledge": types.FIL(sector.InitialPledge).Short(), - "Activation": lcli.EpochTime(currEpoch, sector.Activation), - "Expiration": lcli.EpochTime(currEpoch, sector.Expiration), - "MaxExpiration": lcli.EpochTime(currEpoch, MaxExpiration), - "MaxExtendNow": lcli.EpochTime(currEpoch, MaxExtendNow), + "Activation": cliutil.EpochTime(currEpoch, sector.Activation), + "Expiration": cliutil.EpochTime(currEpoch, sector.Expiration), + "MaxExpiration": cliutil.EpochTime(currEpoch, MaxExpiration), + "MaxExtendNow": cliutil.EpochTime(currEpoch, MaxExtendNow), }) } @@ -1909,7 +1910,7 @@ var sectorsExpiredCmd = &cli.Command{ toRemove = append(toRemove, s) } - fmt.Printf("%d%s\t%s\t%s\n", s, rmMsg, st.State, lcli.EpochTime(head.Height(), st.Expiration)) + fmt.Printf("%d%s\t%s\t%s\n", s, rmMsg, st.State, cliutil.EpochTime(head.Height(), st.Expiration)) return nil }) From b540d10b489b12028b0f7b7c3fd67e3ed4660140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 31 Oct 2022 12:46:00 +0000 Subject: [PATCH 06/13] cli: Move ClientExport to cliutil --- cli/client_retr.go | 73 ++-------------------------------------- cli/util/retrieval.go | 78 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 70 deletions(-) create mode 100644 cli/util/retrieval.go diff --git a/cli/client_retr.go b/cli/client_retr.go index 9aa893f3c93..b619a28719f 100644 --- a/cli/client_retr.go +++ b/cli/client_retr.go @@ -3,14 +3,9 @@ package cli import ( "bytes" "context" - "encoding/json" "fmt" "io" - "io/ioutil" - "net/http" - "net/url" "os" - "path" "sort" "strings" "time" @@ -29,8 +24,6 @@ import ( "github.com/ipld/go-ipld-prime/traversal/selector/builder" selectorparse "github.com/ipld/go-ipld-prime/traversal/selector/parse" textselector "github.com/ipld/go-ipld-selector-text-lite" - "github.com/multiformats/go-multiaddr" - manet "github.com/multiformats/go-multiaddr/net" "github.com/urfave/cli/v2" "golang.org/x/xerrors" @@ -40,6 +33,7 @@ import ( lapi "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/chain/types" + cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/filecoin-project/lotus/markets/utils" "github.com/filecoin-project/lotus/node/repo" ) @@ -337,67 +331,6 @@ Examples: }, } -func ApiAddrToUrl(apiAddr string) (*url.URL, error) { - ma, err := multiaddr.NewMultiaddr(apiAddr) - if err == nil { - _, addr, err := manet.DialArgs(ma) - if err != nil { - return nil, err - } - // todo: make cliutil helpers for this - apiAddr = "http://" + addr - } - aa, err := url.Parse(apiAddr) - if err != nil { - return nil, xerrors.Errorf("parsing api address: %w", err) - } - switch aa.Scheme { - case "ws": - aa.Scheme = "http" - case "wss": - aa.Scheme = "https" - } - - return aa, nil -} - -func ClientExportStream(apiAddr string, apiAuth http.Header, eref lapi.ExportRef, car bool) (io.ReadCloser, error) { - rj, err := json.Marshal(eref) - if err != nil { - return nil, xerrors.Errorf("marshaling export ref: %w", err) - } - - aa, err := ApiAddrToUrl(apiAddr) - if err != nil { - return nil, err - } - - aa.Path = path.Join(aa.Path, "rest/v0/export") - req, err := http.NewRequest("GET", fmt.Sprintf("%s?car=%t&export=%s", aa, car, url.QueryEscape(string(rj))), nil) - if err != nil { - return nil, err - } - - req.Header = apiAuth - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - - if resp.StatusCode != http.StatusOK { - em, err := ioutil.ReadAll(resp.Body) - if err != nil { - return nil, xerrors.Errorf("reading error body: %w", err) - } - - resp.Body.Close() // nolint - return nil, xerrors.Errorf("getting root car: http %d: %s", resp.StatusCode, string(em)) - } - - return resp.Body, nil -} - var clientRetrieveCatCmd = &cli.Command{ Name: "cat", Usage: "Show data from network", @@ -447,7 +380,7 @@ var clientRetrieveCatCmd = &cli.Command{ eref.DAGs = append(eref.DAGs, lapi.DagSpec{DataSelector: &sel}) } - rc, err := ClientExportStream(ainfo.Addr, ainfo.AuthHeader(), *eref, false) + rc, err := cliutil.ClientExportStream(ainfo.Addr, ainfo.AuthHeader(), *eref, false) if err != nil { return err } @@ -535,7 +468,7 @@ var clientRetrieveLsCmd = &cli.Command{ DataSelector: &dataSelector, }) - rc, err := ClientExportStream(ainfo.Addr, ainfo.AuthHeader(), *eref, true) + rc, err := cliutil.ClientExportStream(ainfo.Addr, ainfo.AuthHeader(), *eref, true) if err != nil { return xerrors.Errorf("export: %w", err) } diff --git a/cli/util/retrieval.go b/cli/util/retrieval.go new file mode 100644 index 00000000000..3a2ef60770a --- /dev/null +++ b/cli/util/retrieval.go @@ -0,0 +1,78 @@ +package cliutil + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "path" + + "github.com/multiformats/go-multiaddr" + manet "github.com/multiformats/go-multiaddr/net" + "golang.org/x/xerrors" + + "github.com/filecoin-project/lotus/api" +) + +func ApiAddrToUrl(apiAddr string) (*url.URL, error) { + ma, err := multiaddr.NewMultiaddr(apiAddr) + if err == nil { + _, addr, err := manet.DialArgs(ma) + if err != nil { + return nil, err + } + // todo: make cliutil helpers for this + apiAddr = "http://" + addr + } + aa, err := url.Parse(apiAddr) + if err != nil { + return nil, xerrors.Errorf("parsing api address: %w", err) + } + switch aa.Scheme { + case "ws": + aa.Scheme = "http" + case "wss": + aa.Scheme = "https" + } + + return aa, nil +} + +func ClientExportStream(apiAddr string, apiAuth http.Header, eref api.ExportRef, car bool) (io.ReadCloser, error) { + rj, err := json.Marshal(eref) + if err != nil { + return nil, xerrors.Errorf("marshaling export ref: %w", err) + } + + aa, err := ApiAddrToUrl(apiAddr) + if err != nil { + return nil, err + } + + aa.Path = path.Join(aa.Path, "rest/v0/export") + req, err := http.NewRequest("GET", fmt.Sprintf("%s?car=%t&export=%s", aa, car, url.QueryEscape(string(rj))), nil) + if err != nil { + return nil, err + } + + req.Header = apiAuth + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != http.StatusOK { + em, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, xerrors.Errorf("reading error body: %w", err) + } + + resp.Body.Close() // nolint + return nil, xerrors.Errorf("getting root car: http %d: %s", resp.StatusCode, string(em)) + } + + return resp.Body, nil +} From 5f5cc794f0eaded11187a5b7705c34924c43d835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 31 Oct 2022 17:15:09 +0000 Subject: [PATCH 07/13] make config not depend on ffi --- itests/kit/ensemble.go | 6 ++-- itests/wdpost_worker_config_test.go | 17 +++++---- node/builder_miner.go | 3 +- node/config/def.go | 17 +++++++-- node/config/dep_test.go | 34 ++++++++++++++++++ node/config/storage.go | 26 -------------- node/config/types.go | 3 +- node/modules/storageminer.go | 6 ++-- storage/sealer/manager.go | 56 ++++------------------------- 9 files changed, 72 insertions(+), 96 deletions(-) create mode 100644 node/config/dep_test.go diff --git a/itests/kit/ensemble.go b/itests/kit/ensemble.go index 8fa781e65d1..0734b1f70f1 100644 --- a/itests/kit/ensemble.go +++ b/itests/kit/ensemble.go @@ -632,7 +632,7 @@ func (n *Ensemble) Start() *Ensemble { // disable resource filtering so that local worker gets assigned tasks // regardless of system pressure. - node.Override(new(sectorstorage.Config), func() sectorstorage.Config { + node.Override(new(config.SealingConfig), func() config.SealingConfig { scfg := config.DefaultStorageMiner() if noLocal { @@ -645,8 +645,8 @@ func (n *Ensemble) Start() *Ensemble { scfg.Storage.Assigner = assigner scfg.Storage.DisallowRemoteFinalize = disallowRemoteFinalize - scfg.Storage.ResourceFiltering = sectorstorage.ResourceFilteringDisabled - return scfg.StorageManager() + scfg.Storage.ResourceFiltering = config.ResourceFilteringDisabled + return scfg.Sealing }), // upgrades diff --git a/itests/wdpost_worker_config_test.go b/itests/wdpost_worker_config_test.go index a84896eae1a..5172335a81b 100644 --- a/itests/wdpost_worker_config_test.go +++ b/itests/wdpost_worker_config_test.go @@ -17,7 +17,6 @@ import ( "github.com/filecoin-project/lotus/node/config" "github.com/filecoin-project/lotus/node/impl" "github.com/filecoin-project/lotus/node/modules" - "github.com/filecoin-project/lotus/storage/sealer" "github.com/filecoin-project/lotus/storage/sealer/sealtasks" "github.com/filecoin-project/lotus/storage/sealer/storiface" "github.com/filecoin-project/lotus/storage/wdpost" @@ -35,10 +34,10 @@ func TestWindowPostNoBuiltinWindow(t *testing.T) { kit.PresealSectors(sectors), // 2 sectors per partition, 2 partitions in all 48 deadlines kit.LatestActorsAt(-1), kit.ConstructorOpts( - node.Override(new(sealer.Config), func() sealer.Config { - c := config.DefaultStorageMiner().StorageManager() - c.DisableBuiltinWindowPoSt = true - return c + node.Override(new(*config.ProvingConfig), func() *config.ProvingConfig { + c := config.DefaultStorageMiner() + c.Proving.DisableBuiltinWindowPoSt = true + return &c.Proving }), node.Override(new(*wdpost.WindowPoStScheduler), modules.WindowPostScheduler( config.DefaultStorageMiner().Fees, @@ -92,10 +91,10 @@ func TestWindowPostNoBuiltinWindowWithWorker(t *testing.T) { kit.PresealSectors(sectors), // 2 sectors per partition, 2 partitions in all 48 deadlines kit.LatestActorsAt(-1), kit.ConstructorOpts( - node.Override(new(sealer.Config), func() sealer.Config { - c := config.DefaultStorageMiner().StorageManager() - c.DisableBuiltinWindowPoSt = true - return c + node.Override(new(*config.ProvingConfig), func() *config.ProvingConfig { + c := config.DefaultStorageMiner() + c.Proving.DisableBuiltinWindowPoSt = true + return &c.Proving }), node.Override(new(*wdpost.WindowPoStScheduler), modules.WindowPostScheduler( config.DefaultStorageMiner().Fees, diff --git a/node/builder_miner.go b/node/builder_miner.go index cd7b4ec8d1a..2d33727f804 100644 --- a/node/builder_miner.go +++ b/node/builder_miner.go @@ -224,7 +224,8 @@ func ConfigStorageMiner(c interface{}) Option { Override(new(storagemarket.StorageProviderNode), storageadapter.NewProviderNodeAdapter(&cfg.Fees, &cfg.Dealmaking)), ), - Override(new(sectorstorage.Config), cfg.StorageManager()), + Override(new(config.SealerConfig), cfg.Storage), + Override(new(config.ProvingConfig), cfg.Proving), Override(new(*ctladdr.AddressSelector), modules.AddressSelector(&cfg.Addresses)), ) } diff --git a/node/config/def.go b/node/config/def.go index a6e6fc66aa3..390ae38b1f0 100644 --- a/node/config/def.go +++ b/node/config/def.go @@ -15,7 +15,6 @@ import ( "github.com/filecoin-project/lotus/chain/actors/builtin" "github.com/filecoin-project/lotus/chain/actors/policy" "github.com/filecoin-project/lotus/chain/types" - "github.com/filecoin-project/lotus/storage/sealer" ) const ( @@ -162,7 +161,7 @@ func DefaultStorageMiner() *StorageMiner { Assigner: "utilization", // By default use the hardware resource filtering strategy. - ResourceFiltering: sealer.ResourceFilteringHardware, + ResourceFiltering: ResourceFilteringHardware, }, Dealmaking: DealmakingConfig{ @@ -274,3 +273,17 @@ func (dur Duration) MarshalText() ([]byte, error) { d := time.Duration(dur) return []byte(d.String()), nil } + +// ResourceFilteringStrategy is an enum indicating the kinds of resource +// filtering strategies that can be configured for workers. +type ResourceFilteringStrategy string + +const ( + // ResourceFilteringHardware specifies that available hardware resources + // should be evaluated when scheduling a task against the worker. + ResourceFilteringHardware = ResourceFilteringStrategy("hardware") + + // ResourceFilteringDisabled disables resource filtering against this + // worker. The scheduler may assign any task to this worker. + ResourceFilteringDisabled = ResourceFilteringStrategy("disabled") +) diff --git a/node/config/dep_test.go b/node/config/dep_test.go new file mode 100644 index 00000000000..91167e690f1 --- /dev/null +++ b/node/config/dep_test.go @@ -0,0 +1,34 @@ +package config + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" +) + +func goCmd() string { + var exeSuffix string + if runtime.GOOS == "windows" { + exeSuffix = ".exe" + } + path := filepath.Join(runtime.GOROOT(), "bin", "go"+exeSuffix) + if _, err := os.Stat(path); err == nil { + return path + } + return "go" +} + +func TestDoesntDependOnFFI(t *testing.T) { + deps, err := exec.Command(goCmd(), "list", "-deps", "github.com/filecoin-project/lotus/node/config").Output() + if err != nil { + t.Fatal(err) + } + for _, pkg := range strings.Fields(string(deps)) { + if pkg == "github.com/filecoin-project/filecoin-ffi" { + t.Fatal("config depends on filecoin-ffi") + } + } +} diff --git a/node/config/storage.go b/node/config/storage.go index 0c72eabd149..f4eac811c74 100644 --- a/node/config/storage.go +++ b/node/config/storage.go @@ -9,7 +9,6 @@ import ( "golang.org/x/xerrors" "github.com/filecoin-project/lotus/storage/paths" - "github.com/filecoin-project/lotus/storage/sealer" ) func StorageFromFile(path string, def *paths.StorageConfig) (*paths.StorageConfig, error) { @@ -50,28 +49,3 @@ func WriteStorageFile(path string, config paths.StorageConfig) error { return nil } - -func (c *StorageMiner) StorageManager() sealer.Config { - return sealer.Config{ - ParallelFetchLimit: c.Storage.ParallelFetchLimit, - AllowSectorDownload: c.Storage.AllowSectorDownload, - AllowAddPiece: c.Storage.AllowAddPiece, - AllowPreCommit1: c.Storage.AllowPreCommit1, - AllowPreCommit2: c.Storage.AllowPreCommit2, - AllowCommit: c.Storage.AllowCommit, - AllowUnseal: c.Storage.AllowUnseal, - AllowReplicaUpdate: c.Storage.AllowReplicaUpdate, - AllowProveReplicaUpdate2: c.Storage.AllowProveReplicaUpdate2, - AllowRegenSectorKey: c.Storage.AllowRegenSectorKey, - ResourceFiltering: c.Storage.ResourceFiltering, - DisallowRemoteFinalize: c.Storage.DisallowRemoteFinalize, - - LocalWorkerName: c.Storage.LocalWorkerName, - - Assigner: c.Storage.Assigner, - - ParallelCheckLimit: c.Proving.ParallelCheckLimit, - DisableBuiltinWindowPoSt: c.Proving.DisableBuiltinWindowPoSt, - DisableBuiltinWinningPoSt: c.Proving.DisableBuiltinWinningPoSt, - } -} diff --git a/node/config/types.go b/node/config/types.go index dbfa2e43272..06aa7fd64a6 100644 --- a/node/config/types.go +++ b/node/config/types.go @@ -4,7 +4,6 @@ import ( "github.com/ipfs/go-cid" "github.com/filecoin-project/lotus/chain/types" - "github.com/filecoin-project/lotus/storage/sealer" ) // // NOTE: ONLY PUT STRUCT DEFINITIONS IN THIS FILE @@ -452,7 +451,7 @@ type SealerConfig struct { // ResourceFiltering instructs the system which resource filtering strategy // to use when evaluating tasks against this worker. An empty value defaults // to "hardware". - ResourceFiltering sealer.ResourceFilteringStrategy + ResourceFiltering ResourceFilteringStrategy } type BatchFeeConfig struct { diff --git a/node/modules/storageminer.go b/node/modules/storageminer.go index 4497271edde..94b7572efbf 100644 --- a/node/modules/storageminer.go +++ b/node/modules/storageminer.go @@ -794,17 +794,17 @@ func LocalStorage(mctx helpers.MetricsCtx, lc fx.Lifecycle, ls paths.LocalStorag return paths.NewLocal(ctx, ls, si, urls) } -func RemoteStorage(lstor *paths.Local, si paths.SectorIndex, sa sealer.StorageAuth, sc sealer.Config) *paths.Remote { +func RemoteStorage(lstor *paths.Local, si paths.SectorIndex, sa sealer.StorageAuth, sc config.SealerConfig) *paths.Remote { return paths.NewRemote(lstor, si, http.Header(sa), sc.ParallelFetchLimit, &paths.DefaultPartialFileHandler{}) } -func SectorStorage(mctx helpers.MetricsCtx, lc fx.Lifecycle, lstor *paths.Local, stor paths.Store, ls paths.LocalStorage, si paths.SectorIndex, sc sealer.Config, ds dtypes.MetadataDS) (*sealer.Manager, error) { +func SectorStorage(mctx helpers.MetricsCtx, lc fx.Lifecycle, lstor *paths.Local, stor paths.Store, ls paths.LocalStorage, si paths.SectorIndex, sc config.SealerConfig, pc config.ProvingConfig, ds dtypes.MetadataDS) (*sealer.Manager, error) { ctx := helpers.LifecycleCtx(mctx, lc) wsts := statestore.New(namespace.Wrap(ds, WorkerCallsPrefix)) smsts := statestore.New(namespace.Wrap(ds, ManagerWorkPrefix)) - sst, err := sealer.New(ctx, lstor, stor, ls, si, sc, wsts, smsts) + sst, err := sealer.New(ctx, lstor, stor, ls, si, sc, pc, wsts, smsts) if err != nil { return nil, err } diff --git a/storage/sealer/manager.go b/storage/sealer/manager.go index b0c023b09fb..38202286c0a 100644 --- a/storage/sealer/manager.go +++ b/storage/sealer/manager.go @@ -3,6 +3,7 @@ package sealer import ( "context" "errors" + "github.com/filecoin-project/lotus/node/config" "io" "net/http" "sort" @@ -90,57 +91,12 @@ type result struct { err error } -// ResourceFilteringStrategy is an enum indicating the kinds of resource -// filtering strategies that can be configured for workers. -type ResourceFilteringStrategy string - -const ( - // ResourceFilteringHardware specifies that available hardware resources - // should be evaluated when scheduling a task against the worker. - ResourceFilteringHardware = ResourceFilteringStrategy("hardware") - - // ResourceFilteringDisabled disables resource filtering against this - // worker. The scheduler may assign any task to this worker. - ResourceFilteringDisabled = ResourceFilteringStrategy("disabled") -) - -type Config struct { - ParallelFetchLimit int - - // Local worker config - AllowSectorDownload bool - AllowAddPiece bool - AllowPreCommit1 bool - AllowPreCommit2 bool - AllowCommit bool - AllowUnseal bool - AllowReplicaUpdate bool - AllowProveReplicaUpdate2 bool - AllowRegenSectorKey bool - - LocalWorkerName string - - // ResourceFiltering instructs the system which resource filtering strategy - // to use when evaluating tasks against this worker. An empty value defaults - // to "hardware". - ResourceFiltering ResourceFilteringStrategy - - // PoSt config - ParallelCheckLimit int - DisableBuiltinWindowPoSt bool - DisableBuiltinWinningPoSt bool - - DisallowRemoteFinalize bool - - Assigner string -} - type StorageAuth http.Header type WorkerStateStore *statestore.StateStore type ManagerStateStore *statestore.StateStore -func New(ctx context.Context, lstor *paths.Local, stor paths.Store, ls paths.LocalStorage, si paths.SectorIndex, sc Config, wss WorkerStateStore, mss ManagerStateStore) (*Manager, error) { +func New(ctx context.Context, lstor *paths.Local, stor paths.Store, ls paths.LocalStorage, si paths.SectorIndex, sc config.SealerConfig, pc config.ProvingConfig, wss WorkerStateStore, mss ManagerStateStore) (*Manager, error) { prover, err := ffiwrapper.New(&readonlyProvider{stor: lstor, index: si}) if err != nil { return nil, xerrors.Errorf("creating prover instance: %w", err) @@ -164,9 +120,9 @@ func New(ctx context.Context, lstor *paths.Local, stor paths.Store, ls paths.Loc localProver: prover, - parallelCheckLimit: sc.ParallelCheckLimit, - disableBuiltinWindowPoSt: sc.DisableBuiltinWindowPoSt, - disableBuiltinWinningPoSt: sc.DisableBuiltinWinningPoSt, + parallelCheckLimit: pc.ParallelCheckLimit, + disableBuiltinWindowPoSt: pc.DisableBuiltinWindowPoSt, + disableBuiltinWinningPoSt: pc.DisableBuiltinWinningPoSt, disallowRemoteFinalize: sc.DisallowRemoteFinalize, work: mss, @@ -212,7 +168,7 @@ func New(ctx context.Context, lstor *paths.Local, stor paths.Store, ls paths.Loc } wcfg := WorkerConfig{ - IgnoreResourceFiltering: sc.ResourceFiltering == ResourceFilteringDisabled, + IgnoreResourceFiltering: sc.ResourceFiltering == config.ResourceFilteringDisabled, TaskTypes: localTasks, Name: sc.LocalWorkerName, } From ec89424c4251656e55620f7345f3e655d713f8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Tue, 1 Nov 2022 11:01:31 +0000 Subject: [PATCH 08/13] make repo not depend on ffi --- cmd/lotus-miner/init.go | 15 ++--- cmd/lotus-miner/init_restore.go | 14 ++--- cmd/lotus-miner/init_service.go | 4 +- cmd/lotus-miner/storage.go | 3 +- cmd/lotus-seed/seed/seed.go | 3 +- cmd/lotus-worker/main.go | 8 +-- cmd/lotus-worker/sealworker/rpc.go | 8 +-- cmd/lotus-worker/storage.go | 3 +- itests/kit/ensemble.go | 10 ++-- itests/kit/node_miner.go | 5 +- itests/kit/node_worker.go | 5 +- itests/path_detach_redeclare_test.go | 7 +-- itests/path_type_filters_test.go | 25 ++++---- itests/sector_finalize_early_test.go | 6 +- node/config/doc_gen.go | 2 +- node/config/storage.go | 10 ++-- node/repo/fsrepo.go | 12 ++-- node/repo/interface.go | 6 +- node/repo/memrepo.go | 17 +++--- storage/paths/local.go | 66 ++-------------------- storage/paths/local_test.go | 8 +-- storage/paths/localstorage_cached.go | 5 +- storage/paths/remote_test.go | 6 +- storage/sealer/manager.go | 10 ++-- storage/sealer/manager_test.go | 14 ++--- storage/sealer/storiface/storage.go | 58 +++++++++++++++++++ testplans/lotus-soup/testkit/role_miner.go | 9 ++- 27 files changed, 166 insertions(+), 173 deletions(-) diff --git a/cmd/lotus-miner/init.go b/cmd/lotus-miner/init.go index 2d094b55a78..66a6691af18 100644 --- a/cmd/lotus-miner/init.go +++ b/cmd/lotus-miner/init.go @@ -49,6 +49,7 @@ import ( "github.com/filecoin-project/lotus/journal" "github.com/filecoin-project/lotus/journal/fsjournal" storageminer "github.com/filecoin-project/lotus/miner" + "github.com/filecoin-project/lotus/node/config" "github.com/filecoin-project/lotus/node/modules" "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/node/repo" @@ -218,7 +219,7 @@ var initCmd = &cli.Command{ return err } - var localPaths []paths.LocalPath + var localPaths []storiface.LocalPath if pssb := cctx.StringSlice("pre-sealed-sectors"); len(pssb) != 0 { log.Infof("Setting up storage config with presealed sectors: %v", pssb) @@ -228,14 +229,14 @@ var initCmd = &cli.Command{ if err != nil { return err } - localPaths = append(localPaths, paths.LocalPath{ + localPaths = append(localPaths, storiface.LocalPath{ Path: psp, }) } } if !cctx.Bool("no-local-storage") { - b, err := json.MarshalIndent(&paths.LocalStorageMeta{ + b, err := json.MarshalIndent(&storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 10, CanSeal: true, @@ -249,12 +250,12 @@ var initCmd = &cli.Command{ return xerrors.Errorf("persisting storage metadata (%s): %w", filepath.Join(lr.Path(), "sectorstore.json"), err) } - localPaths = append(localPaths, paths.LocalPath{ + localPaths = append(localPaths, storiface.LocalPath{ Path: lr.Path(), }) } - if err := lr.SetStorage(func(sc *paths.StorageConfig) { + if err := lr.SetStorage(func(sc *storiface.StorageConfig) { sc.StoragePaths = append(sc.StoragePaths, localPaths...) }); err != nil { return xerrors.Errorf("set storage config: %w", err) @@ -471,7 +472,7 @@ func storageMinerInit(ctx context.Context, cctx *cli.Context, api v1api.FullNode } stor := paths.NewRemote(lstor, si, http.Header(sa), 10, &paths.DefaultPartialFileHandler{}) - smgr, err := sealer.New(ctx, lstor, stor, lr, si, sealer.Config{ + smgr, err := sealer.New(ctx, lstor, stor, lr, si, config.SealerConfig{ ParallelFetchLimit: 10, AllowAddPiece: true, AllowPreCommit1: true, @@ -481,7 +482,7 @@ func storageMinerInit(ctx context.Context, cctx *cli.Context, api v1api.FullNode AllowReplicaUpdate: true, AllowProveReplicaUpdate2: true, AllowRegenSectorKey: true, - }, wsts, smsts) + }, config.ProvingConfig{}, wsts, smsts) if err != nil { return err } diff --git a/cmd/lotus-miner/init_restore.go b/cmd/lotus-miner/init_restore.go index 3d179f3ba68..618825a27b5 100644 --- a/cmd/lotus-miner/init_restore.go +++ b/cmd/lotus-miner/init_restore.go @@ -27,7 +27,7 @@ import ( "github.com/filecoin-project/lotus/lib/backupds" "github.com/filecoin-project/lotus/node/config" "github.com/filecoin-project/lotus/node/repo" - "github.com/filecoin-project/lotus/storage/paths" + "github.com/filecoin-project/lotus/storage/sealer/storiface" ) var restoreCmd = &cli.Command{ @@ -52,7 +52,7 @@ var restoreCmd = &cli.Command{ ctx := lcli.ReqContext(cctx) log.Info("Initializing lotus miner using a backup") - var storageCfg *paths.StorageConfig + var storageCfg *storiface.StorageConfig if cctx.IsSet("storage-config") { cf, err := homedir.Expand(cctx.String("storage-config")) if err != nil { @@ -64,7 +64,7 @@ var restoreCmd = &cli.Command{ return xerrors.Errorf("reading storage config: %w", err) } - storageCfg = &paths.StorageConfig{} + storageCfg = &storiface.StorageConfig{} err = json.Unmarshal(cfb, storageCfg) if err != nil { return xerrors.Errorf("cannot unmarshal json for storage config: %w", err) @@ -95,7 +95,7 @@ var restoreCmd = &cli.Command{ }, } -func restore(ctx context.Context, cctx *cli.Context, targetPath string, strConfig *paths.StorageConfig, manageConfig func(*config.StorageMiner) error, after func(api lapi.FullNode, addr address.Address, peerid peer.ID, mi api.MinerInfo) error) error { +func restore(ctx context.Context, cctx *cli.Context, targetPath string, strConfig *storiface.StorageConfig, manageConfig func(*config.StorageMiner) error, after func(api lapi.FullNode, addr address.Address, peerid peer.ID, mi api.MinerInfo) error) error { if cctx.NArg() != 1 { return lcli.IncorrectNumArgs(cctx) } @@ -214,7 +214,7 @@ func restore(ctx context.Context, cctx *cli.Context, targetPath string, strConfi if strConfig != nil { log.Info("Restoring storage path config") - err = lr.SetStorage(func(scfg *paths.StorageConfig) { + err = lr.SetStorage(func(scfg *storiface.StorageConfig) { *scfg = *strConfig }) if err != nil { @@ -223,8 +223,8 @@ func restore(ctx context.Context, cctx *cli.Context, targetPath string, strConfi } else { log.Warn("--storage-config NOT SET. NO SECTOR PATHS WILL BE CONFIGURED") // setting empty config to allow miner to be started - if err := lr.SetStorage(func(sc *paths.StorageConfig) { - sc.StoragePaths = append(sc.StoragePaths, paths.LocalPath{}) + if err := lr.SetStorage(func(sc *storiface.StorageConfig) { + sc.StoragePaths = append(sc.StoragePaths, storiface.LocalPath{}) }); err != nil { return xerrors.Errorf("set storage config: %w", err) } diff --git a/cmd/lotus-miner/init_service.go b/cmd/lotus-miner/init_service.go index 41838965a0c..235e4e4c8cc 100644 --- a/cmd/lotus-miner/init_service.go +++ b/cmd/lotus-miner/init_service.go @@ -17,7 +17,7 @@ import ( lcli "github.com/filecoin-project/lotus/cli" cliutil "github.com/filecoin-project/lotus/cli/util" "github.com/filecoin-project/lotus/node/config" - "github.com/filecoin-project/lotus/storage/paths" + "github.com/filecoin-project/lotus/storage/sealer/storiface" ) const ( @@ -78,7 +78,7 @@ var serviceCmd = &cli.Command{ return xerrors.Errorf("please provide Lotus markets repo path via flag %s", FlagMarketsRepo) } - if err := restore(ctx, cctx, repoPath, &paths.StorageConfig{}, func(cfg *config.StorageMiner) error { + if err := restore(ctx, cctx, repoPath, &storiface.StorageConfig{}, func(cfg *config.StorageMiner) error { cfg.Subsystems.EnableMarkets = es.Contains(MarketsService) cfg.Subsystems.EnableMining = false cfg.Subsystems.EnableSealing = false diff --git a/cmd/lotus-miner/storage.go b/cmd/lotus-miner/storage.go index 290d128e4f0..b5bfb730daf 100644 --- a/cmd/lotus-miner/storage.go +++ b/cmd/lotus-miner/storage.go @@ -29,7 +29,6 @@ import ( "github.com/filecoin-project/lotus/chain/types" lcli "github.com/filecoin-project/lotus/cli" "github.com/filecoin-project/lotus/lib/tablewriter" - "github.com/filecoin-project/lotus/storage/paths" sealing "github.com/filecoin-project/lotus/storage/pipeline" "github.com/filecoin-project/lotus/storage/sealer/fsutil" "github.com/filecoin-project/lotus/storage/sealer/storiface" @@ -148,7 +147,7 @@ over time } } - cfg := &paths.LocalStorageMeta{ + cfg := &storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: cctx.Uint64("weight"), CanSeal: cctx.Bool("seal"), diff --git a/cmd/lotus-seed/seed/seed.go b/cmd/lotus-seed/seed/seed.go index 70ee77921fc..3b6359e0f5f 100644 --- a/cmd/lotus-seed/seed/seed.go +++ b/cmd/lotus-seed/seed/seed.go @@ -27,7 +27,6 @@ import ( "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/chain/wallet/key" "github.com/filecoin-project/lotus/genesis" - "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/ffiwrapper" "github.com/filecoin-project/lotus/storage/sealer/ffiwrapper/basicfs" "github.com/filecoin-project/lotus/storage/sealer/storiface" @@ -126,7 +125,7 @@ func PreSeal(maddr address.Address, spt abi.RegisteredSealProof, offset abi.Sect } { - b, err := json.MarshalIndent(&paths.LocalStorageMeta{ + b, err := json.MarshalIndent(&storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 0, // read-only CanSeal: false, diff --git a/cmd/lotus-worker/main.go b/cmd/lotus-worker/main.go index 8b8db5aa738..afee6f1e13f 100644 --- a/cmd/lotus-worker/main.go +++ b/cmd/lotus-worker/main.go @@ -447,10 +447,10 @@ var runCmd = &cli.Command{ return err } - var localPaths []paths.LocalPath + var localPaths []storiface.LocalPath if !cctx.Bool("no-local-storage") { - b, err := json.MarshalIndent(&paths.LocalStorageMeta{ + b, err := json.MarshalIndent(&storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 10, CanSeal: true, @@ -464,12 +464,12 @@ var runCmd = &cli.Command{ return xerrors.Errorf("persisting storage metadata (%s): %w", filepath.Join(lr.Path(), "sectorstore.json"), err) } - localPaths = append(localPaths, paths.LocalPath{ + localPaths = append(localPaths, storiface.LocalPath{ Path: lr.Path(), }) } - if err := lr.SetStorage(func(sc *paths.StorageConfig) { + if err := lr.SetStorage(func(sc *storiface.StorageConfig) { sc.StoragePaths = append(sc.StoragePaths, localPaths...) }); err != nil { return xerrors.Errorf("set storage config: %w", err) diff --git a/cmd/lotus-worker/sealworker/rpc.go b/cmd/lotus-worker/sealworker/rpc.go index 7d84b5c8b0c..97f78942e59 100644 --- a/cmd/lotus-worker/sealworker/rpc.go +++ b/cmd/lotus-worker/sealworker/rpc.go @@ -92,8 +92,8 @@ func (w *Worker) StorageAddLocal(ctx context.Context, path string) error { return xerrors.Errorf("opening local path: %w", err) } - if err := w.Storage.SetStorage(func(sc *paths.StorageConfig) { - sc.StoragePaths = append(sc.StoragePaths, paths.LocalPath{Path: path}) + if err := w.Storage.SetStorage(func(sc *storiface.StorageConfig) { + sc.StoragePaths = append(sc.StoragePaths, storiface.LocalPath{Path: path}) }); err != nil { return xerrors.Errorf("get storage config: %w", err) } @@ -127,8 +127,8 @@ func (w *Worker) StorageDetachLocal(ctx context.Context, path string) error { // drop from the persisted storage.json var found bool - if err := w.Storage.SetStorage(func(sc *paths.StorageConfig) { - out := make([]paths.LocalPath, 0, len(sc.StoragePaths)) + if err := w.Storage.SetStorage(func(sc *storiface.StorageConfig) { + out := make([]storiface.LocalPath, 0, len(sc.StoragePaths)) for _, storagePath := range sc.StoragePaths { if storagePath.Path != path { out = append(out, storagePath) diff --git a/cmd/lotus-worker/storage.go b/cmd/lotus-worker/storage.go index 0736ffbfb88..6b5994c172f 100644 --- a/cmd/lotus-worker/storage.go +++ b/cmd/lotus-worker/storage.go @@ -13,7 +13,6 @@ import ( "golang.org/x/xerrors" lcli "github.com/filecoin-project/lotus/cli" - "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -103,7 +102,7 @@ var storageAttachCmd = &cli.Command{ } } - cfg := &paths.LocalStorageMeta{ + cfg := &storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: cctx.Uint64("weight"), CanSeal: cctx.Bool("seal"), diff --git a/itests/kit/ensemble.go b/itests/kit/ensemble.go index 0734b1f70f1..107e7faa923 100644 --- a/itests/kit/ensemble.go +++ b/itests/kit/ensemble.go @@ -586,11 +586,11 @@ func (n *Ensemble) Start() *Ensemble { psd := m.PresealDir noPaths := m.options.noStorage - err := lr.SetStorage(func(sc *paths.StorageConfig) { + err := lr.SetStorage(func(sc *storiface.StorageConfig) { if noPaths { - sc.StoragePaths = []paths.LocalPath{} + sc.StoragePaths = []storiface.LocalPath{} } - sc.StoragePaths = append(sc.StoragePaths, paths.LocalPath{Path: psd}) + sc.StoragePaths = append(sc.StoragePaths, storiface.LocalPath{Path: psd}) }) require.NoError(n.t, err) @@ -737,8 +737,8 @@ func (n *Ensemble) Start() *Ensemble { require.NoError(n.t, err) if m.options.noStorage { - err := lr.SetStorage(func(sc *paths.StorageConfig) { - sc.StoragePaths = []paths.LocalPath{} + err := lr.SetStorage(func(sc *storiface.StorageConfig) { + sc.StoragePaths = []storiface.LocalPath{} }) require.NoError(n.t, err) } diff --git a/itests/kit/node_miner.go b/itests/kit/node_miner.go index 83f6178f701..032cef87c5c 100644 --- a/itests/kit/node_miner.go +++ b/itests/kit/node_miner.go @@ -26,7 +26,6 @@ import ( "github.com/filecoin-project/lotus/build" "github.com/filecoin-project/lotus/chain/wallet/key" "github.com/filecoin-project/lotus/miner" - "github.com/filecoin-project/lotus/storage/paths" sealing "github.com/filecoin-project/lotus/storage/pipeline" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -175,7 +174,7 @@ func (tm *TestMiner) FlushSealingBatches(ctx context.Context) { const metaFile = "sectorstore.json" -func (tm *TestMiner) AddStorage(ctx context.Context, t *testing.T, conf func(*paths.LocalStorageMeta)) storiface.ID { +func (tm *TestMiner) AddStorage(ctx context.Context, t *testing.T, conf func(*storiface.LocalStorageMeta)) storiface.ID { p := t.TempDir() if err := os.MkdirAll(p, 0755); err != nil { @@ -189,7 +188,7 @@ func (tm *TestMiner) AddStorage(ctx context.Context, t *testing.T, conf func(*pa require.NoError(t, err) } - cfg := &paths.LocalStorageMeta{ + cfg := &storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 10, CanSeal: false, diff --git a/itests/kit/node_worker.go b/itests/kit/node_worker.go index 3a6a55c5515..ac200fb0fb2 100644 --- a/itests/kit/node_worker.go +++ b/itests/kit/node_worker.go @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/require" "github.com/filecoin-project/lotus/api" - "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -38,7 +37,7 @@ type TestWorker struct { options nodeOpts } -func (tm *TestWorker) AddStorage(ctx context.Context, t *testing.T, conf func(*paths.LocalStorageMeta)) storiface.ID { +func (tm *TestWorker) AddStorage(ctx context.Context, t *testing.T, conf func(*storiface.LocalStorageMeta)) storiface.ID { p := t.TempDir() if err := os.MkdirAll(p, 0755); err != nil { @@ -52,7 +51,7 @@ func (tm *TestWorker) AddStorage(ctx context.Context, t *testing.T, conf func(*p require.NoError(t, err) } - cfg := &paths.LocalStorageMeta{ + cfg := &storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 10, CanSeal: false, diff --git a/itests/path_detach_redeclare_test.go b/itests/path_detach_redeclare_test.go index 124266b7d94..24cabadfd67 100644 --- a/itests/path_detach_redeclare_test.go +++ b/itests/path_detach_redeclare_test.go @@ -15,7 +15,6 @@ import ( "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/itests/kit" - "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/sealtasks" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -74,7 +73,7 @@ func TestPathDetachRedeclare(t *testing.T) { checkSectors(ctx, t, client, miner, 2, 2) // attach a new path - newId := miner.AddStorage(ctx, t, func(cfg *paths.LocalStorageMeta) { + newId := miner.AddStorage(ctx, t, func(cfg *storiface.LocalStorageMeta) { cfg.CanStore = true }) @@ -194,7 +193,7 @@ func TestPathDetachRedeclareWorker(t *testing.T) { checkSectors(ctx, t, client, miner, 2, 2) // attach a new path - newId := sealw.AddStorage(ctx, t, func(cfg *paths.LocalStorageMeta) { + newId := sealw.AddStorage(ctx, t, func(cfg *storiface.LocalStorageMeta) { cfg.CanStore = true }) @@ -239,7 +238,7 @@ func TestPathDetachRedeclareWorker(t *testing.T) { require.Len(t, local, 0) // add a new one again, and move the sectors there - newId = sealw.AddStorage(ctx, t, func(cfg *paths.LocalStorageMeta) { + newId = sealw.AddStorage(ctx, t, func(cfg *storiface.LocalStorageMeta) { cfg.CanStore = true }) diff --git a/itests/path_type_filters_test.go b/itests/path_type_filters_test.go index 03dd5ea16f9..d41e2c2159a 100644 --- a/itests/path_type_filters_test.go +++ b/itests/path_type_filters_test.go @@ -10,7 +10,6 @@ import ( "github.com/stretchr/testify/require" "github.com/filecoin-project/lotus/itests/kit" - "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/sealtasks" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -45,7 +44,7 @@ func TestPathTypeFilters(t *testing.T) { } runTest(t, "invalid-type-alert", func(t *testing.T, ctx context.Context, miner *kit.TestMiner, run func()) { - slU := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + slU := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanSeal = true meta.AllowTypes = []string{"unsealed", "seeled"} }) @@ -79,18 +78,18 @@ func TestPathTypeFilters(t *testing.T) { runTest(t, "seal-to-stor-unseal-allowdeny", func(t *testing.T, ctx context.Context, miner *kit.TestMiner, run func()) { // allow all types in the sealing path - sealScratch := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + sealScratch := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanSeal = true }) // unsealed storage - unsStor := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + unsStor := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanStore = true meta.AllowTypes = []string{"unsealed"} }) // other storage - sealStor := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + sealStor := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanStore = true meta.DenyTypes = []string{"unsealed"} }) @@ -115,14 +114,14 @@ func TestPathTypeFilters(t *testing.T) { runTest(t, "sealstor-unseal-allowdeny", func(t *testing.T, ctx context.Context, miner *kit.TestMiner, run func()) { // unsealed storage - unsStor := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + unsStor := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanStore = true meta.CanSeal = true meta.AllowTypes = []string{"unsealed"} }) // other storage - sealStor := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + sealStor := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanStore = true meta.CanSeal = true meta.DenyTypes = []string{"unsealed"} @@ -147,29 +146,29 @@ func TestPathTypeFilters(t *testing.T) { runTest(t, "seal-store-allseparate", func(t *testing.T, ctx context.Context, miner *kit.TestMiner, run func()) { // sealing stores - slU := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + slU := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanSeal = true meta.AllowTypes = []string{"unsealed"} }) - slS := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + slS := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanSeal = true meta.AllowTypes = []string{"sealed"} }) - slC := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + slC := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanSeal = true meta.AllowTypes = []string{"cache"} }) // storage stores - stU := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + stU := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanStore = true meta.AllowTypes = []string{"unsealed"} }) - stS := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + stS := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanStore = true meta.AllowTypes = []string{"sealed"} }) - stC := miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + stC := miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.CanStore = true meta.AllowTypes = []string{"cache"} }) diff --git a/itests/sector_finalize_early_test.go b/itests/sector_finalize_early_test.go index 8678e6a282c..fb7d9d94d4c 100644 --- a/itests/sector_finalize_early_test.go +++ b/itests/sector_finalize_early_test.go @@ -11,7 +11,7 @@ import ( "github.com/filecoin-project/lotus/itests/kit" "github.com/filecoin-project/lotus/node/config" - "github.com/filecoin-project/lotus/storage/paths" + "github.com/filecoin-project/lotus/storage/sealer/storiface" ) func TestDealsWithFinalizeEarly(t *testing.T) { @@ -36,11 +36,11 @@ func TestDealsWithFinalizeEarly(t *testing.T) { ctx := context.Background() - miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.Weight = 1000000000 meta.CanSeal = true }) - miner.AddStorage(ctx, t, func(meta *paths.LocalStorageMeta) { + miner.AddStorage(ctx, t, func(meta *storiface.LocalStorageMeta) { meta.Weight = 1000000000 meta.CanStore = true }) diff --git a/node/config/doc_gen.go b/node/config/doc_gen.go index 902cb1a058e..b815e8f64bb 100644 --- a/node/config/doc_gen.go +++ b/node/config/doc_gen.go @@ -894,7 +894,7 @@ If you see stuck Finalize tasks after enabling this setting, check }, { Name: "ResourceFiltering", - Type: "sealer.ResourceFilteringStrategy", + Type: "ResourceFilteringStrategy", Comment: `ResourceFiltering instructs the system which resource filtering strategy to use when evaluating tasks against this worker. An empty value defaults diff --git a/node/config/storage.go b/node/config/storage.go index f4eac811c74..2c9d880f92b 100644 --- a/node/config/storage.go +++ b/node/config/storage.go @@ -8,10 +8,10 @@ import ( "golang.org/x/xerrors" - "github.com/filecoin-project/lotus/storage/paths" + "github.com/filecoin-project/lotus/storage/sealer/storiface" ) -func StorageFromFile(path string, def *paths.StorageConfig) (*paths.StorageConfig, error) { +func StorageFromFile(path string, def *storiface.StorageConfig) (*storiface.StorageConfig, error) { file, err := os.Open(path) switch { case os.IsNotExist(err): @@ -27,8 +27,8 @@ func StorageFromFile(path string, def *paths.StorageConfig) (*paths.StorageConfi return StorageFromReader(file) } -func StorageFromReader(reader io.Reader) (*paths.StorageConfig, error) { - var cfg paths.StorageConfig +func StorageFromReader(reader io.Reader) (*storiface.StorageConfig, error) { + var cfg storiface.StorageConfig err := json.NewDecoder(reader).Decode(&cfg) if err != nil { return nil, err @@ -37,7 +37,7 @@ func StorageFromReader(reader io.Reader) (*paths.StorageConfig, error) { return &cfg, nil } -func WriteStorageFile(path string, config paths.StorageConfig) error { +func WriteStorageFile(path string, config storiface.StorageConfig) error { b, err := json.MarshalIndent(config, "", " ") if err != nil { return xerrors.Errorf("marshaling storage config: %w", err) diff --git a/node/repo/fsrepo.go b/node/repo/fsrepo.go index 9327575dd5b..68550e38947 100644 --- a/node/repo/fsrepo.go +++ b/node/repo/fsrepo.go @@ -25,8 +25,8 @@ import ( badgerbs "github.com/filecoin-project/lotus/blockstore/badger" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/node/config" - "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/fsutil" + "github.com/filecoin-project/lotus/storage/sealer/storiface" ) const ( @@ -572,26 +572,26 @@ func (fsr *fsLockedRepo) SetConfig(c func(interface{})) error { return nil } -func (fsr *fsLockedRepo) GetStorage() (paths.StorageConfig, error) { +func (fsr *fsLockedRepo) GetStorage() (storiface.StorageConfig, error) { fsr.storageLk.Lock() defer fsr.storageLk.Unlock() return fsr.getStorage(nil) } -func (fsr *fsLockedRepo) getStorage(def *paths.StorageConfig) (paths.StorageConfig, error) { +func (fsr *fsLockedRepo) getStorage(def *storiface.StorageConfig) (storiface.StorageConfig, error) { c, err := config.StorageFromFile(fsr.join(fsStorageConfig), def) if err != nil { - return paths.StorageConfig{}, err + return storiface.StorageConfig{}, err } return *c, nil } -func (fsr *fsLockedRepo) SetStorage(c func(*paths.StorageConfig)) error { +func (fsr *fsLockedRepo) SetStorage(c func(*storiface.StorageConfig)) error { fsr.storageLk.Lock() defer fsr.storageLk.Unlock() - sc, err := fsr.getStorage(&paths.StorageConfig{}) + sc, err := fsr.getStorage(&storiface.StorageConfig{}) if err != nil { return xerrors.Errorf("get storage: %w", err) } diff --git a/node/repo/interface.go b/node/repo/interface.go index 4f029471375..dd083955956 100644 --- a/node/repo/interface.go +++ b/node/repo/interface.go @@ -9,8 +9,8 @@ import ( "github.com/filecoin-project/lotus/blockstore" "github.com/filecoin-project/lotus/chain/types" - "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/fsutil" + "github.com/filecoin-project/lotus/storage/sealer/storiface" ) // BlockstoreDomain represents the domain of a blockstore. @@ -73,8 +73,8 @@ type LockedRepo interface { Config() (interface{}, error) SetConfig(func(interface{})) error - GetStorage() (paths.StorageConfig, error) - SetStorage(func(*paths.StorageConfig)) error + GetStorage() (storiface.StorageConfig, error) + SetStorage(func(*storiface.StorageConfig)) error Stat(path string) (fsutil.FsStat, error) DiskUsage(path string) (int64, error) diff --git a/node/repo/memrepo.go b/node/repo/memrepo.go index 53fd1eeeeb3..61d960872c6 100644 --- a/node/repo/memrepo.go +++ b/node/repo/memrepo.go @@ -18,7 +18,6 @@ import ( "github.com/filecoin-project/lotus/blockstore" "github.com/filecoin-project/lotus/chain/types" "github.com/filecoin-project/lotus/node/config" - "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/fsutil" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -37,7 +36,7 @@ type MemRepo struct { keystore map[string]types.KeyInfo blockstore blockstore.Blockstore - sc *paths.StorageConfig + sc *storiface.StorageConfig tempDir string // holds the current config value @@ -59,13 +58,13 @@ func (lmem *lockedMemRepo) RepoType() RepoType { return lmem.t } -func (lmem *lockedMemRepo) GetStorage() (paths.StorageConfig, error) { +func (lmem *lockedMemRepo) GetStorage() (storiface.StorageConfig, error) { if err := lmem.checkToken(); err != nil { - return paths.StorageConfig{}, err + return storiface.StorageConfig{}, err } if lmem.mem.sc == nil { - lmem.mem.sc = &paths.StorageConfig{StoragePaths: []paths.LocalPath{ + lmem.mem.sc = &storiface.StorageConfig{StoragePaths: []storiface.LocalPath{ {Path: lmem.Path()}, }} } @@ -73,7 +72,7 @@ func (lmem *lockedMemRepo) GetStorage() (paths.StorageConfig, error) { return *lmem.mem.sc, nil } -func (lmem *lockedMemRepo) SetStorage(c func(*paths.StorageConfig)) error { +func (lmem *lockedMemRepo) SetStorage(c func(*storiface.StorageConfig)) error { if err := lmem.checkToken(); err != nil { return err } @@ -126,14 +125,14 @@ func (lmem *lockedMemRepo) Path() string { } func (lmem *lockedMemRepo) initSectorStore(t string) { - if err := config.WriteStorageFile(filepath.Join(t, fsStorageConfig), paths.StorageConfig{ - StoragePaths: []paths.LocalPath{ + if err := config.WriteStorageFile(filepath.Join(t, fsStorageConfig), storiface.StorageConfig{ + StoragePaths: []storiface.LocalPath{ {Path: t}, }}); err != nil { panic(err) } - b, err := json.MarshalIndent(&paths.LocalStorageMeta{ + b, err := json.MarshalIndent(&storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 10, CanSeal: true, diff --git a/storage/paths/local.go b/storage/paths/local.go index ec146ba5ac7..6cd0e557abb 100644 --- a/storage/paths/local.go +++ b/storage/paths/local.go @@ -21,67 +21,9 @@ import ( "github.com/filecoin-project/lotus/storage/sealer/storiface" ) -// LocalStorageMeta [path]/sectorstore.json -type LocalStorageMeta struct { - ID storiface.ID - - // A high weight means data is more likely to be stored in this path - Weight uint64 // 0 = readonly - - // Intermediate data for the sealing process will be stored here - CanSeal bool - - // Finalized sectors that will be proved over time will be stored here - CanStore bool - - // MaxStorage specifies the maximum number of bytes to use for sector storage - // (0 = unlimited) - MaxStorage uint64 - - // List of storage groups this path belongs to - Groups []string - - // List of storage groups to which data from this path can be moved. If none - // are specified, allow to all - AllowTo []string - - // AllowTypes lists sector file types which are allowed to be put into this - // path. If empty, all file types are allowed. - // - // Valid values: - // - "unsealed" - // - "sealed" - // - "cache" - // - "update" - // - "update-cache" - // Any other value will generate a warning and be ignored. - AllowTypes []string - - // DenyTypes lists sector file types which aren't allowed to be put into this - // path. - // - // Valid values: - // - "unsealed" - // - "sealed" - // - "cache" - // - "update" - // - "update-cache" - // Any other value will generate a warning and be ignored. - DenyTypes []string -} - -// StorageConfig .lotusstorage/storage.json -type StorageConfig struct { - StoragePaths []LocalPath -} - -type LocalPath struct { - Path string -} - type LocalStorage interface { - GetStorage() (StorageConfig, error) - SetStorage(func(*StorageConfig)) error + GetStorage() (storiface.StorageConfig, error) + SetStorage(func(*storiface.StorageConfig)) error Stat(path string) (fsutil.FsStat, error) @@ -213,7 +155,7 @@ func (st *Local) OpenPath(ctx context.Context, p string) error { return xerrors.Errorf("reading storage metadata for %s: %w", p, err) } - var meta LocalStorageMeta + var meta storiface.LocalStorageMeta if err := json.Unmarshal(mb, &meta); err != nil { return xerrors.Errorf("unmarshalling storage metadata for %s: %w", p, err) } @@ -309,7 +251,7 @@ func (st *Local) Redeclare(ctx context.Context, filterId *storiface.ID, dropMiss return xerrors.Errorf("reading storage metadata for %s: %w", p.local, err) } - var meta LocalStorageMeta + var meta storiface.LocalStorageMeta if err := json.Unmarshal(mb, &meta); err != nil { return xerrors.Errorf("unmarshalling storage metadata for %s: %w", p.local, err) } diff --git a/storage/paths/local_test.go b/storage/paths/local_test.go index 83e8e27fdb2..6b9f4a54519 100644 --- a/storage/paths/local_test.go +++ b/storage/paths/local_test.go @@ -19,18 +19,18 @@ const pathSize = 16 << 20 type TestingLocalStorage struct { root string - c StorageConfig + c storiface.StorageConfig } func (t *TestingLocalStorage) DiskUsage(path string) (int64, error) { return 1, nil } -func (t *TestingLocalStorage) GetStorage() (StorageConfig, error) { +func (t *TestingLocalStorage) GetStorage() (storiface.StorageConfig, error) { return t.c, nil } -func (t *TestingLocalStorage) SetStorage(f func(*StorageConfig)) error { +func (t *TestingLocalStorage) SetStorage(f func(*storiface.StorageConfig)) error { f(&t.c) return nil } @@ -51,7 +51,7 @@ func (t *TestingLocalStorage) init(subpath string) error { metaFile := filepath.Join(path, MetaFile) - meta := &LocalStorageMeta{ + meta := &storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 1, CanSeal: true, diff --git a/storage/paths/localstorage_cached.go b/storage/paths/localstorage_cached.go index 4ccabb15e99..cac0a44b66c 100644 --- a/storage/paths/localstorage_cached.go +++ b/storage/paths/localstorage_cached.go @@ -7,6 +7,7 @@ import ( lru "github.com/hashicorp/golang-lru" "github.com/filecoin-project/lotus/storage/sealer/fsutil" + "github.com/filecoin-project/lotus/storage/sealer/storiface" ) var StatTimeout = 5 * time.Second @@ -47,11 +48,11 @@ type diskUsageResult struct { time time.Time } -func (c *cachedLocalStorage) GetStorage() (StorageConfig, error) { +func (c *cachedLocalStorage) GetStorage() (storiface.StorageConfig, error) { return c.base.GetStorage() } -func (c *cachedLocalStorage) SetStorage(f func(*StorageConfig)) error { +func (c *cachedLocalStorage) SetStorage(f func(*storiface.StorageConfig)) error { return c.base.SetStorage(f) } diff --git a/storage/paths/remote_test.go b/storage/paths/remote_test.go index a7bd6bf4003..2d7fe2c73d9 100644 --- a/storage/paths/remote_test.go +++ b/storage/paths/remote_test.go @@ -38,7 +38,7 @@ func createTestStorage(t *testing.T, p string, seal bool, att ...*paths.Local) s } } - cfg := &paths.LocalStorageMeta{ + cfg := &storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 10, CanSeal: seal, @@ -77,8 +77,8 @@ func TestMoveShared(t *testing.T) { _ = lr.Close() }) - err = lr.SetStorage(func(config *paths.StorageConfig) { - *config = paths.StorageConfig{} + err = lr.SetStorage(func(config *storiface.StorageConfig) { + *config = storiface.StorageConfig{} }) require.NoError(t, err) diff --git a/storage/sealer/manager.go b/storage/sealer/manager.go index 38202286c0a..5a5c104b926 100644 --- a/storage/sealer/manager.go +++ b/storage/sealer/manager.go @@ -3,7 +3,6 @@ package sealer import ( "context" "errors" - "github.com/filecoin-project/lotus/node/config" "io" "net/http" "sort" @@ -20,6 +19,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-statestore" + "github.com/filecoin-project/lotus/node/config" "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/ffiwrapper" "github.com/filecoin-project/lotus/storage/sealer/fsutil" @@ -191,8 +191,8 @@ func (m *Manager) AddLocalStorage(ctx context.Context, path string) error { return xerrors.Errorf("opening local path: %w", err) } - if err := m.ls.SetStorage(func(sc *paths.StorageConfig) { - sc.StoragePaths = append(sc.StoragePaths, paths.LocalPath{Path: path}) + if err := m.ls.SetStorage(func(sc *storiface.StorageConfig) { + sc.StoragePaths = append(sc.StoragePaths, storiface.LocalPath{Path: path}) }); err != nil { return xerrors.Errorf("get storage config: %w", err) } @@ -225,8 +225,8 @@ func (m *Manager) DetachLocalStorage(ctx context.Context, path string) error { // drop from the persisted storage.json var found bool - if err := m.ls.SetStorage(func(sc *paths.StorageConfig) { - out := make([]paths.LocalPath, 0, len(sc.StoragePaths)) + if err := m.ls.SetStorage(func(sc *storiface.StorageConfig) { + out := make([]storiface.LocalPath, 0, len(sc.StoragePaths)) for _, storagePath := range sc.StoragePaths { if storagePath.Path != path { out = append(out, storagePath) diff --git a/storage/sealer/manager_test.go b/storage/sealer/manager_test.go index 739cfdd2418..5759d0bc7ec 100644 --- a/storage/sealer/manager_test.go +++ b/storage/sealer/manager_test.go @@ -39,7 +39,7 @@ func init() { logging.SetAllLoggers(logging.LevelDebug) } -type testStorage paths.StorageConfig +type testStorage storiface.StorageConfig func (t testStorage) DiskUsage(path string) (int64, error) { return 1, nil // close enough @@ -50,7 +50,7 @@ func newTestStorage(t *testing.T) *testStorage { require.NoError(t, err) { - b, err := json.MarshalIndent(&paths.LocalStorageMeta{ + b, err := json.MarshalIndent(&storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 1, CanSeal: true, @@ -63,7 +63,7 @@ func newTestStorage(t *testing.T) *testStorage { } return &testStorage{ - StoragePaths: []paths.LocalPath{ + StoragePaths: []storiface.LocalPath{ {Path: tp}, }, } @@ -82,12 +82,12 @@ func (t testStorage) cleanup() { } } -func (t testStorage) GetStorage() (paths.StorageConfig, error) { - return paths.StorageConfig(t), nil +func (t testStorage) GetStorage() (storiface.StorageConfig, error) { + return storiface.StorageConfig(t), nil } -func (t *testStorage) SetStorage(f func(*paths.StorageConfig)) error { - f((*paths.StorageConfig)(t)) +func (t *testStorage) SetStorage(f func(*storiface.StorageConfig)) error { + f((*storiface.StorageConfig)(t)) return nil } diff --git a/storage/sealer/storiface/storage.go b/storage/sealer/storiface/storage.go index 6d6063c54e2..a5aa8907a1f 100644 --- a/storage/sealer/storiface/storage.go +++ b/storage/sealer/storiface/storage.go @@ -153,3 +153,61 @@ type SecDataHttpHeader struct { Key string Value string } + +// StorageConfig .lotusstorage/storage.json +type StorageConfig struct { + StoragePaths []LocalPath +} + +type LocalPath struct { + Path string +} + +// LocalStorageMeta [path]/sectorstore.json +type LocalStorageMeta struct { + ID ID + + // A high weight means data is more likely to be stored in this path + Weight uint64 // 0 = readonly + + // Intermediate data for the sealing process will be stored here + CanSeal bool + + // Finalized sectors that will be proved over time will be stored here + CanStore bool + + // MaxStorage specifies the maximum number of bytes to use for sector storage + // (0 = unlimited) + MaxStorage uint64 + + // List of storage groups this path belongs to + Groups []string + + // List of storage groups to which data from this path can be moved. If none + // are specified, allow to all + AllowTo []string + + // AllowTypes lists sector file types which are allowed to be put into this + // path. If empty, all file types are allowed. + // + // Valid values: + // - "unsealed" + // - "sealed" + // - "cache" + // - "update" + // - "update-cache" + // Any other value will generate a warning and be ignored. + AllowTypes []string + + // DenyTypes lists sector file types which aren't allowed to be put into this + // path. + // + // Valid values: + // - "unsealed" + // - "sealed" + // - "cache" + // - "update" + // - "update-cache" + // Any other value will generate a warning and be ignored. + DenyTypes []string +} diff --git a/testplans/lotus-soup/testkit/role_miner.go b/testplans/lotus-soup/testkit/role_miner.go index 1a3319add24..3b74a50cb2e 100644 --- a/testplans/lotus-soup/testkit/role_miner.go +++ b/testplans/lotus-soup/testkit/role_miner.go @@ -40,7 +40,6 @@ import ( "github.com/filecoin-project/lotus/node/config" "github.com/filecoin-project/lotus/node/impl" "github.com/filecoin-project/lotus/node/repo" - "github.com/filecoin-project/lotus/storage/paths" sealing "github.com/filecoin-project/lotus/storage/pipeline" "github.com/filecoin-project/lotus/storage/sealer/storiface" ) @@ -198,9 +197,9 @@ func PrepareMiner(t *TestEnvironment) (*LotusMiner, error) { } } - var localPaths []paths.LocalPath + var localPaths []storiface.LocalPath - b, err := json.MarshalIndent(&paths.LocalStorageMeta{ + b, err := json.MarshalIndent(&storiface.LocalStorageMeta{ ID: storiface.ID(uuid.New().String()), Weight: 10, CanSeal: true, @@ -214,11 +213,11 @@ func PrepareMiner(t *TestEnvironment) (*LotusMiner, error) { return nil, fmt.Errorf("persisting storage metadata (%s): %w", filepath.Join(lr.Path(), "sectorstore.json"), err) } - localPaths = append(localPaths, paths.LocalPath{ + localPaths = append(localPaths, storiface.LocalPath{ Path: lr.Path(), }) - if err := lr.SetStorage(func(sc *paths.StorageConfig) { + if err := lr.SetStorage(func(sc *storiface.StorageConfig) { sc.StoragePaths = append(sc.StoragePaths, localPaths...) }); err != nil { return nil, err From 1513aab4c8eec3831b0dc8309fb968e115977159 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Fri, 4 Nov 2022 16:59:38 +0000 Subject: [PATCH 09/13] netbs: Add more missing locks --- markets/retrievaladapter/client_blockstore.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/markets/retrievaladapter/client_blockstore.go b/markets/retrievaladapter/client_blockstore.go index de56b36418c..212b47758af 100644 --- a/markets/retrievaladapter/client_blockstore.go +++ b/markets/retrievaladapter/client_blockstore.go @@ -103,6 +103,9 @@ func (a *APIBlockstoreAccessor) RegisterApiStore(sid api.RemoteStoreID, st *lbst a.remoteStores[sid] = st st.OnClose(func() { + a.accessLk.Lock() + defer a.accessLk.Unlock() + if _, has := a.remoteStores[sid]; has { delete(a.remoteStores, sid) } From fcad93dc10cb38ec382907f02e6d9527263b1c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Sun, 6 Nov 2022 16:38:25 +0000 Subject: [PATCH 10/13] netbs: Fix lint --- documentation/en/default-lotus-miner-config.toml | 2 +- itests/kit/ensemble.go | 4 ++-- itests/wdpost_worker_config_test.go | 8 ++++---- itests/worker_test.go | 4 ++-- storage/sealer/piece_provider_test.go | 9 +++++---- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/documentation/en/default-lotus-miner-config.toml b/documentation/en/default-lotus-miner-config.toml index b51e325e885..c18ed86fe6a 100644 --- a/documentation/en/default-lotus-miner-config.toml +++ b/documentation/en/default-lotus-miner-config.toml @@ -694,7 +694,7 @@ # to use when evaluating tasks against this worker. An empty value defaults # to "hardware". # - # type: sealer.ResourceFilteringStrategy + # type: ResourceFilteringStrategy # env var: LOTUS_STORAGE_RESOURCEFILTERING #ResourceFiltering = "hardware" diff --git a/itests/kit/ensemble.go b/itests/kit/ensemble.go index 107e7faa923..e08ccb0a9a1 100644 --- a/itests/kit/ensemble.go +++ b/itests/kit/ensemble.go @@ -632,7 +632,7 @@ func (n *Ensemble) Start() *Ensemble { // disable resource filtering so that local worker gets assigned tasks // regardless of system pressure. - node.Override(new(config.SealingConfig), func() config.SealingConfig { + node.Override(new(config.SealerConfig), func() config.SealerConfig { scfg := config.DefaultStorageMiner() if noLocal { @@ -646,7 +646,7 @@ func (n *Ensemble) Start() *Ensemble { scfg.Storage.Assigner = assigner scfg.Storage.DisallowRemoteFinalize = disallowRemoteFinalize scfg.Storage.ResourceFiltering = config.ResourceFilteringDisabled - return scfg.Sealing + return scfg.Storage }), // upgrades diff --git a/itests/wdpost_worker_config_test.go b/itests/wdpost_worker_config_test.go index 5172335a81b..d1672c20f08 100644 --- a/itests/wdpost_worker_config_test.go +++ b/itests/wdpost_worker_config_test.go @@ -34,10 +34,10 @@ func TestWindowPostNoBuiltinWindow(t *testing.T) { kit.PresealSectors(sectors), // 2 sectors per partition, 2 partitions in all 48 deadlines kit.LatestActorsAt(-1), kit.ConstructorOpts( - node.Override(new(*config.ProvingConfig), func() *config.ProvingConfig { + node.Override(new(config.ProvingConfig), func() config.ProvingConfig { c := config.DefaultStorageMiner() c.Proving.DisableBuiltinWindowPoSt = true - return &c.Proving + return c.Proving }), node.Override(new(*wdpost.WindowPoStScheduler), modules.WindowPostScheduler( config.DefaultStorageMiner().Fees, @@ -91,10 +91,10 @@ func TestWindowPostNoBuiltinWindowWithWorker(t *testing.T) { kit.PresealSectors(sectors), // 2 sectors per partition, 2 partitions in all 48 deadlines kit.LatestActorsAt(-1), kit.ConstructorOpts( - node.Override(new(*config.ProvingConfig), func() *config.ProvingConfig { + node.Override(new(config.ProvingConfig), func() config.ProvingConfig { c := config.DefaultStorageMiner() c.Proving.DisableBuiltinWindowPoSt = true - return &c.Proving + return c.Proving }), node.Override(new(*wdpost.WindowPoStScheduler), modules.WindowPostScheduler( config.DefaultStorageMiner().Fees, diff --git a/itests/worker_test.go b/itests/worker_test.go index 5b26f481c68..2e372288481 100644 --- a/itests/worker_test.go +++ b/itests/worker_test.go @@ -408,10 +408,10 @@ func TestWindowPostWorkerManualPoSt(t *testing.T) { func TestSchedulerRemoveRequest(t *testing.T) { ctx := context.Background() - _, miner, worker, ens := kit.EnsembleWorker(t, kit.WithAllSubsystems(), kit.ThroughRPC(), kit.WithNoLocalSealing(true), + _, miner, worker, _ := kit.EnsembleWorker(t, kit.WithAllSubsystems(), kit.ThroughRPC(), kit.WithNoLocalSealing(true), kit.WithTaskTypes([]sealtasks.TaskType{sealtasks.TTFetch, sealtasks.TTCommit1, sealtasks.TTFinalize, sealtasks.TTDataCid, sealtasks.TTAddPiece, sealtasks.TTPreCommit1, sealtasks.TTCommit2, sealtasks.TTUnseal})) // no mock proofs - ens.InterconnectAll().BeginMining(50 * time.Millisecond) + //ens.InterconnectAll().BeginMining(50 * time.Millisecond) e, err := worker.Enabled(ctx) require.NoError(t, err) diff --git a/storage/sealer/piece_provider_test.go b/storage/sealer/piece_provider_test.go index c4c71bc5308..3605b2597f4 100644 --- a/storage/sealer/piece_provider_test.go +++ b/storage/sealer/piece_provider_test.go @@ -21,6 +21,7 @@ import ( "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-statestore" + "github.com/filecoin-project/lotus/node/config" "github.com/filecoin-project/lotus/storage/paths" "github.com/filecoin-project/lotus/storage/sealer/sealtasks" "github.com/filecoin-project/lotus/storage/sealer/storiface" @@ -30,7 +31,7 @@ import ( // only uses miner and does NOT use any remote worker. func TestPieceProviderSimpleNoRemoteWorker(t *testing.T) { // Set up sector storage manager - sealerCfg := Config{ + sealerCfg := config.SealerConfig{ ParallelFetchLimit: 10, AllowAddPiece: true, AllowPreCommit1: true, @@ -89,7 +90,7 @@ func TestReadPieceRemoteWorkers(t *testing.T) { logging.SetAllLoggers(logging.LevelDebug) // miner's worker can only add pieces to an unsealed sector. - sealerCfg := Config{ + sealerCfg := config.SealerConfig{ ParallelFetchLimit: 10, AllowAddPiece: true, AllowPreCommit1: false, @@ -198,7 +199,7 @@ func generatePieceData(size uint64) []byte { return bz } -func newPieceProviderTestHarness(t *testing.T, mgrConfig Config, sectorProofType abi.RegisteredSealProof) *pieceProviderTestHarness { +func newPieceProviderTestHarness(t *testing.T, mgrConfig config.SealerConfig, sectorProofType abi.RegisteredSealProof) *pieceProviderTestHarness { ctx := context.Background() // listen on tcp socket to create an http server later address := "0.0.0.0:0" @@ -217,7 +218,7 @@ func newPieceProviderTestHarness(t *testing.T, mgrConfig Config, sectorProofType wsts := statestore.New(namespace.Wrap(dstore, datastore.NewKey("/worker/calls"))) smsts := statestore.New(namespace.Wrap(dstore, datastore.NewKey("/stmgr/calls"))) - mgr, err := New(ctx, localStore, remoteStore, storage, index, mgrConfig, wsts, smsts) + mgr, err := New(ctx, localStore, remoteStore, storage, index, mgrConfig, config.ProvingConfig{}, wsts, smsts) require.NoError(t, err) // start a http server on the manager to serve sector file requests. From 401359646aba6b8312c4c141611b5cfaaac1bcd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Sun, 6 Nov 2022 20:00:52 +0000 Subject: [PATCH 11/13] netbs: Address review --- blockstore/net.go | 41 +++++++++++-------- blockstore/net_serve.go | 1 + markets/retrievaladapter/client_blockstore.go | 2 +- node/impl/client/client.go | 3 +- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/blockstore/net.go b/blockstore/net.go index fa8b24591fb..c627cae09aa 100644 --- a/blockstore/net.go +++ b/blockstore/net.go @@ -19,12 +19,12 @@ import ( type NetRPCReqType byte const ( - NRpcHas NetRPCReqType = iota - NRpcGet NetRPCReqType = iota - NRpcGetSize NetRPCReqType = iota - NRpcPut NetRPCReqType = iota - NRpcDelete NetRPCReqType = iota - NRpcList NetRPCReqType = iota + NRpcHas NetRPCReqType = iota + NRpcGet + NRpcGetSize + NRpcPut + NRpcDelete + NRpcList // todo cancel req ) @@ -32,16 +32,16 @@ const ( type NetRPCRespType byte const ( - NRpcOK NetRPCRespType = iota - NRpcErr NetRPCRespType = iota - NRpcMore NetRPCRespType = iota + NRpcOK NetRPCRespType = iota + NRpcErr + NRpcMore ) type NetRPCErrType byte const ( - NRpcErrGeneric NetRPCErrType = iota - NRpcErrNotFound NetRPCErrType = iota + NRpcErrGeneric NetRPCErrType = iota + NRpcErrNotFound ) type NetRpcReq struct { @@ -87,7 +87,7 @@ type NetworkStore struct { closed chan struct{} closeLk sync.Mutex - onClose func() + onClose []func() } func NewNetworkStore(mss msgio.ReadWriteCloser) *NetworkStore { @@ -143,7 +143,7 @@ func (n *NetworkStore) OnClose(cb func()) { case <-n.closed: cb() default: - n.onClose = cb + n.onClose = append(n.onClose, cb) } } @@ -154,7 +154,9 @@ func (n *NetworkStore) receive() { close(n.closed) if n.onClose != nil { - n.onClose() + for _, f := range n.onClose { + f() + } } }() @@ -203,6 +205,7 @@ func (n *NetworkStore) sendRpc(rt NetRPCReqType, cids []cid.Cid, data [][]byte) n.respLk.Lock() if n.respMap == nil { + n.respLk.Unlock() return 0, nil, xerrors.Errorf("netstore closed") } n.respMap[rid] = respCh @@ -218,22 +221,24 @@ func (n *NetworkStore) sendRpc(rt NetRPCReqType, cids []cid.Cid, data [][]byte) var rbuf bytes.Buffer // todo buffer pool if err := req.MarshalCBOR(&rbuf); err != nil { n.respLk.Lock() + defer n.respLk.Unlock() + if n.respMap == nil { return 0, nil, xerrors.Errorf("netstore closed") } delete(n.respMap, rid) - n.respLk.Unlock() return 0, nil, err } if err := n.msgStream.WriteMsg(rbuf.Bytes()); err != nil { n.respLk.Lock() + defer n.respLk.Unlock() + if n.respMap == nil { return 0, nil, xerrors.Errorf("netstore closed") } delete(n.respMap, rid) - n.respLk.Unlock() return 0, nil, err } @@ -260,10 +265,10 @@ func (n *NetworkStore) waitResp(ctx context.Context, rch <-chan NetRpcResp, rid } else { err = xerrors.Errorf("block not found, but cid was null") } - default: - err = xerrors.Errorf("unknown error type") case NRpcErrGeneric: err = xerrors.Errorf("generic error") + default: + err = xerrors.Errorf("unknown error type") } return NetRpcResp{}, xerrors.Errorf("netstore error response: %s (%w)", e.Msg, err) diff --git a/blockstore/net_serve.go b/blockstore/net_serve.go index 25226c5c503..f2a63f3813b 100644 --- a/blockstore/net_serve.go +++ b/blockstore/net_serve.go @@ -19,6 +19,7 @@ type NetworkStoreHandler struct { bs Blockstore } +// NOTE: This code isn't yet hardened to accept untrusted input. See TODOs here and in net.go func HandleNetBstoreStream(ctx context.Context, bs Blockstore, mss msgio.ReadWriteCloser) *NetworkStoreHandler { ns := &NetworkStoreHandler{ msgStream: mss, diff --git a/markets/retrievaladapter/client_blockstore.go b/markets/retrievaladapter/client_blockstore.go index 212b47758af..37a80013aec 100644 --- a/markets/retrievaladapter/client_blockstore.go +++ b/markets/retrievaladapter/client_blockstore.go @@ -77,7 +77,7 @@ func (a *APIBlockstoreAccessor) Done(id retrievalmarket.DealID) error { return a.sub.Done(id) } -func (a *APIBlockstoreAccessor) UseRetrievalStore(id retrievalmarket.DealID, sid api.RemoteStoreID) error { +func (a *APIBlockstoreAccessor) RegisterDealToRetrievalStore(id retrievalmarket.DealID, sid api.RemoteStoreID) error { a.accessLk.Lock() defer a.accessLk.Unlock() diff --git a/node/impl/client/client.go b/node/impl/client/client.go index 86da6500d64..fef4d91e379 100644 --- a/node/impl/client/client.go +++ b/node/impl/client/client.go @@ -849,7 +849,7 @@ func (a *API) doRetrieval(ctx context.Context, order api.RetrievalOrder, sel dat id := a.Retrieval.NextID() if order.RemoteStore != nil { - if err := a.ApiBlockstoreAccessor.UseRetrievalStore(id, *order.RemoteStore); err != nil { + if err := a.ApiBlockstoreAccessor.RegisterDealToRetrievalStore(id, *order.RemoteStore); err != nil { return 0, xerrors.Errorf("registering api store: %w", err) } } @@ -1030,6 +1030,7 @@ func (a *API) outputCAR(ctx context.Context, ds format.DAGService, bs bstore.Blo root, dagSpec.selector, func(node format.Node) error { + // if we're exporting merkle proofs for this dag, export all nodes read by the traversal if dagSpec.exportAll { lk.Lock() defer lk.Unlock() From 53e43a402a553e97ed42a39899ad8c4c0f592b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 7 Nov 2022 12:52:31 +0000 Subject: [PATCH 12/13] netbs: Drop client code for allkeys for now --- blockstore/net.go | 45 +---------------------------------------- blockstore/net_serve.go | 4 ++-- 2 files changed, 3 insertions(+), 46 deletions(-) diff --git a/blockstore/net.go b/blockstore/net.go index c627cae09aa..77da764a57d 100644 --- a/blockstore/net.go +++ b/blockstore/net.go @@ -24,7 +24,6 @@ const ( NRpcGetSize NRpcPut NRpcDelete - NRpcList // todo cancel req ) @@ -403,49 +402,7 @@ func (n *NetworkStore) DeleteMany(ctx context.Context, cids []cid.Cid) error { } func (n *NetworkStore) AllKeysChan(ctx context.Context) (<-chan cid.Cid, error) { - req, rch, err := n.sendRpc(NRpcList, nil, nil) - if err != nil { - return nil, err - } - - outCh := make(chan cid.Cid, 16) - - go func() { - defer close(outCh) - // todo defer cancel request - - for { - if rch == nil { - return - } - - resp, err := n.waitResp(ctx, rch, req) - if err != nil { - return - } - - switch resp.Type { - case NRpcOK, NRpcMore: - c, err := cid.Cast(resp.Data) - if err != nil { - return - } - - // todo propagate backpressure - select { - case outCh <- c: - case <-ctx.Done(): - return - } - - rch = resp.next - default: - return - } - } - }() - - return outCh, err + return nil, xerrors.Errorf("not supported") } func (n *NetworkStore) HashOnRead(enabled bool) { diff --git a/blockstore/net_serve.go b/blockstore/net_serve.go index f2a63f3813b..2540c845e81 100644 --- a/blockstore/net_serve.go +++ b/blockstore/net_serve.go @@ -175,8 +175,8 @@ func (h *NetworkStoreHandler) handle(ctx context.Context) { log.Warnw("writing response", "error", err) return } - case NRpcList: - if err := h.respondError(req.ID, xerrors.New("list todo"), cid.Undef); err != nil { + default: + if err := h.respondError(req.ID, xerrors.New("unsupported request type"), cid.Undef); err != nil { log.Warnw("writing error response", "error", err) return } From 888f97a35f6a4e227254b79b5efb7fad7a002f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Mon, 7 Nov 2022 15:14:56 +0000 Subject: [PATCH 13/13] netbs: Add an integration test --- .circleci/config.yml | 5 ++ build/openrpc/full.json.gz | Bin 28456 -> 29070 bytes go.mod | 2 +- itests/deals_remote_retrieval_test.go | 104 ++++++++++++++++++++++++++ itests/kit/deals.go | 16 +++- itests/kit/node_full.go | 1 + itests/kit/rpc.go | 2 +- 7 files changed, 127 insertions(+), 3 deletions(-) create mode 100644 itests/deals_remote_retrieval_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 4af2bfc12ab..71c05ee51f0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -896,6 +896,11 @@ workflows: suite: itest-deals_publish target: "./itests/deals_publish_test.go" + - test: + name: test-itest-deals_remote_retrieval + suite: itest-deals_remote_retrieval + target: "./itests/deals_remote_retrieval_test.go" + - test: name: test-itest-deals_retry_deal_no_funds suite: itest-deals_retry_deal_no_funds diff --git a/build/openrpc/full.json.gz b/build/openrpc/full.json.gz index bd7b14d30d2535c7abdbe09a933db97bcea57e54..88ddb3f997dceeddf178b502c97529183d4178f6 100644 GIT binary patch delta 28941 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 z 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 diff --git a/go.mod b/go.mod index dbe41d95ebc..dad5ca5e7cf 100644 --- a/go.mod +++ b/go.mod @@ -63,6 +63,7 @@ require ( github.com/golang/mock v1.6.0 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.7.4 + github.com/gorilla/websocket v1.5.0 github.com/hako/durafmt v0.0.0-20200710122514-c0fb7b4da026 github.com/hannahhoward/go-pubsub v0.0.0-20200423002714-8d62886cc36e github.com/hashicorp/go-multierror v1.1.1 @@ -214,7 +215,6 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/go-cmp v0.5.8 // indirect github.com/google/gopacket v1.1.19 // indirect - github.com/gorilla/websocket v1.5.0 // indirect github.com/hannahhoward/cbor-gen-for v0.0.0-20200817222906-ea96cece81f1 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/huin/goupnp v1.0.3 // indirect diff --git a/itests/deals_remote_retrieval_test.go b/itests/deals_remote_retrieval_test.go new file mode 100644 index 00000000000..c0a37e69e33 --- /dev/null +++ b/itests/deals_remote_retrieval_test.go @@ -0,0 +1,104 @@ +package itests + +import ( + "bytes" + "context" + "fmt" + "io" + "net/url" + "os" + "path" + "testing" + "time" + + "github.com/google/uuid" + "github.com/gorilla/websocket" + "github.com/ipld/go-car" + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/abi" + + "github.com/filecoin-project/lotus/api" + bstore "github.com/filecoin-project/lotus/blockstore" + "github.com/filecoin-project/lotus/itests/kit" +) + +func TestNetStoreRetrieval(t *testing.T) { + kit.QuietMiningLogs() + + blocktime := 5 * time.Millisecond + ctx := context.Background() + + full, miner, ens := kit.EnsembleMinimal(t, kit.MockProofs(), kit.ThroughRPC()) + ens.InterconnectAll().BeginMining(blocktime) + + time.Sleep(5 * time.Second) + + // For these tests where the block time is artificially short, just use + // a deal start epoch that is guaranteed to be far enough in the future + // so that the deal starts sealing in time + dealStartEpoch := abi.ChainEpoch(2 << 12) + + rseed := 7 + + dh := kit.NewDealHarness(t, full, miner, miner) + dealCid, res, _ := dh.MakeOnlineDeal(context.Background(), kit.MakeFullDealParams{ + Rseed: rseed, + StartEpoch: dealStartEpoch, + UseCARFileForStorageDeal: true, + }) + + // create deal store + id := uuid.New() + rstore := bstore.NewMemorySync() + + au, err := url.Parse(full.ListenURL) + require.NoError(t, err) + + switch au.Scheme { + case "http": + au.Scheme = "ws" + case "https": + au.Scheme = "wss" + } + + au.Path = path.Join(au.Path, "/rest/v0/store/"+id.String()) + + conn, _, err := websocket.DefaultDialer.Dial(au.String(), nil) + require.NoError(t, err) + + _ = bstore.HandleNetBstoreWS(ctx, rstore, conn) + + dh.PerformRetrievalWithOrder(ctx, dealCid, res.Root, false, func(offer api.QueryOffer, address address.Address) api.RetrievalOrder { + order := offer.Order(address) + + order.RemoteStore = &id + + return order + }) + + // check blockstore blocks + carv1FilePath, _ := kit.CreateRandomCARv1(t, rseed, 200) + cb, err := os.ReadFile(carv1FilePath) + require.NoError(t, err) + + cr, err := car.NewCarReader(bytes.NewReader(cb)) + require.NoError(t, err) + + var blocks int + for { + cb, err := cr.Next() + if err == io.EOF { + fmt.Println("blocks: ", blocks) + return + } + require.NoError(t, err) + + sb, err := rstore.Get(ctx, cb.Cid()) + require.NoError(t, err) + require.EqualValues(t, cb.RawData(), sb.RawData()) + + blocks++ + } +} diff --git a/itests/kit/deals.go b/itests/kit/deals.go index 794a6380328..1f3a7dfb59d 100644 --- a/itests/kit/deals.go +++ b/itests/kit/deals.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" + "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-fil-markets/retrievalmarket" "github.com/filecoin-project/go-fil-markets/shared_testutil" "github.com/filecoin-project/go-fil-markets/storagemarket" @@ -308,6 +309,12 @@ func (dh *DealHarness) StartSealingWaiting(ctx context.Context) { } func (dh *DealHarness) PerformRetrieval(ctx context.Context, deal *cid.Cid, root cid.Cid, carExport bool, offers ...api.QueryOffer) (path string) { + return dh.PerformRetrievalWithOrder(ctx, deal, root, carExport, func(offer api.QueryOffer, a address.Address) api.RetrievalOrder { + return offer.Order(a) + }, offers...) +} + +func (dh *DealHarness) PerformRetrievalWithOrder(ctx context.Context, deal *cid.Cid, root cid.Cid, carExport bool, makeOrder func(api.QueryOffer, address.Address) api.RetrievalOrder, offers ...api.QueryOffer) (path string) { var offer api.QueryOffer if len(offers) == 0 { // perform retrieval. @@ -331,7 +338,9 @@ func (dh *DealHarness) PerformRetrieval(ctx context.Context, deal *cid.Cid, root updates, err := dh.client.ClientGetRetrievalUpdates(updatesCtx) require.NoError(dh.t, err) - retrievalRes, err := dh.client.ClientRetrieve(ctx, offer.Order(caddr)) + order := makeOrder(offer, caddr) + + retrievalRes, err := dh.client.ClientRetrieve(ctx, order) require.NoError(dh.t, err) consumeEvents: for { @@ -357,6 +366,11 @@ consumeEvents: } cancel() + if order.RemoteStore != nil { + // if we're retrieving into a remote store, skip export + return "" + } + require.NoError(dh.t, dh.client.ClientExport(ctx, api.ExportRef{ Root: root, diff --git a/itests/kit/node_full.go b/itests/kit/node_full.go index 710962e6a03..5460d915633 100644 --- a/itests/kit/node_full.go +++ b/itests/kit/node_full.go @@ -27,6 +27,7 @@ type TestFullNode struct { // ListenAddr is the address on which an API server is listening, if an // API server is created for this Node. ListenAddr multiaddr.Multiaddr + ListenURL string DefaultKey *key.Key options nodeOpts diff --git a/itests/kit/rpc.go b/itests/kit/rpc.go index 45fb095d5b1..260eb970d47 100644 --- a/itests/kit/rpc.go +++ b/itests/kit/rpc.go @@ -65,7 +65,7 @@ func fullRpc(t *testing.T, f *TestFullNode) *TestFullNode { cl, stop, err := client.NewFullNodeRPCV1(context.Background(), "ws://"+srv.Listener.Addr().String()+"/rpc/v1", nil) require.NoError(t, err) t.Cleanup(stop) - f.ListenAddr, f.FullNode = maddr, cl + f.ListenAddr, f.ListenURL, f.FullNode = maddr, srv.URL, cl return f }