From 75668507245d7cc27a591859d0a7e27f9ef45de7 Mon Sep 17 00:00:00 2001 From: Sergey Cherepanov Date: Tue, 24 Oct 2023 19:20:01 +0200 Subject: [PATCH 01/10] new redis index --- Makefile | 2 +- cmd/filenode.go | 28 +- cmd/filenode_test.go | 13 +- filenode/filenode.go | 95 +- filenode/filenode_test.go | 127 +- filenode/rpchandler.go | 19 +- go.mod | 22 +- go.sum | 36 +- index/bind.go | 120 ++ index/bind_test.go | 98 + index/cidentry.go | 52 + index/cids.go | 189 ++ index/cids_test.go | 130 ++ index/entry.go | 117 ++ index/index.go | 230 ++- index/index_test.go | 52 + index/indexproto/index.pb.go | 1616 +++++++++++++++++ index/indexproto/protos/index.proto | 41 + index/loader.go | 238 +++ index/loader_test.go | 77 + index/migrate.go | 106 ++ index/migrate_test.go | 38 + index/mock_index/mock_index.go | 238 +-- index/redisindex/bind.go | 72 - index/redisindex/bind_test.go | 104 -- index/redisindex/fcidlist.go | 76 - index/redisindex/indexproto/index.pb.go | 582 ------ .../redisindex/indexproto/protos/index.proto | 15 - index/redisindex/redisindex.go | 372 ---- index/redisindex/redisindex_test.go | 220 --- index/redisindex/unbind.go | 86 - index/redisindex/unbind_test.go | 62 - index/testdata/oldspace.zip | Bin 0 -> 145662 bytes index/unbind.go | 138 ++ index/unbind_test.go | 93 + store/filedevstore/filedevstore.go | 18 +- store/mock_store/mock_store.go | 47 +- store/s3store/config.go | 1 + store/s3store/s3store.go | 46 +- store/s3store/s3store_test.go | 12 +- store/store.go | 4 + 41 files changed, 3728 insertions(+), 1904 deletions(-) create mode 100644 index/bind.go create mode 100644 index/bind_test.go create mode 100644 index/cidentry.go create mode 100644 index/cids.go create mode 100644 index/cids_test.go create mode 100644 index/entry.go create mode 100644 index/index_test.go create mode 100644 index/indexproto/index.pb.go create mode 100644 index/indexproto/protos/index.proto create mode 100644 index/loader.go create mode 100644 index/loader_test.go create mode 100644 index/migrate.go create mode 100644 index/migrate_test.go delete mode 100644 index/redisindex/bind.go delete mode 100644 index/redisindex/bind_test.go delete mode 100644 index/redisindex/fcidlist.go delete mode 100644 index/redisindex/indexproto/index.pb.go delete mode 100644 index/redisindex/indexproto/protos/index.proto delete mode 100644 index/redisindex/redisindex.go delete mode 100644 index/redisindex/redisindex_test.go delete mode 100644 index/redisindex/unbind.go delete mode 100644 index/redisindex/unbind_test.go create mode 100644 index/testdata/oldspace.zip create mode 100644 index/unbind.go create mode 100644 index/unbind_test.go diff --git a/Makefile b/Makefile index 16757d80..eca4f31e 100644 --- a/Makefile +++ b/Makefile @@ -29,4 +29,4 @@ deps: go build -o deps github.com/gogo/protobuf/protoc-gen-gogofaster proto: - protoc --gogofaster_out=:. index/redisindex/indexproto/protos/*.proto + protoc --gogofaster_out=:. index/indexproto/protos/*.proto diff --git a/cmd/filenode.go b/cmd/filenode.go index 5f954dcd..d16a6d77 100644 --- a/cmd/filenode.go +++ b/cmd/filenode.go @@ -4,12 +4,13 @@ import ( "context" "flag" "fmt" - "github.com/anyproto/any-sync-filenode/account" - "github.com/anyproto/any-sync-filenode/config" - "github.com/anyproto/any-sync-filenode/filenode" - "github.com/anyproto/any-sync-filenode/index/redisindex" - "github.com/anyproto/any-sync-filenode/limit" - "github.com/anyproto/any-sync-filenode/redisprovider" + "net/http" + _ "net/http/pprof" + "os" + "os/signal" + "syscall" + "time" + "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/app/logger" "github.com/anyproto/any-sync/coordinator/coordinatorclient" @@ -24,12 +25,13 @@ import ( "github.com/anyproto/any-sync/nodeconf" "github.com/anyproto/any-sync/nodeconf/nodeconfstore" "go.uber.org/zap" - "net/http" - _ "net/http/pprof" - "os" - "os/signal" - "syscall" - "time" + + "github.com/anyproto/any-sync-filenode/account" + "github.com/anyproto/any-sync-filenode/config" + "github.com/anyproto/any-sync-filenode/filenode" + "github.com/anyproto/any-sync-filenode/index" + "github.com/anyproto/any-sync-filenode/limit" + "github.com/anyproto/any-sync-filenode/redisprovider" // import this to keep govvv in go.mod on mod tidy _ "github.com/ahmetb/govvv/integration-test/app-different-package/mypkg" @@ -114,7 +116,7 @@ func Bootstrap(a *app.App) { Register(limit.New()). Register(store()). Register(redisprovider.New()). - Register(redisindex.New()). + Register(index.New()). Register(metric.New()). Register(server.New()). Register(filenode.New()) diff --git a/cmd/filenode_test.go b/cmd/filenode_test.go index 6dd447ef..55db994d 100644 --- a/cmd/filenode_test.go +++ b/cmd/filenode_test.go @@ -2,15 +2,17 @@ package main import ( "context" - "github.com/anyproto/any-sync-filenode/config" - "github.com/anyproto/any-sync-filenode/store/s3store" + "os" + "testing" + commonaccount "github.com/anyproto/any-sync/accountservice" "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/metric" "github.com/anyproto/any-sync/net/rpc" "github.com/stretchr/testify/require" - "os" - "testing" + + "github.com/anyproto/any-sync-filenode/config" + "github.com/anyproto/any-sync-filenode/store/s3store" ) var ctx = context.Background() @@ -27,7 +29,8 @@ func TestBootstrap(t *testing.T) { Drpc: rpc.Config{}, Metric: metric.Config{}, S3Store: s3store.Config{ - Bucket: "test", + Bucket: "test", + IndexBucket: "testIndex", }, FileDevStore: config.FileDevStore{}, NetworkStorePath: tmpDir, diff --git a/filenode/filenode.go b/filenode/filenode.go index 6e857b2c..23b5d7d8 100644 --- a/filenode/filenode.go +++ b/filenode/filenode.go @@ -3,11 +3,7 @@ package filenode import ( "context" "errors" - "github.com/anyproto/any-sync-filenode/index" - "github.com/anyproto/any-sync-filenode/index/redisindex" - "github.com/anyproto/any-sync-filenode/limit" - "github.com/anyproto/any-sync-filenode/store" - "github.com/anyproto/any-sync-filenode/testutil" + "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/app/logger" "github.com/anyproto/any-sync/commonfile/fileblockstore" @@ -17,6 +13,10 @@ import ( "github.com/anyproto/any-sync/net/rpc/server" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" + + "github.com/anyproto/any-sync-filenode/index" + "github.com/anyproto/any-sync-filenode/limit" + "github.com/anyproto/any-sync-filenode/store" ) const CName = "filenode.filenode" @@ -45,7 +45,7 @@ type fileNode struct { func (fn *fileNode) Init(a *app.App) (err error) { fn.store = a.MustComponent(fileblockstore.CName).(store.Store) - fn.index = a.MustComponent(redisindex.CName).(index.Index) + fn.index = a.MustComponent(index.CName).(index.Index) fn.limit = a.MustComponent(limit.CName).(limit.Limit) fn.handler = &rpcHandler{f: fn} fn.metric = a.MustComponent(metric.CName).(metric.Metric) @@ -57,7 +57,7 @@ func (fn *fileNode) Name() (name string) { } func (fn *fileNode) Get(ctx context.Context, k cid.Cid) (blocks.Block, error) { - exists, err := fn.index.Exists(ctx, k) + exists, err := fn.index.CidExists(ctx, k) if err != nil { return nil, err } @@ -72,12 +72,12 @@ func (fn *fileNode) Add(ctx context.Context, spaceId string, fileId string, bs [ if err != nil { return err } - unlock, err := fn.index.Lock(ctx, testutil.BlocksToKeys(bs)) + unlock, err := fn.index.BlocksLock(ctx, bs) if err != nil { return err } defer unlock() - toUpload, err := fn.index.GetNonExistentBlocks(ctx, bs) + toUpload, err := fn.index.BlocksGetNonExistent(ctx, bs) if err != nil { return err } @@ -85,12 +85,20 @@ func (fn *fileNode) Add(ctx context.Context, spaceId string, fileId string, bs [ if err = fn.store.Add(ctx, toUpload); err != nil { return err } + if err = fn.index.BlocksAdd(ctx, bs); err != nil { + return err + } + } + cidEntries, err := fn.index.CidEntriesByBlocks(ctx, bs) + if err != nil { + return err } - return fn.index.Bind(ctx, storeKey, fileId, bs) + defer cidEntries.Release() + return fn.index.FileBind(ctx, storeKey, fileId, cidEntries) } func (fn *fileNode) Check(ctx context.Context, spaceId string, cids ...cid.Cid) (result []*fileproto.BlockAvailability, err error) { - var storeKey string + var storeKey index.Key if spaceId != "" { if storeKey, err = fn.StoreKey(ctx, spaceId, false); err != nil { return @@ -99,7 +107,7 @@ func (fn *fileNode) Check(ctx context.Context, spaceId string, cids ...cid.Cid) result = make([]*fileproto.BlockAvailability, 0, len(cids)) var inSpaceM = make(map[string]struct{}) if spaceId != "" { - inSpace, err := fn.index.ExistsInStorage(ctx, storeKey, cids) + inSpace, err := fn.index.CidExistsInSpace(ctx, storeKey, cids) if err != nil { return nil, err } @@ -116,7 +124,7 @@ func (fn *fileNode) Check(ctx context.Context, spaceId string, cids ...cid.Cid) res.Status = fileproto.AvailabilityStatus_ExistsInSpace } else { var ex bool - if ex, err = fn.index.Exists(ctx, k); err != nil { + if ex, err = fn.index.CidExists(ctx, k); err != nil { return nil, err } else if ex { res.Status = fileproto.AvailabilityStatus_Exists @@ -132,39 +140,36 @@ func (fn *fileNode) BlocksBind(ctx context.Context, spaceId, fileId string, cids if err != nil { return err } - unlock, err := fn.index.Lock(ctx, cids) + cidEntries, err := fn.index.CidEntries(ctx, cids) if err != nil { return err } - defer unlock() - return fn.index.BindCids(ctx, storeKey, fileId, cids) + defer cidEntries.Release() + return fn.index.FileBind(ctx, storeKey, fileId, cidEntries) } -func (fn *fileNode) StoreKey(ctx context.Context, spaceId string, checkLimit bool) (storageKey string, err error) { +func (fn *fileNode) StoreKey(ctx context.Context, spaceId string, checkLimit bool) (storageKey index.Key, err error) { if spaceId == "" { - return "", fileprotoerr.ErrForbidden + return storageKey, fileprotoerr.ErrForbidden } // this call also confirms that space exists and valid - limitBytes, storageKey, err := fn.limit.Check(ctx, spaceId) + limitBytes, groupId, err := fn.limit.Check(ctx, spaceId) if err != nil { return } - if storageKey != spaceId { - // try to move store to the new key - mErr := fn.index.MoveStorage(ctx, spaceId, storageKey) - if mErr != nil && mErr != index.ErrStorageNotFound && mErr != index.ErrTargetStorageExists { - return "", mErr - } + storageKey = index.Key{ + GroupId: groupId, + SpaceId: spaceId, } if checkLimit { - currentSize, e := fn.index.StorageSize(ctx, storageKey) + info, e := fn.index.GroupInfo(ctx, groupId) if e != nil { - return "", e + return storageKey, e } - if currentSize >= limitBytes { - return "", fileprotoerr.ErrSpaceLimitExceeded + if info.BytesUsage >= limitBytes { + return storageKey, fileprotoerr.ErrSpaceLimitExceeded } } return @@ -177,15 +182,9 @@ func (fn *fileNode) SpaceInfo(ctx context.Context, spaceId string) (info *filepr if info.LimitBytes, storageKey, err = fn.limit.Check(ctx, spaceId); err != nil { return nil, err } - if info.UsageBytes, err = fn.index.StorageSize(ctx, storageKey); err != nil { - return nil, err - } - si, err := fn.index.StorageInfo(ctx, storageKey) - if err != nil { - return nil, err - } - info.CidsCount = uint64(si.CidCount) - info.FilesCount = uint64(si.FileCount) + + // TODO: + _ = storageKey return } @@ -195,25 +194,29 @@ func (fn *fileNode) FilesDelete(ctx context.Context, spaceId string, fileIds []s return } for _, fileId := range fileIds { - if err = fn.index.UnBind(ctx, storeKey, fileId); err != nil { + if err = fn.index.FileUnbind(ctx, storeKey, fileId); err != nil { return } } return } -func (fn *fileNode) FileInfo(ctx context.Context, spaceId, fileId string) (info *fileproto.FileInfo, err error) { +func (fn *fileNode) FileInfo(ctx context.Context, spaceId string, fileIds ...string) (info []*fileproto.FileInfo, err error) { storeKey, err := fn.StoreKey(ctx, spaceId, false) if err != nil { return } - fi, err := fn.index.FileInfo(ctx, storeKey, fileId) + fis, err := fn.index.FileInfo(ctx, storeKey, fileIds...) if err != nil { return nil, err } - return &fileproto.FileInfo{ - FileId: fileId, - UsageBytes: fi.BytesUsage, - CidsCount: fi.CidCount, - }, nil + info = make([]*fileproto.FileInfo, len(fis)) + for i, fi := range fis { + info[i] = &fileproto.FileInfo{ + FileId: fileIds[i], + UsageBytes: fi.BytesUsage, + CidsCount: fi.CidCount, + } + } + return } diff --git a/filenode/filenode_test.go b/filenode/filenode_test.go index d0ef4a83..8e667a0f 100644 --- a/filenode/filenode_test.go +++ b/filenode/filenode_test.go @@ -2,14 +2,8 @@ package filenode import ( "context" - "github.com/anyproto/any-sync-filenode/config" - "github.com/anyproto/any-sync-filenode/index" - "github.com/anyproto/any-sync-filenode/index/mock_index" - "github.com/anyproto/any-sync-filenode/index/redisindex" - "github.com/anyproto/any-sync-filenode/limit" - "github.com/anyproto/any-sync-filenode/limit/mock_limit" - "github.com/anyproto/any-sync-filenode/store/mock_store" - "github.com/anyproto/any-sync-filenode/testutil" + "testing" + "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/commonfile/fileblockstore" "github.com/anyproto/any-sync/commonfile/fileproto" @@ -21,7 +15,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" - "testing" + + "github.com/anyproto/any-sync-filenode/config" + "github.com/anyproto/any-sync-filenode/index" + "github.com/anyproto/any-sync-filenode/index/mock_index" + "github.com/anyproto/any-sync-filenode/limit" + "github.com/anyproto/any-sync-filenode/limit/mock_limit" + "github.com/anyproto/any-sync-filenode/store/mock_store" + "github.com/anyproto/any-sync-filenode/testutil" ) var ctx = context.Background() @@ -31,22 +32,22 @@ func TestFileNode_Add(t *testing.T) { fx := newFixture(t) defer fx.Finish(t) var ( - spaceId = testutil.NewRandSpaceId() - storeKey = "sk:" + spaceId + storeKey = newRandKey() fileId = testutil.NewRandCid().String() b = testutil.NewRandBlock(1024) ) - fx.limit.EXPECT().Check(ctx, spaceId).Return(uint64(123), storeKey, nil) - fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() - fx.index.EXPECT().StorageSize(ctx, storeKey).Return(uint64(120), nil) - fx.index.EXPECT().Lock(ctx, []cid.Cid{b.Cid()}).Return(func() {}, nil) - fx.index.EXPECT().GetNonExistentBlocks(ctx, []blocks.Block{b}).Return([]blocks.Block{b}, nil) + fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).Return(uint64(123), storeKey.GroupId, nil) + fx.index.EXPECT().GroupInfo(ctx, storeKey.GroupId).Return(index.GroupInfo{BytesUsage: uint64(120)}, nil) + fx.index.EXPECT().BlocksLock(ctx, []blocks.Block{b}).Return(func() {}, nil) + fx.index.EXPECT().BlocksGetNonExistent(ctx, []blocks.Block{b}).Return([]blocks.Block{b}, nil) fx.store.EXPECT().Add(ctx, []blocks.Block{b}) - fx.index.EXPECT().Bind(ctx, storeKey, fileId, []blocks.Block{b}) + fx.index.EXPECT().BlocksAdd(ctx, []blocks.Block{b}) + fx.index.EXPECT().CidEntriesByBlocks(ctx, []blocks.Block{b}).Return(&index.CidEntries{}, nil) + fx.index.EXPECT().FileBind(ctx, storeKey, fileId, gomock.Any()) resp, err := fx.handler.BlockPush(ctx, &fileproto.BlockPushRequest{ - SpaceId: spaceId, + SpaceId: storeKey.SpaceId, FileId: fileId, Cid: b.Cid().Bytes(), Data: b.RawData(), @@ -54,22 +55,22 @@ func TestFileNode_Add(t *testing.T) { require.NoError(t, err) require.NotNil(t, resp) }) + t.Run("limit exceed", func(t *testing.T) { fx := newFixture(t) defer fx.Finish(t) var ( - spaceId = testutil.NewRandSpaceId() - storeKey = "sk:" + spaceId + storeKey = newRandKey() fileId = testutil.NewRandCid().String() b = testutil.NewRandBlock(1024) ) - fx.limit.EXPECT().Check(ctx, spaceId).Return(uint64(123), storeKey, nil) - fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() - fx.index.EXPECT().StorageSize(ctx, storeKey).Return(uint64(124), nil) + fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).Return(uint64(123), storeKey.GroupId, nil) + //fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() + fx.index.EXPECT().GroupInfo(ctx, storeKey.GroupId).Return(index.GroupInfo{BytesUsage: uint64(124)}, nil) resp, err := fx.handler.BlockPush(ctx, &fileproto.BlockPushRequest{ - SpaceId: spaceId, + SpaceId: storeKey.SpaceId, FileId: fileId, Cid: b.Cid().Bytes(), Data: b.RawData(), @@ -81,14 +82,14 @@ func TestFileNode_Add(t *testing.T) { fx := newFixture(t) defer fx.Finish(t) var ( - spaceId = testutil.NewRandSpaceId() - fileId = testutil.NewRandCid().String() - b = testutil.NewRandBlock(1024) - b2 = testutil.NewRandBlock(10) + storeKey = newRandKey() + fileId = testutil.NewRandCid().String() + b = testutil.NewRandBlock(1024) + b2 = testutil.NewRandBlock(10) ) resp, err := fx.handler.BlockPush(ctx, &fileproto.BlockPushRequest{ - SpaceId: spaceId, + SpaceId: storeKey.SpaceId, FileId: fileId, Cid: b2.Cid().Bytes(), Data: b.RawData(), @@ -114,6 +115,7 @@ func TestFileNode_Add(t *testing.T) { require.EqualError(t, err, fileprotoerr.ErrQuerySizeExceeded.Error()) assert.Nil(t, resp) }) + } func TestFileNode_Get(t *testing.T) { @@ -122,7 +124,7 @@ func TestFileNode_Get(t *testing.T) { defer fx.Finish(t) spaceId := testutil.NewRandSpaceId() b := testutil.NewRandBlock(10) - fx.index.EXPECT().Exists(gomock.Any(), b.Cid()).Return(true, nil) + fx.index.EXPECT().CidExists(gomock.Any(), b.Cid()).Return(true, nil) fx.store.EXPECT().Get(ctx, b.Cid()).Return(b, nil) resp, err := fx.handler.BlockGet(ctx, &fileproto.BlockGetRequest{ SpaceId: spaceId, @@ -136,7 +138,7 @@ func TestFileNode_Get(t *testing.T) { defer fx.Finish(t) spaceId := testutil.NewRandSpaceId() b := testutil.NewRandBlock(10) - fx.index.EXPECT().Exists(gomock.Any(), b.Cid()).Return(false, nil) + fx.index.EXPECT().CidExists(gomock.Any(), b.Cid()).Return(false, nil) resp, err := fx.handler.BlockGet(ctx, &fileproto.BlockGetRequest{ SpaceId: spaceId, Cid: b.Cid().Bytes(), @@ -149,20 +151,19 @@ func TestFileNode_Get(t *testing.T) { func TestFileNode_Check(t *testing.T) { fx := newFixture(t) defer fx.Finish(t) - var spaceId = testutil.NewRandSpaceId() - var storeKey = "sk:" + spaceId + var storeKey = newRandKey() var bs = testutil.NewRandBlocks(3) cids := make([][]byte, len(bs)) for _, b := range bs { cids = append(cids, b.Cid().Bytes()) } - fx.limit.EXPECT().Check(ctx, spaceId).Return(uint64(100000), storeKey, nil) - fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() - fx.index.EXPECT().ExistsInStorage(ctx, storeKey, testutil.BlocksToKeys(bs)).Return(testutil.BlocksToKeys(bs[:1]), nil) - fx.index.EXPECT().Exists(ctx, bs[1].Cid()).Return(true, nil) - fx.index.EXPECT().Exists(ctx, bs[2].Cid()).Return(false, nil) + fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).Return(uint64(100000), storeKey.GroupId, nil) + //fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() + fx.index.EXPECT().CidExistsInSpace(ctx, storeKey, testutil.BlocksToKeys(bs)).Return(testutil.BlocksToKeys(bs[:1]), nil) + fx.index.EXPECT().CidExists(ctx, bs[1].Cid()).Return(true, nil) + fx.index.EXPECT().CidExists(ctx, bs[2].Cid()).Return(false, nil) resp, err := fx.handler.BlocksCheck(ctx, &fileproto.BlocksCheckRequest{ - SpaceId: spaceId, + SpaceId: storeKey.SpaceId, Cids: cids, }) require.NoError(t, err) @@ -176,31 +177,32 @@ func TestFileNode_BlocksBind(t *testing.T) { fx := newFixture(t) defer fx.Finish(t) var ( - spaceId = testutil.NewRandSpaceId() - storeKey = "sk:" + spaceId - fileId = testutil.NewRandCid().String() - bs = testutil.NewRandBlocks(3) - cidsB = make([][]byte, len(bs)) - cids = make([]cid.Cid, len(bs)) + storeKey = newRandKey() + fileId = testutil.NewRandCid().String() + bs = testutil.NewRandBlocks(3) + cidsB = make([][]byte, len(bs)) + cids = make([]cid.Cid, len(bs)) + cidEntries = &index.CidEntries{} ) for i, b := range bs { cids[i] = b.Cid() cidsB[i] = b.Cid().Bytes() } - fx.limit.EXPECT().Check(ctx, spaceId).Return(uint64(123), storeKey, nil) - fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() - fx.index.EXPECT().StorageSize(ctx, storeKey).Return(uint64(12), nil) - fx.index.EXPECT().Lock(ctx, cids).Return(func() {}, nil) - fx.index.EXPECT().BindCids(ctx, storeKey, fileId, cids) + fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).Return(uint64(123), storeKey.GroupId, nil) + // fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() + fx.index.EXPECT().GroupInfo(ctx, storeKey.GroupId).Return(index.GroupInfo{BytesUsage: 12}, nil) + fx.index.EXPECT().CidEntries(ctx, cids).Return(cidEntries, nil) + fx.index.EXPECT().FileBind(ctx, storeKey, fileId, cidEntries) resp, err := fx.handler.BlocksBind(ctx, &fileproto.BlocksBindRequest{ - SpaceId: spaceId, + SpaceId: storeKey.SpaceId, FileId: fileId, Cids: cidsB, }) require.NotNil(t, resp) require.NoError(t, err) + } func TestFileNode_FileInfo(t *testing.T) { @@ -208,18 +210,16 @@ func TestFileNode_FileInfo(t *testing.T) { defer fx.Finish(t) var ( - spaceId = testutil.NewRandSpaceId() - storeKey = "sk:" + spaceId + storeKey = newRandKey() fileId1 = testutil.NewRandCid().String() fileId2 = testutil.NewRandCid().String() ) - fx.limit.EXPECT().Check(ctx, spaceId).AnyTimes().Return(uint64(100000), storeKey, nil) - fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() - fx.index.EXPECT().FileInfo(ctx, storeKey, fileId1).Return(index.FileInfo{1, 1}, nil) - fx.index.EXPECT().FileInfo(ctx, storeKey, fileId2).Return(index.FileInfo{2, 2}, nil) + fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).AnyTimes().Return(uint64(100000), storeKey.GroupId, nil) + //fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() + fx.index.EXPECT().FileInfo(ctx, storeKey, fileId1, fileId2).Return([]index.FileInfo{{1, 1}, {2, 2}}, nil) resp, err := fx.handler.FilesInfo(ctx, &fileproto.FilesInfoRequest{ - SpaceId: spaceId, + SpaceId: storeKey.SpaceId, FileIds: []string{fileId1, fileId2}, }) require.NoError(t, err) @@ -240,8 +240,10 @@ func newFixture(t *testing.T) *fixture { a: new(app.App), } - fx.index.EXPECT().Name().Return(redisindex.CName).AnyTimes() + fx.index.EXPECT().Name().Return(index.CName).AnyTimes() fx.index.EXPECT().Init(gomock.Any()).AnyTimes() + fx.index.EXPECT().Run(gomock.Any()).AnyTimes() + fx.index.EXPECT().Close(gomock.Any()).AnyTimes() fx.store.EXPECT().Name().Return(fileblockstore.CName).AnyTimes() fx.store.EXPECT().Init(gomock.Any()).AnyTimes() @@ -270,3 +272,10 @@ func (fx *fixture) Finish(t *testing.T) { fx.ctrl.Finish() require.NoError(t, fx.a.Close(ctx)) } + +func newRandKey() index.Key { + return index.Key{ + SpaceId: testutil.NewRandSpaceId(), + GroupId: "A" + testutil.NewRandCid().String(), + } +} diff --git a/filenode/rpchandler.go b/filenode/rpchandler.go index 5ed4e633..59c28a0a 100644 --- a/filenode/rpchandler.go +++ b/filenode/rpchandler.go @@ -2,18 +2,19 @@ package filenode import ( "context" + "time" + "github.com/anyproto/any-sync/commonfile/fileproto" "github.com/anyproto/any-sync/commonfile/fileproto/fileprotoerr" "github.com/anyproto/any-sync/metric" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "go.uber.org/zap" - "time" ) const ( cidSizeLimit = 2 << 20 // 2 Mb - fileInfoReqLimit = 100 + fileInfoReqLimit = 1000 ) type rpcHandler struct { @@ -163,21 +164,11 @@ func (r rpcHandler) FilesInfo(ctx context.Context, req *fileproto.FilesInfoReque err = fileprotoerr.ErrQuerySizeExceeded return } - resp = &fileproto.FilesInfoResponse{ - FilesInfo: make([]*fileproto.FileInfo, len(req.FileIds)), - } - _, err = r.f.StoreKey(ctx, req.SpaceId, false) + resp = &fileproto.FilesInfoResponse{} + resp.FilesInfo, err = r.f.FileInfo(ctx, req.SpaceId, req.FileIds...) if err != nil { return nil, err } - var info *fileproto.FileInfo - for i, fileId := range req.FileIds { - info, err = r.f.FileInfo(ctx, req.SpaceId, fileId) - if err != nil { - return nil, err - } - resp.FilesInfo[i] = info - } return resp, nil } diff --git a/go.mod b/go.mod index 9dc71d84..a0f93371 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/anyproto/any-sync-filenode go 1.21 require ( + github.com/OneOfOne/xxhash v1.2.2 github.com/ahmetb/govvv v0.3.0 - github.com/anyproto/any-sync v0.3.1 + github.com/anyproto/any-sync v0.3.3 github.com/aws/aws-sdk-go v1.45.15 + github.com/cespare/xxhash/v2 v2.2.0 github.com/go-redsync/redsync/v4 v4.9.4 github.com/gogo/protobuf v1.3.2 github.com/golang/snappy v0.0.4 @@ -26,16 +28,14 @@ require ( github.com/anyproto/go-slip21 v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/gobwas/glob v0.2.3 // indirect - github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b // indirect + github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect @@ -60,21 +60,21 @@ require ( github.com/multiformats/go-varint v0.0.7 // indirect github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.16.0 // indirect - github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/client_golang v1.17.0 // indirect + github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect - github.com/quic-go/qtls-go1-20 v0.3.3 // indirect - github.com/quic-go/quic-go v0.38.1 // indirect + github.com/quic-go/qtls-go1-20 v0.3.4 // indirect + github.com/quic-go/quic-go v0.39.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/zeebo/errs v1.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/crypto v0.13.0 // indirect + golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.15.0 // indirect - golang.org/x/sys v0.12.0 // indirect + golang.org/x/net v0.16.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect google.golang.org/protobuf v1.31.0 // indirect lukechampine.com/blake3 v1.2.1 // indirect diff --git a/go.sum b/go.sum index 13cc00cc..ed5221ed 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ahmetb/govvv v0.3.0 h1:YGLGwEyiUwHFy5eh/RUhdupbuaCGBYn5T5GWXp+WJB0= github.com/ahmetb/govvv v0.3.0/go.mod h1:4WRFpdWtc/YtKgPFwa1dr5+9hiRY5uKAL08bOlxOR6s= -github.com/anyproto/any-sync v0.3.1 h1:AHsIYyhM9J+eqKVjnsuGgT/4u+f47JyEhfIBMnDLIIA= -github.com/anyproto/any-sync v0.3.1/go.mod h1:v0w3l3FBWjzNgg5t8aWlI+aYkcA8kLaoJFfr/GHsWYk= +github.com/anyproto/any-sync v0.3.3 h1:ZD62Geii/ZXaT1laetMFHgWx362Jwx3NM0iUTi/YARA= +github.com/anyproto/any-sync v0.3.3/go.mod h1:Zw7xOQjBVxA0Z+awQxMVj7Ve5/Dbqu2UW9R+8z5upvM= github.com/anyproto/go-chash v0.1.0 h1:I9meTPjXFRfXZHRJzjOHC/XF7Q5vzysKkiT/grsogXY= github.com/anyproto/go-chash v0.1.0/go.mod h1:0UjNQi3PDazP0fINpFYu6VKhuna+W/V+1vpXHAfNgLY= github.com/anyproto/go-slip10 v1.0.0 h1:uAEtSuudR3jJBOfkOXf3bErxVoxbuKwdoJN55M1i6IA= @@ -92,8 +92,8 @@ github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b h1:h9U78+dx9a4BKdQkBBos92HalKpaGKHrp+3Uo6yTodo= -github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -203,18 +203,18 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= +github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= +github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= -github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM= -github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.38.1 h1:M36YWA5dEhEeT+slOu/SwMEucbYd0YFidxG3KlGPZaE= -github.com/quic-go/quic-go v0.38.1/go.mod h1:ijnZM7JsFIkp4cRyjxJNIzdSfCLmUMg9wdyhGmg+SN4= +github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= +github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So= +github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= github.com/redis/go-redis/v9 v9.0.2/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= github.com/redis/go-redis/v9 v9.2.0 h1:zwMdX0A4eVzse46YN18QhuDiM4uf3JmkOB4VZrdt5uI= github.com/redis/go-redis/v9 v9.2.0/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= @@ -276,8 +276,8 @@ golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -309,8 +309,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -352,8 +352,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/index/bind.go b/index/bind.go new file mode 100644 index 00000000..7f996d53 --- /dev/null +++ b/index/bind.go @@ -0,0 +1,120 @@ +package index + +import ( + "context" + + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +func (ri *redisIndex) FileBind(ctx context.Context, key Key, fileId string, cids *CidEntries) (err error) { + var ( + sk = spaceKey(key) + gk = groupKey(key) + ) + _, gRelease, err := ri.AcquireKey(ctx, gk) + if err != nil { + return + } + defer gRelease() + _, sRelease, err := ri.AcquireKey(ctx, sk) + if err != nil { + return + } + defer sRelease() + + // get file entry + fileInfo, isNewFile, err := ri.getFileEntry(ctx, key, fileId) + if err != nil { + return + } + + // make a list of indexes of non-exists cids + var newFileCidIdx = make([]int, 0, len(cids.entries)) + for i, c := range cids.entries { + if !fileInfo.Exists(c.Cid.String()) { + newFileCidIdx = append(newFileCidIdx, i) + fileInfo.Cids = append(fileInfo.Cids, c.Cid.String()) + fileInfo.Size_ += c.Size_ + } + } + + // all cids exists, nothing to do + if len(newFileCidIdx) == 0 { + return + } + + // get all cids from space and group in one pipeline + var ( + cidExistSpaceCmds = make([]*redis.BoolCmd, len(newFileCidIdx)) + cidExistGroupCmds = make([]*redis.BoolCmd, len(newFileCidIdx)) + ) + _, err = ri.cl.Pipelined(ctx, func(pipe redis.Pipeliner) error { + for i, idx := range newFileCidIdx { + ck := cidKey(cids.entries[idx].Cid) + cidExistSpaceCmds[i] = pipe.HExists(ctx, sk, ck) + cidExistGroupCmds[i] = pipe.HExists(ctx, gk, ck) + } + return nil + }) + if err != nil { + return + } + + // load group and space info + spaceInfo, err := ri.getSpaceEntry(ctx, key) + if err != nil { + return + } + groupInfo, err := ri.getGroupEntry(ctx, key) + if err != nil { + return + } + + // calculate new group and space stats + for i, idx := range newFileCidIdx { + ex, err := cidExistGroupCmds[i].Result() + if err != nil { + return err + } + if !ex { + spaceInfo.CidCount++ + spaceInfo.Size_ += cids.entries[idx].Size_ + } + ex, err = cidExistSpaceCmds[i].Result() + if err != nil { + return err + } + if !ex { + groupInfo.CidCount++ + groupInfo.Size_ += cids.entries[idx].Size_ + } + } + if isNewFile { + spaceInfo.FileCount++ + } + + // make group and space updates in one tx + _, err = ri.cl.TxPipelined(ctx, func(tx redis.Pipeliner) error { + // increment cid refs + for _, idx := range newFileCidIdx { + ck := cidKey(cids.entries[idx].Cid) + tx.HIncrBy(ctx, gk, ck, 1) + tx.HIncrBy(ctx, sk, ck, 1) + } + // save info + spaceInfo.Save(ctx, key, tx) + groupInfo.Save(ctx, key, tx) + fileInfo.Save(ctx, key, fileId, tx) + return nil + }) + + // update cids + for _, idx := range newFileCidIdx { + cids.entries[idx].AddGroupId(key.GroupId) + if saveErr := cids.entries[idx].Save(ctx, ri.cl); saveErr != nil { + log.WarnCtx(ctx, "unable to save cid info", zap.Error(saveErr), zap.String("cid", cids.entries[idx].Cid.String())) + } + } + return +} diff --git a/index/bind_test.go b/index/bind_test.go new file mode 100644 index 00000000..35346a19 --- /dev/null +++ b/index/bind_test.go @@ -0,0 +1,98 @@ +package index + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anyproto/any-sync-filenode/testutil" +) + +func TestRedisIndex_Bind(t *testing.T) { + t.Run("first add", func(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + bs := testutil.NewRandBlocks(3) + var sumSize uint64 + for _, b := range bs { + sumSize += uint64(len(b.RawData())) + } + key := newRandKey() + fileId := testutil.NewRandCid().String() + require.NoError(t, fx.BlocksAdd(ctx, bs)) + cids, err := fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + defer cids.Release() + + require.NoError(t, fx.FileBind(ctx, key, fileId, cids)) + fInfo, err := fx.FileInfo(ctx, key, fileId) + require.NoError(t, err) + assert.Equal(t, uint32(len(bs)), fInfo[0].CidCount) + assert.Equal(t, sumSize, fInfo[0].BytesUsage) + }) + + t.Run("two files with same cids", func(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + bs := testutil.NewRandBlocks(3) + var sumSize uint64 + for _, b := range bs { + sumSize += uint64(len(b.RawData())) + } + key := newRandKey() + fileId1 := testutil.NewRandCid().String() + fileId2 := testutil.NewRandCid().String() + require.NoError(t, fx.BlocksAdd(ctx, bs)) + + cidsA, err := fx.CidEntriesByBlocks(ctx, bs[:2]) + require.NoError(t, err) + require.NoError(t, fx.FileBind(ctx, key, fileId1, cidsA)) + cidsA.Release() + + cidsB, err := fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + defer cidsB.Release() + require.NoError(t, fx.FileBind(ctx, key, fileId2, cidsB)) + + spaceInfo, err := fx.SpaceInfo(ctx, key) + require.NoError(t, err) + assert.Equal(t, SpaceInfo{ + BytesUsage: sumSize, + FileCount: 2, + }, spaceInfo) + + groupInfo, err := fx.GroupInfo(ctx, key.GroupId) + require.NoError(t, err) + assert.Equal(t, GroupInfo{ + BytesUsage: sumSize, + CidsCount: uint32(len(bs)), + }, groupInfo) + + }) + + t.Run("bind twice", func(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + bs := testutil.NewRandBlocks(3) + var sumSize uint64 + for _, b := range bs { + sumSize += uint64(len(b.RawData())) + } + key := newRandKey() + fileId := testutil.NewRandCid().String() + + require.NoError(t, fx.BlocksAdd(ctx, bs)) + cids, err := fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + defer cids.Release() + + require.NoError(t, fx.FileBind(ctx, key, fileId, cids)) + require.NoError(t, fx.FileBind(ctx, key, fileId, cids)) + + fInfo, err := fx.FileInfo(ctx, key, fileId) + require.NoError(t, err) + assert.Equal(t, uint32(len(bs)), fInfo[0].CidCount) + assert.Equal(t, sumSize, fInfo[0].BytesUsage) + }) +} diff --git a/index/cidentry.go b/index/cidentry.go new file mode 100644 index 00000000..20c1bd7f --- /dev/null +++ b/index/cidentry.go @@ -0,0 +1,52 @@ +package index + +import ( + "context" + "slices" + "time" + + "github.com/ipfs/go-cid" + "github.com/redis/go-redis/v9" + + "github.com/anyproto/any-sync-filenode/index/indexproto" +) + +type CidEntries struct { + entries []*cidEntry +} + +func (ce *CidEntries) Release() { + for _, entry := range ce.entries { + if entry.release != nil { + entry.release() + } + } + return +} + +type cidEntry struct { + Cid cid.Cid + release func() + *indexproto.CidEntry +} + +func (ce *cidEntry) AddGroupId(groupId string) { + if !slices.Contains(ce.GroupIds, groupId) { + ce.GroupIds = append(ce.GroupIds, groupId) + } +} + +func (ce *cidEntry) RemoveGroupId(id string) { + ce.GroupIds = slices.DeleteFunc(ce.GroupIds, func(s string) bool { + return s == id + }) +} + +func (ce *cidEntry) Save(ctx context.Context, cl redis.Cmdable) error { + ce.UpdateTime = time.Now().Unix() + data, err := ce.Marshal() + if err != nil { + return err + } + return cl.Set(ctx, cidKey(ce.Cid), data, 0).Err() +} diff --git a/index/cids.go b/index/cids.go new file mode 100644 index 00000000..275972d4 --- /dev/null +++ b/index/cids.go @@ -0,0 +1,189 @@ +package index + +import ( + "context" + "errors" + "time" + + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" + + "github.com/anyproto/any-sync-filenode/index/indexproto" +) + +const ( + cidCount = "cidCount.{system}" + cidSizeSumKey = "cidSizeSum.{system}" +) + +func (ri *redisIndex) CidExists(ctx context.Context, c cid.Cid) (ok bool, err error) { + return ri.CheckKey(ctx, cidKey(c)) +} + +func (ri *redisIndex) CidEntries(ctx context.Context, cids []cid.Cid) (entries *CidEntries, err error) { + entries = &CidEntries{} + for _, c := range cids { + if err = ri.getAndAddToEntries(ctx, entries, c); err != nil { + return nil, err + } + } + return entries, nil +} + +func (ri *redisIndex) CidEntriesByString(ctx context.Context, cids []string) (entries *CidEntries, err error) { + entries = &CidEntries{} + var c cid.Cid + for _, cs := range cids { + c, err = cid.Decode(cs) + if err != nil { + return + } + if err = ri.getAndAddToEntries(ctx, entries, c); err != nil { + return nil, err + } + } + return entries, nil +} + +func (ri *redisIndex) CidEntriesByBlocks(ctx context.Context, bs []blocks.Block) (entries *CidEntries, err error) { + entries = &CidEntries{} + var visited = make(map[string]struct{}) + for _, b := range bs { + if _, ok := visited[b.Cid().KeyString()]; ok { + continue + } + if err = ri.getAndAddToEntries(ctx, entries, b.Cid()); err != nil { + return nil, err + } + visited[b.Cid().KeyString()] = struct{}{} + } + return entries, nil +} + +func (ri *redisIndex) getAndAddToEntries(ctx context.Context, entries *CidEntries, c cid.Cid) (err error) { + ok, release, err := ri.AcquireKey(ctx, cidKey(c)) + if err != nil { + return + } + if !ok { + release() + return ErrCidsNotExist + } + entry, err := ri.getCidEntry(ctx, c) + if err != nil { + release() + return err + } + entry.release = release + entries.entries = append(entries.entries, entry) + return +} + +func (ri *redisIndex) BlocksAdd(ctx context.Context, bs []blocks.Block) (err error) { + for _, b := range bs { + exists, release, err := ri.AcquireKey(ctx, cidKey(b.Cid())) + if err != nil { + return err + } + if !exists { + if _, err = ri.createCidEntry(ctx, b); err != nil { + release() + return err + } + } else { + log.WarnCtx(ctx, "attempt to add existing block", zap.String("cid", b.Cid().String())) + } + release() + } + return +} + +func (ri *redisIndex) CidExistsInSpace(ctx context.Context, k Key, cids []cid.Cid) (exists []cid.Cid, err error) { + _, release, err := ri.AcquireKey(ctx, spaceKey(k)) + if err != nil { + return + } + defer release() + var existsRes = make([]*redis.BoolCmd, len(cids)) + _, err = ri.cl.Pipelined(ctx, func(pipe redis.Pipeliner) error { + for i, c := range cids { + existsRes[i] = pipe.HExists(ctx, spaceKey(k), cidKey(c)) + } + return nil + }) + for i, c := range cids { + ex, e := existsRes[i].Result() + if e != nil { + return nil, e + } + if ex { + exists = append(exists, c) + } + } + return +} + +func (ri *redisIndex) getCidEntry(ctx context.Context, c cid.Cid) (entry *cidEntry, err error) { + ck := cidKey(c) + cidData, err := ri.cl.Get(ctx, ck).Result() + if err != nil { + if errors.Is(err, redis.Nil) { + err = ErrCidsNotExist + } + return + } + protoEntry := &indexproto.CidEntry{} + err = protoEntry.Unmarshal([]byte(cidData)) + if err != nil { + return + } + entry = &cidEntry{ + Cid: c, + CidEntry: protoEntry, + } + if err = ri.initCidEntry(ctx, entry); err != nil { + return nil, err + } + return +} + +func (ri *redisIndex) createCidEntry(ctx context.Context, b blocks.Block) (entry *cidEntry, err error) { + now := time.Now().Unix() + entry = &cidEntry{ + Cid: b.Cid(), + CidEntry: &indexproto.CidEntry{ + Size_: uint64(len(b.RawData())), + CreateTime: now, + UpdateTime: now, + }, + } + if err = ri.initCidEntry(ctx, entry); err != nil { + return nil, err + } + return +} + +func (ri *redisIndex) initCidEntry(ctx context.Context, entry *cidEntry) (err error) { + if entry.Version == 0 { + entry.Version = 1 + if err != nil { + return + } + if err = entry.Save(ctx, ri.cl); err != nil { + return + } + _, err = ri.cl.Pipelined(ctx, func(pipe redis.Pipeliner) error { + if e := pipe.IncrBy(ctx, cidSizeSumKey, int64(entry.Size_)).Err(); e != nil { + return e + } + if e := pipe.Incr(ctx, cidCount).Err(); e != nil { + return e + } + return nil + }) + return err + } + return +} diff --git a/index/cids_test.go b/index/cids_test.go new file mode 100644 index 00000000..1610b20b --- /dev/null +++ b/index/cids_test.go @@ -0,0 +1,130 @@ +package index + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anyproto/any-sync-filenode/index/indexproto" + "github.com/anyproto/any-sync-filenode/testutil" +) + +func TestRedisIndex_BlocksAdd(t *testing.T) { + bs := testutil.NewRandBlocks(5) + fx := newFixture(t) + defer fx.Finish(t) + + require.NoError(t, fx.BlocksAdd(ctx, bs)) + + result, err := fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + defer result.Release() + + require.Len(t, result.entries, len(bs)) + for _, e := range result.entries { + assert.NotEmpty(t, e.Size_) + assert.NotEmpty(t, e.CreateTime) + assert.NotEmpty(t, e.UpdateTime) + assert.NotEmpty(t, e.Version) + } +} + +func TestRedisIndex_CidEntries(t *testing.T) { + t.Run("success", func(t *testing.T) { + bs := testutil.NewRandBlocks(5) + fx := newFixture(t) + defer fx.Finish(t) + + require.NoError(t, fx.BlocksAdd(ctx, bs)) + + cids := testutil.BlocksToKeys(bs) + + result, err := fx.CidEntries(ctx, cids) + defer result.Release() + require.NoError(t, err) + require.Len(t, result.entries, len(bs)) + }) + t.Run("not all cids", func(t *testing.T) { + bs := testutil.NewRandBlocks(5) + fx := newFixture(t) + defer fx.Finish(t) + + require.NoError(t, fx.BlocksAdd(ctx, bs[:3])) + + cids := testutil.BlocksToKeys(bs) + + _, err := fx.CidEntries(ctx, cids) + assert.EqualError(t, err, ErrCidsNotExist.Error()) + }) + t.Run("migrate old cids", func(t *testing.T) { + bs := testutil.NewRandBlocks(5) + fx := newFixture(t) + defer fx.Finish(t) + + for _, b := range bs { + // save old entry, without version + entry := &cidEntry{ + Cid: b.Cid(), + CidEntry: &indexproto.CidEntry{ + Size_: uint64(len(b.RawData())), + CreateTime: 1, + UpdateTime: 2, + }, + } + require.NoError(t, entry.Save(ctx, fx.cl)) + } + + cids := testutil.BlocksToKeys(bs) + + result, err := fx.CidEntries(ctx, cids) + defer result.Release() + require.NoError(t, err) + require.Len(t, result.entries, len(bs)) + for _, e := range result.entries { + assert.NotEmpty(t, e.Size_) + assert.NotEmpty(t, e.CreateTime) + assert.NotEmpty(t, e.UpdateTime) + assert.NotEmpty(t, e.Version) + } + }) +} + +func TestRedisIndex_CidExistsInSpace(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + + key := newRandKey() + + bs := testutil.NewRandBlocks(5) + require.NoError(t, fx.BlocksAdd(ctx, bs)) + + cids, err := fx.CidEntriesByBlocks(ctx, bs[:2]) + require.NoError(t, err) + require.NoError(t, fx.FileBind(ctx, key, "fileId", cids)) + cids.Release() + + exists, err := fx.CidExistsInSpace(ctx, key, testutil.BlocksToKeys(bs)) + require.NoError(t, err) + + require.Len(t, exists, 2) + assert.Equal(t, testutil.BlocksToKeys(bs[:2]), exists) +} + +func TestRedisIndex_CidExists(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + + bs := testutil.NewRandBlocks(5) + require.NoError(t, fx.BlocksAdd(ctx, bs[:2])) + + for i, b := range bs { + ok, err := fx.CidExists(ctx, b.Cid()) + require.NoError(t, err) + if i < 2 { + assert.True(t, ok) + } else { + assert.False(t, ok) + } + } +} diff --git a/index/entry.go b/index/entry.go new file mode 100644 index 00000000..3849ccfb --- /dev/null +++ b/index/entry.go @@ -0,0 +1,117 @@ +package index + +import ( + "context" + "errors" + "slices" + "time" + + "github.com/redis/go-redis/v9" + + "github.com/anyproto/any-sync-filenode/index/indexproto" +) + +type fileEntry struct { + *indexproto.FileEntry +} + +func (f *fileEntry) Exists(c string) (ok bool) { + return slices.Contains(f.Cids, c) +} + +func (f *fileEntry) Save(ctx context.Context, k Key, fileId string, cl redis.Pipeliner) { + f.UpdateTime = time.Now().Unix() + data, err := f.Marshal() + if err != nil { + return + } + cl.HSet(ctx, spaceKey(k), fileKey(fileId), data) +} + +func (ri *redisIndex) getFileEntry(ctx context.Context, k Key, fileId string) (entry *fileEntry, isCreated bool, err error) { + result, err := ri.cl.HGet(ctx, spaceKey(k), fileKey(fileId)).Result() + if err != nil && !errors.Is(err, redis.Nil) { + return + } + if errors.Is(err, redis.Nil) { + return &fileEntry{ + FileEntry: &indexproto.FileEntry{ + CreateTime: time.Now().Unix(), + }, + }, true, nil + } + fileEntryProto := &indexproto.FileEntry{} + if err = fileEntryProto.Unmarshal([]byte(result)); err != nil { + return + } + return &fileEntry{FileEntry: fileEntryProto}, false, nil +} + +type spaceEntry struct { + *indexproto.SpaceEntry +} + +func (f *spaceEntry) Save(ctx context.Context, k Key, cl redis.Pipeliner) { + f.UpdateTime = time.Now().Unix() + data, err := f.Marshal() + if err != nil { + return + } + cl.HSet(ctx, spaceKey(k), infoKey, data) +} + +func (ri *redisIndex) getSpaceEntry(ctx context.Context, key Key) (entry *spaceEntry, err error) { + result, err := ri.cl.HGet(ctx, spaceKey(key), infoKey).Result() + if err != nil && !errors.Is(err, redis.Nil) { + return + } + if errors.Is(err, redis.Nil) { + now := time.Now().Unix() + return &spaceEntry{ + SpaceEntry: &indexproto.SpaceEntry{ + CreateTime: now, + UpdateTime: now, + GroupId: key.GroupId, + }, + }, nil + } + spaceEntryProto := &indexproto.SpaceEntry{} + if err = spaceEntryProto.Unmarshal([]byte(result)); err != nil { + return + } + return &spaceEntry{SpaceEntry: spaceEntryProto}, nil +} + +type groupEntry struct { + *indexproto.GroupEntry +} + +func (f *groupEntry) Save(ctx context.Context, k Key, cl redis.Pipeliner) { + f.UpdateTime = time.Now().Unix() + data, err := f.Marshal() + if err != nil { + return + } + cl.HSet(ctx, groupKey(k), infoKey, data) +} + +func (ri *redisIndex) getGroupEntry(ctx context.Context, key Key) (entry *groupEntry, err error) { + result, err := ri.cl.HGet(ctx, groupKey(key), infoKey).Result() + if err != nil && !errors.Is(err, redis.Nil) { + return + } + if errors.Is(err, redis.Nil) { + now := time.Now().Unix() + return &groupEntry{ + GroupEntry: &indexproto.GroupEntry{ + CreateTime: now, + UpdateTime: now, + }, + }, nil + } + groupEntryProto := &indexproto.GroupEntry{} + if err = groupEntryProto.Unmarshal([]byte(result)); err != nil { + return + } + return &groupEntry{GroupEntry: groupEntryProto}, nil +} diff --git a/index/index.go b/index/index.go index 147376ae..03d5ac9e 100644 --- a/index/index.go +++ b/index/index.go @@ -4,11 +4,27 @@ package index import ( "context" "errors" + "strconv" + "time" + + "github.com/OneOfOne/xxhash" "github.com/anyproto/any-sync/app" + "github.com/anyproto/any-sync/app/logger" + "github.com/anyproto/any-sync/util/periodicsync" + "github.com/go-redsync/redsync/v4" + "github.com/go-redsync/redsync/v4/redis/goredis/v9" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" + "github.com/redis/go-redis/v9" + + "github.com/anyproto/any-sync-filenode/redisprovider" + "github.com/anyproto/any-sync-filenode/store/s3store" ) +const CName = "filenode.index" + +var log = logger.NewNamed(CName) + var ( ErrCidsNotExist = errors.New("cids not exist") ErrTargetStorageExists = errors.New("target storage exists") @@ -16,29 +32,203 @@ var ( ) type Index interface { - Exists(ctx context.Context, k cid.Cid) (exists bool, err error) - IsAllExists(ctx context.Context, cids []cid.Cid) (exists bool, err error) - StorageInfo(ctx context.Context, key string) (info StorageInfo, err error) - GetNonExistentBlocks(ctx context.Context, bs []blocks.Block) (nonExists []blocks.Block, err error) - Bind(ctx context.Context, key, fileId string, bs []blocks.Block) error - BindCids(ctx context.Context, key string, fileId string, cids []cid.Cid) error - UnBind(ctx context.Context, key, fileId string) (err error) - ExistsInStorage(ctx context.Context, key string, ks []cid.Cid) (exists []cid.Cid, err error) - FileInfo(ctx context.Context, key, fileId string) (info FileInfo, err error) - StorageSize(ctx context.Context, key string) (size uint64, err error) - Lock(ctx context.Context, ks []cid.Cid) (unlock func(), err error) - AddBlocks(ctx context.Context, upload []blocks.Block) error - MoveStorage(ctx context.Context, fromKey, toKey string) error - app.Component -} - -type StorageInfo struct { - Key string - FileCount int - CidCount int + FileBind(ctx context.Context, key Key, fileId string, cidEntries *CidEntries) (err error) + FileUnbind(ctx context.Context, kye Key, fileId string) (err error) + FileInfo(ctx context.Context, key Key, fileIds ...string) (fileInfo []FileInfo, err error) + + GroupInfo(ctx context.Context, groupId string) (info GroupInfo, err error) + SpaceInfo(ctx context.Context, key Key) (info SpaceInfo, err error) + + BlocksGetNonExistent(ctx context.Context, bs []blocks.Block) (nonExistent []blocks.Block, err error) + BlocksLock(ctx context.Context, bs []blocks.Block) (unlock func(), err error) + BlocksAdd(ctx context.Context, bs []blocks.Block) (err error) + + CidExists(ctx context.Context, c cid.Cid) (ok bool, err error) + CidEntries(ctx context.Context, cids []cid.Cid) (entries *CidEntries, err error) + CidEntriesByBlocks(ctx context.Context, bs []blocks.Block) (entries *CidEntries, err error) + CidExistsInSpace(ctx context.Context, k Key, cids []cid.Cid) (exists []cid.Cid, err error) + + app.ComponentRunnable +} + +func New() Index { + return &redisIndex{} +} + +type Key struct { + GroupId string + SpaceId string +} + +type GroupInfo struct { + BytesUsage uint64 + CidsCount uint32 +} + +type SpaceInfo struct { + BytesUsage uint64 + FileCount uint32 } type FileInfo struct { BytesUsage uint64 CidCount uint32 } + +/* + Redis db structure: + CIDS: + c:{cid}: proto(Entry) + cidCount.{system}: int + cidSizeSum.{system}: int + STORES: + g:{groupId}: map + c:{cidId} -> int(refCount) + info: proto(GroupEntry} + s:{spaceId}: map + f:{fileId}: proto(FileEntry) + c:{cidId} -> int(refCount) + info: proto(SpaceEntry) + +*/ + +type redisIndex struct { + cl redis.UniversalClient + redsync *redsync.Redsync + persistStore persistentStore + persistTtl time.Duration + ticker periodicsync.PeriodicSync +} + +func (ri *redisIndex) Init(a *app.App) (err error) { + ri.cl = a.MustComponent(redisprovider.CName).(redisprovider.RedisProvider).Redis() + ri.persistStore = a.MustComponent(s3store.CName).(persistentStore) + ri.redsync = redsync.New(goredis.NewPool(ri.cl)) + // todo: move to config + ri.persistTtl = time.Hour + return +} + +func (ri *redisIndex) Name() (name string) { + return CName +} + +func (ri *redisIndex) Run(ctx context.Context) (err error) { + ri.ticker = periodicsync.NewPeriodicSync(60, time.Minute*10, func(ctx context.Context) error { + ri.PersistKeys(ctx) + return nil + }, log) + return +} + +func (ri *redisIndex) FileInfo(ctx context.Context, key Key, fileIds ...string) (fileInfos []FileInfo, err error) { + _, release, err := ri.AcquireKey(ctx, spaceKey(key)) + if err != nil { + return + } + defer release() + fileInfos = make([]FileInfo, len(fileIds)) + for i, fileId := range fileIds { + fEntry, _, err := ri.getFileEntry(ctx, key, fileId) + if err != nil { + return nil, err + } + fileInfos[i] = FileInfo{ + BytesUsage: fEntry.Size_, + CidCount: uint32(len(fEntry.Cids)), + } + } + return +} + +func (ri *redisIndex) BlocksGetNonExistent(ctx context.Context, bs []blocks.Block) (nonExistent []blocks.Block, err error) { + for _, b := range bs { + ex, err := ri.CheckKey(ctx, cidKey(b.Cid())) + if err != nil { + return nil, err + } + if !ex { + nonExistent = append(nonExistent, b) + } + } + return +} + +func (ri *redisIndex) BlocksLock(ctx context.Context, bs []blocks.Block) (unlock func(), err error) { + var lockers = make([]*redsync.Mutex, 0, len(bs)) + + unlock = func() { + for _, l := range lockers { + _, _ = l.Unlock() + } + } + + for _, b := range bs { + l := ri.redsync.NewMutex("_lock:b:"+b.Cid().String(), redsync.WithExpiry(time.Minute)) + if err = l.LockContext(ctx); err != nil { + unlock() + return nil, err + } + lockers = append(lockers, l) + } + return +} + +func (ri *redisIndex) GroupInfo(ctx context.Context, groupId string) (info GroupInfo, err error) { + _, release, err := ri.AcquireKey(ctx, groupKey(Key{GroupId: groupId})) + if err != nil { + return + } + defer release() + sEntry, err := ri.getGroupEntry(ctx, Key{GroupId: groupId}) + if err != nil { + return + } + return GroupInfo{ + BytesUsage: sEntry.Size_, + CidsCount: sEntry.CidCount, + }, nil +} + +func (ri *redisIndex) SpaceInfo(ctx context.Context, key Key) (info SpaceInfo, err error) { + _, release, err := ri.AcquireKey(ctx, spaceKey(key)) + if err != nil { + return + } + defer release() + sEntry, err := ri.getSpaceEntry(ctx, key) + if err != nil { + return + } + return SpaceInfo{ + BytesUsage: sEntry.Size_, + FileCount: sEntry.FileCount, + }, nil +} + +func (ri *redisIndex) Close(ctx context.Context) error { + if ri.ticker != nil { + ri.ticker.Close() + } + return nil +} + +func cidKey(c cid.Cid) string { + return "c:" + c.String() +} + +func spaceKey(k Key) string { + hash := strconv.FormatUint(uint64(xxhash.ChecksumString32(k.GroupId)), 36) + return "s:" + k.SpaceId + ".{" + hash + "}" +} + +func groupKey(k Key) string { + hash := strconv.FormatUint(uint64(xxhash.ChecksumString32(k.GroupId)), 36) + return "g:" + k.GroupId + ".{" + hash + "}" +} + +func fileKey(fileId string) string { + return "f:" + fileId +} + +const infoKey = "info" diff --git a/index/index_test.go b/index/index_test.go new file mode 100644 index 00000000..a8f8b5e8 --- /dev/null +++ b/index/index_test.go @@ -0,0 +1,52 @@ +package index + +import ( + "context" + "testing" + + "github.com/anyproto/any-sync/app" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/anyproto/any-sync-filenode/redisprovider/testredisprovider" + "github.com/anyproto/any-sync-filenode/store/mock_store" + "github.com/anyproto/any-sync-filenode/store/s3store" + "github.com/anyproto/any-sync-filenode/testutil" +) + +var ctx = context.Background() + +func newRandKey() Key { + return Key{ + SpaceId: testutil.NewRandSpaceId(), + GroupId: "A" + testutil.NewRandCid().String(), + } +} + +func newFixture(t *testing.T) (fx *fixture) { + ctrl := gomock.NewController(t) + fx = &fixture{ + redisIndex: New().(*redisIndex), + ctrl: ctrl, + persistStore: mock_store.NewMockStore(ctrl), + a: new(app.App), + } + fx.persistStore.EXPECT().Name().Return(s3store.CName).AnyTimes() + fx.persistStore.EXPECT().Init(gomock.Any()).AnyTimes() + + fx.a.Register(testredisprovider.NewTestRedisProvider()).Register(fx.redisIndex).Register(fx.persistStore) + require.NoError(t, fx.a.Start(ctx)) + return +} + +type fixture struct { + *redisIndex + a *app.App + ctrl *gomock.Controller + persistStore *mock_store.MockStore +} + +func (fx *fixture) Finish(t require.TestingT) { + require.NoError(t, fx.a.Close(ctx)) + fx.ctrl.Finish() +} diff --git a/index/indexproto/index.pb.go b/index/indexproto/index.pb.go new file mode 100644 index 00000000..16bf4cdb --- /dev/null +++ b/index/indexproto/index.pb.go @@ -0,0 +1,1616 @@ +// Code generated by protoc-gen-gogo. DO NOT EDIT. +// source: index/redisindex/indexproto/protos/index.proto + +package indexproto + +import ( + fmt "fmt" + io "io" + math "math" + math_bits "math/bits" + + proto "github.com/gogo/protobuf/proto" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package + +type CidEntry struct { + Size_ uint64 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` + CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` + UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` + Refs int32 `protobuf:"varint,4,opt,name=refs,proto3" json:"refs,omitempty"` + GroupIds []string `protobuf:"bytes,5,rep,name=groupIds,proto3" json:"groupIds,omitempty"` + Version uint32 `protobuf:"varint,6,opt,name=version,proto3" json:"version,omitempty"` +} + +func (m *CidEntry) Reset() { *m = CidEntry{} } +func (m *CidEntry) String() string { return proto.CompactTextString(m) } +func (*CidEntry) ProtoMessage() {} +func (*CidEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_01af1a9166444478, []int{0} +} +func (m *CidEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CidEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CidEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CidEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_CidEntry.Merge(m, src) +} +func (m *CidEntry) XXX_Size() int { + return m.Size() +} +func (m *CidEntry) XXX_DiscardUnknown() { + xxx_messageInfo_CidEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_CidEntry proto.InternalMessageInfo + +func (m *CidEntry) GetSize_() uint64 { + if m != nil { + return m.Size_ + } + return 0 +} + +func (m *CidEntry) GetCreateTime() int64 { + if m != nil { + return m.CreateTime + } + return 0 +} + +func (m *CidEntry) GetUpdateTime() int64 { + if m != nil { + return m.UpdateTime + } + return 0 +} + +func (m *CidEntry) GetRefs() int32 { + if m != nil { + return m.Refs + } + return 0 +} + +func (m *CidEntry) GetGroupIds() []string { + if m != nil { + return m.GroupIds + } + return nil +} + +func (m *CidEntry) GetVersion() uint32 { + if m != nil { + return m.Version + } + return 0 +} + +type CidList struct { + Cids [][]byte `protobuf:"bytes,1,rep,name=cids,proto3" json:"cids,omitempty"` +} + +func (m *CidList) Reset() { *m = CidList{} } +func (m *CidList) String() string { return proto.CompactTextString(m) } +func (*CidList) ProtoMessage() {} +func (*CidList) Descriptor() ([]byte, []int) { + return fileDescriptor_01af1a9166444478, []int{1} +} +func (m *CidList) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *CidList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_CidList.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *CidList) XXX_Merge(src proto.Message) { + xxx_messageInfo_CidList.Merge(m, src) +} +func (m *CidList) XXX_Size() int { + return m.Size() +} +func (m *CidList) XXX_DiscardUnknown() { + xxx_messageInfo_CidList.DiscardUnknown(m) +} + +var xxx_messageInfo_CidList proto.InternalMessageInfo + +func (m *CidList) GetCids() [][]byte { + if m != nil { + return m.Cids + } + return nil +} + +type GroupEntry struct { + GroupId string `protobuf:"bytes,1,opt,name=groupId,proto3" json:"groupId,omitempty"` + CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` + UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` + Size_ uint64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + CidCount uint32 `protobuf:"varint,5,opt,name=cidCount,proto3" json:"cidCount,omitempty"` +} + +func (m *GroupEntry) Reset() { *m = GroupEntry{} } +func (m *GroupEntry) String() string { return proto.CompactTextString(m) } +func (*GroupEntry) ProtoMessage() {} +func (*GroupEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_01af1a9166444478, []int{2} +} +func (m *GroupEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *GroupEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_GroupEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *GroupEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_GroupEntry.Merge(m, src) +} +func (m *GroupEntry) XXX_Size() int { + return m.Size() +} +func (m *GroupEntry) XXX_DiscardUnknown() { + xxx_messageInfo_GroupEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_GroupEntry proto.InternalMessageInfo + +func (m *GroupEntry) GetGroupId() string { + if m != nil { + return m.GroupId + } + return "" +} + +func (m *GroupEntry) GetCreateTime() int64 { + if m != nil { + return m.CreateTime + } + return 0 +} + +func (m *GroupEntry) GetUpdateTime() int64 { + if m != nil { + return m.UpdateTime + } + return 0 +} + +func (m *GroupEntry) GetSize_() uint64 { + if m != nil { + return m.Size_ + } + return 0 +} + +func (m *GroupEntry) GetCidCount() uint32 { + if m != nil { + return m.CidCount + } + return 0 +} + +type SpaceEntry struct { + GroupId string `protobuf:"bytes,1,opt,name=groupId,proto3" json:"groupId,omitempty"` + CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` + UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` + Size_ uint64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + FileCount uint32 `protobuf:"varint,5,opt,name=fileCount,proto3" json:"fileCount,omitempty"` + CidCount uint32 `protobuf:"varint,6,opt,name=cidCount,proto3" json:"cidCount,omitempty"` +} + +func (m *SpaceEntry) Reset() { *m = SpaceEntry{} } +func (m *SpaceEntry) String() string { return proto.CompactTextString(m) } +func (*SpaceEntry) ProtoMessage() {} +func (*SpaceEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_01af1a9166444478, []int{3} +} +func (m *SpaceEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *SpaceEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_SpaceEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *SpaceEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_SpaceEntry.Merge(m, src) +} +func (m *SpaceEntry) XXX_Size() int { + return m.Size() +} +func (m *SpaceEntry) XXX_DiscardUnknown() { + xxx_messageInfo_SpaceEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_SpaceEntry proto.InternalMessageInfo + +func (m *SpaceEntry) GetGroupId() string { + if m != nil { + return m.GroupId + } + return "" +} + +func (m *SpaceEntry) GetCreateTime() int64 { + if m != nil { + return m.CreateTime + } + return 0 +} + +func (m *SpaceEntry) GetUpdateTime() int64 { + if m != nil { + return m.UpdateTime + } + return 0 +} + +func (m *SpaceEntry) GetSize_() uint64 { + if m != nil { + return m.Size_ + } + return 0 +} + +func (m *SpaceEntry) GetFileCount() uint32 { + if m != nil { + return m.FileCount + } + return 0 +} + +func (m *SpaceEntry) GetCidCount() uint32 { + if m != nil { + return m.CidCount + } + return 0 +} + +type FileEntry struct { + Cids []string `protobuf:"bytes,1,rep,name=cids,proto3" json:"cids,omitempty"` + Size_ uint64 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` + CreateTime int64 `protobuf:"varint,3,opt,name=createTime,proto3" json:"createTime,omitempty"` + UpdateTime int64 `protobuf:"varint,4,opt,name=updateTime,proto3" json:"updateTime,omitempty"` +} + +func (m *FileEntry) Reset() { *m = FileEntry{} } +func (m *FileEntry) String() string { return proto.CompactTextString(m) } +func (*FileEntry) ProtoMessage() {} +func (*FileEntry) Descriptor() ([]byte, []int) { + return fileDescriptor_01af1a9166444478, []int{4} +} +func (m *FileEntry) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *FileEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_FileEntry.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalToSizedBuffer(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (m *FileEntry) XXX_Merge(src proto.Message) { + xxx_messageInfo_FileEntry.Merge(m, src) +} +func (m *FileEntry) XXX_Size() int { + return m.Size() +} +func (m *FileEntry) XXX_DiscardUnknown() { + xxx_messageInfo_FileEntry.DiscardUnknown(m) +} + +var xxx_messageInfo_FileEntry proto.InternalMessageInfo + +func (m *FileEntry) GetCids() []string { + if m != nil { + return m.Cids + } + return nil +} + +func (m *FileEntry) GetSize_() uint64 { + if m != nil { + return m.Size_ + } + return 0 +} + +func (m *FileEntry) GetCreateTime() int64 { + if m != nil { + return m.CreateTime + } + return 0 +} + +func (m *FileEntry) GetUpdateTime() int64 { + if m != nil { + return m.UpdateTime + } + return 0 +} + +func init() { + proto.RegisterType((*CidEntry)(nil), "fileIndexProto.CidEntry") + proto.RegisterType((*CidList)(nil), "fileIndexProto.CidList") + proto.RegisterType((*GroupEntry)(nil), "fileIndexProto.GroupEntry") + proto.RegisterType((*SpaceEntry)(nil), "fileIndexProto.SpaceEntry") + proto.RegisterType((*FileEntry)(nil), "fileIndexProto.FileEntry") +} + +func init() { + proto.RegisterFile("index/redisindex/indexproto/protos/index.proto", fileDescriptor_01af1a9166444478) +} + +var fileDescriptor_01af1a9166444478 = []byte{ + // 347 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x52, 0x3d, 0x4e, 0xf3, 0x40, + 0x10, 0xcd, 0xc6, 0xce, 0x8f, 0x47, 0xdf, 0x47, 0xe1, 0x6a, 0x15, 0x60, 0x65, 0xb9, 0xda, 0x2a, + 0x29, 0x10, 0x17, 0x20, 0x02, 0x14, 0x89, 0x02, 0x2d, 0x54, 0x74, 0xc1, 0xbb, 0x41, 0x23, 0x05, + 0xdb, 0xda, 0x75, 0x10, 0x70, 0x0a, 0x2a, 0xce, 0xc0, 0x01, 0x38, 0x04, 0x65, 0x4a, 0x4a, 0x94, + 0x5c, 0x04, 0xed, 0x9a, 0x38, 0x26, 0x12, 0xd0, 0x20, 0xd1, 0x8c, 0xe7, 0xbd, 0x91, 0xc6, 0xef, + 0xbd, 0x1d, 0xe8, 0x63, 0x2a, 0xd5, 0xed, 0x40, 0x2b, 0x89, 0xa6, 0x6c, 0x5d, 0xcd, 0x75, 0x56, + 0x64, 0x03, 0x57, 0x4d, 0xc9, 0xf4, 0x1d, 0x08, 0xb7, 0x26, 0x38, 0x55, 0x23, 0x4b, 0x9c, 0x5a, + 0x1c, 0x3f, 0x11, 0xe8, 0x0e, 0x51, 0x1e, 0xa6, 0x85, 0xbe, 0x0b, 0x43, 0xf0, 0x0d, 0xde, 0x2b, + 0x4a, 0x22, 0xc2, 0x7d, 0xe1, 0xfa, 0x90, 0x01, 0x24, 0x5a, 0x8d, 0x0b, 0x75, 0x8e, 0xd7, 0x8a, + 0x36, 0x23, 0xc2, 0x3d, 0x51, 0x63, 0xec, 0x7c, 0x96, 0xcb, 0xd5, 0xdc, 0x2b, 0xe7, 0x6b, 0xc6, + 0xee, 0xd4, 0x6a, 0x62, 0xa8, 0x1f, 0x11, 0xde, 0x12, 0xae, 0x0f, 0x7b, 0xd0, 0xbd, 0xd2, 0xd9, + 0x2c, 0x1f, 0x49, 0x43, 0x5b, 0x91, 0xc7, 0x03, 0x51, 0xe1, 0x90, 0x42, 0xe7, 0x46, 0x69, 0x83, + 0x59, 0x4a, 0xdb, 0x11, 0xe1, 0xff, 0xc5, 0x0a, 0xc6, 0xbb, 0xd0, 0x19, 0xa2, 0x3c, 0x41, 0x53, + 0xd8, 0xa5, 0x09, 0x4a, 0x43, 0x49, 0xe4, 0xf1, 0x7f, 0xc2, 0xf5, 0xf1, 0x23, 0x01, 0x38, 0xb6, + 0x5b, 0x4a, 0x2f, 0x14, 0x3a, 0x1f, 0x3b, 0x9d, 0x9d, 0x40, 0xac, 0xe0, 0x6f, 0x38, 0x72, 0x29, + 0xf9, 0xb5, 0x94, 0x7a, 0xd0, 0x4d, 0x50, 0x0e, 0xb3, 0x59, 0x5a, 0xd0, 0x96, 0x93, 0x5d, 0xe1, + 0xf8, 0x99, 0x00, 0x9c, 0xe5, 0xe3, 0x44, 0xfd, 0x85, 0xb0, 0x1d, 0x08, 0xec, 0x8b, 0xd7, 0x95, + 0xad, 0x89, 0x4f, 0xb2, 0xdb, 0x1b, 0xb2, 0x0d, 0x04, 0x47, 0x38, 0x55, 0xd5, 0x65, 0x54, 0x81, + 0x07, 0x65, 0xe0, 0xd5, 0xef, 0x9a, 0x5f, 0x5e, 0x8b, 0xf7, 0x83, 0x05, 0x7f, 0xd3, 0xc2, 0xc1, + 0xfe, 0xcb, 0x82, 0x91, 0xf9, 0x82, 0x91, 0xb7, 0x05, 0x23, 0x0f, 0x4b, 0xd6, 0x98, 0x2f, 0x59, + 0xe3, 0x75, 0xc9, 0x1a, 0x17, 0xdb, 0xdf, 0x1c, 0xfa, 0x65, 0xdb, 0x7d, 0xf6, 0xde, 0x03, 0x00, + 0x00, 0xff, 0xff, 0x0b, 0x6f, 0x04, 0xe8, 0x0e, 0x03, 0x00, 0x00, +} + +func (m *CidEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CidEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CidEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.Version != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.Version)) + i-- + dAtA[i] = 0x30 + } + if len(m.GroupIds) > 0 { + for iNdEx := len(m.GroupIds) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.GroupIds[iNdEx]) + copy(dAtA[i:], m.GroupIds[iNdEx]) + i = encodeVarintIndex(dAtA, i, uint64(len(m.GroupIds[iNdEx]))) + i-- + dAtA[i] = 0x2a + } + } + if m.Refs != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.Refs)) + i-- + dAtA[i] = 0x20 + } + if m.UpdateTime != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.UpdateTime)) + i-- + dAtA[i] = 0x18 + } + if m.CreateTime != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.CreateTime)) + i-- + dAtA[i] = 0x10 + } + if m.Size_ != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.Size_)) + i-- + dAtA[i] = 0x8 + } + return len(dAtA) - i, nil +} + +func (m *CidList) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *CidList) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *CidList) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if len(m.Cids) > 0 { + for iNdEx := len(m.Cids) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Cids[iNdEx]) + copy(dAtA[i:], m.Cids[iNdEx]) + i = encodeVarintIndex(dAtA, i, uint64(len(m.Cids[iNdEx]))) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func (m *GroupEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *GroupEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *GroupEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.CidCount != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.CidCount)) + i-- + dAtA[i] = 0x28 + } + if m.Size_ != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.Size_)) + i-- + dAtA[i] = 0x20 + } + if m.UpdateTime != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.UpdateTime)) + i-- + dAtA[i] = 0x18 + } + if m.CreateTime != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.CreateTime)) + i-- + dAtA[i] = 0x10 + } + if len(m.GroupId) > 0 { + i -= len(m.GroupId) + copy(dAtA[i:], m.GroupId) + i = encodeVarintIndex(dAtA, i, uint64(len(m.GroupId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *SpaceEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *SpaceEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *SpaceEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.CidCount != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.CidCount)) + i-- + dAtA[i] = 0x30 + } + if m.FileCount != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.FileCount)) + i-- + dAtA[i] = 0x28 + } + if m.Size_ != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.Size_)) + i-- + dAtA[i] = 0x20 + } + if m.UpdateTime != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.UpdateTime)) + i-- + dAtA[i] = 0x18 + } + if m.CreateTime != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.CreateTime)) + i-- + dAtA[i] = 0x10 + } + if len(m.GroupId) > 0 { + i -= len(m.GroupId) + copy(dAtA[i:], m.GroupId) + i = encodeVarintIndex(dAtA, i, uint64(len(m.GroupId))) + i-- + dAtA[i] = 0xa + } + return len(dAtA) - i, nil +} + +func (m *FileEntry) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalToSizedBuffer(dAtA[:size]) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *FileEntry) MarshalTo(dAtA []byte) (int, error) { + size := m.Size() + return m.MarshalToSizedBuffer(dAtA[:size]) +} + +func (m *FileEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { + i := len(dAtA) + _ = i + var l int + _ = l + if m.UpdateTime != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.UpdateTime)) + i-- + dAtA[i] = 0x20 + } + if m.CreateTime != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.CreateTime)) + i-- + dAtA[i] = 0x18 + } + if m.Size_ != 0 { + i = encodeVarintIndex(dAtA, i, uint64(m.Size_)) + i-- + dAtA[i] = 0x10 + } + if len(m.Cids) > 0 { + for iNdEx := len(m.Cids) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.Cids[iNdEx]) + copy(dAtA[i:], m.Cids[iNdEx]) + i = encodeVarintIndex(dAtA, i, uint64(len(m.Cids[iNdEx]))) + i-- + dAtA[i] = 0xa + } + } + return len(dAtA) - i, nil +} + +func encodeVarintIndex(dAtA []byte, offset int, v uint64) int { + offset -= sovIndex(v) + base := offset + for v >= 1<<7 { + dAtA[offset] = uint8(v&0x7f | 0x80) + v >>= 7 + offset++ + } + dAtA[offset] = uint8(v) + return base +} +func (m *CidEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Size_ != 0 { + n += 1 + sovIndex(uint64(m.Size_)) + } + if m.CreateTime != 0 { + n += 1 + sovIndex(uint64(m.CreateTime)) + } + if m.UpdateTime != 0 { + n += 1 + sovIndex(uint64(m.UpdateTime)) + } + if m.Refs != 0 { + n += 1 + sovIndex(uint64(m.Refs)) + } + if len(m.GroupIds) > 0 { + for _, s := range m.GroupIds { + l = len(s) + n += 1 + l + sovIndex(uint64(l)) + } + } + if m.Version != 0 { + n += 1 + sovIndex(uint64(m.Version)) + } + return n +} + +func (m *CidList) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Cids) > 0 { + for _, b := range m.Cids { + l = len(b) + n += 1 + l + sovIndex(uint64(l)) + } + } + return n +} + +func (m *GroupEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.GroupId) + if l > 0 { + n += 1 + l + sovIndex(uint64(l)) + } + if m.CreateTime != 0 { + n += 1 + sovIndex(uint64(m.CreateTime)) + } + if m.UpdateTime != 0 { + n += 1 + sovIndex(uint64(m.UpdateTime)) + } + if m.Size_ != 0 { + n += 1 + sovIndex(uint64(m.Size_)) + } + if m.CidCount != 0 { + n += 1 + sovIndex(uint64(m.CidCount)) + } + return n +} + +func (m *SpaceEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.GroupId) + if l > 0 { + n += 1 + l + sovIndex(uint64(l)) + } + if m.CreateTime != 0 { + n += 1 + sovIndex(uint64(m.CreateTime)) + } + if m.UpdateTime != 0 { + n += 1 + sovIndex(uint64(m.UpdateTime)) + } + if m.Size_ != 0 { + n += 1 + sovIndex(uint64(m.Size_)) + } + if m.FileCount != 0 { + n += 1 + sovIndex(uint64(m.FileCount)) + } + if m.CidCount != 0 { + n += 1 + sovIndex(uint64(m.CidCount)) + } + return n +} + +func (m *FileEntry) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Cids) > 0 { + for _, s := range m.Cids { + l = len(s) + n += 1 + l + sovIndex(uint64(l)) + } + } + if m.Size_ != 0 { + n += 1 + sovIndex(uint64(m.Size_)) + } + if m.CreateTime != 0 { + n += 1 + sovIndex(uint64(m.CreateTime)) + } + if m.UpdateTime != 0 { + n += 1 + sovIndex(uint64(m.UpdateTime)) + } + return n +} + +func sovIndex(x uint64) (n int) { + return (math_bits.Len64(x|1) + 6) / 7 +} +func sozIndex(x uint64) (n int) { + return sovIndex(uint64((x << 1) ^ uint64((int64(x) >> 63)))) +} +func (m *CidEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CidEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CidEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Size_", wireType) + } + m.Size_ = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Size_ |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CreateTime", wireType) + } + m.CreateTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CreateTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UpdateTime", wireType) + } + m.UpdateTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UpdateTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Refs", wireType) + } + m.Refs = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Refs |= int32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field GroupIds", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthIndex + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthIndex + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.GroupIds = append(m.GroupIds, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType) + } + m.Version = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Version |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipIndex(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthIndex + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *CidList) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: CidList: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: CidList: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Cids", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= int(b&0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthIndex + } + postIndex := iNdEx + byteLen + if postIndex < 0 { + return ErrInvalidLengthIndex + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Cids = append(m.Cids, make([]byte, postIndex-iNdEx)) + copy(m.Cids[len(m.Cids)-1], dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipIndex(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthIndex + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *GroupEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: GroupEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: GroupEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field GroupId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthIndex + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthIndex + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.GroupId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CreateTime", wireType) + } + m.CreateTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CreateTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UpdateTime", wireType) + } + m.UpdateTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UpdateTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Size_", wireType) + } + m.Size_ = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Size_ |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CidCount", wireType) + } + m.CidCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CidCount |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipIndex(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthIndex + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *SpaceEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: SpaceEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: SpaceEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field GroupId", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthIndex + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthIndex + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.GroupId = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CreateTime", wireType) + } + m.CreateTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CreateTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UpdateTime", wireType) + } + m.UpdateTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UpdateTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Size_", wireType) + } + m.Size_ = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Size_ |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field FileCount", wireType) + } + m.FileCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.FileCount |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CidCount", wireType) + } + m.CidCount = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CidCount |= uint32(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipIndex(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthIndex + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *FileEntry) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: FileEntry: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: FileEntry: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Cids", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthIndex + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthIndex + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Cids = append(m.Cids, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Size_", wireType) + } + m.Size_ = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Size_ |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 3: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field CreateTime", wireType) + } + m.CreateTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.CreateTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 4: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field UpdateTime", wireType) + } + m.UpdateTime = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.UpdateTime |= int64(b&0x7F) << shift + if b < 0x80 { + break + } + } + default: + iNdEx = preIndex + skippy, err := skipIndex(dAtA[iNdEx:]) + if err != nil { + return err + } + if (skippy < 0) || (iNdEx+skippy) < 0 { + return ErrInvalidLengthIndex + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func skipIndex(dAtA []byte) (n int, err error) { + l := len(dAtA) + iNdEx := 0 + depth := 0 + for iNdEx < l { + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowIndex + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + wireType := int(wire & 0x7) + switch wireType { + case 0: + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowIndex + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + iNdEx++ + if dAtA[iNdEx-1] < 0x80 { + break + } + } + case 1: + iNdEx += 8 + case 2: + var length int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrIntOverflowIndex + } + if iNdEx >= l { + return 0, io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + length |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if length < 0 { + return 0, ErrInvalidLengthIndex + } + iNdEx += length + case 3: + depth++ + case 4: + if depth == 0 { + return 0, ErrUnexpectedEndOfGroupIndex + } + depth-- + case 5: + iNdEx += 4 + default: + return 0, fmt.Errorf("proto: illegal wireType %d", wireType) + } + if iNdEx < 0 { + return 0, ErrInvalidLengthIndex + } + if depth == 0 { + return iNdEx, nil + } + } + return 0, io.ErrUnexpectedEOF +} + +var ( + ErrInvalidLengthIndex = fmt.Errorf("proto: negative length found during unmarshaling") + ErrIntOverflowIndex = fmt.Errorf("proto: integer overflow") + ErrUnexpectedEndOfGroupIndex = fmt.Errorf("proto: unexpected end of group") +) diff --git a/index/indexproto/protos/index.proto b/index/indexproto/protos/index.proto new file mode 100644 index 00000000..99c89069 --- /dev/null +++ b/index/indexproto/protos/index.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; +package fileIndexProto; + +option go_package = "index/indexproto"; + +message CidEntry { + uint64 size = 1; + int64 createTime = 2; + int64 updateTime = 3; + int32 refs = 4; + repeated string groupIds = 5; + uint32 version = 6; +} + +message CidList { + repeated bytes cids = 1; +} + +message GroupEntry { + string groupId = 1; + int64 createTime = 2; + int64 updateTime = 3; + uint64 size = 4; + uint32 cidCount = 5; +} + +message SpaceEntry { + string groupId = 1; + int64 createTime = 2; + int64 updateTime = 3; + uint64 size = 4; + uint32 fileCount = 5; + uint32 cidCount = 6; +} + +message FileEntry { + repeated string cids = 1; + uint64 size = 2; + int64 createTime = 3; + int64 updateTime = 4; +} \ No newline at end of file diff --git a/index/loader.go b/index/loader.go new file mode 100644 index 00000000..0888b436 --- /dev/null +++ b/index/loader.go @@ -0,0 +1,238 @@ +package index + +import ( + "context" + "errors" + "math/rand" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/cespare/xxhash/v2" + "github.com/go-redsync/redsync/v4" + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +const ( + partitionCount = 256 + + persistThreads = 10 +) + +var partitions = make([]int, partitionCount) + +func init() { + for i := range partitions { + partitions[i] = i + } +} + +type persistentStore interface { + IndexGet(ctx context.Context, key string) (value []byte, err error) + IndexPut(ctx context.Context, key string, value []byte) (err error) +} + +func bloomFilterKey(key string) string { + sum := xxhash.Sum64String(key) % partitionCount + return "bf:{" + strconv.FormatUint(sum, 10) + "}" +} + +func storeKey(key string) string { + sum := xxhash.Sum64String(key) % partitionCount + return "store:{" + strconv.FormatUint(sum, 10) + "}" +} + +func (ri *redisIndex) CheckKey(ctx context.Context, key string) (exists bool, err error) { + var release func() + if exists, release, err = ri.acquireKey(ctx, key); err != nil { + return + } + release() + return +} + +func (ri *redisIndex) AcquireKey(ctx context.Context, key string) (exists bool, release func(), err error) { + if exists, release, err = ri.acquireKey(ctx, key); err != nil { + return + } + if err = ri.updateKeyUsage(ctx, key); err != nil { + release() + return false, nil, err + } + return +} + +func (ri *redisIndex) acquireKey(ctx context.Context, key string) (exists bool, release func(), err error) { + mu := ri.redsync.NewMutex("_lock:"+key, redsync.WithExpiry(time.Minute)) + if err = mu.LockContext(ctx); err != nil { + return + } + release = func() { + _, _ = mu.Unlock() + } + + // update activity by key + if err = ri.updateKeyUsage(ctx, key); err != nil { + release() + return false, nil, err + } + + // check in redis + ex, err := ri.cl.Exists(ctx, key).Result() + if err != nil { + release() + return + } + // already in redis + if ex > 0 { + return true, release, nil + } + + // check bloom filter + bfKey := bloomFilterKey(key) + bloomEx, err := ri.cl.BFExists(ctx, bfKey, key).Result() + if err != nil { + release() + return false, nil, err + } + // not in bloom filter, item not exists + if !bloomEx { + return false, release, nil + } + + // try to load from persistent store + val, err := ri.persistStore.IndexGet(ctx, key) + if err != nil { + release() + return false, nil, err + } + // nil means not found + if val == nil { + return false, release, nil + } + if err = ri.cl.Restore(ctx, key, 0, string(val)).Err(); err != nil { + release() + return false, nil, err + } + return true, release, nil +} +func (ri *redisIndex) updateKeyUsage(ctx context.Context, key string) (err error) { + sKey := storeKey(key) + return ri.cl.ZAdd(ctx, sKey, redis.Z{ + Score: float64(time.Now().Unix()), + Member: key, + }).Err() +} + +func (ri *redisIndex) PersistKeys(ctx context.Context) { + st := time.Now() + rand.Shuffle(len(partitions), func(i, j int) { + partitions[i], partitions[j] = partitions[j], partitions[i] + }) + stat := &persistStat{} + var wg sync.WaitGroup + wg.Add(len(partitions)) + var limiter = make(chan struct{}, persistThreads) + for _, part := range partitions { + limiter <- struct{}{} + go func(p int) { + defer func() { + <-limiter + wg.Done() + }() + if e := ri.persistKeys(ctx, p, stat); e != nil { + log.Warn("persist part error", zap.Error(e), zap.Int("part", p)) + } + }(part) + } + wg.Wait() + log.Info("persist", + zap.Duration("dur", time.Since(st)), + zap.Int32("handled", stat.handled.Load()), + zap.Int32("deleted", stat.deleted.Load()), + zap.Int32("missed", stat.missed.Load()), + zap.Int32("errors", stat.errors.Load()), + zap.Int32("moved", stat.moved.Load()), + zap.Int32("moved kbs", stat.movedBytes.Load()/1024), + ) +} + +func (ri *redisIndex) persistKeys(ctx context.Context, part int, stat *persistStat) (err error) { + deadline := time.Now().Add(-ri.persistTtl).Unix() + sk := "store:{" + strconv.FormatInt(int64(part), 10) + "}" + keys, err := ri.cl.ZRangeByScore(ctx, sk, &redis.ZRangeBy{ + Min: "0", + Max: strconv.FormatInt(deadline, 10), + }).Result() + if err != nil { + return + } + for _, k := range keys { + if err = ri.persistKey(ctx, sk, k, deadline, stat); err != nil { + return + } + } + return +} + +func (ri *redisIndex) persistKey(ctx context.Context, storeKey, key string, deadline int64, stat *persistStat) (err error) { + mu := ri.redsync.NewMutex("_lock:" + key) + if err = mu.LockContext(ctx); err != nil { + return + } + defer func() { + _, _ = mu.Unlock() + }() + + stat.handled.Add(1) + + // make sure lastActivity not changed + res, err := ri.cl.ZMScore(ctx, storeKey, key).Result() + if err != nil { + stat.errors.Add(1) + return + } + if int64(res[0]) > deadline { + stat.missed.Add(1) + return + } + + dump, err := ri.cl.Dump(ctx, key).Result() + if err != nil && !errors.Is(err, redis.Nil) { + stat.errors.Add(1) + return + } + // key was removed - just remove it from store queue + if errors.Is(err, redis.Nil) { + stat.deleted.Add(1) + return ri.cl.ZRem(ctx, storeKey, key).Err() + } + + // persist the dump + if err = ri.persistStore.IndexPut(ctx, key, []byte(dump)); err != nil { + return + } + // remove from queue and add to bloom filter + _, err = ri.cl.TxPipelined(ctx, func(tx redis.Pipeliner) error { + tx.ZRem(ctx, storeKey, key) + tx.BFAdd(ctx, bloomFilterKey(key), key) + return nil + }) + + stat.moved.Add(1) + stat.movedBytes.Add(int32(len(dump))) + + // remove key + return ri.cl.Del(ctx, key).Err() +} + +type persistStat struct { + handled atomic.Int32 + moved atomic.Int32 + movedBytes atomic.Int32 + missed atomic.Int32 + deleted atomic.Int32 + errors atomic.Int32 +} diff --git a/index/loader_test.go b/index/loader_test.go new file mode 100644 index 00000000..1d1c5126 --- /dev/null +++ b/index/loader_test.go @@ -0,0 +1,77 @@ +package index + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + + "github.com/anyproto/any-sync-filenode/testutil" +) + +func TestRedisIndex_PersistKeys(t *testing.T) { + t.Run("no keys", func(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + fx.persistTtl = time.Second * 2 + bs := testutil.NewRandBlocks(5) + require.NoError(t, fx.BlocksAdd(ctx, bs)) + fx.PersistKeys(ctx) + }) + t.Run("persist", func(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + fx.persistTtl = time.Second + bs := testutil.NewRandBlocks(5) + require.NoError(t, fx.BlocksAdd(ctx, bs)) + for _, b := range bs { + fx.persistStore.EXPECT().IndexPut(ctx, cidKey(b.Cid()), gomock.Any()) + } + + time.Sleep(time.Second * 3) + bs2 := testutil.NewRandBlocks(5) + require.NoError(t, fx.BlocksAdd(ctx, bs2)) + + fx.PersistKeys(ctx) + + for _, b := range bs { + res, err := fx.cl.BFExists(ctx, bloomFilterKey(cidKey(b.Cid())), cidKey(b.Cid())).Result() + require.NoError(t, err) + assert.True(t, res) + } + }) +} + +func TestRedisIndex_AcquireKey(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + fx.persistTtl = time.Second + bs := testutil.NewRandBlocks(5) + require.NoError(t, fx.BlocksAdd(ctx, bs)) + for _, b := range bs { + fx.persistStore.EXPECT().IndexPut(ctx, cidKey(b.Cid()), gomock.Any()).Do(func(_ context.Context, key string, value []byte) { + if key == cidKey(bs[0].Cid()) { + fx.persistStore.EXPECT().IndexGet(ctx, key).Return(nil, nil) + } else { + fx.persistStore.EXPECT().IndexGet(ctx, key).Return(value, nil) + } + }) + } + time.Sleep(time.Second * 3) + fx.PersistKeys(ctx) + + for i, b := range bs { + ex, release, err := fx.AcquireKey(ctx, cidKey(b.Cid())) + require.NoError(t, err) + if i == 0 { + require.False(t, ex) + } else { + require.True(t, ex) + } + release() + } + +} diff --git a/index/migrate.go b/index/migrate.go new file mode 100644 index 00000000..4885a80b --- /dev/null +++ b/index/migrate.go @@ -0,0 +1,106 @@ +package index + +import ( + "context" + "strings" + "time" + + "github.com/go-redsync/redsync/v4" + "github.com/golang/snappy" + "github.com/ipfs/go-cid" + "go.uber.org/zap" + + "github.com/anyproto/any-sync-filenode/index/indexproto" +) + +func (ri *redisIndex) Migrate(ctx context.Context, key Key) (err error) { + st := time.Now() + // fast check before lock the key + var checkKeys = []string{ + "s:" + key.SpaceId, + "s:" + key.GroupId, + } + var migrateKey string + for _, checkKey := range checkKeys { + ex, err := ri.cl.Exists(ctx, checkKey).Result() + if err != nil { + return err + } + if ex != 0 { + migrateKey = checkKey + break + } + } + // old keys doesn't exist + if migrateKey == "" { + return + } + + // lock the key + mu := ri.redsync.NewMutex("_lock:"+migrateKey, redsync.WithExpiry(time.Minute)) + if err = mu.LockContext(ctx); err != nil { + return + } + defer func() { + _, _ = mu.Unlock() + }() + + // another check under lock + ex, err := ri.cl.Exists(ctx, migrateKey).Result() + if err != nil { + return + } + if ex == 0 { + // key doesn't exist - another node did the migration + return + } + + keys, err := ri.cl.HKeys(ctx, migrateKey).Result() + if err != nil { + return + } + + for _, k := range keys { + if strings.HasPrefix(k, "f:") { + if err = ri.migrateFile(ctx, key, migrateKey, k); err != nil { + return + } + } + } + if err = ri.cl.Del(ctx, migrateKey).Err(); err != nil { + return + } + log.Info("space migrated", zap.String("spaceId", key.SpaceId), zap.Duration("dur", time.Since(st))) + return +} + +func (ri *redisIndex) migrateFile(ctx context.Context, key Key, migrateKey, fileKey string) (err error) { + data, err := ri.cl.HGet(ctx, migrateKey, fileKey).Result() + if err != nil { + return + } + + encodedData, err := snappy.Decode(nil, []byte(data)) + if err != nil { + return + } + oldList := &indexproto.CidList{} + if err = oldList.Unmarshal(encodedData); err != nil { + return + } + + var cidList = make([]cid.Cid, len(oldList.Cids)) + for i, cb := range oldList.Cids { + if cidList[i], err = cid.Cast(cb); err != nil { + return + } + } + + cidEntries, err := ri.CidEntries(ctx, cidList) + if err != nil { + return + } + defer cidEntries.Release() + + return ri.FileBind(ctx, key, fileKey[2:], cidEntries) +} diff --git a/index/migrate_test.go b/index/migrate_test.go new file mode 100644 index 00000000..d76399c0 --- /dev/null +++ b/index/migrate_test.go @@ -0,0 +1,38 @@ +package index + +import ( + "archive/zip" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRedisIndex_Migrate(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + + // load old space keys + zrd, err := zip.OpenReader("testdata/oldspace.zip") + require.NoError(t, err) + defer zrd.Close() + for _, zf := range zrd.File { + zfr, err := zf.Open() + require.NoError(t, err) + data, err := io.ReadAll(zfr) + require.NoError(t, err) + require.NoError(t, fx.cl.Restore(ctx, zf.Name, 0, string(data)).Err()) + _ = zfr.Close() + } + + // migrate + expectedSize := uint64(18248267) + key := newRandKey() + key.SpaceId = "bafyreic65hvluhooz7u43hniptb4uokmpaqm6b2aneym77pivurjt4csze.2e2j1mpearah" + require.NoError(t, fx.Migrate(ctx, key)) + + spaceInfo, err := fx.SpaceInfo(ctx, key) + require.NoError(t, err) + assert.Equal(t, expectedSize, spaceInfo.BytesUsage) +} diff --git a/index/mock_index/mock_index.go b/index/mock_index/mock_index.go index bd82cbed..2e3c08da 100644 --- a/index/mock_index/mock_index.go +++ b/index/mock_index/mock_index.go @@ -1,6 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/anyproto/any-sync-filenode/index (interfaces: Index) - +// +// Generated by this command: +// +// mockgen -destination mock_index/mock_index.go github.com/anyproto/any-sync-filenode/index Index +// // Package mock_index is a generated GoMock package. package mock_index @@ -38,164 +42,199 @@ func (m *MockIndex) EXPECT() *MockIndexMockRecorder { return m.recorder } -// AddBlocks mocks base method. -func (m *MockIndex) AddBlocks(arg0 context.Context, arg1 []blocks.Block) error { +// BlocksAdd mocks base method. +func (m *MockIndex) BlocksAdd(arg0 context.Context, arg1 []blocks.Block) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddBlocks", arg0, arg1) + ret := m.ctrl.Call(m, "BlocksAdd", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// AddBlocks indicates an expected call of AddBlocks. -func (mr *MockIndexMockRecorder) AddBlocks(arg0, arg1 interface{}) *gomock.Call { +// BlocksAdd indicates an expected call of BlocksAdd. +func (mr *MockIndexMockRecorder) BlocksAdd(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBlocks", reflect.TypeOf((*MockIndex)(nil).AddBlocks), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlocksAdd", reflect.TypeOf((*MockIndex)(nil).BlocksAdd), arg0, arg1) } -// Bind mocks base method. -func (m *MockIndex) Bind(arg0 context.Context, arg1, arg2 string, arg3 []blocks.Block) error { +// BlocksGetNonExistent mocks base method. +func (m *MockIndex) BlocksGetNonExistent(arg0 context.Context, arg1 []blocks.Block) ([]blocks.Block, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Bind", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "BlocksGetNonExistent", arg0, arg1) + ret0, _ := ret[0].([]blocks.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// Bind indicates an expected call of Bind. -func (mr *MockIndexMockRecorder) Bind(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +// BlocksGetNonExistent indicates an expected call of BlocksGetNonExistent. +func (mr *MockIndexMockRecorder) BlocksGetNonExistent(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bind", reflect.TypeOf((*MockIndex)(nil).Bind), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlocksGetNonExistent", reflect.TypeOf((*MockIndex)(nil).BlocksGetNonExistent), arg0, arg1) } -// BindCids mocks base method. -func (m *MockIndex) BindCids(arg0 context.Context, arg1, arg2 string, arg3 []cid.Cid) error { +// BlocksLock mocks base method. +func (m *MockIndex) BlocksLock(arg0 context.Context, arg1 []blocks.Block) (func(), error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "BindCids", arg0, arg1, arg2, arg3) - ret0, _ := ret[0].(error) - return ret0 + ret := m.ctrl.Call(m, "BlocksLock", arg0, arg1) + ret0, _ := ret[0].(func()) + ret1, _ := ret[1].(error) + return ret0, ret1 } -// BindCids indicates an expected call of BindCids. -func (mr *MockIndexMockRecorder) BindCids(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +// BlocksLock indicates an expected call of BlocksLock. +func (mr *MockIndexMockRecorder) BlocksLock(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BindCids", reflect.TypeOf((*MockIndex)(nil).BindCids), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BlocksLock", reflect.TypeOf((*MockIndex)(nil).BlocksLock), arg0, arg1) } -// Exists mocks base method. -func (m *MockIndex) Exists(arg0 context.Context, arg1 cid.Cid) (bool, error) { +// CidEntries mocks base method. +func (m *MockIndex) CidEntries(arg0 context.Context, arg1 []cid.Cid) (*index.CidEntries, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Exists", arg0, arg1) - ret0, _ := ret[0].(bool) + ret := m.ctrl.Call(m, "CidEntries", arg0, arg1) + ret0, _ := ret[0].(*index.CidEntries) ret1, _ := ret[1].(error) return ret0, ret1 } -// Exists indicates an expected call of Exists. -func (mr *MockIndexMockRecorder) Exists(arg0, arg1 interface{}) *gomock.Call { +// CidEntries indicates an expected call of CidEntries. +func (mr *MockIndexMockRecorder) CidEntries(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Exists", reflect.TypeOf((*MockIndex)(nil).Exists), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CidEntries", reflect.TypeOf((*MockIndex)(nil).CidEntries), arg0, arg1) } -// ExistsInStorage mocks base method. -func (m *MockIndex) ExistsInStorage(arg0 context.Context, arg1 string, arg2 []cid.Cid) ([]cid.Cid, error) { +// CidEntriesByBlocks mocks base method. +func (m *MockIndex) CidEntriesByBlocks(arg0 context.Context, arg1 []blocks.Block) (*index.CidEntries, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ExistsInStorage", arg0, arg1, arg2) - ret0, _ := ret[0].([]cid.Cid) + ret := m.ctrl.Call(m, "CidEntriesByBlocks", arg0, arg1) + ret0, _ := ret[0].(*index.CidEntries) ret1, _ := ret[1].(error) return ret0, ret1 } -// ExistsInStorage indicates an expected call of ExistsInStorage. -func (mr *MockIndexMockRecorder) ExistsInStorage(arg0, arg1, arg2 interface{}) *gomock.Call { +// CidEntriesByBlocks indicates an expected call of CidEntriesByBlocks. +func (mr *MockIndexMockRecorder) CidEntriesByBlocks(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExistsInStorage", reflect.TypeOf((*MockIndex)(nil).ExistsInStorage), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CidEntriesByBlocks", reflect.TypeOf((*MockIndex)(nil).CidEntriesByBlocks), arg0, arg1) } -// FileInfo mocks base method. -func (m *MockIndex) FileInfo(arg0 context.Context, arg1, arg2 string) (index.FileInfo, error) { +// CidExists mocks base method. +func (m *MockIndex) CidExists(arg0 context.Context, arg1 cid.Cid) (bool, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FileInfo", arg0, arg1, arg2) - ret0, _ := ret[0].(index.FileInfo) + ret := m.ctrl.Call(m, "CidExists", arg0, arg1) + ret0, _ := ret[0].(bool) ret1, _ := ret[1].(error) return ret0, ret1 } -// FileInfo indicates an expected call of FileInfo. -func (mr *MockIndexMockRecorder) FileInfo(arg0, arg1, arg2 interface{}) *gomock.Call { +// CidExists indicates an expected call of CidExists. +func (mr *MockIndexMockRecorder) CidExists(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileInfo", reflect.TypeOf((*MockIndex)(nil).FileInfo), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CidExists", reflect.TypeOf((*MockIndex)(nil).CidExists), arg0, arg1) } -// GetNonExistentBlocks mocks base method. -func (m *MockIndex) GetNonExistentBlocks(arg0 context.Context, arg1 []blocks.Block) ([]blocks.Block, error) { +// CidExistsInSpace mocks base method. +func (m *MockIndex) CidExistsInSpace(arg0 context.Context, arg1 index.Key, arg2 []cid.Cid) ([]cid.Cid, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetNonExistentBlocks", arg0, arg1) - ret0, _ := ret[0].([]blocks.Block) + ret := m.ctrl.Call(m, "CidExistsInSpace", arg0, arg1, arg2) + ret0, _ := ret[0].([]cid.Cid) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetNonExistentBlocks indicates an expected call of GetNonExistentBlocks. -func (mr *MockIndexMockRecorder) GetNonExistentBlocks(arg0, arg1 interface{}) *gomock.Call { +// CidExistsInSpace indicates an expected call of CidExistsInSpace. +func (mr *MockIndexMockRecorder) CidExistsInSpace(arg0, arg1, arg2 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNonExistentBlocks", reflect.TypeOf((*MockIndex)(nil).GetNonExistentBlocks), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CidExistsInSpace", reflect.TypeOf((*MockIndex)(nil).CidExistsInSpace), arg0, arg1, arg2) } -// Init mocks base method. -func (m *MockIndex) Init(arg0 *app.App) error { +// Close mocks base method. +func (m *MockIndex) Close(arg0 context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Init", arg0) + ret := m.ctrl.Call(m, "Close", arg0) ret0, _ := ret[0].(error) return ret0 } -// Init indicates an expected call of Init. -func (mr *MockIndexMockRecorder) Init(arg0 interface{}) *gomock.Call { +// Close indicates an expected call of Close. +func (mr *MockIndexMockRecorder) Close(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIndex)(nil).Init), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockIndex)(nil).Close), arg0) } -// IsAllExists mocks base method. -func (m *MockIndex) IsAllExists(arg0 context.Context, arg1 []cid.Cid) (bool, error) { +// FileBind mocks base method. +func (m *MockIndex) FileBind(arg0 context.Context, arg1 index.Key, arg2 string, arg3 *index.CidEntries) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsAllExists", arg0, arg1) - ret0, _ := ret[0].(bool) + ret := m.ctrl.Call(m, "FileBind", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// FileBind indicates an expected call of FileBind. +func (mr *MockIndexMockRecorder) FileBind(arg0, arg1, arg2, arg3 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileBind", reflect.TypeOf((*MockIndex)(nil).FileBind), arg0, arg1, arg2, arg3) +} + +// FileInfo mocks base method. +func (m *MockIndex) FileInfo(arg0 context.Context, arg1 index.Key, arg2 ...string) ([]index.FileInfo, error) { + m.ctrl.T.Helper() + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "FileInfo", varargs...) + ret0, _ := ret[0].([]index.FileInfo) ret1, _ := ret[1].(error) return ret0, ret1 } -// IsAllExists indicates an expected call of IsAllExists. -func (mr *MockIndexMockRecorder) IsAllExists(arg0, arg1 interface{}) *gomock.Call { +// FileInfo indicates an expected call of FileInfo. +func (mr *MockIndexMockRecorder) FileInfo(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsAllExists", reflect.TypeOf((*MockIndex)(nil).IsAllExists), arg0, arg1) + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileInfo", reflect.TypeOf((*MockIndex)(nil).FileInfo), varargs...) } -// Lock mocks base method. -func (m *MockIndex) Lock(arg0 context.Context, arg1 []cid.Cid) (func(), error) { +// FileUnbind mocks base method. +func (m *MockIndex) FileUnbind(arg0 context.Context, arg1 index.Key, arg2 string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Lock", arg0, arg1) - ret0, _ := ret[0].(func()) + ret := m.ctrl.Call(m, "FileUnbind", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// FileUnbind indicates an expected call of FileUnbind. +func (mr *MockIndexMockRecorder) FileUnbind(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileUnbind", reflect.TypeOf((*MockIndex)(nil).FileUnbind), arg0, arg1, arg2) +} + +// GroupInfo mocks base method. +func (m *MockIndex) GroupInfo(arg0 context.Context, arg1 string) (index.GroupInfo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GroupInfo", arg0, arg1) + ret0, _ := ret[0].(index.GroupInfo) ret1, _ := ret[1].(error) return ret0, ret1 } -// Lock indicates an expected call of Lock. -func (mr *MockIndexMockRecorder) Lock(arg0, arg1 interface{}) *gomock.Call { +// GroupInfo indicates an expected call of GroupInfo. +func (mr *MockIndexMockRecorder) GroupInfo(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockIndex)(nil).Lock), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GroupInfo", reflect.TypeOf((*MockIndex)(nil).GroupInfo), arg0, arg1) } -// MoveStorage mocks base method. -func (m *MockIndex) MoveStorage(arg0 context.Context, arg1, arg2 string) error { +// Init mocks base method. +func (m *MockIndex) Init(arg0 *app.App) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "MoveStorage", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "Init", arg0) ret0, _ := ret[0].(error) return ret0 } -// MoveStorage indicates an expected call of MoveStorage. -func (mr *MockIndexMockRecorder) MoveStorage(arg0, arg1, arg2 interface{}) *gomock.Call { +// Init indicates an expected call of Init. +func (mr *MockIndexMockRecorder) Init(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MoveStorage", reflect.TypeOf((*MockIndex)(nil).MoveStorage), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIndex)(nil).Init), arg0) } // Name mocks base method. @@ -212,46 +251,31 @@ func (mr *MockIndexMockRecorder) Name() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Name", reflect.TypeOf((*MockIndex)(nil).Name)) } -// StorageInfo mocks base method. -func (m *MockIndex) StorageInfo(arg0 context.Context, arg1 string) (index.StorageInfo, error) { +// Run mocks base method. +func (m *MockIndex) Run(arg0 context.Context) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StorageInfo", arg0, arg1) - ret0, _ := ret[0].(index.StorageInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 + ret := m.ctrl.Call(m, "Run", arg0) + ret0, _ := ret[0].(error) + return ret0 } -// StorageInfo indicates an expected call of StorageInfo. -func (mr *MockIndexMockRecorder) StorageInfo(arg0, arg1 interface{}) *gomock.Call { +// Run indicates an expected call of Run. +func (mr *MockIndexMockRecorder) Run(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorageInfo", reflect.TypeOf((*MockIndex)(nil).StorageInfo), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockIndex)(nil).Run), arg0) } -// StorageSize mocks base method. -func (m *MockIndex) StorageSize(arg0 context.Context, arg1 string) (uint64, error) { +// SpaceInfo mocks base method. +func (m *MockIndex) SpaceInfo(arg0 context.Context, arg1 index.Key) (index.SpaceInfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "StorageSize", arg0, arg1) - ret0, _ := ret[0].(uint64) + ret := m.ctrl.Call(m, "SpaceInfo", arg0, arg1) + ret0, _ := ret[0].(index.SpaceInfo) ret1, _ := ret[1].(error) return ret0, ret1 } -// StorageSize indicates an expected call of StorageSize. -func (mr *MockIndexMockRecorder) StorageSize(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StorageSize", reflect.TypeOf((*MockIndex)(nil).StorageSize), arg0, arg1) -} - -// UnBind mocks base method. -func (m *MockIndex) UnBind(arg0 context.Context, arg1, arg2 string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UnBind", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// UnBind indicates an expected call of UnBind. -func (mr *MockIndexMockRecorder) UnBind(arg0, arg1, arg2 interface{}) *gomock.Call { +// SpaceInfo indicates an expected call of SpaceInfo. +func (mr *MockIndexMockRecorder) SpaceInfo(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnBind", reflect.TypeOf((*MockIndex)(nil).UnBind), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SpaceInfo", reflect.TypeOf((*MockIndex)(nil).SpaceInfo), arg0, arg1) } diff --git a/index/redisindex/bind.go b/index/redisindex/bind.go deleted file mode 100644 index 6bd9b4fe..00000000 --- a/index/redisindex/bind.go +++ /dev/null @@ -1,72 +0,0 @@ -package redisindex - -import ( - "context" - "github.com/redis/go-redis/v9" -) - -type bindOp struct { - sk string - fk string - ri *redisIndex -} - -func (op *bindOp) Bind(ctx context.Context, cids []CidInfo) (newCids []CidInfo, err error) { - // fetch existing or create new file cids list - fCids, err := op.ri.newFileCidList(ctx, op.sk, op.fk) - if err != nil { - return nil, err - } - defer fCids.Unlock() - - // make a list of indexes non-exists cids - var newInFileIdx = make([]int, 0, len(cids)) - for i, c := range cids { - if !fCids.Exists(c.Cid) { - newInFileIdx = append(newInFileIdx, i) - fCids.Add(c.Cid) - } - } - - // all cids exists, nothing to do - if len(newInFileIdx) == 0 { - return nil, nil - } - - // make a list of results, data will be available after executing of pipeline - var execResults = make([]*redis.IntCmd, len(newInFileIdx)) - // do updates in one pipeline - _, err = op.ri.cl.TxPipelined(ctx, func(pipe redis.Pipeliner) error { - for i, idx := range newInFileIdx { - ck := cids[idx].Cid.String() - execResults[i] = pipe.HIncrBy(ctx, op.sk, ck, 1) - if err = execResults[i].Err(); err != nil { - return err - } - } - return fCids.Save(ctx, pipe) - }) - if err != nil { - return - } - - // check for newly added cids - // calculate spaceSize increase - // make a list of finally added cids - var spaceIncreaseSize uint64 - for i, res := range execResults { - if newCounter, err := res.Result(); err == nil && newCounter == 1 { - c := cids[newInFileIdx[i]] - spaceIncreaseSize += c.Size_ - newCids = append(newCids, c) - } - } - - // increment space size - if spaceIncreaseSize > 0 { - if err = op.ri.cl.HIncrBy(ctx, op.sk, storeSizeKey, int64(spaceIncreaseSize)).Err(); err != nil { - return nil, err - } - } - return -} diff --git a/index/redisindex/bind_test.go b/index/redisindex/bind_test.go deleted file mode 100644 index cee8048d..00000000 --- a/index/redisindex/bind_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package redisindex - -import ( - "github.com/anyproto/any-sync-filenode/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func TestRedisIndex_Bind(t *testing.T) { - t.Run("first add", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - bs := testutil.NewRandBlocks(3) - var sumSize uint64 - for _, b := range bs { - sumSize += uint64(len(b.RawData())) - } - spaceId := testutil.NewRandSpaceId() - fileId := testutil.NewRandCid().String() - - require.NoError(t, fx.Bind(ctx, spaceId, fileId, bs)) - size, err := fx.StorageSize(ctx, spaceId) - require.NoError(t, err) - assert.Equal(t, sumSize, size) - }) - t.Run("two files with same cids", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - bs := testutil.NewRandBlocks(3) - var sumSize uint64 - for _, b := range bs { - sumSize += uint64(len(b.RawData())) - } - spaceId := testutil.NewRandSpaceId() - fileId1 := testutil.NewRandCid().String() - fileId2 := testutil.NewRandCid().String() - - require.NoError(t, fx.Bind(ctx, spaceId, fileId1, bs[:2])) - require.NoError(t, fx.Bind(ctx, spaceId, fileId2, bs)) - size, err := fx.StorageSize(ctx, spaceId) - require.NoError(t, err) - assert.Equal(t, sumSize, size) - }) - t.Run("bind twice", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - bs := testutil.NewRandBlocks(3) - var sumSize uint64 - for _, b := range bs { - sumSize += uint64(len(b.RawData())) - } - spaceId := testutil.NewRandSpaceId() - fileId := testutil.NewRandCid().String() - - require.NoError(t, fx.Bind(ctx, spaceId, fileId, bs)) - require.NoError(t, fx.Bind(ctx, spaceId, fileId, bs)) - size, err := fx.StorageSize(ctx, spaceId) - require.NoError(t, err) - assert.Equal(t, sumSize, size) - }) -} - -func TestRedisIndex_BindCids(t *testing.T) { - t.Run("success", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - bs := testutil.NewRandBlocks(3) - var sumSize uint64 - for _, b := range bs { - sumSize += uint64(len(b.RawData())) - } - spaceId := testutil.NewRandSpaceId() - fileId1 := testutil.NewRandCid().String() - fileId2 := testutil.NewRandCid().String() - - require.NoError(t, fx.Bind(ctx, spaceId, fileId1, bs)) - require.NoError(t, fx.BindCids(ctx, spaceId, fileId2, testutil.BlocksToKeys(bs))) - size, err := fx.StorageSize(ctx, spaceId) - require.NoError(t, err) - assert.Equal(t, sumSize, size) - - fi1, err := fx.FileInfo(ctx, spaceId, fileId1) - require.NoError(t, err) - fi2, err := fx.FileInfo(ctx, spaceId, fileId2) - require.NoError(t, err) - - assert.Equal(t, uint32(len(bs)), fi1.CidCount) - assert.Equal(t, size, fi1.BytesUsage) - assert.Equal(t, fi1, fi2) - }) - t.Run("bind not existing cids", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - bs := testutil.NewRandBlocks(3) - var sumSize uint64 - for _, b := range bs { - sumSize += uint64(len(b.RawData())) - } - spaceId := testutil.NewRandSpaceId() - fileId1 := testutil.NewRandCid().String() - require.Error(t, fx.BindCids(ctx, spaceId, fileId1, testutil.BlocksToKeys(bs))) - }) -} diff --git a/index/redisindex/fcidlist.go b/index/redisindex/fcidlist.go deleted file mode 100644 index d5b34b8e..00000000 --- a/index/redisindex/fcidlist.go +++ /dev/null @@ -1,76 +0,0 @@ -package redisindex - -import ( - "bytes" - "context" - "github.com/anyproto/any-sync-filenode/index/redisindex/indexproto" - "github.com/go-redsync/redsync/v4" - "github.com/golang/snappy" - "github.com/ipfs/go-cid" - "github.com/redis/go-redis/v9" -) - -func (r *redisIndex) newFileCidList(ctx context.Context, sk, fk string) (l *fileCidList, err error) { - lock := r.redsync.NewMutex("_lock:fk:" + sk + ":" + fk) - if err = lock.LockContext(ctx); err != nil { - return nil, err - } - res, err := r.cl.HGet(ctx, sk, fk).Result() - if err == redis.Nil { - err = nil - } - if err != nil { - return - } - l = &fileCidList{ - CidList: &indexproto.CidList{}, - cl: r.cl, - sk: sk, - fk: fk, - lock: lock, - } - if len(res) == 0 { - return - } - data, err := snappy.Decode(nil, []byte(res)) - if err != nil { - return - } - if err = l.CidList.Unmarshal(data); err != nil { - return - } - return -} - -type fileCidList struct { - *indexproto.CidList - cl redis.UniversalClient - sk, fk string - lock *redsync.Mutex -} - -func (cl *fileCidList) Exists(k cid.Cid) bool { - for _, c := range cl.Cids { - if bytes.Equal(k.Bytes(), c) { - return true - } - } - return false -} - -func (cl *fileCidList) Add(k cid.Cid) { - cl.Cids = append(cl.Cids, k.Bytes()) -} - -func (cl *fileCidList) Unlock() { - _, _ = cl.lock.Unlock() -} - -func (cl *fileCidList) Save(ctx context.Context, tx redis.Pipeliner) (err error) { - if len(cl.Cids) == 0 { - return tx.HDel(ctx, cl.sk, cl.fk).Err() - } - data, _ := cl.Marshal() - data = snappy.Encode(nil, data) - return tx.HSet(ctx, cl.sk, cl.fk, data).Err() -} diff --git a/index/redisindex/indexproto/index.pb.go b/index/redisindex/indexproto/index.pb.go deleted file mode 100644 index 7655481c..00000000 --- a/index/redisindex/indexproto/index.pb.go +++ /dev/null @@ -1,582 +0,0 @@ -// Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: index/redisindex/indexproto/protos/index.proto - -package indexproto - -import ( - fmt "fmt" - proto "github.com/gogo/protobuf/proto" - io "io" - math "math" - math_bits "math/bits" -) - -// Reference imports to suppress errors if they are not otherwise used. -var _ = proto.Marshal -var _ = fmt.Errorf -var _ = math.Inf - -// This is a compile-time assertion to ensure that this generated file -// is compatible with the proto package it is being compiled against. -// A compilation error at this line likely means your copy of the -// proto package needs to be updated. -const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package - -type CidEntry struct { - Size_ uint64 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` - CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` - UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` - Refs int32 `protobuf:"varint,4,opt,name=refs,proto3" json:"refs,omitempty"` -} - -func (m *CidEntry) Reset() { *m = CidEntry{} } -func (m *CidEntry) String() string { return proto.CompactTextString(m) } -func (*CidEntry) ProtoMessage() {} -func (*CidEntry) Descriptor() ([]byte, []int) { - return fileDescriptor_01af1a9166444478, []int{0} -} -func (m *CidEntry) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *CidEntry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_CidEntry.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *CidEntry) XXX_Merge(src proto.Message) { - xxx_messageInfo_CidEntry.Merge(m, src) -} -func (m *CidEntry) XXX_Size() int { - return m.Size() -} -func (m *CidEntry) XXX_DiscardUnknown() { - xxx_messageInfo_CidEntry.DiscardUnknown(m) -} - -var xxx_messageInfo_CidEntry proto.InternalMessageInfo - -func (m *CidEntry) GetSize_() uint64 { - if m != nil { - return m.Size_ - } - return 0 -} - -func (m *CidEntry) GetCreateTime() int64 { - if m != nil { - return m.CreateTime - } - return 0 -} - -func (m *CidEntry) GetUpdateTime() int64 { - if m != nil { - return m.UpdateTime - } - return 0 -} - -func (m *CidEntry) GetRefs() int32 { - if m != nil { - return m.Refs - } - return 0 -} - -type CidList struct { - Cids [][]byte `protobuf:"bytes,1,rep,name=cids,proto3" json:"cids,omitempty"` -} - -func (m *CidList) Reset() { *m = CidList{} } -func (m *CidList) String() string { return proto.CompactTextString(m) } -func (*CidList) ProtoMessage() {} -func (*CidList) Descriptor() ([]byte, []int) { - return fileDescriptor_01af1a9166444478, []int{1} -} -func (m *CidList) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *CidList) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_CidList.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *CidList) XXX_Merge(src proto.Message) { - xxx_messageInfo_CidList.Merge(m, src) -} -func (m *CidList) XXX_Size() int { - return m.Size() -} -func (m *CidList) XXX_DiscardUnknown() { - xxx_messageInfo_CidList.DiscardUnknown(m) -} - -var xxx_messageInfo_CidList proto.InternalMessageInfo - -func (m *CidList) GetCids() [][]byte { - if m != nil { - return m.Cids - } - return nil -} - -func init() { - proto.RegisterType((*CidEntry)(nil), "fileIndexProto.CidEntry") - proto.RegisterType((*CidList)(nil), "fileIndexProto.CidList") -} - -func init() { - proto.RegisterFile("index/redisindex/indexproto/protos/index.proto", fileDescriptor_01af1a9166444478) -} - -var fileDescriptor_01af1a9166444478 = []byte{ - // 206 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0xcb, 0xcc, 0x4b, 0x49, - 0xad, 0xd0, 0x2f, 0x4a, 0x4d, 0xc9, 0x2c, 0x86, 0x30, 0xc1, 0x64, 0x41, 0x51, 0x7e, 0x49, 0xbe, - 0x3e, 0x98, 0x2c, 0x86, 0x88, 0xe8, 0x81, 0x39, 0x42, 0x7c, 0x69, 0x99, 0x39, 0xa9, 0x9e, 0x20, - 0x81, 0x00, 0x10, 0x5f, 0xa9, 0x88, 0x8b, 0xc3, 0x39, 0x33, 0xc5, 0x35, 0xaf, 0xa4, 0xa8, 0x52, - 0x48, 0x88, 0x8b, 0xa5, 0x38, 0xb3, 0x2a, 0x55, 0x82, 0x51, 0x81, 0x51, 0x83, 0x25, 0x08, 0xcc, - 0x16, 0x92, 0xe3, 0xe2, 0x4a, 0x2e, 0x4a, 0x4d, 0x2c, 0x49, 0x0d, 0xc9, 0xcc, 0x4d, 0x95, 0x60, - 0x52, 0x60, 0xd4, 0x60, 0x0e, 0x42, 0x12, 0x01, 0xc9, 0x97, 0x16, 0xa4, 0xc0, 0xe4, 0x99, 0x21, - 0xf2, 0x08, 0x11, 0x90, 0x99, 0x45, 0xa9, 0x69, 0xc5, 0x12, 0x2c, 0x0a, 0x8c, 0x1a, 0xac, 0x41, - 0x60, 0xb6, 0x92, 0x2c, 0x17, 0xbb, 0x73, 0x66, 0x8a, 0x4f, 0x66, 0x71, 0x09, 0x48, 0x3a, 0x39, - 0x33, 0xa5, 0x58, 0x82, 0x51, 0x81, 0x59, 0x83, 0x27, 0x08, 0xcc, 0x76, 0x32, 0x3d, 0xf1, 0x48, - 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4, 0x18, 0x27, 0x3c, 0x96, 0x63, 0xb8, 0xf0, - 0x58, 0x8e, 0xe1, 0xc6, 0x63, 0x39, 0x86, 0x28, 0x69, 0x3c, 0x9e, 0x4d, 0x62, 0x03, 0x53, 0xc6, - 0x80, 0x00, 0x00, 0x00, 0xff, 0xff, 0xd6, 0xa3, 0x20, 0x4c, 0x12, 0x01, 0x00, 0x00, -} - -func (m *CidEntry) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *CidEntry) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *CidEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if m.Refs != 0 { - i = encodeVarintIndex(dAtA, i, uint64(m.Refs)) - i-- - dAtA[i] = 0x20 - } - if m.UpdateTime != 0 { - i = encodeVarintIndex(dAtA, i, uint64(m.UpdateTime)) - i-- - dAtA[i] = 0x18 - } - if m.CreateTime != 0 { - i = encodeVarintIndex(dAtA, i, uint64(m.CreateTime)) - i-- - dAtA[i] = 0x10 - } - if m.Size_ != 0 { - i = encodeVarintIndex(dAtA, i, uint64(m.Size_)) - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - -func (m *CidList) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *CidList) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *CidList) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Cids) > 0 { - for iNdEx := len(m.Cids) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.Cids[iNdEx]) - copy(dAtA[i:], m.Cids[iNdEx]) - i = encodeVarintIndex(dAtA, i, uint64(len(m.Cids[iNdEx]))) - i-- - dAtA[i] = 0xa - } - } - return len(dAtA) - i, nil -} - -func encodeVarintIndex(dAtA []byte, offset int, v uint64) int { - offset -= sovIndex(v) - base := offset - for v >= 1<<7 { - dAtA[offset] = uint8(v&0x7f | 0x80) - v >>= 7 - offset++ - } - dAtA[offset] = uint8(v) - return base -} -func (m *CidEntry) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Size_ != 0 { - n += 1 + sovIndex(uint64(m.Size_)) - } - if m.CreateTime != 0 { - n += 1 + sovIndex(uint64(m.CreateTime)) - } - if m.UpdateTime != 0 { - n += 1 + sovIndex(uint64(m.UpdateTime)) - } - if m.Refs != 0 { - n += 1 + sovIndex(uint64(m.Refs)) - } - return n -} - -func (m *CidList) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if len(m.Cids) > 0 { - for _, b := range m.Cids { - l = len(b) - n += 1 + l + sovIndex(uint64(l)) - } - } - return n -} - -func sovIndex(x uint64) (n int) { - return (math_bits.Len64(x|1) + 6) / 7 -} -func sozIndex(x uint64) (n int) { - return sovIndex(uint64((x << 1) ^ uint64((int64(x) >> 63)))) -} -func (m *CidEntry) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIndex - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: CidEntry: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: CidEntry: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Size_", wireType) - } - m.Size_ = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIndex - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Size_ |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field CreateTime", wireType) - } - m.CreateTime = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIndex - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.CreateTime |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field UpdateTime", wireType) - } - m.UpdateTime = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIndex - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.UpdateTime |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 4: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Refs", wireType) - } - m.Refs = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIndex - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Refs |= int32(b&0x7F) << shift - if b < 0x80 { - break - } - } - default: - iNdEx = preIndex - skippy, err := skipIndex(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthIndex - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func (m *CidList) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIndex - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: CidList: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: CidList: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Cids", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIndex - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthIndex - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthIndex - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Cids = append(m.Cids, make([]byte, postIndex-iNdEx)) - copy(m.Cids[len(m.Cids)-1], dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipIndex(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthIndex - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} -func skipIndex(dAtA []byte) (n int, err error) { - l := len(dAtA) - iNdEx := 0 - depth := 0 - for iNdEx < l { - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowIndex - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= (uint64(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - wireType := int(wire & 0x7) - switch wireType { - case 0: - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowIndex - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - iNdEx++ - if dAtA[iNdEx-1] < 0x80 { - break - } - } - case 1: - iNdEx += 8 - case 2: - var length int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return 0, ErrIntOverflowIndex - } - if iNdEx >= l { - return 0, io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - length |= (int(b) & 0x7F) << shift - if b < 0x80 { - break - } - } - if length < 0 { - return 0, ErrInvalidLengthIndex - } - iNdEx += length - case 3: - depth++ - case 4: - if depth == 0 { - return 0, ErrUnexpectedEndOfGroupIndex - } - depth-- - case 5: - iNdEx += 4 - default: - return 0, fmt.Errorf("proto: illegal wireType %d", wireType) - } - if iNdEx < 0 { - return 0, ErrInvalidLengthIndex - } - if depth == 0 { - return iNdEx, nil - } - } - return 0, io.ErrUnexpectedEOF -} - -var ( - ErrInvalidLengthIndex = fmt.Errorf("proto: negative length found during unmarshaling") - ErrIntOverflowIndex = fmt.Errorf("proto: integer overflow") - ErrUnexpectedEndOfGroupIndex = fmt.Errorf("proto: unexpected end of group") -) diff --git a/index/redisindex/indexproto/protos/index.proto b/index/redisindex/indexproto/protos/index.proto deleted file mode 100644 index f524a08f..00000000 --- a/index/redisindex/indexproto/protos/index.proto +++ /dev/null @@ -1,15 +0,0 @@ -syntax = "proto3"; -package fileIndexProto; - -option go_package = "index/redisindex/indexproto"; - -message CidEntry { - uint64 size = 1; - int64 createTime = 2; - int64 updateTime = 3; - int32 refs = 4; -} - -message CidList { - repeated bytes cids = 1; -} \ No newline at end of file diff --git a/index/redisindex/redisindex.go b/index/redisindex/redisindex.go deleted file mode 100644 index 735bde5d..00000000 --- a/index/redisindex/redisindex.go +++ /dev/null @@ -1,372 +0,0 @@ -package redisindex - -import ( - "context" - "github.com/anyproto/any-sync-filenode/index" - "github.com/anyproto/any-sync-filenode/index/redisindex/indexproto" - "github.com/anyproto/any-sync-filenode/redisprovider" - "github.com/anyproto/any-sync/app" - "github.com/anyproto/any-sync/app/logger" - "github.com/go-redsync/redsync/v4" - "github.com/go-redsync/redsync/v4/redis/goredis/v9" - blocks "github.com/ipfs/go-block-format" - "github.com/ipfs/go-cid" - "github.com/redis/go-redis/v9" - "strconv" - "strings" - "time" -) - -/* - Redis db structure: - CIDS: - c:{cid}: proto(Entry) - - STORES: - s:{storeKey}: map - f:{fileId} -> snappy(proto(CidList)) - {cid} -> int(refCount) - size -> int(summarySize) - -*/ - -const CName = "filenode.redisindex" - -var log = logger.NewNamed(CName) - -const ( - storeSizeKey = "size" -) - -func New() index.Index { - return new(redisIndex) -} - -type CidInfo struct { - Cid cid.Cid - *indexproto.CidEntry -} - -type redisIndex struct { - cl redis.UniversalClient - redsync *redsync.Redsync -} - -func (r *redisIndex) Init(a *app.App) (err error) { - r.cl = a.MustComponent(redisprovider.CName).(redisprovider.RedisProvider).Redis() - r.redsync = redsync.New(goredis.NewPool(r.cl)) - return nil -} - -func (r *redisIndex) Name() (name string) { - return CName -} - -func (r *redisIndex) Exists(ctx context.Context, k cid.Cid) (exists bool, err error) { - res, err := r.cl.Exists(ctx, cidKey(k.String())).Result() - if err != nil { - return - } - return res != 0, nil -} - -func (r *redisIndex) IsAllExists(ctx context.Context, cids []cid.Cid) (exists bool, err error) { - for _, c := range cids { - if ex, e := r.Exists(ctx, c); e != nil { - return false, e - } else if !ex { - return false, nil - } - } - return true, nil -} - -func (r *redisIndex) GetNonExistentBlocks(ctx context.Context, bs []blocks.Block) (nonExistent []blocks.Block, err error) { - nonExistent = make([]blocks.Block, 0, len(bs)) - for _, b := range bs { - if ex, e := r.Exists(ctx, b.Cid()); e != nil { - return nil, e - } else if !ex { - nonExistent = append(nonExistent, b) - } - } - return -} - -func (r *redisIndex) Bind(ctx context.Context, key, fileId string, bs []blocks.Block) error { - bop := bindOp{ - sk: storageKey(key), - fk: fileIdKey(fileId), - ri: r, - } - newCids, err := bop.Bind(ctx, r.cidInfoByBlocks(bs)) - if err != nil { - return err - } - return r.cidsAddRef(ctx, newCids) -} - -func (r *redisIndex) BindCids(ctx context.Context, key, fileId string, ks []cid.Cid) error { - cids, err := r.cidInfoByKeys(ctx, ks) - if err != nil { - return err - } - if len(cids) != len(ks) { - return index.ErrCidsNotExist - } - bop := bindOp{ - sk: storageKey(key), - fk: fileIdKey(fileId), - ri: r, - } - newCids, err := bop.Bind(ctx, cids) - if err != nil { - return err - } - return r.cidsAddRef(ctx, newCids) -} - -func (r *redisIndex) UnBind(ctx context.Context, spaceId, fileId string) (err error) { - uop := unbindOp{ - sk: storageKey(spaceId), - fk: fileIdKey(fileId), - ri: r, - } - removedCids, err := uop.Unbind(ctx) - if err != nil { - return - } - return r.cidsRemoveRef(ctx, removedCids) -} - -func (r *redisIndex) ExistsInStorage(ctx context.Context, spaceId string, ks []cid.Cid) (exists []cid.Cid, err error) { - var sk = storageKey(spaceId) - cidKeys := make([]string, len(ks)) - for i, k := range ks { - cidKeys[i] = k.String() - } - result, err := r.cl.HMGet(ctx, sk, cidKeys...).Result() - if err != nil { - return - } - exists = make([]cid.Cid, 0, len(ks)) - for i, v := range result { - if v != nil { - exists = append(exists, ks[i]) - } - } - return -} - -func (r *redisIndex) StorageSize(ctx context.Context, key string) (size uint64, err error) { - result, err := r.cl.HGet(ctx, storageKey(key), storeSizeKey).Result() - if err != nil { - if err == redis.Nil { - err = nil - } - return - } - return strconv.ParseUint(result, 10, 64) -} - -func (r *redisIndex) StorageInfo(ctx context.Context, key string) (info index.StorageInfo, err error) { - res, err := r.cl.HKeys(ctx, storageKey(key)).Result() - if err != nil { - if err == redis.Nil { - err = nil - } - return - } - for _, r := range res { - if strings.HasPrefix(r, "f:") { - info.FileCount++ - } else if r != storeSizeKey { - info.CidCount++ - } - } - info.Key = key - return -} - -func (r *redisIndex) FileInfo(ctx context.Context, key, fileId string) (info index.FileInfo, err error) { - fcl, err := r.newFileCidList(ctx, storageKey(key), fileIdKey(fileId)) - if err != nil { - return - } - defer fcl.Unlock() - cids, err := r.cidInfoByByteKeys(ctx, fcl.Cids) - if err != nil { - return - } - for _, c := range cids { - info.CidCount++ - info.BytesUsage += c.Size_ - } - return -} - -func (r *redisIndex) MoveStorage(ctx context.Context, fromKey, toKey string) (err error) { - ok, err := r.cl.RenameNX(ctx, storageKey(fromKey), storageKey(toKey)).Result() - if err != nil { - if err.Error() == "ERR no such key" { - return index.ErrStorageNotFound - } - return err - } - if !ok { - ex, err := r.cl.Exists(ctx, storageKey(toKey)).Result() - if err != nil { - return err - } - if ex > 0 { - return index.ErrTargetStorageExists - } - } - return -} - -func (r *redisIndex) cidsAddRef(ctx context.Context, cids []CidInfo) error { - now := time.Now() - for _, c := range cids { - ck := cidKey(c.Cid.String()) - res, err := r.cl.Get(ctx, ck).Result() - if err == redis.Nil { - err = nil - } - if err != nil { - return err - } - entry := &indexproto.CidEntry{} - if len(res) != 0 { - if err = entry.Unmarshal([]byte(res)); err != nil { - return err - } - } - if entry.CreateTime == 0 { - entry.CreateTime = now.Unix() - } - entry.UpdateTime = now.Unix() - entry.Refs++ - entry.Size_ = c.Size_ - - data, _ := entry.Marshal() - if err = r.cl.Set(ctx, ck, data, 0).Err(); err != nil { - return err - } - } - return nil -} - -func (r *redisIndex) cidsRemoveRef(ctx context.Context, cids []CidInfo) error { - now := time.Now() - for _, c := range cids { - ck := cidKey(c.Cid.String()) - res, err := r.cl.Get(ctx, ck).Result() - if err == redis.Nil { - continue - } - if err != nil { - return err - } - if len(res) == 0 { - continue - } - entry := &indexproto.CidEntry{} - if err = entry.Unmarshal([]byte(res)); err != nil { - return err - } - entry.UpdateTime = now.Unix() - entry.Refs-- - - // TODO: syncpool - data, _ := entry.Marshal() - if err = r.cl.Set(ctx, ck, data, 0).Err(); err != nil { - return err - } - } - return nil -} - -func (r *redisIndex) Lock(ctx context.Context, ks []cid.Cid) (unlock func(), err error) { - var lockers = make([]*redsync.Mutex, 0, len(ks)) - unlock = func() { - for _, l := range lockers { - _, _ = l.Unlock() - } - } - for _, k := range ks { - l := r.redsync.NewMutex("_lock:" + k.String()) - if err = l.LockContext(ctx); err != nil { - unlock() - return nil, err - } - lockers = append(lockers, l) - } - return -} - -func (r *redisIndex) AddBlocks(ctx context.Context, bs []blocks.Block) error { - cids := r.cidInfoByBlocks(bs) - if err := r.cidsAddRef(ctx, cids); err != nil { - return err - } - return r.cidsRemoveRef(ctx, cids) -} - -func (r *redisIndex) cidInfoByBlocks(bs []blocks.Block) (info []CidInfo) { - info = make([]CidInfo, len(bs)) - for i := range bs { - info[i] = CidInfo{ - Cid: bs[i].Cid(), - CidEntry: &indexproto.CidEntry{ - Size_: uint64(len(bs[i].RawData())), - }, - } - } - return -} - -func (r *redisIndex) cidInfoByKeys(ctx context.Context, ks []cid.Cid) (info []CidInfo, err error) { - info = make([]CidInfo, 0, len(ks)) - for _, c := range ks { - var res string - res, err = r.cl.Get(ctx, cidKey(c.String())).Result() - if err == redis.Nil { - continue - } - if err != nil { - return - } - entry := &indexproto.CidEntry{} - if err = entry.Unmarshal([]byte(res)); err != nil { - return - } - info = append(info, CidInfo{ - Cid: c, - CidEntry: entry, - }) - } - return -} - -func (r *redisIndex) cidInfoByByteKeys(ctx context.Context, ks [][]byte) (info []CidInfo, err error) { - var cids = make([]cid.Cid, 0, len(ks)) - for _, k := range ks { - if c, e := cid.Cast(k); e == nil { - cids = append(cids, c) - } - } - return r.cidInfoByKeys(ctx, cids) -} - -func storageKey(storeKey string) string { - return "s:" + storeKey -} - -func fileIdKey(fileId string) string { - return "f:" + fileId -} - -func cidKey(k string) string { - return "c:" + k -} diff --git a/index/redisindex/redisindex_test.go b/index/redisindex/redisindex_test.go deleted file mode 100644 index 902f53d8..00000000 --- a/index/redisindex/redisindex_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package redisindex - -import ( - "context" - "github.com/anyproto/any-sync-filenode/index" - "github.com/anyproto/any-sync-filenode/redisprovider/testredisprovider" - "github.com/anyproto/any-sync-filenode/testutil" - "github.com/anyproto/any-sync/app" - blocks "github.com/ipfs/go-block-format" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "math/rand" - "strings" - "testing" - "time" -) - -var ctx = context.Background() - -func TestRedisIndex_Exists(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - storeKey := testutil.NewRandSpaceId() - var bs = make([]blocks.Block, 1) - for i := range bs { - bs[i] = testutil.NewRandBlock(rand.Intn(256 * 1024)) - } - require.NoError(t, fx.Bind(ctx, storeKey, "", bs)) - ex, err := fx.Exists(ctx, bs[0].Cid()) - require.NoError(t, err) - assert.True(t, ex) -} - -func TestRedisIndex_ExistsInSpace(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - storeKey := testutil.NewRandSpaceId() - var bs = make([]blocks.Block, 2) - for i := range bs { - bs[i] = testutil.NewRandBlock(rand.Intn(256 * 1024)) - } - require.NoError(t, fx.Bind(ctx, storeKey, testutil.NewRandCid().String(), bs[:1])) - ex, err := fx.ExistsInStorage(ctx, storeKey, testutil.BlocksToKeys(bs)) - require.NoError(t, err) - assert.Len(t, ex, 1) -} - -func TestRedisIndex_IsAllExists(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - storeKey := testutil.NewRandSpaceId() - fileId := testutil.NewRandCid().String() - var bs = make([]blocks.Block, 2) - for i := range bs { - bs[i] = testutil.NewRandBlock(rand.Intn(256 * 1024)) - } - require.NoError(t, fx.Bind(ctx, storeKey, fileId, bs[:1])) - keys := testutil.BlocksToKeys(bs) - exists, err := fx.IsAllExists(ctx, keys) - require.NoError(t, err) - assert.False(t, exists) - exists, err = fx.IsAllExists(ctx, keys[:1]) - require.NoError(t, err) - assert.True(t, exists) -} - -func TestRedisIndex_GetNonExistentBlocks(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - storeKey := testutil.NewRandSpaceId() - fileId := testutil.NewRandCid().String() - var bs = make([]blocks.Block, 2) - for i := range bs { - bs[i] = testutil.NewRandBlock(rand.Intn(256 * 1024)) - } - require.NoError(t, fx.Bind(ctx, storeKey, fileId, bs[:1])) - - nonExistent, err := fx.GetNonExistentBlocks(ctx, bs) - require.NoError(t, err) - require.Len(t, nonExistent, 1) - assert.Equal(t, bs[1:], nonExistent) -} - -func TestRedisIndex_SpaceSize(t *testing.T) { - t.Run("space not found", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - storeKey := testutil.NewRandSpaceId() - size, err := fx.StorageSize(ctx, storeKey) - require.NoError(t, err) - assert.Empty(t, size) - }) - t.Run("success", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - var storeKey = testutil.NewRandSpaceId() - var expectedSize int - fileId := testutil.NewRandCid().String() - var bs = make([]blocks.Block, 2) - for i := range bs { - bs[i] = testutil.NewRandBlock(1024) - expectedSize += 1024 - } - require.NoError(t, fx.AddBlocks(ctx, bs)) - require.NoError(t, fx.Bind(ctx, storeKey, fileId, bs)) - - size, err := fx.StorageSize(ctx, storeKey) - require.NoError(t, err) - assert.Equal(t, expectedSize, int(size)) - }) -} - -func TestRedisIndex_Lock(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - - var bs = make([]blocks.Block, 3) - for i := range bs { - bs[i] = testutil.NewRandBlock(rand.Intn(1024)) - } - - unlock, err := fx.Lock(ctx, testutil.BlocksToKeys(bs[1:])) - require.NoError(t, err) - tCtx, cancel := context.WithTimeout(ctx, time.Second/2) - defer cancel() - _, err = fx.Lock(tCtx, testutil.BlocksToKeys(bs)) - require.Error(t, err) - unlock() -} - -func TestRedisIndex_MoveStorage(t *testing.T) { - const ( - oldKey = "oldKey" - newKey = "newKey" - ) - t.Run("success", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - require.NoError(t, fx.Bind(ctx, oldKey, "fid", testutil.NewRandBlocks(1))) - require.NoError(t, fx.MoveStorage(ctx, oldKey, newKey)) - }) - t.Run("err storage not found", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - assert.EqualError(t, fx.MoveStorage(ctx, oldKey, newKey), index.ErrStorageNotFound.Error()) - }) - t.Run("err taget exists", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - require.NoError(t, fx.Bind(ctx, oldKey, "fid", testutil.NewRandBlocks(1))) - require.NoError(t, fx.Bind(ctx, newKey, "fid", testutil.NewRandBlocks(1))) - assert.EqualError(t, fx.MoveStorage(ctx, oldKey, newKey), index.ErrTargetStorageExists.Error()) - }) -} - -func Test100KCids(t *testing.T) { - t.Skip() - fx := newFixture(t) - defer fx.Finish(t) - for i := 0; i < 10; i++ { - st := time.Now() - var bs = make([]blocks.Block, 10000) - for n := range bs { - bs[n] = testutil.NewRandBlock(rand.Intn(256)) - } - storeKey := testutil.NewRandSpaceId() - fileId := testutil.NewRandCid().String() - require.NoError(t, fx.Bind(ctx, storeKey, fileId, bs)) - t.Logf("bound %d cid for a %v", len(bs), time.Since(st)) - st = time.Now() - sz, err := fx.StorageSize(ctx, storeKey) - require.NoError(t, err) - t.Logf("space size is %d, dur: %v", sz, time.Since(st)) - } - info, err := fx.cl.Info(ctx, "memory").Result() - require.NoError(t, err) - infoS := strings.Split(info, "\n") - for _, i := range infoS { - if strings.HasPrefix(i, "used_memory_human") { - t.Log(i) - } - } -} - -func TestRedisIndex_AddBlocks(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - - var bs = make([]blocks.Block, 3) - for i := range bs { - bs[i] = testutil.NewRandBlock(rand.Intn(1024)) - } - - require.NoError(t, fx.AddBlocks(ctx, bs)) - - for _, b := range bs { - ex, err := fx.Exists(ctx, b.Cid()) - require.NoError(t, err) - assert.True(t, ex) - } -} - -func newFixture(t require.TestingT) (fx *fixture) { - fx = &fixture{ - redisIndex: New().(*redisIndex), - a: new(app.App), - } - fx.a.Register(testredisprovider.NewTestRedisProvider()).Register(fx.redisIndex) - require.NoError(t, fx.a.Start(ctx)) - return -} - -type fixture struct { - *redisIndex - a *app.App -} - -func (fx *fixture) Finish(t require.TestingT) { - require.NoError(t, fx.a.Close(ctx)) -} diff --git a/index/redisindex/unbind.go b/index/redisindex/unbind.go deleted file mode 100644 index 74ba2305..00000000 --- a/index/redisindex/unbind.go +++ /dev/null @@ -1,86 +0,0 @@ -package redisindex - -import ( - "context" - "github.com/ipfs/go-cid" - "github.com/redis/go-redis/v9" - "go.uber.org/zap" -) - -type unbindOp struct { - sk string - fk string - ri *redisIndex -} - -func (op *unbindOp) Unbind(ctx context.Context) (removedCids []CidInfo, err error) { - // fetch cids list - fCids, err := op.ri.newFileCidList(ctx, op.sk, op.fk) - if err != nil { - return nil, err - } - defer fCids.Unlock() - - // no cids by file, probably it doesn't exist - if len(fCids.Cids) == 0 { - return nil, nil - } - - // fetch cids info - cids, err := op.ri.cidInfoByByteKeys(ctx, fCids.Cids) - if err != nil { - return nil, err - } - // additional check - if len(cids) != len(fCids.Cids) { - log.Warn("can't fetch all file cids") - } - fCids.Cids = nil - - // we need to lock cids here - cidsToLock := make([]cid.Cid, len(cids)) - for i, c := range cids { - cidsToLock[i] = c.Cid - } - unlock, err := op.ri.Lock(ctx, cidsToLock) - if err != nil { - return nil, err - } - defer unlock() - - // make a list of results, data will be available after executing of pipeline - var execResults = make([]*redis.IntCmd, len(cids)) - // do updates in one pipeline - _, err = op.ri.cl.TxPipelined(ctx, func(pipe redis.Pipeliner) error { - for i, c := range cids { - execResults[i] = pipe.HIncrBy(ctx, op.sk, c.Cid.String(), -1) - if err = execResults[i].Err(); err != nil { - return err - } - } - return fCids.Save(ctx, pipe) - }) - - // check for cids that were removed from a space - // remove cids without reference - // make a list with removed cids - // calculate space size decrease - var spaceDecreaseSize uint64 - for i, res := range execResults { - if counter, err := res.Result(); err == nil && counter <= 0 { - if e := op.ri.cl.HDel(ctx, op.sk, cids[i].Cid.String()).Err(); e != nil { - log.Warn("can't remove cid from a space", zap.Error(e)) - } - removedCids = append(removedCids, cids[i]) - spaceDecreaseSize += cids[i].Size_ - } - } - - // increment space size - if spaceDecreaseSize > 0 { - if err = op.ri.cl.HIncrBy(ctx, op.sk, storeSizeKey, -int64(spaceDecreaseSize)).Err(); err != nil { - return nil, err - } - } - return -} diff --git a/index/redisindex/unbind_test.go b/index/redisindex/unbind_test.go deleted file mode 100644 index 7ea65352..00000000 --- a/index/redisindex/unbind_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package redisindex - -import ( - "github.com/anyproto/any-sync-filenode/testutil" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "testing" -) - -func TestRedisIndex_UnBind(t *testing.T) { - t.Run("unbind non existent", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - spaceId := testutil.NewRandSpaceId() - require.NoError(t, fx.UnBind(ctx, spaceId, testutil.NewRandCid().String())) - }) - t.Run("unbind single", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - spaceId := testutil.NewRandSpaceId() - bs := testutil.NewRandBlocks(5) - fileId := testutil.NewRandCid().String() - require.NoError(t, fx.Bind(ctx, spaceId, fileId, bs)) - - require.NoError(t, fx.UnBind(ctx, spaceId, fileId)) - size, err := fx.StorageSize(ctx, spaceId) - require.NoError(t, err) - assert.Empty(t, size) - - var cidsB [][]byte - for _, b := range bs { - cidsB = append(cidsB, b.Cid().Bytes()) - } - - res, err := fx.cidInfoByByteKeys(ctx, cidsB) - require.NoError(t, err) - for _, r := range res { - assert.Equal(t, int32(0), r.Refs) - } - }) - t.Run("unbind two files", func(t *testing.T) { - fx := newFixture(t) - defer fx.Finish(t) - spaceId := testutil.NewRandSpaceId() - bs := testutil.NewRandBlocks(5) - fileId1 := testutil.NewRandCid().String() - fileId2 := testutil.NewRandCid().String() - require.NoError(t, fx.Bind(ctx, spaceId, fileId1, bs)) - require.NoError(t, fx.Bind(ctx, spaceId, fileId2, bs[:3])) - - si, err := fx.StorageInfo(ctx, spaceId) - require.NoError(t, err) - assert.Equal(t, 2, si.FileCount) - assert.Equal(t, len(bs), si.CidCount) - - require.NoError(t, fx.UnBind(ctx, spaceId, fileId1)) - si, err = fx.StorageInfo(ctx, spaceId) - require.NoError(t, err) - assert.Equal(t, 1, si.FileCount) - assert.Equal(t, 3, si.CidCount) - }) -} diff --git a/index/testdata/oldspace.zip b/index/testdata/oldspace.zip new file mode 100644 index 0000000000000000000000000000000000000000..193796d1cc50326f3f20b31898ac9539429861dd GIT binary patch literal 145662 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 z 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 literal 0 HcmV?d00001 diff --git a/index/unbind.go b/index/unbind.go new file mode 100644 index 00000000..6483fab1 --- /dev/null +++ b/index/unbind.go @@ -0,0 +1,138 @@ +package index + +import ( + "context" + + "github.com/redis/go-redis/v9" + "go.uber.org/zap" +) + +func (ri *redisIndex) FileUnbind(ctx context.Context, key Key, fileId string) (err error) { + var ( + sk = spaceKey(key) + gk = groupKey(key) + ) + _, gRelease, err := ri.AcquireKey(ctx, gk) + if err != nil { + return + } + defer gRelease() + _, sRelease, err := ri.AcquireKey(ctx, sk) + if err != nil { + return + } + defer sRelease() + + // get file entry + fileInfo, isNewFile, err := ri.getFileEntry(ctx, key, fileId) + if err != nil { + return + } + if isNewFile { + // means file doesn't exist + return nil + } + + // fetch cids + cids, err := ri.CidEntriesByString(ctx, fileInfo.Cids) + if err != nil { + return err + } + defer cids.Release() + + // fetch cid refs in one pipeline + var ( + groupCidRefs = make([]*redis.StringCmd, len(cids.entries)) + spaceCidRefs = make([]*redis.StringCmd, len(cids.entries)) + ) + _, err = ri.cl.Pipelined(ctx, func(pipe redis.Pipeliner) error { + for i, c := range cids.entries { + groupCidRefs[i] = pipe.HGet(ctx, gk, cidKey(c.Cid)) + spaceCidRefs[i] = pipe.HGet(ctx, sk, cidKey(c.Cid)) + } + return nil + }) + if err != nil { + return + } + + // load group and space info + spaceInfo, err := ri.getSpaceEntry(ctx, key) + if err != nil { + return + } + groupInfo, err := ri.getGroupEntry(ctx, key) + if err != nil { + return + } + + // update info and calculate changes + var ( + groupRemoveKeys = make([]string, 0, len(cids.entries)) + spaceRemoveKeys = make([]string, 0, len(cids.entries)) + groupDecrKeys = make([]string, 0, len(cids.entries)) + spaceDecrKeys = make([]string, 0, len(cids.entries)) + affectedCidIdx = make([]int, 0, len(cids.entries)) + ) + + spaceInfo.FileCount-- + for i, c := range cids.entries { + res, err := groupCidRefs[i].Result() + if err != nil { + return err + } + ck := cidKey(c.Cid) + if res == "1" { + groupRemoveKeys = append(groupRemoveKeys, ck) + groupInfo.Size_ -= c.Size_ + groupInfo.CidCount-- + affectedCidIdx = append(affectedCidIdx, i) + } else { + groupDecrKeys = append(groupDecrKeys, ck) + } + res, err = spaceCidRefs[i].Result() + if err != nil { + return err + } + if res == "1" { + spaceRemoveKeys = append(spaceRemoveKeys, ck) + spaceInfo.Size_ -= c.Size_ + spaceInfo.CidCount-- + } else { + spaceDecrKeys = append(spaceDecrKeys, ck) + } + } + + // do updates in one tx + _, err = ri.cl.TxPipelined(ctx, func(tx redis.Pipeliner) error { + tx.HDel(ctx, sk, fileKey(fileId)) + if len(spaceRemoveKeys) != 0 { + tx.HDel(ctx, sk, spaceRemoveKeys...) + } + if len(groupRemoveKeys) != 0 { + tx.HDel(ctx, gk, groupRemoveKeys...) + } + if len(spaceDecrKeys) != 0 { + for _, k := range spaceDecrKeys { + tx.HIncrBy(ctx, sk, k, -1) + } + } + if len(groupDecrKeys) != 0 { + for _, k := range groupDecrKeys { + tx.HIncrBy(ctx, gk, k, -1) + } + } + spaceInfo.Save(ctx, key, tx) + groupInfo.Save(ctx, key, tx) + return nil + }) + + // update cids + for _, idx := range affectedCidIdx { + cids.entries[idx].RemoveGroupId(key.GroupId) + if saveErr := cids.entries[idx].Save(ctx, ri.cl); saveErr != nil { + log.WarnCtx(ctx, "unable to save cid info", zap.Error(saveErr), zap.String("cid", cids.entries[idx].Cid.String())) + } + } + return +} diff --git a/index/unbind_test.go b/index/unbind_test.go new file mode 100644 index 00000000..9c82ca37 --- /dev/null +++ b/index/unbind_test.go @@ -0,0 +1,93 @@ +package index + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/anyproto/any-sync-filenode/testutil" +) + +func TestRedisIndex_UnBind(t *testing.T) { + t.Run("unbind non existent", func(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + key := newRandKey() + require.NoError(t, fx.FileUnbind(ctx, key, testutil.NewRandCid().String())) + }) + t.Run("unbind single", func(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + key := newRandKey() + bs := testutil.NewRandBlocks(5) + + require.NoError(t, fx.BlocksAdd(ctx, bs)) + cids, err := fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + + fileId := testutil.NewRandCid().String() + require.NoError(t, fx.FileBind(ctx, key, fileId, cids)) + cids.Release() + + require.NoError(t, fx.FileUnbind(ctx, key, fileId)) + + groupInfo, err := fx.GroupInfo(ctx, key.GroupId) + require.NoError(t, err) + assert.Empty(t, groupInfo.CidsCount) + assert.Empty(t, groupInfo.BytesUsage) + spaceInfo, err := fx.SpaceInfo(ctx, key) + require.NoError(t, err) + assert.Empty(t, spaceInfo.FileCount) + assert.Empty(t, spaceInfo.BytesUsage) + + cids, err = fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + defer cids.Release() + for _, r := range cids.entries { + assert.NotContains(t, r.GroupIds, key.GroupId) + } + }) + t.Run("unbind intersection file", func(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + key := newRandKey() + bs := testutil.NewRandBlocks(5) + var file1Size = uint64(len(bs[0].RawData()) + len(bs[1].RawData())) + require.NoError(t, fx.BlocksAdd(ctx, bs)) + cids, err := fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + cids.Release() + + // bind to files with intersected cids + fileId1 := testutil.NewRandCid().String() + fileId2 := testutil.NewRandCid().String() + + cids1, err := fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + require.NoError(t, fx.FileBind(ctx, key, fileId1, cids1)) + cids1.Release() + + cids2, err := fx.CidEntriesByBlocks(ctx, bs[:2]) + require.NoError(t, err) + require.NoError(t, fx.FileBind(ctx, key, fileId2, cids2)) + cids2.Release() + + // remove file1 + require.NoError(t, fx.FileUnbind(ctx, key, fileId1)) + + groupInfo, err := fx.GroupInfo(ctx, key.GroupId) + require.NoError(t, err) + assert.Equal(t, uint32(2), groupInfo.CidsCount) + assert.Equal(t, file1Size, groupInfo.BytesUsage) + spaceInfo, err := fx.SpaceInfo(ctx, key) + require.NoError(t, err) + assert.Equal(t, uint32(1), spaceInfo.FileCount) + assert.Equal(t, file1Size, spaceInfo.BytesUsage) + + cids, err = fx.CidEntriesByBlocks(ctx, bs) + require.NoError(t, err) + defer cids.Release() + + }) +} diff --git a/store/filedevstore/filedevstore.go b/store/filedevstore/filedevstore.go index 45482276..dcadb762 100644 --- a/store/filedevstore/filedevstore.go +++ b/store/filedevstore/filedevstore.go @@ -3,15 +3,17 @@ package filedevstore import ( "context" "fmt" - "github.com/anyproto/any-sync-filenode/config" - "github.com/anyproto/any-sync-filenode/store" + "os" + "path/filepath" + "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/app/logger" "github.com/anyproto/any-sync/commonfile/fileblockstore" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" - "os" - "path/filepath" + + "github.com/anyproto/any-sync-filenode/config" + "github.com/anyproto/any-sync-filenode/store" ) const CName = fileblockstore.CName @@ -96,3 +98,11 @@ func (s *fsstore) DeleteMany(ctx context.Context, toDelete []cid.Cid) error { } return nil } + +func (s *fsstore) IndexGet(ctx context.Context, key string) (value []byte, err error) { + return os.ReadFile(filepath.Join(s.path, key)) +} + +func (s *fsstore) IndexPut(ctx context.Context, key string, value []byte) (err error) { + return os.WriteFile(filepath.Join(s.path, key), value, 0777) +} diff --git a/store/mock_store/mock_store.go b/store/mock_store/mock_store.go index 1357d2f1..07c45c03 100644 --- a/store/mock_store/mock_store.go +++ b/store/mock_store/mock_store.go @@ -1,6 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/anyproto/any-sync-filenode/store (interfaces: Store) - +// +// Generated by this command: +// +// mockgen -destination mock_store/mock_store.go github.com/anyproto/any-sync-filenode/store Store +// // Package mock_store is a generated GoMock package. package mock_store @@ -46,7 +50,7 @@ func (m *MockStore) Add(arg0 context.Context, arg1 []blocks.Block) error { } // Add indicates an expected call of Add. -func (mr *MockStoreMockRecorder) Add(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) Add(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Add", reflect.TypeOf((*MockStore)(nil).Add), arg0, arg1) } @@ -60,7 +64,7 @@ func (m *MockStore) Delete(arg0 context.Context, arg1 cid.Cid) error { } // Delete indicates an expected call of Delete. -func (mr *MockStoreMockRecorder) Delete(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) Delete(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStore)(nil).Delete), arg0, arg1) } @@ -74,7 +78,7 @@ func (m *MockStore) DeleteMany(arg0 context.Context, arg1 []cid.Cid) error { } // DeleteMany indicates an expected call of DeleteMany. -func (mr *MockStoreMockRecorder) DeleteMany(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) DeleteMany(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteMany", reflect.TypeOf((*MockStore)(nil).DeleteMany), arg0, arg1) } @@ -89,7 +93,7 @@ func (m *MockStore) Get(arg0 context.Context, arg1 cid.Cid) (blocks.Block, error } // Get indicates an expected call of Get. -func (mr *MockStoreMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) Get(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStore)(nil).Get), arg0, arg1) } @@ -103,11 +107,40 @@ func (m *MockStore) GetMany(arg0 context.Context, arg1 []cid.Cid) <-chan blocks. } // GetMany indicates an expected call of GetMany. -func (mr *MockStoreMockRecorder) GetMany(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) GetMany(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMany", reflect.TypeOf((*MockStore)(nil).GetMany), arg0, arg1) } +// IndexGet mocks base method. +func (m *MockStore) IndexGet(arg0 context.Context, arg1 string) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IndexGet", arg0, arg1) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// IndexGet indicates an expected call of IndexGet. +func (mr *MockStoreMockRecorder) IndexGet(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexGet", reflect.TypeOf((*MockStore)(nil).IndexGet), arg0, arg1) +} + +// IndexPut mocks base method. +func (m *MockStore) IndexPut(arg0 context.Context, arg1 string, arg2 []byte) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IndexPut", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// IndexPut indicates an expected call of IndexPut. +func (mr *MockStoreMockRecorder) IndexPut(arg0, arg1, arg2 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IndexPut", reflect.TypeOf((*MockStore)(nil).IndexPut), arg0, arg1, arg2) +} + // Init mocks base method. func (m *MockStore) Init(arg0 *app.App) error { m.ctrl.T.Helper() @@ -117,7 +150,7 @@ func (m *MockStore) Init(arg0 *app.App) error { } // Init indicates an expected call of Init. -func (mr *MockStoreMockRecorder) Init(arg0 interface{}) *gomock.Call { +func (mr *MockStoreMockRecorder) Init(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockStore)(nil).Init), arg0) } diff --git a/store/s3store/config.go b/store/s3store/config.go index 2138a0fa..8ecdb6d1 100644 --- a/store/s3store/config.go +++ b/store/s3store/config.go @@ -13,6 +13,7 @@ type Config struct { Profile string `yaml:"profile"` Region string `yaml:"region"` Bucket string `yaml:"bucket"` + IndexBucket string `yaml:"indexBucket"` Endpoint string `yaml:"endpoint"` MaxThreads int `yaml:"maxThreads"` Credentials Credentials `yaml:"credentials"` diff --git a/store/s3store/s3store.go b/store/s3store/s3store.go index 18c2c9a4..c12ddc04 100644 --- a/store/s3store/s3store.go +++ b/store/s3store/s3store.go @@ -5,10 +5,10 @@ import ( "context" "fmt" "io" + "strings" "sync" "time" - "github.com/anyproto/any-sync-filenode/store" "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/app/logger" "github.com/anyproto/any-sync/commonfile/fileblockstore" @@ -19,6 +19,8 @@ import ( blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "go.uber.org/zap" + + "github.com/anyproto/any-sync-filenode/store" ) const CName = fileblockstore.CName @@ -35,10 +37,11 @@ type S3Store interface { } type s3store struct { - bucket *string - client *s3.S3 - limiter chan struct{} - sess *session.Session + bucket *string + indexBucket *string + client *s3.S3 + limiter chan struct{} + sess *session.Session } func (s *s3store) Init(a *app.App) (err error) { @@ -49,6 +52,9 @@ func (s *s3store) Init(a *app.App) (err error) { if conf.Bucket == "" { return fmt.Errorf("s3 bucket is empty") } + if conf.IndexBucket == "" { + return fmt.Errorf("s3 index bucket is empty") + } if conf.MaxThreads <= 0 { conf.MaxThreads = 16 } @@ -81,6 +87,8 @@ func (s *s3store) Init(a *app.App) (err error) { return fmt.Errorf("failed to create session to s3: %v", err) } s.bucket = aws.String(conf.Bucket) + s.indexBucket = aws.String(conf.IndexBucket) + s.client = s3.New(s.sess) s.limiter = make(chan struct{}, conf.MaxThreads) return nil @@ -104,6 +112,9 @@ func (s *s3store) Get(ctx context.Context, k cid.Cid) (blocks.Block, error) { Key: aws.String(k.String()), }) if err != nil { + if strings.HasPrefix(err.Error(), s3.ErrCodeNoSuchKey) { + return nil, fileblockstore.ErrCIDNotFound + } return nil, err } defer obj.Body.Close() @@ -203,6 +214,31 @@ func (s *s3store) Delete(ctx context.Context, c cid.Cid) error { return err } +func (s *s3store) IndexGet(ctx context.Context, key string) (value []byte, err error) { + obj, err := s.client.GetObjectWithContext(ctx, &s3.GetObjectInput{ + Bucket: s.indexBucket, + Key: aws.String(key), + }) + if err != nil { + if strings.HasPrefix(err.Error(), s3.ErrCodeNoSuchKey) { + // nil value means not found + return nil, nil + } + return nil, err + } + defer obj.Body.Close() + return io.ReadAll(obj.Body) +} + +func (s *s3store) IndexPut(ctx context.Context, key string, data []byte) (err error) { + _, err = s.client.PutObjectWithContext(ctx, &s3.PutObjectInput{ + Key: aws.String(key), + Body: bytes.NewReader(data), + Bucket: s.indexBucket, + }) + return +} + func (s *s3store) Close(ctx context.Context) (err error) { return nil } diff --git a/store/s3store/s3store_test.go b/store/s3store/s3store_test.go index 75a44892..55c9694f 100644 --- a/store/s3store/s3store_test.go +++ b/store/s3store/s3store_test.go @@ -3,13 +3,14 @@ package s3store import ( "context" "fmt" + "testing" + "time" + "github.com/anyproto/any-sync/app" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "testing" - "time" ) var ctx = context.Background() @@ -57,8 +58,9 @@ func (c config) Name() string { return "config" } func (c config) GetS3Store() Config { return Config{ - Region: "eu-central-1", - Bucket: "anytype-test", - MaxThreads: 4, + Region: "eu-central-1", + Bucket: "anytype-test", + IndexBucket: "anytype-test", + MaxThreads: 4, } } diff --git a/store/store.go b/store/store.go index f50ca768..e780bf1e 100644 --- a/store/store.go +++ b/store/store.go @@ -3,6 +3,7 @@ package store import ( "context" + "github.com/anyproto/any-sync/app" "github.com/anyproto/any-sync/commonfile/fileblockstore" "github.com/ipfs/go-cid" @@ -11,5 +12,8 @@ import ( type Store interface { fileblockstore.BlockStore DeleteMany(ctx context.Context, toDelete []cid.Cid) error + + IndexGet(ctx context.Context, key string) (value []byte, err error) + IndexPut(ctx context.Context, key string, value []byte) (err error) app.Component } From f8bebf569c01bebcdb98948d55b1b2f7193c3f55 Mon Sep 17 00:00:00 2001 From: Sergey Cherepanov Date: Tue, 24 Oct 2023 19:23:52 +0200 Subject: [PATCH 02/10] call index migrate --- filenode/filenode.go | 5 +++++ filenode/filenode_test.go | 9 +++++---- index/index.go | 2 ++ index/mock_index/mock_index.go | 14 ++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/filenode/filenode.go b/filenode/filenode.go index 23b5d7d8..a640a02c 100644 --- a/filenode/filenode.go +++ b/filenode/filenode.go @@ -13,6 +13,7 @@ import ( "github.com/anyproto/any-sync/net/rpc/server" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" + "go.uber.org/zap" "github.com/anyproto/any-sync-filenode/index" "github.com/anyproto/any-sync-filenode/limit" @@ -163,6 +164,10 @@ func (fn *fileNode) StoreKey(ctx context.Context, spaceId string, checkLimit boo SpaceId: spaceId, } + if e := fn.index.Migrate(ctx, storageKey); e != nil { + log.WarnCtx(ctx, "space migrate error", zap.String("spaceId", spaceId), zap.Error(e)) + } + if checkLimit { info, e := fn.index.GroupInfo(ctx, groupId) if e != nil { diff --git a/filenode/filenode_test.go b/filenode/filenode_test.go index 8e667a0f..63fef96b 100644 --- a/filenode/filenode_test.go +++ b/filenode/filenode_test.go @@ -38,6 +38,7 @@ func TestFileNode_Add(t *testing.T) { ) fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).Return(uint64(123), storeKey.GroupId, nil) + fx.index.EXPECT().Migrate(ctx, storeKey) fx.index.EXPECT().GroupInfo(ctx, storeKey.GroupId).Return(index.GroupInfo{BytesUsage: uint64(120)}, nil) fx.index.EXPECT().BlocksLock(ctx, []blocks.Block{b}).Return(func() {}, nil) fx.index.EXPECT().BlocksGetNonExistent(ctx, []blocks.Block{b}).Return([]blocks.Block{b}, nil) @@ -66,7 +67,7 @@ func TestFileNode_Add(t *testing.T) { ) fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).Return(uint64(123), storeKey.GroupId, nil) - //fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() + fx.index.EXPECT().Migrate(ctx, storeKey) fx.index.EXPECT().GroupInfo(ctx, storeKey.GroupId).Return(index.GroupInfo{BytesUsage: uint64(124)}, nil) resp, err := fx.handler.BlockPush(ctx, &fileproto.BlockPushRequest{ @@ -158,7 +159,7 @@ func TestFileNode_Check(t *testing.T) { cids = append(cids, b.Cid().Bytes()) } fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).Return(uint64(100000), storeKey.GroupId, nil) - //fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() + fx.index.EXPECT().Migrate(ctx, storeKey) fx.index.EXPECT().CidExistsInSpace(ctx, storeKey, testutil.BlocksToKeys(bs)).Return(testutil.BlocksToKeys(bs[:1]), nil) fx.index.EXPECT().CidExists(ctx, bs[1].Cid()).Return(true, nil) fx.index.EXPECT().CidExists(ctx, bs[2].Cid()).Return(false, nil) @@ -190,7 +191,7 @@ func TestFileNode_BlocksBind(t *testing.T) { } fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).Return(uint64(123), storeKey.GroupId, nil) - // fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() + fx.index.EXPECT().Migrate(ctx, storeKey) fx.index.EXPECT().GroupInfo(ctx, storeKey.GroupId).Return(index.GroupInfo{BytesUsage: 12}, nil) fx.index.EXPECT().CidEntries(ctx, cids).Return(cidEntries, nil) fx.index.EXPECT().FileBind(ctx, storeKey, fileId, cidEntries) @@ -215,7 +216,7 @@ func TestFileNode_FileInfo(t *testing.T) { fileId2 = testutil.NewRandCid().String() ) fx.limit.EXPECT().Check(ctx, storeKey.SpaceId).AnyTimes().Return(uint64(100000), storeKey.GroupId, nil) - //fx.index.EXPECT().MoveStorage(ctx, spaceId, storeKey).AnyTimes() + fx.index.EXPECT().Migrate(ctx, storeKey) fx.index.EXPECT().FileInfo(ctx, storeKey, fileId1, fileId2).Return([]index.FileInfo{{1, 1}, {2, 2}}, nil) resp, err := fx.handler.FilesInfo(ctx, &fileproto.FilesInfoRequest{ diff --git a/index/index.go b/index/index.go index 03d5ac9e..043123e7 100644 --- a/index/index.go +++ b/index/index.go @@ -47,6 +47,8 @@ type Index interface { CidEntries(ctx context.Context, cids []cid.Cid) (entries *CidEntries, err error) CidEntriesByBlocks(ctx context.Context, bs []blocks.Block) (entries *CidEntries, err error) CidExistsInSpace(ctx context.Context, k Key, cids []cid.Cid) (exists []cid.Cid, err error) + + Migrate(ctx context.Context, key Key) error app.ComponentRunnable } diff --git a/index/mock_index/mock_index.go b/index/mock_index/mock_index.go index 2e3c08da..06da3f40 100644 --- a/index/mock_index/mock_index.go +++ b/index/mock_index/mock_index.go @@ -237,6 +237,20 @@ func (mr *MockIndexMockRecorder) Init(arg0 any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIndex)(nil).Init), arg0) } +// Migrate mocks base method. +func (m *MockIndex) Migrate(arg0 context.Context, arg1 index.Key) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Migrate", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Migrate indicates an expected call of Migrate. +func (mr *MockIndexMockRecorder) Migrate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockIndex)(nil).Migrate), arg0, arg1) +} + // Name mocks base method. func (m *MockIndex) Name() string { m.ctrl.T.Helper() From a5b47b386c799bd72082c0690262a2c0cc045eed Mon Sep 17 00:00:00 2001 From: Sergey Cherepanov Date: Tue, 24 Oct 2023 20:10:48 +0200 Subject: [PATCH 03/10] fix --- etc/any-sync-filenode.yml | 1 + index/index.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/etc/any-sync-filenode.yml b/etc/any-sync-filenode.yml index 2d42ff57..babba9c6 100755 --- a/etc/any-sync-filenode.yml +++ b/etc/any-sync-filenode.yml @@ -21,6 +21,7 @@ s3Store: region: eu-central-1 profile: default bucket: anytype-test + indexBucket: anytype-test maxThreads: 16 redis: diff --git a/index/index.go b/index/index.go index 043123e7..db147f5c 100644 --- a/index/index.go +++ b/index/index.go @@ -47,7 +47,7 @@ type Index interface { CidEntries(ctx context.Context, cids []cid.Cid) (entries *CidEntries, err error) CidEntriesByBlocks(ctx context.Context, bs []blocks.Block) (entries *CidEntries, err error) CidExistsInSpace(ctx context.Context, k Key, cids []cid.Cid) (exists []cid.Cid, err error) - + Migrate(ctx context.Context, key Key) error app.ComponentRunnable @@ -120,6 +120,7 @@ func (ri *redisIndex) Run(ctx context.Context) (err error) { ri.PersistKeys(ctx) return nil }, log) + ri.ticker.Run() return } From 9f4c9b0973654529ef0f31ace085445f1c42c9cd Mon Sep 17 00:00:00 2001 From: Sergey Cherepanov Date: Tue, 24 Oct 2023 20:14:32 +0200 Subject: [PATCH 04/10] add redis bf run assertion --- redisprovider/redisprovider.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/redisprovider/redisprovider.go b/redisprovider/redisprovider.go index 2d74beca..e79529cd 100644 --- a/redisprovider/redisprovider.go +++ b/redisprovider/redisprovider.go @@ -2,6 +2,7 @@ package redisprovider import ( "context" + "github.com/anyproto/any-sync/app" "github.com/redis/go-redis/v9" ) @@ -48,7 +49,13 @@ func (r *redisProvider) Name() (name string) { } func (r *redisProvider) Run(ctx context.Context) (err error) { - return r.redis.Ping(ctx).Err() + if err = r.redis.Ping(ctx).Err(); err != nil { + return + } + if err = r.redis.BFAdd(ctx, "_test_bf", 1).Err(); err != nil { + return + } + return r.redis.Del(ctx, "_test_bf").Err() } func (r *redisProvider) Redis() redis.UniversalClient { From 018a298e9d9f16c52f7d38ae564ce16105fa65d4 Mon Sep 17 00:00:00 2001 From: Sergey Cherepanov Date: Wed, 25 Oct 2023 13:43:01 +0200 Subject: [PATCH 05/10] remove cid.GroupIds, return to Refs --- index/bind.go | 2 +- index/cidentry.go | 13 --- index/indexproto/index.pb.go | 135 ++++++++-------------------- index/indexproto/protos/index.proto | 3 +- index/unbind.go | 2 +- index/unbind_test.go | 2 +- 6 files changed, 43 insertions(+), 114 deletions(-) diff --git a/index/bind.go b/index/bind.go index 7f996d53..cee6a172 100644 --- a/index/bind.go +++ b/index/bind.go @@ -111,7 +111,7 @@ func (ri *redisIndex) FileBind(ctx context.Context, key Key, fileId string, cids // update cids for _, idx := range newFileCidIdx { - cids.entries[idx].AddGroupId(key.GroupId) + cids.entries[idx].Refs++ if saveErr := cids.entries[idx].Save(ctx, ri.cl); saveErr != nil { log.WarnCtx(ctx, "unable to save cid info", zap.Error(saveErr), zap.String("cid", cids.entries[idx].Cid.String())) } diff --git a/index/cidentry.go b/index/cidentry.go index 20c1bd7f..f2eac8ce 100644 --- a/index/cidentry.go +++ b/index/cidentry.go @@ -2,7 +2,6 @@ package index import ( "context" - "slices" "time" "github.com/ipfs/go-cid" @@ -30,18 +29,6 @@ type cidEntry struct { *indexproto.CidEntry } -func (ce *cidEntry) AddGroupId(groupId string) { - if !slices.Contains(ce.GroupIds, groupId) { - ce.GroupIds = append(ce.GroupIds, groupId) - } -} - -func (ce *cidEntry) RemoveGroupId(id string) { - ce.GroupIds = slices.DeleteFunc(ce.GroupIds, func(s string) bool { - return s == id - }) -} - func (ce *cidEntry) Save(ctx context.Context, cl redis.Cmdable) error { ce.UpdateTime = time.Now().Unix() data, err := ce.Marshal() diff --git a/index/indexproto/index.pb.go b/index/indexproto/index.pb.go index 16bf4cdb..3bdbd292 100644 --- a/index/indexproto/index.pb.go +++ b/index/indexproto/index.pb.go @@ -1,15 +1,14 @@ // Code generated by protoc-gen-gogo. DO NOT EDIT. -// source: index/redisindex/indexproto/protos/index.proto +// source: index/indexproto/protos/index.proto package indexproto import ( fmt "fmt" + proto "github.com/gogo/protobuf/proto" io "io" math "math" math_bits "math/bits" - - proto "github.com/gogo/protobuf/proto" ) // Reference imports to suppress errors if they are not otherwise used. @@ -24,19 +23,18 @@ var _ = math.Inf const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type CidEntry struct { - Size_ uint64 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` - CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` - UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` - Refs int32 `protobuf:"varint,4,opt,name=refs,proto3" json:"refs,omitempty"` - GroupIds []string `protobuf:"bytes,5,rep,name=groupIds,proto3" json:"groupIds,omitempty"` - Version uint32 `protobuf:"varint,6,opt,name=version,proto3" json:"version,omitempty"` + Size_ uint64 `protobuf:"varint,1,opt,name=size,proto3" json:"size,omitempty"` + CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` + UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` + Refs int32 `protobuf:"varint,4,opt,name=refs,proto3" json:"refs,omitempty"` + Version uint32 `protobuf:"varint,5,opt,name=version,proto3" json:"version,omitempty"` } func (m *CidEntry) Reset() { *m = CidEntry{} } func (m *CidEntry) String() string { return proto.CompactTextString(m) } func (*CidEntry) ProtoMessage() {} func (*CidEntry) Descriptor() ([]byte, []int) { - return fileDescriptor_01af1a9166444478, []int{0} + return fileDescriptor_f1f29953df8d243b, []int{0} } func (m *CidEntry) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -93,13 +91,6 @@ func (m *CidEntry) GetRefs() int32 { return 0 } -func (m *CidEntry) GetGroupIds() []string { - if m != nil { - return m.GroupIds - } - return nil -} - func (m *CidEntry) GetVersion() uint32 { if m != nil { return m.Version @@ -115,7 +106,7 @@ func (m *CidList) Reset() { *m = CidList{} } func (m *CidList) String() string { return proto.CompactTextString(m) } func (*CidList) ProtoMessage() {} func (*CidList) Descriptor() ([]byte, []int) { - return fileDescriptor_01af1a9166444478, []int{1} + return fileDescriptor_f1f29953df8d243b, []int{1} } func (m *CidList) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -163,7 +154,7 @@ func (m *GroupEntry) Reset() { *m = GroupEntry{} } func (m *GroupEntry) String() string { return proto.CompactTextString(m) } func (*GroupEntry) ProtoMessage() {} func (*GroupEntry) Descriptor() ([]byte, []int) { - return fileDescriptor_01af1a9166444478, []int{2} + return fileDescriptor_f1f29953df8d243b, []int{2} } func (m *GroupEntry) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -240,7 +231,7 @@ func (m *SpaceEntry) Reset() { *m = SpaceEntry{} } func (m *SpaceEntry) String() string { return proto.CompactTextString(m) } func (*SpaceEntry) ProtoMessage() {} func (*SpaceEntry) Descriptor() ([]byte, []int) { - return fileDescriptor_01af1a9166444478, []int{3} + return fileDescriptor_f1f29953df8d243b, []int{3} } func (m *SpaceEntry) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -322,7 +313,7 @@ func (m *FileEntry) Reset() { *m = FileEntry{} } func (m *FileEntry) String() string { return proto.CompactTextString(m) } func (*FileEntry) ProtoMessage() {} func (*FileEntry) Descriptor() ([]byte, []int) { - return fileDescriptor_01af1a9166444478, []int{4} + return fileDescriptor_f1f29953df8d243b, []int{4} } func (m *FileEntry) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -388,33 +379,32 @@ func init() { } func init() { - proto.RegisterFile("index/redisindex/indexproto/protos/index.proto", fileDescriptor_01af1a9166444478) -} - -var fileDescriptor_01af1a9166444478 = []byte{ - // 347 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x52, 0x3d, 0x4e, 0xf3, 0x40, - 0x10, 0xcd, 0xc6, 0xce, 0x8f, 0x47, 0xdf, 0x47, 0xe1, 0x6a, 0x15, 0x60, 0x65, 0xb9, 0xda, 0x2a, - 0x29, 0x10, 0x17, 0x20, 0x02, 0x14, 0x89, 0x02, 0x2d, 0x54, 0x74, 0xc1, 0xbb, 0x41, 0x23, 0x05, - 0xdb, 0xda, 0x75, 0x10, 0x70, 0x0a, 0x2a, 0xce, 0xc0, 0x01, 0x38, 0x04, 0x65, 0x4a, 0x4a, 0x94, - 0x5c, 0x04, 0xed, 0x9a, 0x38, 0x26, 0x12, 0xd0, 0x20, 0xd1, 0x8c, 0xe7, 0xbd, 0x91, 0xc6, 0xef, - 0xbd, 0x1d, 0xe8, 0x63, 0x2a, 0xd5, 0xed, 0x40, 0x2b, 0x89, 0xa6, 0x6c, 0x5d, 0xcd, 0x75, 0x56, - 0x64, 0x03, 0x57, 0x4d, 0xc9, 0xf4, 0x1d, 0x08, 0xb7, 0x26, 0x38, 0x55, 0x23, 0x4b, 0x9c, 0x5a, - 0x1c, 0x3f, 0x11, 0xe8, 0x0e, 0x51, 0x1e, 0xa6, 0x85, 0xbe, 0x0b, 0x43, 0xf0, 0x0d, 0xde, 0x2b, - 0x4a, 0x22, 0xc2, 0x7d, 0xe1, 0xfa, 0x90, 0x01, 0x24, 0x5a, 0x8d, 0x0b, 0x75, 0x8e, 0xd7, 0x8a, - 0x36, 0x23, 0xc2, 0x3d, 0x51, 0x63, 0xec, 0x7c, 0x96, 0xcb, 0xd5, 0xdc, 0x2b, 0xe7, 0x6b, 0xc6, - 0xee, 0xd4, 0x6a, 0x62, 0xa8, 0x1f, 0x11, 0xde, 0x12, 0xae, 0x0f, 0x7b, 0xd0, 0xbd, 0xd2, 0xd9, - 0x2c, 0x1f, 0x49, 0x43, 0x5b, 0x91, 0xc7, 0x03, 0x51, 0xe1, 0x90, 0x42, 0xe7, 0x46, 0x69, 0x83, - 0x59, 0x4a, 0xdb, 0x11, 0xe1, 0xff, 0xc5, 0x0a, 0xc6, 0xbb, 0xd0, 0x19, 0xa2, 0x3c, 0x41, 0x53, - 0xd8, 0xa5, 0x09, 0x4a, 0x43, 0x49, 0xe4, 0xf1, 0x7f, 0xc2, 0xf5, 0xf1, 0x23, 0x01, 0x38, 0xb6, - 0x5b, 0x4a, 0x2f, 0x14, 0x3a, 0x1f, 0x3b, 0x9d, 0x9d, 0x40, 0xac, 0xe0, 0x6f, 0x38, 0x72, 0x29, - 0xf9, 0xb5, 0x94, 0x7a, 0xd0, 0x4d, 0x50, 0x0e, 0xb3, 0x59, 0x5a, 0xd0, 0x96, 0x93, 0x5d, 0xe1, - 0xf8, 0x99, 0x00, 0x9c, 0xe5, 0xe3, 0x44, 0xfd, 0x85, 0xb0, 0x1d, 0x08, 0xec, 0x8b, 0xd7, 0x95, - 0xad, 0x89, 0x4f, 0xb2, 0xdb, 0x1b, 0xb2, 0x0d, 0x04, 0x47, 0x38, 0x55, 0xd5, 0x65, 0x54, 0x81, - 0x07, 0x65, 0xe0, 0xd5, 0xef, 0x9a, 0x5f, 0x5e, 0x8b, 0xf7, 0x83, 0x05, 0x7f, 0xd3, 0xc2, 0xc1, - 0xfe, 0xcb, 0x82, 0x91, 0xf9, 0x82, 0x91, 0xb7, 0x05, 0x23, 0x0f, 0x4b, 0xd6, 0x98, 0x2f, 0x59, - 0xe3, 0x75, 0xc9, 0x1a, 0x17, 0xdb, 0xdf, 0x1c, 0xfa, 0x65, 0xdb, 0x7d, 0xf6, 0xde, 0x03, 0x00, - 0x00, 0xff, 0xff, 0x0b, 0x6f, 0x04, 0xe8, 0x0e, 0x03, 0x00, 0x00, + proto.RegisterFile("index/indexproto/protos/index.proto", fileDescriptor_f1f29953df8d243b) +} + +var fileDescriptor_f1f29953df8d243b = []byte{ + // 326 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0xce, 0xcc, 0x4b, 0x49, + 0xad, 0xd0, 0x07, 0x93, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0xfa, 0x60, 0xb2, 0x18, 0x22, 0xa2, 0x07, + 0xe6, 0x08, 0xf1, 0xa5, 0x65, 0xe6, 0xa4, 0x7a, 0x82, 0x04, 0x02, 0x40, 0x7c, 0xa5, 0x1e, 0x46, + 0x2e, 0x0e, 0xe7, 0xcc, 0x14, 0xd7, 0xbc, 0x92, 0xa2, 0x4a, 0x21, 0x21, 0x2e, 0x96, 0xe2, 0xcc, + 0xaa, 0x54, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x96, 0x20, 0x30, 0x5b, 0x48, 0x8e, 0x8b, 0x2b, 0xb9, + 0x28, 0x35, 0xb1, 0x24, 0x35, 0x24, 0x33, 0x37, 0x55, 0x82, 0x49, 0x81, 0x51, 0x83, 0x39, 0x08, + 0x49, 0x04, 0x24, 0x5f, 0x5a, 0x90, 0x02, 0x93, 0x67, 0x86, 0xc8, 0x23, 0x44, 0x40, 0x66, 0x16, + 0xa5, 0xa6, 0x15, 0x4b, 0xb0, 0x28, 0x30, 0x6a, 0xb0, 0x06, 0x81, 0xd9, 0x42, 0x12, 0x5c, 0xec, + 0x65, 0xa9, 0x45, 0xc5, 0x99, 0xf9, 0x79, 0x12, 0xac, 0x0a, 0x8c, 0x1a, 0xbc, 0x41, 0x30, 0xae, + 0x92, 0x2c, 0x17, 0xbb, 0x73, 0x66, 0x8a, 0x4f, 0x66, 0x71, 0x09, 0x48, 0x63, 0x72, 0x66, 0x4a, + 0xb1, 0x04, 0xa3, 0x02, 0xb3, 0x06, 0x4f, 0x10, 0x98, 0xad, 0x34, 0x8d, 0x91, 0x8b, 0xcb, 0xbd, + 0x28, 0xbf, 0xb4, 0x00, 0xe2, 0x5e, 0x09, 0x2e, 0xf6, 0x74, 0x10, 0xcf, 0x33, 0x05, 0xec, 0x64, + 0xce, 0x20, 0x18, 0x97, 0x1a, 0xae, 0x06, 0x87, 0x04, 0x0b, 0x52, 0x48, 0x48, 0x71, 0x71, 0x24, + 0x67, 0xa6, 0x38, 0xe7, 0x97, 0xe6, 0x95, 0x40, 0x9d, 0x0d, 0xe7, 0x2b, 0x6d, 0x61, 0xe4, 0xe2, + 0x0a, 0x2e, 0x48, 0x4c, 0x4e, 0x1d, 0x08, 0x87, 0xc9, 0x70, 0x71, 0x82, 0x62, 0x15, 0xd9, 0x65, + 0x08, 0x01, 0x14, 0x67, 0xb3, 0xa1, 0x39, 0xbb, 0x98, 0x8b, 0xd3, 0x2d, 0x33, 0x27, 0x15, 0x1e, + 0xfb, 0xf0, 0x00, 0xe7, 0x84, 0x04, 0x38, 0xdc, 0x3a, 0x26, 0x9c, 0x29, 0x82, 0x99, 0x80, 0x17, + 0x58, 0xd0, 0xbd, 0xe0, 0xa4, 0x75, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, + 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, 0x72, 0x0c, 0x51, + 0x02, 0xe8, 0x29, 0x38, 0x89, 0x0d, 0x4c, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x85, 0x61, + 0x15, 0x22, 0xdc, 0x02, 0x00, 0x00, } func (m *CidEntry) Marshal() (dAtA []byte, err error) { @@ -440,16 +430,7 @@ func (m *CidEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { if m.Version != 0 { i = encodeVarintIndex(dAtA, i, uint64(m.Version)) i-- - dAtA[i] = 0x30 - } - if len(m.GroupIds) > 0 { - for iNdEx := len(m.GroupIds) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.GroupIds[iNdEx]) - copy(dAtA[i:], m.GroupIds[iNdEx]) - i = encodeVarintIndex(dAtA, i, uint64(len(m.GroupIds[iNdEx]))) - i-- - dAtA[i] = 0x2a - } + dAtA[i] = 0x28 } if m.Refs != 0 { i = encodeVarintIndex(dAtA, i, uint64(m.Refs)) @@ -687,12 +668,6 @@ func (m *CidEntry) Size() (n int) { if m.Refs != 0 { n += 1 + sovIndex(uint64(m.Refs)) } - if len(m.GroupIds) > 0 { - for _, s := range m.GroupIds { - l = len(s) - n += 1 + l + sovIndex(uint64(l)) - } - } if m.Version != 0 { n += 1 + sovIndex(uint64(m.Version)) } @@ -903,38 +878,6 @@ func (m *CidEntry) Unmarshal(dAtA []byte) error { } } case 5: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field GroupIds", wireType) - } - var stringLen uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowIndex - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - stringLen |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - intStringLen := int(stringLen) - if intStringLen < 0 { - return ErrInvalidLengthIndex - } - postIndex := iNdEx + intStringLen - if postIndex < 0 { - return ErrInvalidLengthIndex - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.GroupIds = append(m.GroupIds, string(dAtA[iNdEx:postIndex])) - iNdEx = postIndex - case 6: if wireType != 0 { return fmt.Errorf("proto: wrong wireType = %d for field Version", wireType) } diff --git a/index/indexproto/protos/index.proto b/index/indexproto/protos/index.proto index 99c89069..32592310 100644 --- a/index/indexproto/protos/index.proto +++ b/index/indexproto/protos/index.proto @@ -8,8 +8,7 @@ message CidEntry { int64 createTime = 2; int64 updateTime = 3; int32 refs = 4; - repeated string groupIds = 5; - uint32 version = 6; + uint32 version = 5; } message CidList { diff --git a/index/unbind.go b/index/unbind.go index 6483fab1..c8a6ef76 100644 --- a/index/unbind.go +++ b/index/unbind.go @@ -129,7 +129,7 @@ func (ri *redisIndex) FileUnbind(ctx context.Context, key Key, fileId string) (e // update cids for _, idx := range affectedCidIdx { - cids.entries[idx].RemoveGroupId(key.GroupId) + cids.entries[idx].Refs-- if saveErr := cids.entries[idx].Save(ctx, ri.cl); saveErr != nil { log.WarnCtx(ctx, "unable to save cid info", zap.Error(saveErr), zap.String("cid", cids.entries[idx].Cid.String())) } diff --git a/index/unbind_test.go b/index/unbind_test.go index 9c82ca37..b53e9b07 100644 --- a/index/unbind_test.go +++ b/index/unbind_test.go @@ -45,7 +45,7 @@ func TestRedisIndex_UnBind(t *testing.T) { require.NoError(t, err) defer cids.Release() for _, r := range cids.entries { - assert.NotContains(t, r.GroupIds, key.GroupId) + assert.Equal(t, int32(0), r.Refs) } }) t.Run("unbind intersection file", func(t *testing.T) { From e6ee08bb631460ed92a19bee45fd1e0b84812d15 Mon Sep 17 00:00:00 2001 From: Sergey Cherepanov Date: Wed, 25 Oct 2023 14:34:07 +0200 Subject: [PATCH 06/10] accountInfo cmd, store spaceIds on the group, fixes --- filenode/filenode.go | 65 ++++++++++++--- filenode/filenode_test.go | 33 ++++++++ filenode/rpchandler.go | 15 ++++ go.mod | 6 +- go.sum | 12 +-- index/bind.go | 1 + index/bind_test.go | 8 +- index/entry.go | 6 ++ index/index.go | 16 ++-- index/indexproto/index.pb.go | 120 ++++++++++++++++++++-------- index/indexproto/protos/index.proto | 5 +- index/mock_index/mock_index.go | 13 ++- index/unbind.go | 22 +++-- index/unbind_test.go | 2 +- 14 files changed, 247 insertions(+), 77 deletions(-) diff --git a/filenode/filenode.go b/filenode/filenode.go index a640a02c..31222e48 100644 --- a/filenode/filenode.go +++ b/filenode/filenode.go @@ -181,15 +181,61 @@ func (fn *fileNode) StoreKey(ctx context.Context, spaceId string, checkLimit boo } func (fn *fileNode) SpaceInfo(ctx context.Context, spaceId string) (info *fileproto.SpaceInfoResponse, err error) { - info = &fileproto.SpaceInfoResponse{} + var ( + storageKey = index.Key{SpaceId: spaceId} + limitBytes uint64 + ) + if limitBytes, storageKey.GroupId, err = fn.limit.Check(ctx, spaceId); err != nil { + return nil, err + } + groupInfo, err := fn.index.GroupInfo(ctx, storageKey.GroupId) + if err != nil { + return nil, err + } + if info, err = fn.spaceInfo(ctx, storageKey, groupInfo); err != nil { + return nil, err + } + info.LimitBytes = limitBytes + return +} + +func (fn *fileNode) AccountInfo(ctx context.Context) (info *fileproto.AccountInfoResponse, err error) { + info = &fileproto.AccountInfoResponse{} // we have space/identity validation in limit.Check - var storageKey string - if info.LimitBytes, storageKey, err = fn.limit.Check(ctx, spaceId); err != nil { + var groupId string + + if info.LimitBytes, groupId, err = fn.limit.Check(ctx, ""); err != nil { + return nil, err + } + + groupInfo, err := fn.index.GroupInfo(ctx, groupId) + if err != nil { return nil, err } + info.TotalCidsCount = groupInfo.CidsCount + info.TotalUsageBytes = groupInfo.BytesUsage + for _, spaceId := range groupInfo.SpaceIds { + spaceInfo, err := fn.spaceInfo(ctx, index.Key{GroupId: groupId, SpaceId: spaceId}, groupInfo) + if err != nil { + return nil, err + } + spaceInfo.LimitBytes = info.LimitBytes + info.Spaces = append(info.Spaces, spaceInfo) + } + return +} - // TODO: - _ = storageKey +func (fn *fileNode) spaceInfo(ctx context.Context, key index.Key, groupInfo index.GroupInfo) (info *fileproto.SpaceInfoResponse, err error) { + info = &fileproto.SpaceInfoResponse{} + info.SpaceId = key.SpaceId + spaceInfo, err := fn.index.SpaceInfo(ctx, key) + if err != nil { + return nil, err + } + info.TotalUsageBytes = groupInfo.BytesUsage + info.FilesCount = uint64(spaceInfo.FileCount) + info.CidsCount = spaceInfo.CidsCount + info.SpaceUsageBytes = spaceInfo.BytesUsage return } @@ -198,12 +244,7 @@ func (fn *fileNode) FilesDelete(ctx context.Context, spaceId string, fileIds []s if err != nil { return } - for _, fileId := range fileIds { - if err = fn.index.FileUnbind(ctx, storeKey, fileId); err != nil { - return - } - } - return + return fn.index.FileUnbind(ctx, storeKey, fileIds...) } func (fn *fileNode) FileInfo(ctx context.Context, spaceId string, fileIds ...string) (info []*fileproto.FileInfo, err error) { @@ -220,7 +261,7 @@ func (fn *fileNode) FileInfo(ctx context.Context, spaceId string, fileIds ...str info[i] = &fileproto.FileInfo{ FileId: fileIds[i], UsageBytes: fi.BytesUsage, - CidsCount: fi.CidCount, + CidsCount: uint32(fi.CidsCount), } } return diff --git a/filenode/filenode_test.go b/filenode/filenode_test.go index 63fef96b..e03e34c3 100644 --- a/filenode/filenode_test.go +++ b/filenode/filenode_test.go @@ -229,6 +229,39 @@ func TestFileNode_FileInfo(t *testing.T) { assert.Equal(t, uint64(2), resp.FilesInfo[1].UsageBytes) } +func TestFileNode_AccountInfo(t *testing.T) { + fx := newFixture(t) + defer fx.Finish(t) + + var ( + storeKey = newRandKey() + ) + fx.limit.EXPECT().Check(ctx, "").AnyTimes().Return(uint64(100000), storeKey.GroupId, nil) + fx.index.EXPECT().GroupInfo(ctx, storeKey.GroupId).Return(index.GroupInfo{ + BytesUsage: 100, + CidsCount: 10, + SpaceIds: []string{storeKey.SpaceId}, + }, nil) + fx.index.EXPECT().SpaceInfo(ctx, storeKey).Return(index.SpaceInfo{ + BytesUsage: 90, + CidsCount: 9, + FileCount: 1, + }, nil) + + resp, err := fx.handler.AccountInfo(ctx, &fileproto.AccountInfoRequest{}) + require.NoError(t, err) + require.Len(t, resp.Spaces, 1) + assert.Equal(t, uint64(100), resp.TotalUsageBytes) + assert.Equal(t, uint64(10), resp.TotalCidsCount) + assert.Equal(t, uint64(100000), resp.LimitBytes) + + assert.Equal(t, uint64(90), resp.Spaces[0].SpaceUsageBytes) + assert.Equal(t, uint64(9), resp.Spaces[0].CidsCount) + assert.Equal(t, uint64(1), resp.Spaces[0].FilesCount) + assert.Equal(t, uint64(100000), resp.Spaces[0].LimitBytes) + assert.Equal(t, uint64(100), resp.Spaces[0].TotalUsageBytes) +} + func newFixture(t *testing.T) *fixture { ctrl := gomock.NewController(t) fx := &fixture{ diff --git a/filenode/rpchandler.go b/filenode/rpchandler.go index 59c28a0a..761faada 100644 --- a/filenode/rpchandler.go +++ b/filenode/rpchandler.go @@ -202,6 +202,21 @@ func (r rpcHandler) SpaceInfo(ctx context.Context, req *fileproto.SpaceInfoReque return } +func (r rpcHandler) AccountInfo(ctx context.Context, req *fileproto.AccountInfoRequest) (resp *fileproto.AccountInfoResponse, err error) { + st := time.Now() + defer func() { + r.f.metric.RequestLog(ctx, + "file.accountInfo", + metric.TotalDur(time.Since(st)), + zap.Error(err), + ) + }() + if resp, err = r.f.AccountInfo(ctx); err != nil { + return + } + return +} + func convertCids(bCids [][]byte) (cids []cid.Cid) { cids = make([]cid.Cid, 0, len(bCids)) var uniqMap map[string]struct{} diff --git a/go.mod b/go.mod index 0bc44f7e..456edc71 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/OneOfOne/xxhash v1.2.2 github.com/ahmetb/govvv v0.3.0 - github.com/anyproto/any-sync v0.3.3 + github.com/anyproto/any-sync v0.3.5-0.20231025115513-9d662c59b350 github.com/aws/aws-sdk-go v1.46.3 github.com/cespare/xxhash/v2 v2.2.0 github.com/go-redsync/redsync/v4 v4.10.0 @@ -65,7 +65,7 @@ require ( github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect github.com/quic-go/qtls-go1-20 v0.3.4 // indirect - github.com/quic-go/quic-go v0.39.0 // indirect + github.com/quic-go/quic-go v0.39.1 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/zeebo/errs v1.3.0 // indirect @@ -73,7 +73,7 @@ require ( golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.16.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/tools v0.12.1-0.20230815132531-74c255bcf846 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index 899b0825..e5289b6d 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ahmetb/govvv v0.3.0 h1:YGLGwEyiUwHFy5eh/RUhdupbuaCGBYn5T5GWXp+WJB0= github.com/ahmetb/govvv v0.3.0/go.mod h1:4WRFpdWtc/YtKgPFwa1dr5+9hiRY5uKAL08bOlxOR6s= -github.com/anyproto/any-sync v0.3.3 h1:ZD62Geii/ZXaT1laetMFHgWx362Jwx3NM0iUTi/YARA= -github.com/anyproto/any-sync v0.3.3/go.mod h1:Zw7xOQjBVxA0Z+awQxMVj7Ve5/Dbqu2UW9R+8z5upvM= +github.com/anyproto/any-sync v0.3.5-0.20231025115513-9d662c59b350 h1:RdG91WQT6a8V7g2o49c80Mg2FJ79Fx1gLZSMK97ExZU= +github.com/anyproto/any-sync v0.3.5-0.20231025115513-9d662c59b350/go.mod h1:b5VBU4kw1df4ezTYZd9iPEVzPZcDfTGfjis6gUortQc= github.com/anyproto/go-chash v0.1.0 h1:I9meTPjXFRfXZHRJzjOHC/XF7Q5vzysKkiT/grsogXY= github.com/anyproto/go-chash v0.1.0/go.mod h1:0UjNQi3PDazP0fINpFYu6VKhuna+W/V+1vpXHAfNgLY= github.com/anyproto/go-slip10 v1.0.0 h1:uAEtSuudR3jJBOfkOXf3bErxVoxbuKwdoJN55M1i6IA= @@ -155,8 +155,8 @@ github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwa github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY= github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= -github.com/quic-go/quic-go v0.39.0 h1:AgP40iThFMY0bj8jGxROhw3S0FMGa8ryqsmi9tBH3So= -github.com/quic-go/quic-go v0.39.0/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= +github.com/quic-go/quic-go v0.39.1 h1:d/m3oaN/SD2c+f7/yEjZxe2zEVotXprnrCCJ2y/ZZFE= +github.com/quic-go/quic-go v0.39.1/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= github.com/redis/go-redis/v9 v9.2.1 h1:WlYJg71ODF0dVspZZCpYmoF1+U1Jjk9Rwd7pq6QmlCg= github.com/redis/go-redis/v9 v9.2.1/go.mod h1:hdY0cQFCN4fnSYT6TkisLufl/4W5UIXyv0b/CLO2V2M= github.com/redis/rueidis v1.0.19 h1:s65oWtotzlIFN8eMPhyYwxlwLR1lUdhza2KtWprKYSo= @@ -224,8 +224,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/index/bind.go b/index/bind.go index cee6a172..bf1cfb44 100644 --- a/index/bind.go +++ b/index/bind.go @@ -90,6 +90,7 @@ func (ri *redisIndex) FileBind(ctx context.Context, key Key, fileId string, cids groupInfo.Size_ += cids.entries[idx].Size_ } } + groupInfo.AddSpaceId(key.SpaceId) if isNewFile { spaceInfo.FileCount++ } diff --git a/index/bind_test.go b/index/bind_test.go index 35346a19..5d4fae35 100644 --- a/index/bind_test.go +++ b/index/bind_test.go @@ -28,7 +28,7 @@ func TestRedisIndex_Bind(t *testing.T) { require.NoError(t, fx.FileBind(ctx, key, fileId, cids)) fInfo, err := fx.FileInfo(ctx, key, fileId) require.NoError(t, err) - assert.Equal(t, uint32(len(bs)), fInfo[0].CidCount) + assert.Equal(t, uint64(len(bs)), fInfo[0].CidsCount) assert.Equal(t, sumSize, fInfo[0].BytesUsage) }) @@ -60,13 +60,15 @@ func TestRedisIndex_Bind(t *testing.T) { assert.Equal(t, SpaceInfo{ BytesUsage: sumSize, FileCount: 2, + CidsCount: uint64(len(bs)), }, spaceInfo) groupInfo, err := fx.GroupInfo(ctx, key.GroupId) require.NoError(t, err) assert.Equal(t, GroupInfo{ BytesUsage: sumSize, - CidsCount: uint32(len(bs)), + CidsCount: uint64(len(bs)), + SpaceIds: []string{key.SpaceId}, }, groupInfo) }) @@ -92,7 +94,7 @@ func TestRedisIndex_Bind(t *testing.T) { fInfo, err := fx.FileInfo(ctx, key, fileId) require.NoError(t, err) - assert.Equal(t, uint32(len(bs)), fInfo[0].CidCount) + assert.Equal(t, uint64(len(bs)), fInfo[0].CidsCount) assert.Equal(t, sumSize, fInfo[0].BytesUsage) }) } diff --git a/index/entry.go b/index/entry.go index 3849ccfb..b962d3af 100644 --- a/index/entry.go +++ b/index/entry.go @@ -95,6 +95,12 @@ func (f *groupEntry) Save(ctx context.Context, k Key, cl redis.Pipeliner) { cl.HSet(ctx, groupKey(k), infoKey, data) } +func (f *groupEntry) AddSpaceId(spaceId string) { + if !slices.Contains(f.SpaceIds, spaceId) { + f.SpaceIds = append(f.SpaceIds, spaceId) + } +} + func (ri *redisIndex) getGroupEntry(ctx context.Context, key Key) (entry *groupEntry, err error) { result, err := ri.cl.HGet(ctx, groupKey(key), infoKey).Result() if err != nil && !errors.Is(err, redis.Nil) { diff --git a/index/index.go b/index/index.go index db147f5c..c4899cfd 100644 --- a/index/index.go +++ b/index/index.go @@ -26,14 +26,12 @@ const CName = "filenode.index" var log = logger.NewNamed(CName) var ( - ErrCidsNotExist = errors.New("cids not exist") - ErrTargetStorageExists = errors.New("target storage exists") - ErrStorageNotFound = errors.New("storage not found") + ErrCidsNotExist = errors.New("cids not exist") ) type Index interface { FileBind(ctx context.Context, key Key, fileId string, cidEntries *CidEntries) (err error) - FileUnbind(ctx context.Context, kye Key, fileId string) (err error) + FileUnbind(ctx context.Context, kye Key, fileIds ...string) (err error) FileInfo(ctx context.Context, key Key, fileIds ...string) (fileInfo []FileInfo, err error) GroupInfo(ctx context.Context, groupId string) (info GroupInfo, err error) @@ -64,17 +62,19 @@ type Key struct { type GroupInfo struct { BytesUsage uint64 - CidsCount uint32 + CidsCount uint64 + SpaceIds []string } type SpaceInfo struct { BytesUsage uint64 + CidsCount uint64 FileCount uint32 } type FileInfo struct { BytesUsage uint64 - CidCount uint32 + CidsCount uint64 } /* @@ -138,7 +138,7 @@ func (ri *redisIndex) FileInfo(ctx context.Context, key Key, fileIds ...string) } fileInfos[i] = FileInfo{ BytesUsage: fEntry.Size_, - CidCount: uint32(len(fEntry.Cids)), + CidsCount: uint64(len(fEntry.Cids)), } } return @@ -190,6 +190,7 @@ func (ri *redisIndex) GroupInfo(ctx context.Context, groupId string) (info Group return GroupInfo{ BytesUsage: sEntry.Size_, CidsCount: sEntry.CidCount, + SpaceIds: sEntry.SpaceIds, }, nil } @@ -205,6 +206,7 @@ func (ri *redisIndex) SpaceInfo(ctx context.Context, key Key) (info SpaceInfo, e } return SpaceInfo{ BytesUsage: sEntry.Size_, + CidsCount: sEntry.CidCount, FileCount: sEntry.FileCount, }, nil } diff --git a/index/indexproto/index.pb.go b/index/indexproto/index.pb.go index 3bdbd292..80851f09 100644 --- a/index/indexproto/index.pb.go +++ b/index/indexproto/index.pb.go @@ -143,11 +143,12 @@ func (m *CidList) GetCids() [][]byte { } type GroupEntry struct { - GroupId string `protobuf:"bytes,1,opt,name=groupId,proto3" json:"groupId,omitempty"` - CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` - UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` - Size_ uint64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` - CidCount uint32 `protobuf:"varint,5,opt,name=cidCount,proto3" json:"cidCount,omitempty"` + GroupId string `protobuf:"bytes,1,opt,name=groupId,proto3" json:"groupId,omitempty"` + CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` + UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` + Size_ uint64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` + CidCount uint64 `protobuf:"varint,5,opt,name=cidCount,proto3" json:"cidCount,omitempty"` + SpaceIds []string `protobuf:"bytes,6,rep,name=spaceIds,proto3" json:"spaceIds,omitempty"` } func (m *GroupEntry) Reset() { *m = GroupEntry{} } @@ -211,20 +212,27 @@ func (m *GroupEntry) GetSize_() uint64 { return 0 } -func (m *GroupEntry) GetCidCount() uint32 { +func (m *GroupEntry) GetCidCount() uint64 { if m != nil { return m.CidCount } return 0 } +func (m *GroupEntry) GetSpaceIds() []string { + if m != nil { + return m.SpaceIds + } + return nil +} + type SpaceEntry struct { GroupId string `protobuf:"bytes,1,opt,name=groupId,proto3" json:"groupId,omitempty"` CreateTime int64 `protobuf:"varint,2,opt,name=createTime,proto3" json:"createTime,omitempty"` UpdateTime int64 `protobuf:"varint,3,opt,name=updateTime,proto3" json:"updateTime,omitempty"` Size_ uint64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` FileCount uint32 `protobuf:"varint,5,opt,name=fileCount,proto3" json:"fileCount,omitempty"` - CidCount uint32 `protobuf:"varint,6,opt,name=cidCount,proto3" json:"cidCount,omitempty"` + CidCount uint64 `protobuf:"varint,6,opt,name=cidCount,proto3" json:"cidCount,omitempty"` } func (m *SpaceEntry) Reset() { *m = SpaceEntry{} } @@ -295,7 +303,7 @@ func (m *SpaceEntry) GetFileCount() uint32 { return 0 } -func (m *SpaceEntry) GetCidCount() uint32 { +func (m *SpaceEntry) GetCidCount() uint64 { if m != nil { return m.CidCount } @@ -383,28 +391,29 @@ func init() { } var fileDescriptor_f1f29953df8d243b = []byte{ - // 326 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x52, 0xce, 0xcc, 0x4b, 0x49, - 0xad, 0xd0, 0x07, 0x93, 0x05, 0x45, 0xf9, 0x25, 0xf9, 0xfa, 0x60, 0xb2, 0x18, 0x22, 0xa2, 0x07, - 0xe6, 0x08, 0xf1, 0xa5, 0x65, 0xe6, 0xa4, 0x7a, 0x82, 0x04, 0x02, 0x40, 0x7c, 0xa5, 0x1e, 0x46, - 0x2e, 0x0e, 0xe7, 0xcc, 0x14, 0xd7, 0xbc, 0x92, 0xa2, 0x4a, 0x21, 0x21, 0x2e, 0x96, 0xe2, 0xcc, - 0xaa, 0x54, 0x09, 0x46, 0x05, 0x46, 0x0d, 0x96, 0x20, 0x30, 0x5b, 0x48, 0x8e, 0x8b, 0x2b, 0xb9, - 0x28, 0x35, 0xb1, 0x24, 0x35, 0x24, 0x33, 0x37, 0x55, 0x82, 0x49, 0x81, 0x51, 0x83, 0x39, 0x08, - 0x49, 0x04, 0x24, 0x5f, 0x5a, 0x90, 0x02, 0x93, 0x67, 0x86, 0xc8, 0x23, 0x44, 0x40, 0x66, 0x16, - 0xa5, 0xa6, 0x15, 0x4b, 0xb0, 0x28, 0x30, 0x6a, 0xb0, 0x06, 0x81, 0xd9, 0x42, 0x12, 0x5c, 0xec, - 0x65, 0xa9, 0x45, 0xc5, 0x99, 0xf9, 0x79, 0x12, 0xac, 0x0a, 0x8c, 0x1a, 0xbc, 0x41, 0x30, 0xae, - 0x92, 0x2c, 0x17, 0xbb, 0x73, 0x66, 0x8a, 0x4f, 0x66, 0x71, 0x09, 0x48, 0x63, 0x72, 0x66, 0x4a, - 0xb1, 0x04, 0xa3, 0x02, 0xb3, 0x06, 0x4f, 0x10, 0x98, 0xad, 0x34, 0x8d, 0x91, 0x8b, 0xcb, 0xbd, - 0x28, 0xbf, 0xb4, 0x00, 0xe2, 0x5e, 0x09, 0x2e, 0xf6, 0x74, 0x10, 0xcf, 0x33, 0x05, 0xec, 0x64, - 0xce, 0x20, 0x18, 0x97, 0x1a, 0xae, 0x06, 0x87, 0x04, 0x0b, 0x52, 0x48, 0x48, 0x71, 0x71, 0x24, - 0x67, 0xa6, 0x38, 0xe7, 0x97, 0xe6, 0x95, 0x40, 0x9d, 0x0d, 0xe7, 0x2b, 0x6d, 0x61, 0xe4, 0xe2, - 0x0a, 0x2e, 0x48, 0x4c, 0x4e, 0x1d, 0x08, 0x87, 0xc9, 0x70, 0x71, 0x82, 0x62, 0x15, 0xd9, 0x65, - 0x08, 0x01, 0x14, 0x67, 0xb3, 0xa1, 0x39, 0xbb, 0x98, 0x8b, 0xd3, 0x2d, 0x33, 0x27, 0x15, 0x1e, - 0xfb, 0xf0, 0x00, 0xe7, 0x84, 0x04, 0x38, 0xdc, 0x3a, 0x26, 0x9c, 0x29, 0x82, 0x99, 0x80, 0x17, - 0x58, 0xd0, 0xbd, 0xe0, 0xa4, 0x75, 0xe2, 0x91, 0x1c, 0xe3, 0x85, 0x47, 0x72, 0x8c, 0x0f, 0x1e, - 0xc9, 0x31, 0x4e, 0x78, 0x2c, 0xc7, 0x70, 0xe1, 0xb1, 0x1c, 0xc3, 0x8d, 0xc7, 0x72, 0x0c, 0x51, - 0x02, 0xe8, 0x29, 0x38, 0x89, 0x0d, 0x4c, 0x19, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0x85, 0x61, - 0x15, 0x22, 0xdc, 0x02, 0x00, 0x00, + // 342 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x92, 0xcf, 0x4a, 0xc3, 0x40, + 0x10, 0xc6, 0x3b, 0x4d, 0xfa, 0x27, 0x83, 0x15, 0xc9, 0x69, 0x11, 0x5d, 0x42, 0xbc, 0x2c, 0x1e, + 0xec, 0xc1, 0x37, 0xb0, 0xa8, 0x14, 0x3c, 0xc8, 0xea, 0xc9, 0x5b, 0xcd, 0x6e, 0x65, 0xa1, 0x26, + 0x61, 0x37, 0x15, 0xf5, 0x19, 0x3c, 0xf8, 0x2c, 0xe2, 0x43, 0x78, 0xec, 0xd1, 0xa3, 0xb4, 0x2f, + 0x22, 0xbb, 0xa1, 0x49, 0x5b, 0x10, 0x2f, 0x82, 0x97, 0xcd, 0xfc, 0xbe, 0x21, 0xc9, 0xf7, 0xed, + 0x0c, 0x1e, 0xa8, 0x54, 0xc8, 0xc7, 0xbe, 0x3b, 0x73, 0x9d, 0x15, 0x59, 0xdf, 0x9d, 0xa6, 0x54, + 0x8e, 0x1c, 0x84, 0xdb, 0x63, 0x35, 0x91, 0x43, 0x2b, 0x5c, 0x5a, 0x8e, 0x5f, 0x00, 0xbb, 0x03, + 0x25, 0x4e, 0xd3, 0x42, 0x3f, 0x85, 0x21, 0xfa, 0x46, 0x3d, 0x4b, 0x02, 0x11, 0x30, 0x9f, 0xbb, + 0x3a, 0xa4, 0x88, 0x89, 0x96, 0xa3, 0x42, 0x5e, 0xab, 0x7b, 0x49, 0x9a, 0x11, 0x30, 0x8f, 0xaf, + 0x28, 0xb6, 0x3f, 0xcd, 0xc5, 0xb2, 0xef, 0x95, 0xfd, 0x5a, 0xb1, 0xdf, 0xd4, 0x72, 0x6c, 0x88, + 0x1f, 0x01, 0x6b, 0x71, 0x57, 0x87, 0x04, 0x3b, 0x0f, 0x52, 0x1b, 0x95, 0xa5, 0xa4, 0x15, 0x01, + 0xeb, 0xf1, 0x25, 0xc6, 0xfb, 0xd8, 0x19, 0x28, 0x71, 0xa1, 0x4c, 0x61, 0x5f, 0x4c, 0x94, 0x30, + 0x04, 0x22, 0x8f, 0x6d, 0x71, 0x57, 0xc7, 0x6f, 0x80, 0x78, 0xae, 0xb3, 0x69, 0x5e, 0xfa, 0x25, + 0xd8, 0xb9, 0xb3, 0x34, 0x14, 0xce, 0x72, 0xc0, 0x97, 0xf8, 0x17, 0xae, 0xdd, 0x4d, 0xf8, 0x2b, + 0x37, 0xb1, 0x8b, 0xdd, 0x44, 0x89, 0x41, 0x36, 0x4d, 0x0b, 0x67, 0xdb, 0xe7, 0x15, 0xdb, 0x9e, + 0xc9, 0x47, 0x89, 0x1c, 0x0a, 0x43, 0xda, 0x91, 0xc7, 0x02, 0x5e, 0x71, 0xfc, 0x0e, 0x88, 0x57, + 0x16, 0xfe, 0xc3, 0xf4, 0x1e, 0x06, 0x76, 0xe2, 0xb5, 0xeb, 0x1e, 0xaf, 0x85, 0xb5, 0x48, 0xed, + 0xf5, 0x48, 0xb1, 0xc1, 0xe0, 0x4c, 0x4d, 0x64, 0xb5, 0x19, 0xd5, 0x30, 0x82, 0x72, 0x18, 0xd5, + 0xef, 0x9a, 0x3f, 0x6e, 0x8b, 0xf7, 0x4b, 0x04, 0x7f, 0x33, 0xc2, 0xc9, 0xe1, 0xc7, 0x9c, 0xc2, + 0x6c, 0x4e, 0xe1, 0x6b, 0x4e, 0xe1, 0x75, 0x41, 0x1b, 0xb3, 0x05, 0x6d, 0x7c, 0x2e, 0x68, 0xe3, + 0x66, 0x67, 0x73, 0xbb, 0x6f, 0xdb, 0xee, 0x71, 0xfc, 0x1d, 0x00, 0x00, 0xff, 0xff, 0xb9, 0xc4, + 0xb2, 0x7d, 0xf8, 0x02, 0x00, 0x00, } func (m *CidEntry) Marshal() (dAtA []byte, err error) { @@ -507,6 +516,15 @@ func (m *GroupEntry) MarshalToSizedBuffer(dAtA []byte) (int, error) { _ = i var l int _ = l + if len(m.SpaceIds) > 0 { + for iNdEx := len(m.SpaceIds) - 1; iNdEx >= 0; iNdEx-- { + i -= len(m.SpaceIds[iNdEx]) + copy(dAtA[i:], m.SpaceIds[iNdEx]) + i = encodeVarintIndex(dAtA, i, uint64(len(m.SpaceIds[iNdEx]))) + i-- + dAtA[i] = 0x32 + } + } if m.CidCount != 0 { i = encodeVarintIndex(dAtA, i, uint64(m.CidCount)) i-- @@ -711,6 +729,12 @@ func (m *GroupEntry) Size() (n int) { if m.CidCount != 0 { n += 1 + sovIndex(uint64(m.CidCount)) } + if len(m.SpaceIds) > 0 { + for _, s := range m.SpaceIds { + l = len(s) + n += 1 + l + sovIndex(uint64(l)) + } + } return n } @@ -1131,11 +1155,43 @@ func (m *GroupEntry) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - m.CidCount |= uint32(b&0x7F) << shift + m.CidCount |= uint64(b&0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field SpaceIds", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowIndex + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= uint64(b&0x7F) << shift if b < 0x80 { break } } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthIndex + } + postIndex := iNdEx + intStringLen + if postIndex < 0 { + return ErrInvalidLengthIndex + } + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.SpaceIds = append(m.SpaceIds, string(dAtA[iNdEx:postIndex])) + iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipIndex(dAtA[iNdEx:]) @@ -1308,7 +1364,7 @@ func (m *SpaceEntry) Unmarshal(dAtA []byte) error { } b := dAtA[iNdEx] iNdEx++ - m.CidCount |= uint32(b&0x7F) << shift + m.CidCount |= uint64(b&0x7F) << shift if b < 0x80 { break } diff --git a/index/indexproto/protos/index.proto b/index/indexproto/protos/index.proto index 32592310..e220e2a1 100644 --- a/index/indexproto/protos/index.proto +++ b/index/indexproto/protos/index.proto @@ -20,7 +20,8 @@ message GroupEntry { int64 createTime = 2; int64 updateTime = 3; uint64 size = 4; - uint32 cidCount = 5; + uint64 cidCount = 5; + repeated string spaceIds = 6; } message SpaceEntry { @@ -29,7 +30,7 @@ message SpaceEntry { int64 updateTime = 3; uint64 size = 4; uint32 fileCount = 5; - uint32 cidCount = 6; + uint64 cidCount = 6; } message FileEntry { diff --git a/index/mock_index/mock_index.go b/index/mock_index/mock_index.go index 06da3f40..66653489 100644 --- a/index/mock_index/mock_index.go +++ b/index/mock_index/mock_index.go @@ -195,17 +195,22 @@ func (mr *MockIndexMockRecorder) FileInfo(arg0, arg1 any, arg2 ...any) *gomock.C } // FileUnbind mocks base method. -func (m *MockIndex) FileUnbind(arg0 context.Context, arg1 index.Key, arg2 string) error { +func (m *MockIndex) FileUnbind(arg0 context.Context, arg1 index.Key, arg2 ...string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "FileUnbind", arg0, arg1, arg2) + varargs := []any{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "FileUnbind", varargs...) ret0, _ := ret[0].(error) return ret0 } // FileUnbind indicates an expected call of FileUnbind. -func (mr *MockIndexMockRecorder) FileUnbind(arg0, arg1, arg2 any) *gomock.Call { +func (mr *MockIndexMockRecorder) FileUnbind(arg0, arg1 any, arg2 ...any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileUnbind", reflect.TypeOf((*MockIndex)(nil).FileUnbind), arg0, arg1, arg2) + varargs := append([]any{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FileUnbind", reflect.TypeOf((*MockIndex)(nil).FileUnbind), varargs...) } // GroupInfo mocks base method. diff --git a/index/unbind.go b/index/unbind.go index c8a6ef76..9e857510 100644 --- a/index/unbind.go +++ b/index/unbind.go @@ -7,22 +7,30 @@ import ( "go.uber.org/zap" ) -func (ri *redisIndex) FileUnbind(ctx context.Context, key Key, fileId string) (err error) { - var ( - sk = spaceKey(key) - gk = groupKey(key) - ) - _, gRelease, err := ri.AcquireKey(ctx, gk) +func (ri *redisIndex) FileUnbind(ctx context.Context, key Key, fileIds ...string) (err error) { + _, gRelease, err := ri.AcquireKey(ctx, groupKey(key)) if err != nil { return } defer gRelease() - _, sRelease, err := ri.AcquireKey(ctx, sk) + _, sRelease, err := ri.AcquireKey(ctx, spaceKey(key)) if err != nil { return } defer sRelease() + for _, fileId := range fileIds { + if err = ri.fileUnbind(ctx, key, fileId); err != nil { + return + } + } + return +} +func (ri *redisIndex) fileUnbind(ctx context.Context, key Key, fileId string) (err error) { + var ( + sk = spaceKey(key) + gk = groupKey(key) + ) // get file entry fileInfo, isNewFile, err := ri.getFileEntry(ctx, key, fileId) if err != nil { diff --git a/index/unbind_test.go b/index/unbind_test.go index b53e9b07..5fd20031 100644 --- a/index/unbind_test.go +++ b/index/unbind_test.go @@ -78,7 +78,7 @@ func TestRedisIndex_UnBind(t *testing.T) { groupInfo, err := fx.GroupInfo(ctx, key.GroupId) require.NoError(t, err) - assert.Equal(t, uint32(2), groupInfo.CidsCount) + assert.Equal(t, uint64(2), groupInfo.CidsCount) assert.Equal(t, file1Size, groupInfo.BytesUsage) spaceInfo, err := fx.SpaceInfo(ctx, key) require.NoError(t, err) From aea454f2855b64f6c83fe8a9d014c7fca8fb1011 Mon Sep 17 00:00:00 2001 From: Grigory Efimov Date: Wed, 25 Oct 2023 13:37:57 +0000 Subject: [PATCH 07/10] .github/workflows/coverage.yml image redis/redis-stack-server --- .github/workflows/coverage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index d5403b10..61173b4c 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,7 +21,7 @@ jobs: # redis for tests services: redis: - image: redis + image: redis/redis-stack-server options: >- --health-cmd "redis-cli ping" --health-interval 10s From cfc84c1cd0360d3603d4d3c4cdcec190a357cdb1 Mon Sep 17 00:00:00 2001 From: Grigory Efimov Date: Wed, 25 Oct 2023 13:42:07 +0000 Subject: [PATCH 08/10] .github/workflows/release.yml image redis/redis-stack-server --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf16b53a..b7740732 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,7 +49,7 @@ jobs: # redis for tests services: redis: - image: redis + image: redis/redis-stack-server options: >- --health-cmd "redis-cli ping" --health-interval 10s From 3974d3e3a2ad871f496fed9953e461ded597a404 Mon Sep 17 00:00:00 2001 From: Sergey Cherepanov Date: Wed, 25 Oct 2023 15:51:27 +0200 Subject: [PATCH 09/10] update any-sync --- go.mod | 2 +- go.sum | 2 ++ limit/mock_limit/mock_limit.go | 14 +++++++++----- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 456edc71..f41a0b86 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/OneOfOne/xxhash v1.2.2 github.com/ahmetb/govvv v0.3.0 - github.com/anyproto/any-sync v0.3.5-0.20231025115513-9d662c59b350 + github.com/anyproto/any-sync v0.3.5-0.20231025135038-838be4ed5f2b github.com/aws/aws-sdk-go v1.46.3 github.com/cespare/xxhash/v2 v2.2.0 github.com/go-redsync/redsync/v4 v4.10.0 diff --git a/go.sum b/go.sum index e5289b6d..f8deb265 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/ahmetb/govvv v0.3.0 h1:YGLGwEyiUwHFy5eh/RUhdupbuaCGBYn5T5GWXp+WJB0= github.com/ahmetb/govvv v0.3.0/go.mod h1:4WRFpdWtc/YtKgPFwa1dr5+9hiRY5uKAL08bOlxOR6s= github.com/anyproto/any-sync v0.3.5-0.20231025115513-9d662c59b350 h1:RdG91WQT6a8V7g2o49c80Mg2FJ79Fx1gLZSMK97ExZU= github.com/anyproto/any-sync v0.3.5-0.20231025115513-9d662c59b350/go.mod h1:b5VBU4kw1df4ezTYZd9iPEVzPZcDfTGfjis6gUortQc= +github.com/anyproto/any-sync v0.3.5-0.20231025135038-838be4ed5f2b h1:qwtQtdrM5VsamrWdgx8Kg/TJKmBtx8YpdeQYFinXSds= +github.com/anyproto/any-sync v0.3.5-0.20231025135038-838be4ed5f2b/go.mod h1:b5VBU4kw1df4ezTYZd9iPEVzPZcDfTGfjis6gUortQc= github.com/anyproto/go-chash v0.1.0 h1:I9meTPjXFRfXZHRJzjOHC/XF7Q5vzysKkiT/grsogXY= github.com/anyproto/go-chash v0.1.0/go.mod h1:0UjNQi3PDazP0fINpFYu6VKhuna+W/V+1vpXHAfNgLY= github.com/anyproto/go-slip10 v1.0.0 h1:uAEtSuudR3jJBOfkOXf3bErxVoxbuKwdoJN55M1i6IA= diff --git a/limit/mock_limit/mock_limit.go b/limit/mock_limit/mock_limit.go index 59f9585b..982266ab 100644 --- a/limit/mock_limit/mock_limit.go +++ b/limit/mock_limit/mock_limit.go @@ -1,6 +1,10 @@ // Code generated by MockGen. DO NOT EDIT. // Source: github.com/anyproto/any-sync-filenode/limit (interfaces: Limit) - +// +// Generated by this command: +// +// mockgen -destination mock_limit/mock_limit.go github.com/anyproto/any-sync-filenode/limit Limit +// // Package mock_limit is a generated GoMock package. package mock_limit @@ -46,7 +50,7 @@ func (m *MockLimit) Check(arg0 context.Context, arg1 string) (uint64, string, er } // Check indicates an expected call of Check. -func (mr *MockLimitMockRecorder) Check(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockLimitMockRecorder) Check(arg0, arg1 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Check", reflect.TypeOf((*MockLimit)(nil).Check), arg0, arg1) } @@ -60,7 +64,7 @@ func (m *MockLimit) Close(arg0 context.Context) error { } // Close indicates an expected call of Close. -func (mr *MockLimitMockRecorder) Close(arg0 interface{}) *gomock.Call { +func (mr *MockLimitMockRecorder) Close(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockLimit)(nil).Close), arg0) } @@ -74,7 +78,7 @@ func (m *MockLimit) Init(arg0 *app.App) error { } // Init indicates an expected call of Init. -func (mr *MockLimitMockRecorder) Init(arg0 interface{}) *gomock.Call { +func (mr *MockLimitMockRecorder) Init(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockLimit)(nil).Init), arg0) } @@ -102,7 +106,7 @@ func (m *MockLimit) Run(arg0 context.Context) error { } // Run indicates an expected call of Run. -func (mr *MockLimitMockRecorder) Run(arg0 interface{}) *gomock.Call { +func (mr *MockLimitMockRecorder) Run(arg0 any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Run", reflect.TypeOf((*MockLimit)(nil).Run), arg0) } From a2ace89914bd40192027b5c4dc1ca6b4c8ffff9e Mon Sep 17 00:00:00 2001 From: Sergey Cherepanov Date: Wed, 25 Oct 2023 16:22:50 +0200 Subject: [PATCH 10/10] update any-sync --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index f41a0b86..8778ae75 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 require ( github.com/OneOfOne/xxhash v1.2.2 github.com/ahmetb/govvv v0.3.0 - github.com/anyproto/any-sync v0.3.5-0.20231025135038-838be4ed5f2b + github.com/anyproto/any-sync v0.3.5 github.com/aws/aws-sdk-go v1.46.3 github.com/cespare/xxhash/v2 v2.2.0 github.com/go-redsync/redsync/v4 v4.10.0 diff --git a/go.sum b/go.sum index f8deb265..fd74e456 100644 --- a/go.sum +++ b/go.sum @@ -4,10 +4,8 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ahmetb/govvv v0.3.0 h1:YGLGwEyiUwHFy5eh/RUhdupbuaCGBYn5T5GWXp+WJB0= github.com/ahmetb/govvv v0.3.0/go.mod h1:4WRFpdWtc/YtKgPFwa1dr5+9hiRY5uKAL08bOlxOR6s= -github.com/anyproto/any-sync v0.3.5-0.20231025115513-9d662c59b350 h1:RdG91WQT6a8V7g2o49c80Mg2FJ79Fx1gLZSMK97ExZU= -github.com/anyproto/any-sync v0.3.5-0.20231025115513-9d662c59b350/go.mod h1:b5VBU4kw1df4ezTYZd9iPEVzPZcDfTGfjis6gUortQc= -github.com/anyproto/any-sync v0.3.5-0.20231025135038-838be4ed5f2b h1:qwtQtdrM5VsamrWdgx8Kg/TJKmBtx8YpdeQYFinXSds= -github.com/anyproto/any-sync v0.3.5-0.20231025135038-838be4ed5f2b/go.mod h1:b5VBU4kw1df4ezTYZd9iPEVzPZcDfTGfjis6gUortQc= +github.com/anyproto/any-sync v0.3.5 h1:G7hqF2ww8oNs1C3Tve/k3B9PucaFU0UOkS4iopLYA1I= +github.com/anyproto/any-sync v0.3.5/go.mod h1:b5VBU4kw1df4ezTYZd9iPEVzPZcDfTGfjis6gUortQc= github.com/anyproto/go-chash v0.1.0 h1:I9meTPjXFRfXZHRJzjOHC/XF7Q5vzysKkiT/grsogXY= github.com/anyproto/go-chash v0.1.0/go.mod h1:0UjNQi3PDazP0fINpFYu6VKhuna+W/V+1vpXHAfNgLY= github.com/anyproto/go-slip10 v1.0.0 h1:uAEtSuudR3jJBOfkOXf3bErxVoxbuKwdoJN55M1i6IA=