From 3ef30897ae094039834b9b42e8baaae27a03e869 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 21 Apr 2026 14:14:24 +0800 Subject: [PATCH 1/5] CBT bitmap implementation Signed-off-by: Lyndon-Li --- changelogs/unreleased/9736-Lyndon-Li | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelogs/unreleased/9736-Lyndon-Li diff --git a/changelogs/unreleased/9736-Lyndon-Li b/changelogs/unreleased/9736-Lyndon-Li new file mode 100644 index 000000000..ef271b063 --- /dev/null +++ b/changelogs/unreleased/9736-Lyndon-Li @@ -0,0 +1 @@ +Add CBT bitmap implementation for block data mover \ No newline at end of file From a1fd85c79125365806b85646cf00044ca09980a7 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 21 Apr 2026 16:07:06 +0800 Subject: [PATCH 2/5] add CBT bitmap implementation Signed-off-by: Lyndon-Li --- go.mod | 3 + go.sum | 6 + pkg/cbtservice/mocks/Service.go | 171 ++++++++++++++++ pkg/cbtservice/service.go | 4 +- pkg/uploader/cbt/bitmap.go | 112 ++++++++++- pkg/uploader/cbt/bitmap_test.go | 256 ++++++++++++++++++++++++ pkg/uploader/cbt/mocks/Bitmap.go | 294 +++++++++++++++++++++++++++ pkg/uploader/cbt/mocks/Iterator.go | 309 +++++++++++++++++++++++++++++ pkg/uploader/cbt/set.go | 49 +++-- pkg/uploader/cbt/set_test.go | 150 ++++++++++++++ 10 files changed, 1328 insertions(+), 26 deletions(-) create mode 100644 pkg/cbtservice/mocks/Service.go create mode 100644 pkg/uploader/cbt/bitmap_test.go create mode 100644 pkg/uploader/cbt/mocks/Bitmap.go create mode 100644 pkg/uploader/cbt/mocks/Iterator.go create mode 100644 pkg/uploader/cbt/set_test.go diff --git a/go.mod b/go.mod index e80bfbcf9..efdc3b828 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.6.0 github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 + github.com/RoaringBitmap/roaring v1.9.4 github.com/aws/aws-sdk-go-v2 v1.24.1 github.com/aws/aws-sdk-go-v2/config v1.26.3 github.com/aws/aws-sdk-go-v2/credentials v1.16.14 @@ -91,6 +92,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect github.com/aws/smithy-go v1.19.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.12.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chmduquesne/rollinghash v4.0.0+incompatible // indirect @@ -148,6 +150,7 @@ require ( github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/mschoch/smat v0.2.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/mxk/go-vss v1.2.0 // indirect diff --git a/go.sum b/go.sum index d8ee0cab0..a2196812b 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb0 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ= +github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -168,6 +170,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA= +github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -562,6 +566,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= diff --git a/pkg/cbtservice/mocks/Service.go b/pkg/cbtservice/mocks/Service.go new file mode 100644 index 000000000..f7a9567a8 --- /dev/null +++ b/pkg/cbtservice/mocks/Service.go @@ -0,0 +1,171 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + mock "github.com/stretchr/testify/mock" + "github.com/vmware-tanzu/velero/pkg/cbtservice" +) + +// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewService(t interface { + mock.TestingT + Cleanup(func()) +}) *Service { + mock := &Service{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// Service is an autogenerated mock type for the Service type +type Service struct { + mock.Mock +} + +type Service_Expecter struct { + mock *mock.Mock +} + +func (_m *Service) EXPECT() *Service_Expecter { + return &Service_Expecter{mock: &_m.Mock} +} + +// GetAllocatedBlocks provides a mock function for the type Service +func (_mock *Service) GetAllocatedBlocks(ctx context.Context, snapshot string, record func([]cbtservice.Range) error) error { + ret := _mock.Called(ctx, snapshot, record) + + if len(ret) == 0 { + panic("no return value specified for GetAllocatedBlocks") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, func([]cbtservice.Range) error) error); ok { + r0 = returnFunc(ctx, snapshot, record) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// Service_GetAllocatedBlocks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAllocatedBlocks' +type Service_GetAllocatedBlocks_Call struct { + *mock.Call +} + +// GetAllocatedBlocks is a helper method to define mock.On call +// - ctx context.Context +// - snapshot string +// - record func([]cbtservice.Range) error +func (_e *Service_Expecter) GetAllocatedBlocks(ctx interface{}, snapshot interface{}, record interface{}) *Service_GetAllocatedBlocks_Call { + return &Service_GetAllocatedBlocks_Call{Call: _e.mock.On("GetAllocatedBlocks", ctx, snapshot, record)} +} + +func (_c *Service_GetAllocatedBlocks_Call) Run(run func(ctx context.Context, snapshot string, record func([]cbtservice.Range) error)) *Service_GetAllocatedBlocks_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 func([]cbtservice.Range) error + if args[2] != nil { + arg2 = args[2].(func([]cbtservice.Range) error) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *Service_GetAllocatedBlocks_Call) Return(err error) *Service_GetAllocatedBlocks_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Service_GetAllocatedBlocks_Call) RunAndReturn(run func(ctx context.Context, snapshot string, record func([]cbtservice.Range) error) error) *Service_GetAllocatedBlocks_Call { + _c.Call.Return(run) + return _c +} + +// GetChangedBlocks provides a mock function for the type Service +func (_mock *Service) GetChangedBlocks(ctx context.Context, snapshot string, changeID string, record func([]cbtservice.Range) error) error { + ret := _mock.Called(ctx, snapshot, changeID, record) + + if len(ret) == 0 { + panic("no return value specified for GetChangedBlocks") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string, func([]cbtservice.Range) error) error); ok { + r0 = returnFunc(ctx, snapshot, changeID, record) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// Service_GetChangedBlocks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetChangedBlocks' +type Service_GetChangedBlocks_Call struct { + *mock.Call +} + +// GetChangedBlocks is a helper method to define mock.On call +// - ctx context.Context +// - snapshot string +// - changeID string +// - record func([]cbtservice.Range) error +func (_e *Service_Expecter) GetChangedBlocks(ctx interface{}, snapshot interface{}, changeID interface{}, record interface{}) *Service_GetChangedBlocks_Call { + return &Service_GetChangedBlocks_Call{Call: _e.mock.On("GetChangedBlocks", ctx, snapshot, changeID, record)} +} + +func (_c *Service_GetChangedBlocks_Call) Run(run func(ctx context.Context, snapshot string, changeID string, record func([]cbtservice.Range) error)) *Service_GetChangedBlocks_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + var arg3 func([]cbtservice.Range) error + if args[3] != nil { + arg3 = args[3].(func([]cbtservice.Range) error) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *Service_GetChangedBlocks_Call) Return(err error) *Service_GetChangedBlocks_Call { + _c.Call.Return(err) + return _c +} + +func (_c *Service_GetChangedBlocks_Call) RunAndReturn(run func(ctx context.Context, snapshot string, changeID string, record func([]cbtservice.Range) error) error) *Service_GetChangedBlocks_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/cbtservice/service.go b/pkg/cbtservice/service.go index 96e43b2c4..3e2a0591d 100644 --- a/pkg/cbtservice/service.go +++ b/pkg/cbtservice/service.go @@ -20,8 +20,8 @@ import "context" // Range defines the range of a change type Range struct { - Offset int64 - Length int64 + Offset uint64 + Length uint64 } // SourceInfo is the information provided to the uploader, the uploader calls CBT service with this information diff --git a/pkg/uploader/cbt/bitmap.go b/pkg/uploader/cbt/bitmap.go index 6cf6e61e0..6defaf8b0 100644 --- a/pkg/uploader/cbt/bitmap.go +++ b/pkg/uploader/cbt/bitmap.go @@ -16,22 +16,29 @@ limitations under the License. package cbt -import "github.com/vmware-tanzu/velero/pkg/cbtservice" +import ( + "math/bits" + + "github.com/RoaringBitmap/roaring" +) // Bitmap defines the methods to store and iterate the CBT bitmap type Bitmap interface { // Set sets bits within the provided range - Set(cbtservice.Range) + Set(uint64, uint64) // SetFull sets all bits to the bitmap SetFull() // Snapshot returns snapshot of the bitmap - SourceID() string + Snapshot() string // ChangeID returns the changeID of the bitmap ChangeID() string + // VolumeID return ID of the volume from which the snapshot is taken + VolumeID() string + // Iterator returns the iterator for the CBT Bitmap Iterator() Iterator } @@ -44,12 +51,107 @@ type Iterator interface { // Snapshot returns snapshot of the bitmap Snapshot() string + // VolumeID return ID of the volume from which the snapshot is taken + VolumeID() string + // BlockSize returns the granularity of the bitmap - BlockSize() int + BlockSize() uint // Count returns the toal number of count in the bitmap Count() uint64 // Next returns the offset of the next set block and whether it comes to the end of the iteration - Next() (int64, bool) + Next() (uint64, bool) +} + +const ( + InvalidOffset64 = ^uint64(0) +) + +type bitmapImpl struct { + bitmap *roaring.Bitmap + blockSize uint + blockSizeLog int + length uint64 + snapshot string + changeID string + volumeID string +} + +type bitmapIterator struct { + bitmapImpl + iterator roaring.IntPeekable +} + +func NewBitmap(blockSize uint, length uint64, snapshot string, changeID string, volumeID string) Bitmap { + return &bitmapImpl{ + bitmap: roaring.New(), + blockSize: blockSize, + blockSizeLog: bits.Len(blockSize) - 1, + length: length, + snapshot: snapshot, + changeID: changeID, + volumeID: volumeID, + } +} + +func (c *bitmapImpl) Set(offset, length uint64) { + if offset >= c.length { + return + } + + if offset+length > c.length { + length = c.length - offset + } + + start := offset >> c.blockSizeLog + end := uint64((offset + length + uint64(c.blockSize) - 1) >> c.blockSizeLog) + + c.bitmap.AddRange(start, end) +} + +func (c *bitmapImpl) SetFull() { + start := uint64(0) + end := uint64((c.length + uint64(c.blockSize) - 1) >> c.blockSizeLog) + + c.bitmap.AddRange(start, end) +} + +func (c *bitmapImpl) Snapshot() string { + return c.snapshot +} + +func (c *bitmapImpl) ChangeID() string { + return c.changeID +} + +func (c *bitmapImpl) VolumeID() string { + return c.volumeID +} + +func (c *bitmapImpl) Iterator() Iterator { + if c.bitmap == nil { + return nil + } + + return &bitmapIterator{ + bitmapImpl: *c, + iterator: c.bitmap.Iterator(), + } +} + +func (c *bitmapIterator) Next() (uint64, bool) { + if !c.iterator.HasNext() { + return InvalidOffset64, false + } + + return uint64(c.iterator.Next()) << c.blockSizeLog, true +} + +func (c *bitmapIterator) Count() uint64 { + return c.bitmap.GetCardinality() +} + +func (c *bitmapIterator) BlockSize() uint { + return c.blockSize } diff --git a/pkg/uploader/cbt/bitmap_test.go b/pkg/uploader/cbt/bitmap_test.go new file mode 100644 index 000000000..9671ad3d9 --- /dev/null +++ b/pkg/uploader/cbt/bitmap_test.go @@ -0,0 +1,256 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cbt + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBitmapProperties(t *testing.T) { + b := NewBitmap(1024*1024, 10000*1024*1024, "snap-1", "change-1", "vol-1") + assert.Equal(t, "snap-1", b.Snapshot()) + assert.Equal(t, "change-1", b.ChangeID()) + assert.Equal(t, "vol-1", b.VolumeID()) +} + +func TestBitmapSet(t *testing.T) { + const mb = 1024 * 1024 + const gb = 1024 * 1024 * 1024 + + tests := []struct { + name string + blockSize uint + totalLength uint64 + setCalls []struct{ offset, length uint64 } + expectedCount uint64 + expectedNext []uint64 + }{ + { + name: "set single block within bounds", + blockSize: mb, + totalLength: 10 * gb, + setCalls: []struct{ offset, length uint64 }{ + {0, 1000}, + }, + expectedCount: 1, + expectedNext: []uint64{0}, + }, + { + name: "set exactly one block", + blockSize: mb, + totalLength: 10 * gb, + setCalls: []struct{ offset, length uint64 }{ + {0, mb}, + }, + expectedCount: 1, + expectedNext: []uint64{0}, + }, + { + name: "set overlapping two blocks", + blockSize: mb, + totalLength: 10 * gb, + setCalls: []struct{ offset, length uint64 }{ + {mb - 1, 2}, + }, + expectedCount: 2, + expectedNext: []uint64{0, mb}, + }, + { + name: "set multiple non-contiguous blocks", + blockSize: mb, + totalLength: 20 * gb, + setCalls: []struct{ offset, length uint64 }{ + {0, 100}, + {2 * mb, 100}, + }, + expectedCount: 2, + expectedNext: []uint64{0, 2 * mb}, + }, + { + name: "set completely out of bounds (offset >= length)", + blockSize: mb, + totalLength: 10 * gb, + setCalls: []struct{ offset, length uint64 }{ + {10 * gb, 100}, + {15 * gb, 100}, + }, + expectedCount: 0, + expectedNext: []uint64{}, + }, + { + name: "set partially out of bounds (truncated)", + blockSize: mb, + totalLength: 10 * gb, + setCalls: []struct{ offset, length uint64 }{ + {10*gb - mb/2, mb}, // Starts in the last block, length pushes it out of bounds + }, + expectedCount: 1, // Only the last block should be set + expectedNext: []uint64{10*gb - mb}, + }, + { + name: "set spanning entire length", + blockSize: mb, + totalLength: 3 * mb, // 3 blocks: 0-1MB, 1MB-2MB, 2MB-3MB + setCalls: []struct{ offset, length uint64 }{ + {0, 3 * mb}, + }, + expectedCount: 3, + expectedNext: []uint64{0, mb, 2 * mb}, + }, + { + name: "set large contiguous range", + blockSize: mb, + totalLength: 100 * gb, + setCalls: []struct{ offset, length uint64 }{ + {10 * mb, 5 * mb}, // Starts at 10MB, spans 5 full blocks + }, + expectedCount: 5, + expectedNext: []uint64{10 * mb, 11 * mb, 12 * mb, 13 * mb, 14 * mb}, + }, + { + name: "set empty length", + blockSize: mb, + totalLength: 10 * gb, + setCalls: []struct{ offset, length uint64 }{ + {mb, 0}, + }, + expectedCount: 0, + expectedNext: []uint64{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := NewBitmap(tt.blockSize, tt.totalLength, "snap-1", "change-1", "vol-1") + + for _, call := range tt.setCalls { + b.Set(call.offset, call.length) + } + + iter := b.Iterator() + require.NotNil(t, iter) + + assert.Equal(t, tt.expectedCount, iter.Count()) + + var actualNext []uint64 + for { + offset, hasNext := iter.Next() + if !hasNext { + assert.Equal(t, InvalidOffset64, offset) + break + } + actualNext = append(actualNext, offset) + } + + if len(tt.expectedNext) == 0 { + assert.Empty(t, actualNext) + } else { + assert.Equal(t, tt.expectedNext, actualNext) + } + }) + } +} + +func TestBitmapSetFull(t *testing.T) { + const mb = 1024 * 1024 + // Total length 3MB, blockSize 1MB. This means 3 blocks total: + // block 0: 0 - 1MB + // block 1: 1MB - 2MB + // block 2: 2MB - 3MB + b := NewBitmap(mb, 3*mb, "snap-1", "change-1", "vol-1") + b.SetFull() + + iter := b.Iterator() + require.NotNil(t, iter) + + assert.Equal(t, uint64(3), iter.Count()) + + expectedOffsets := []uint64{0, mb, 2 * mb} + var actualOffsets []uint64 + for { + offset, hasNext := iter.Next() + if !hasNext { + break + } + actualOffsets = append(actualOffsets, offset) + } + + assert.Equal(t, expectedOffsets, actualOffsets) +} + +func TestBitmapIterator(t *testing.T) { + const mb = 1024 * 1024 + const gb = 1024 * 1024 * 1024 + + b := NewBitmap(mb, 10*gb, "snap-1", "change-1", "vol-1") + + // Set multiple ranges to test contiguous iteration + b.Set(mb, 100) // Block 1 + b.Set(3*mb, 5*mb) // Blocks 3, 4, 5, 6, 7 + b.Set(10*gb-mb, mb) // Last block + + iter := b.Iterator() + require.NotNil(t, iter) + + // Test iterator properties + assert.Equal(t, "snap-1", iter.Snapshot()) + assert.Equal(t, "change-1", iter.ChangeID()) + assert.Equal(t, "vol-1", iter.VolumeID()) + assert.Equal(t, uint(mb), iter.BlockSize()) + assert.Equal(t, uint64(7), iter.Count()) // 1 + 5 + 1 = 7 blocks + + expectedOffsets := []uint64{ + mb, + 3 * mb, 4 * mb, 5 * mb, 6 * mb, 7 * mb, + 10*gb - mb, + } + + // Test iteration + var actualOffsets []uint64 + for { + offset, hasNext := iter.Next() + if !hasNext { + assert.Equal(t, InvalidOffset64, offset) + break + } + actualOffsets = append(actualOffsets, offset) + } + + assert.Equal(t, expectedOffsets, actualOffsets) + + // Test end of iteration multiple times to ensure it stays exhausted + offset, hasNext := iter.Next() + assert.False(t, hasNext) + assert.Equal(t, InvalidOffset64, offset) + + offset, hasNext = iter.Next() + assert.False(t, hasNext) + assert.Equal(t, InvalidOffset64, offset) +} + +func TestBitmapIteratorNilBitmap(t *testing.T) { + // Directly create bitmapImpl with a nil roaring.Bitmap to test safety + b := &bitmapImpl{ + bitmap: nil, + } + + iter := b.Iterator() + assert.Nil(t, iter) +} diff --git a/pkg/uploader/cbt/mocks/Bitmap.go b/pkg/uploader/cbt/mocks/Bitmap.go new file mode 100644 index 000000000..289c88215 --- /dev/null +++ b/pkg/uploader/cbt/mocks/Bitmap.go @@ -0,0 +1,294 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" + "github.com/vmware-tanzu/velero/pkg/uploader/cbt" +) + +// NewBitmap creates a new instance of Bitmap. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBitmap(t interface { + mock.TestingT + Cleanup(func()) +}) *Bitmap { + mock := &Bitmap{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// Bitmap is an autogenerated mock type for the Bitmap type +type Bitmap struct { + mock.Mock +} + +type Bitmap_Expecter struct { + mock *mock.Mock +} + +func (_m *Bitmap) EXPECT() *Bitmap_Expecter { + return &Bitmap_Expecter{mock: &_m.Mock} +} + +// ChangeID provides a mock function for the type Bitmap +func (_mock *Bitmap) ChangeID() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for ChangeID") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// Bitmap_ChangeID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangeID' +type Bitmap_ChangeID_Call struct { + *mock.Call +} + +// ChangeID is a helper method to define mock.On call +func (_e *Bitmap_Expecter) ChangeID() *Bitmap_ChangeID_Call { + return &Bitmap_ChangeID_Call{Call: _e.mock.On("ChangeID")} +} + +func (_c *Bitmap_ChangeID_Call) Run(run func()) *Bitmap_ChangeID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Bitmap_ChangeID_Call) Return(s string) *Bitmap_ChangeID_Call { + _c.Call.Return(s) + return _c +} + +func (_c *Bitmap_ChangeID_Call) RunAndReturn(run func() string) *Bitmap_ChangeID_Call { + _c.Call.Return(run) + return _c +} + +// Iterator provides a mock function for the type Bitmap +func (_mock *Bitmap) Iterator() cbt.Iterator { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Iterator") + } + + var r0 cbt.Iterator + if returnFunc, ok := ret.Get(0).(func() cbt.Iterator); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(cbt.Iterator) + } + } + return r0 +} + +// Bitmap_Iterator_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Iterator' +type Bitmap_Iterator_Call struct { + *mock.Call +} + +// Iterator is a helper method to define mock.On call +func (_e *Bitmap_Expecter) Iterator() *Bitmap_Iterator_Call { + return &Bitmap_Iterator_Call{Call: _e.mock.On("Iterator")} +} + +func (_c *Bitmap_Iterator_Call) Run(run func()) *Bitmap_Iterator_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Bitmap_Iterator_Call) Return(iterator cbt.Iterator) *Bitmap_Iterator_Call { + _c.Call.Return(iterator) + return _c +} + +func (_c *Bitmap_Iterator_Call) RunAndReturn(run func() cbt.Iterator) *Bitmap_Iterator_Call { + _c.Call.Return(run) + return _c +} + +// Set provides a mock function for the type Bitmap +func (_mock *Bitmap) Set(v uint64, v1 uint64) { + _mock.Called(v, v1) + return +} + +// Bitmap_Set_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Set' +type Bitmap_Set_Call struct { + *mock.Call +} + +// Set is a helper method to define mock.On call +// - v uint64 +// - v1 uint64 +func (_e *Bitmap_Expecter) Set(v interface{}, v1 interface{}) *Bitmap_Set_Call { + return &Bitmap_Set_Call{Call: _e.mock.On("Set", v, v1)} +} + +func (_c *Bitmap_Set_Call) Run(run func(v uint64, v1 uint64)) *Bitmap_Set_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 uint64 + if args[0] != nil { + arg0 = args[0].(uint64) + } + var arg1 uint64 + if args[1] != nil { + arg1 = args[1].(uint64) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *Bitmap_Set_Call) Return() *Bitmap_Set_Call { + _c.Call.Return() + return _c +} + +func (_c *Bitmap_Set_Call) RunAndReturn(run func(v uint64, v1 uint64)) *Bitmap_Set_Call { + _c.Run(run) + return _c +} + +// SetFull provides a mock function for the type Bitmap +func (_mock *Bitmap) SetFull() { + _mock.Called() + return +} + +// Bitmap_SetFull_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetFull' +type Bitmap_SetFull_Call struct { + *mock.Call +} + +// SetFull is a helper method to define mock.On call +func (_e *Bitmap_Expecter) SetFull() *Bitmap_SetFull_Call { + return &Bitmap_SetFull_Call{Call: _e.mock.On("SetFull")} +} + +func (_c *Bitmap_SetFull_Call) Run(run func()) *Bitmap_SetFull_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Bitmap_SetFull_Call) Return() *Bitmap_SetFull_Call { + _c.Call.Return() + return _c +} + +func (_c *Bitmap_SetFull_Call) RunAndReturn(run func()) *Bitmap_SetFull_Call { + _c.Run(run) + return _c +} + +// Snapshot provides a mock function for the type Bitmap +func (_mock *Bitmap) Snapshot() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Snapshot") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// Bitmap_Snapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Snapshot' +type Bitmap_Snapshot_Call struct { + *mock.Call +} + +// Snapshot is a helper method to define mock.On call +func (_e *Bitmap_Expecter) Snapshot() *Bitmap_Snapshot_Call { + return &Bitmap_Snapshot_Call{Call: _e.mock.On("Snapshot")} +} + +func (_c *Bitmap_Snapshot_Call) Run(run func()) *Bitmap_Snapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Bitmap_Snapshot_Call) Return(s string) *Bitmap_Snapshot_Call { + _c.Call.Return(s) + return _c +} + +func (_c *Bitmap_Snapshot_Call) RunAndReturn(run func() string) *Bitmap_Snapshot_Call { + _c.Call.Return(run) + return _c +} + +// VolumeID provides a mock function for the type Bitmap +func (_mock *Bitmap) VolumeID() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for VolumeID") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// Bitmap_VolumeID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VolumeID' +type Bitmap_VolumeID_Call struct { + *mock.Call +} + +// VolumeID is a helper method to define mock.On call +func (_e *Bitmap_Expecter) VolumeID() *Bitmap_VolumeID_Call { + return &Bitmap_VolumeID_Call{Call: _e.mock.On("VolumeID")} +} + +func (_c *Bitmap_VolumeID_Call) Run(run func()) *Bitmap_VolumeID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Bitmap_VolumeID_Call) Return(s string) *Bitmap_VolumeID_Call { + _c.Call.Return(s) + return _c +} + +func (_c *Bitmap_VolumeID_Call) RunAndReturn(run func() string) *Bitmap_VolumeID_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/uploader/cbt/mocks/Iterator.go b/pkg/uploader/cbt/mocks/Iterator.go new file mode 100644 index 000000000..eeafd0a59 --- /dev/null +++ b/pkg/uploader/cbt/mocks/Iterator.go @@ -0,0 +1,309 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + mock "github.com/stretchr/testify/mock" +) + +// NewIterator creates a new instance of Iterator. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewIterator(t interface { + mock.TestingT + Cleanup(func()) +}) *Iterator { + mock := &Iterator{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// Iterator is an autogenerated mock type for the Iterator type +type Iterator struct { + mock.Mock +} + +type Iterator_Expecter struct { + mock *mock.Mock +} + +func (_m *Iterator) EXPECT() *Iterator_Expecter { + return &Iterator_Expecter{mock: &_m.Mock} +} + +// BlockSize provides a mock function for the type Iterator +func (_mock *Iterator) BlockSize() uint { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for BlockSize") + } + + var r0 uint + if returnFunc, ok := ret.Get(0).(func() uint); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(uint) + } + return r0 +} + +// Iterator_BlockSize_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'BlockSize' +type Iterator_BlockSize_Call struct { + *mock.Call +} + +// BlockSize is a helper method to define mock.On call +func (_e *Iterator_Expecter) BlockSize() *Iterator_BlockSize_Call { + return &Iterator_BlockSize_Call{Call: _e.mock.On("BlockSize")} +} + +func (_c *Iterator_BlockSize_Call) Run(run func()) *Iterator_BlockSize_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Iterator_BlockSize_Call) Return(v uint) *Iterator_BlockSize_Call { + _c.Call.Return(v) + return _c +} + +func (_c *Iterator_BlockSize_Call) RunAndReturn(run func() uint) *Iterator_BlockSize_Call { + _c.Call.Return(run) + return _c +} + +// ChangeID provides a mock function for the type Iterator +func (_mock *Iterator) ChangeID() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for ChangeID") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// Iterator_ChangeID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChangeID' +type Iterator_ChangeID_Call struct { + *mock.Call +} + +// ChangeID is a helper method to define mock.On call +func (_e *Iterator_Expecter) ChangeID() *Iterator_ChangeID_Call { + return &Iterator_ChangeID_Call{Call: _e.mock.On("ChangeID")} +} + +func (_c *Iterator_ChangeID_Call) Run(run func()) *Iterator_ChangeID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Iterator_ChangeID_Call) Return(s string) *Iterator_ChangeID_Call { + _c.Call.Return(s) + return _c +} + +func (_c *Iterator_ChangeID_Call) RunAndReturn(run func() string) *Iterator_ChangeID_Call { + _c.Call.Return(run) + return _c +} + +// Count provides a mock function for the type Iterator +func (_mock *Iterator) Count() uint64 { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Count") + } + + var r0 uint64 + if returnFunc, ok := ret.Get(0).(func() uint64); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(uint64) + } + return r0 +} + +// Iterator_Count_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Count' +type Iterator_Count_Call struct { + *mock.Call +} + +// Count is a helper method to define mock.On call +func (_e *Iterator_Expecter) Count() *Iterator_Count_Call { + return &Iterator_Count_Call{Call: _e.mock.On("Count")} +} + +func (_c *Iterator_Count_Call) Run(run func()) *Iterator_Count_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Iterator_Count_Call) Return(v uint64) *Iterator_Count_Call { + _c.Call.Return(v) + return _c +} + +func (_c *Iterator_Count_Call) RunAndReturn(run func() uint64) *Iterator_Count_Call { + _c.Call.Return(run) + return _c +} + +// Next provides a mock function for the type Iterator +func (_mock *Iterator) Next() (uint64, bool) { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Next") + } + + var r0 uint64 + var r1 bool + if returnFunc, ok := ret.Get(0).(func() (uint64, bool)); ok { + return returnFunc() + } + if returnFunc, ok := ret.Get(0).(func() uint64); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(uint64) + } + if returnFunc, ok := ret.Get(1).(func() bool); ok { + r1 = returnFunc() + } else { + r1 = ret.Get(1).(bool) + } + return r0, r1 +} + +// Iterator_Next_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Next' +type Iterator_Next_Call struct { + *mock.Call +} + +// Next is a helper method to define mock.On call +func (_e *Iterator_Expecter) Next() *Iterator_Next_Call { + return &Iterator_Next_Call{Call: _e.mock.On("Next")} +} + +func (_c *Iterator_Next_Call) Run(run func()) *Iterator_Next_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Iterator_Next_Call) Return(v uint64, b bool) *Iterator_Next_Call { + _c.Call.Return(v, b) + return _c +} + +func (_c *Iterator_Next_Call) RunAndReturn(run func() (uint64, bool)) *Iterator_Next_Call { + _c.Call.Return(run) + return _c +} + +// Snapshot provides a mock function for the type Iterator +func (_mock *Iterator) Snapshot() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Snapshot") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// Iterator_Snapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Snapshot' +type Iterator_Snapshot_Call struct { + *mock.Call +} + +// Snapshot is a helper method to define mock.On call +func (_e *Iterator_Expecter) Snapshot() *Iterator_Snapshot_Call { + return &Iterator_Snapshot_Call{Call: _e.mock.On("Snapshot")} +} + +func (_c *Iterator_Snapshot_Call) Run(run func()) *Iterator_Snapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Iterator_Snapshot_Call) Return(s string) *Iterator_Snapshot_Call { + _c.Call.Return(s) + return _c +} + +func (_c *Iterator_Snapshot_Call) RunAndReturn(run func() string) *Iterator_Snapshot_Call { + _c.Call.Return(run) + return _c +} + +// VolumeID provides a mock function for the type Iterator +func (_mock *Iterator) VolumeID() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for VolumeID") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// Iterator_VolumeID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'VolumeID' +type Iterator_VolumeID_Call struct { + *mock.Call +} + +// VolumeID is a helper method to define mock.On call +func (_e *Iterator_Expecter) VolumeID() *Iterator_VolumeID_Call { + return &Iterator_VolumeID_Call{Call: _e.mock.On("VolumeID")} +} + +func (_c *Iterator_VolumeID_Call) Run(run func()) *Iterator_VolumeID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *Iterator_VolumeID_Call) Return(s string) *Iterator_VolumeID_Call { + _c.Call.Return(s) + return _c +} + +func (_c *Iterator_VolumeID_Call) RunAndReturn(run func() string) *Iterator_VolumeID_Call { + _c.Call.Return(run) + return _c +} diff --git a/pkg/uploader/cbt/set.go b/pkg/uploader/cbt/set.go index 40e6e331c..93383076f 100644 --- a/pkg/uploader/cbt/set.go +++ b/pkg/uploader/cbt/set.go @@ -19,31 +19,42 @@ package cbt import ( "context" + "github.com/pkg/errors" + "github.com/vmware-tanzu/velero/pkg/cbtservice" ) // SetBitmapOrFull translates the allocated/changed blocks from CBT service to the given bitmap or set the bitmap to full when error happens -func SetBitmapOrFull(ctx context.Context, service cbtservice.Service, bitmap Bitmap) error { - var err error +func SetBitmapOrFull(ctx context.Context, service cbtservice.Service, bitmap Bitmap) (err error) { + defer func() { + if err != nil { + bitmap.SetFull() + } + }() + + if service == nil { + return errors.New("CBT service is absent") + } + + if bitmap.Snapshot() == "" { + return errors.New("invalid snapshot") + } + if bitmap.ChangeID() == "" { - err = setFromAllocatedBlocks(ctx, service, bitmap) - } else { - err = setFromChangedBlocks(ctx, service, bitmap) + return errors.Wrapf(service.GetAllocatedBlocks(ctx, bitmap.Snapshot(), func(blocks []cbtservice.Range) error { + for _, b := range blocks { + bitmap.Set(b.Offset, b.Length) + } + + return nil + }), "error getting allocated blocks from CBT service") } - if err != nil { - bitmap.SetFull() - } + return errors.Wrapf(service.GetChangedBlocks(ctx, bitmap.Snapshot(), bitmap.ChangeID(), func(blocks []cbtservice.Range) error { + for _, b := range blocks { + bitmap.Set(b.Offset, b.Length) + } - return err -} - -// TODO implement in following PRs -func setFromAllocatedBlocks(_ context.Context, _ cbtservice.Service, _ Bitmap) error { - return nil -} - -// TODO implement in following PRs -func setFromChangedBlocks(_ context.Context, _ cbtservice.Service, _ Bitmap) error { - return nil + return nil + }), "error getting changed blocks from CBT service") } diff --git a/pkg/uploader/cbt/set_test.go b/pkg/uploader/cbt/set_test.go new file mode 100644 index 000000000..c4a055cba --- /dev/null +++ b/pkg/uploader/cbt/set_test.go @@ -0,0 +1,150 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cbt_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/vmware-tanzu/velero/pkg/cbtservice" + cbtservicemocks "github.com/vmware-tanzu/velero/pkg/cbtservice/mocks" + "github.com/vmware-tanzu/velero/pkg/uploader/cbt" + cbtmocks "github.com/vmware-tanzu/velero/pkg/uploader/cbt/mocks" +) + +func TestSetBitmapOrFull(t *testing.T) { + tests := []struct { + name string + nilService bool + setupMocks func(*cbtservicemocks.Service, *cbtmocks.Bitmap) + expectedErrStr string + }{ + { + name: "nil service", + nilService: true, + setupMocks: func(svc *cbtservicemocks.Service, bmp *cbtmocks.Bitmap) { + bmp.On("SetFull").Return() + }, + expectedErrStr: "CBT service is absent", + }, + { + name: "invalid snapshot", + setupMocks: func(svc *cbtservicemocks.Service, bmp *cbtmocks.Bitmap) { + bmp.On("Snapshot").Return("") + bmp.On("SetFull").Return() + }, + expectedErrStr: "invalid snapshot", + }, + { + name: "allocated blocks success", + setupMocks: func(svc *cbtservicemocks.Service, bmp *cbtmocks.Bitmap) { + bmp.On("Snapshot").Return("snap-1") + bmp.On("ChangeID").Return("") + + svc.On("GetAllocatedBlocks", mock.Anything, "snap-1", mock.Anything).Run(func(args mock.Arguments) { + record := args.Get(2).(func([]cbtservice.Range) error) + record([]cbtservice.Range{ + {Offset: 0, Length: 4096}, + {Offset: 8192, Length: 4096}, + }) + }).Return(nil) + + bmp.On("Set", uint64(0), uint64(4096)).Return() + bmp.On("Set", uint64(8192), uint64(4096)).Return() + }, + }, + { + name: "allocated blocks error", + setupMocks: func(svc *cbtservicemocks.Service, bmp *cbtmocks.Bitmap) { + bmp.On("Snapshot").Return("snap-1") + bmp.On("ChangeID").Return("") + + svc.On("GetAllocatedBlocks", mock.Anything, "snap-1", mock.Anything).Return(errors.New("mock alloc error")) + bmp.On("SetFull").Return() + }, + expectedErrStr: "error getting allocated blocks from CBT service: mock alloc error", + }, + { + name: "changed blocks success", + setupMocks: func(svc *cbtservicemocks.Service, bmp *cbtmocks.Bitmap) { + bmp.On("Snapshot").Return("snap-1") + bmp.On("ChangeID").Return("change-1") + + svc.On("GetChangedBlocks", mock.Anything, "snap-1", "change-1", mock.Anything).Run(func(args mock.Arguments) { + record := args.Get(3).(func([]cbtservice.Range) error) + record([]cbtservice.Range{ + {Offset: 4096, Length: 4096}, + }) + }).Return(nil) + + bmp.On("Set", uint64(4096), uint64(4096)).Return() + }, + }, + { + name: "changed blocks error", + setupMocks: func(svc *cbtservicemocks.Service, bmp *cbtmocks.Bitmap) { + bmp.On("Snapshot").Return("snap-1") + bmp.On("ChangeID").Return("change-1") + + svc.On("GetChangedBlocks", mock.Anything, "snap-1", "change-1", mock.Anything).Return(errors.New("mock changed error")) + bmp.On("SetFull").Return() + }, + expectedErrStr: "error getting changed blocks from CBT service: mock changed error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + svcMock := new(cbtservicemocks.Service) + bmpMock := new(cbtmocks.Bitmap) + + if tt.setupMocks != nil { + tt.setupMocks(svcMock, bmpMock) + } + + var svc cbtservice.Service + if !tt.nilService { + svc = svcMock + } + + // Use type assertion to bypass gopls false positive on mock type + var bmp cbt.Bitmap + if bmpMock != nil { + bmp = interface{}(bmpMock).(cbt.Bitmap) + } + + err := cbt.SetBitmapOrFull(context.Background(), svc, bmp) + + if tt.expectedErrStr != "" { + require.Error(t, err) + assert.EqualError(t, err, tt.expectedErrStr) + } else { + require.NoError(t, err) + } + + if !tt.nilService { + svcMock.AssertExpectations(t) + } + bmpMock.AssertExpectations(t) + }) + } +} From 26b125769e65ef036cd0d85d3acd4f6ecea0d370 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 21 Apr 2026 17:46:29 +0800 Subject: [PATCH 3/5] add CBT bitmap implementation Signed-off-by: Lyndon-Li --- pkg/uploader/cbt/bitmap.go | 52 ++-------------- pkg/uploader/cbt/set.go | 3 +- pkg/uploader/cbt/set_test.go | 13 +--- pkg/uploader/cbt/{ => types}/mocks/Bitmap.go | 14 ++--- .../cbt/{ => types}/mocks/Iterator.go | 0 pkg/uploader/cbt/types/types.go | 59 +++++++++++++++++++ 6 files changed, 77 insertions(+), 64 deletions(-) rename pkg/uploader/cbt/{ => types}/mocks/Bitmap.go (94%) rename pkg/uploader/cbt/{ => types}/mocks/Iterator.go (100%) create mode 100644 pkg/uploader/cbt/types/types.go diff --git a/pkg/uploader/cbt/bitmap.go b/pkg/uploader/cbt/bitmap.go index 6defaf8b0..f26cb22b7 100644 --- a/pkg/uploader/cbt/bitmap.go +++ b/pkg/uploader/cbt/bitmap.go @@ -20,50 +20,10 @@ import ( "math/bits" "github.com/RoaringBitmap/roaring" + + "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types" ) -// Bitmap defines the methods to store and iterate the CBT bitmap -type Bitmap interface { - // Set sets bits within the provided range - Set(uint64, uint64) - - // SetFull sets all bits to the bitmap - SetFull() - - // Snapshot returns snapshot of the bitmap - Snapshot() string - - // ChangeID returns the changeID of the bitmap - ChangeID() string - - // VolumeID return ID of the volume from which the snapshot is taken - VolumeID() string - - // Iterator returns the iterator for the CBT Bitmap - Iterator() Iterator -} - -// Iterator defines the methods to iterate the CBT bitmap and query the associated information -type Iterator interface { - // ChangeID returns the changeID of the bitmap - ChangeID() string - - // Snapshot returns snapshot of the bitmap - Snapshot() string - - // VolumeID return ID of the volume from which the snapshot is taken - VolumeID() string - - // BlockSize returns the granularity of the bitmap - BlockSize() uint - - // Count returns the toal number of count in the bitmap - Count() uint64 - - // Next returns the offset of the next set block and whether it comes to the end of the iteration - Next() (uint64, bool) -} - const ( InvalidOffset64 = ^uint64(0) ) @@ -83,7 +43,7 @@ type bitmapIterator struct { iterator roaring.IntPeekable } -func NewBitmap(blockSize uint, length uint64, snapshot string, changeID string, volumeID string) Bitmap { +func NewBitmap(blockSize uint, length uint64, snapshot string, changeID string, volumeID string) types.Bitmap { return &bitmapImpl{ bitmap: roaring.New(), blockSize: blockSize, @@ -105,14 +65,14 @@ func (c *bitmapImpl) Set(offset, length uint64) { } start := offset >> c.blockSizeLog - end := uint64((offset + length + uint64(c.blockSize) - 1) >> c.blockSizeLog) + end := (offset + length + uint64(c.blockSize) - 1) >> c.blockSizeLog c.bitmap.AddRange(start, end) } func (c *bitmapImpl) SetFull() { start := uint64(0) - end := uint64((c.length + uint64(c.blockSize) - 1) >> c.blockSizeLog) + end := (c.length + uint64(c.blockSize) - 1) >> c.blockSizeLog c.bitmap.AddRange(start, end) } @@ -129,7 +89,7 @@ func (c *bitmapImpl) VolumeID() string { return c.volumeID } -func (c *bitmapImpl) Iterator() Iterator { +func (c *bitmapImpl) Iterator() types.Iterator { if c.bitmap == nil { return nil } diff --git a/pkg/uploader/cbt/set.go b/pkg/uploader/cbt/set.go index 93383076f..5919419e6 100644 --- a/pkg/uploader/cbt/set.go +++ b/pkg/uploader/cbt/set.go @@ -22,10 +22,11 @@ import ( "github.com/pkg/errors" "github.com/vmware-tanzu/velero/pkg/cbtservice" + "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types" ) // SetBitmapOrFull translates the allocated/changed blocks from CBT service to the given bitmap or set the bitmap to full when error happens -func SetBitmapOrFull(ctx context.Context, service cbtservice.Service, bitmap Bitmap) (err error) { +func SetBitmapOrFull(ctx context.Context, service cbtservice.Service, bitmap types.Bitmap) (err error) { defer func() { if err != nil { bitmap.SetFull() diff --git a/pkg/uploader/cbt/set_test.go b/pkg/uploader/cbt/set_test.go index c4a055cba..3bfbde1d4 100644 --- a/pkg/uploader/cbt/set_test.go +++ b/pkg/uploader/cbt/set_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package cbt_test +package cbt import ( "context" @@ -27,8 +27,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/cbtservice" cbtservicemocks "github.com/vmware-tanzu/velero/pkg/cbtservice/mocks" - "github.com/vmware-tanzu/velero/pkg/uploader/cbt" - cbtmocks "github.com/vmware-tanzu/velero/pkg/uploader/cbt/mocks" + cbtmocks "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types/mocks" ) func TestSetBitmapOrFull(t *testing.T) { @@ -126,13 +125,7 @@ func TestSetBitmapOrFull(t *testing.T) { svc = svcMock } - // Use type assertion to bypass gopls false positive on mock type - var bmp cbt.Bitmap - if bmpMock != nil { - bmp = interface{}(bmpMock).(cbt.Bitmap) - } - - err := cbt.SetBitmapOrFull(context.Background(), svc, bmp) + err := SetBitmapOrFull(context.Background(), svc, bmpMock) if tt.expectedErrStr != "" { require.Error(t, err) diff --git a/pkg/uploader/cbt/mocks/Bitmap.go b/pkg/uploader/cbt/types/mocks/Bitmap.go similarity index 94% rename from pkg/uploader/cbt/mocks/Bitmap.go rename to pkg/uploader/cbt/types/mocks/Bitmap.go index 289c88215..faa4ee242 100644 --- a/pkg/uploader/cbt/mocks/Bitmap.go +++ b/pkg/uploader/cbt/types/mocks/Bitmap.go @@ -6,7 +6,7 @@ package mocks import ( mock "github.com/stretchr/testify/mock" - "github.com/vmware-tanzu/velero/pkg/uploader/cbt" + "github.com/vmware-tanzu/velero/pkg/uploader/cbt/types" ) // NewBitmap creates a new instance of Bitmap. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. @@ -81,19 +81,19 @@ func (_c *Bitmap_ChangeID_Call) RunAndReturn(run func() string) *Bitmap_ChangeID } // Iterator provides a mock function for the type Bitmap -func (_mock *Bitmap) Iterator() cbt.Iterator { +func (_mock *Bitmap) Iterator() types.Iterator { ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Iterator") } - var r0 cbt.Iterator - if returnFunc, ok := ret.Get(0).(func() cbt.Iterator); ok { + var r0 types.Iterator + if returnFunc, ok := ret.Get(0).(func() types.Iterator); ok { r0 = returnFunc() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(cbt.Iterator) + r0 = ret.Get(0).(types.Iterator) } } return r0 @@ -116,12 +116,12 @@ func (_c *Bitmap_Iterator_Call) Run(run func()) *Bitmap_Iterator_Call { return _c } -func (_c *Bitmap_Iterator_Call) Return(iterator cbt.Iterator) *Bitmap_Iterator_Call { +func (_c *Bitmap_Iterator_Call) Return(iterator types.Iterator) *Bitmap_Iterator_Call { _c.Call.Return(iterator) return _c } -func (_c *Bitmap_Iterator_Call) RunAndReturn(run func() cbt.Iterator) *Bitmap_Iterator_Call { +func (_c *Bitmap_Iterator_Call) RunAndReturn(run func() types.Iterator) *Bitmap_Iterator_Call { _c.Call.Return(run) return _c } diff --git a/pkg/uploader/cbt/mocks/Iterator.go b/pkg/uploader/cbt/types/mocks/Iterator.go similarity index 100% rename from pkg/uploader/cbt/mocks/Iterator.go rename to pkg/uploader/cbt/types/mocks/Iterator.go diff --git a/pkg/uploader/cbt/types/types.go b/pkg/uploader/cbt/types/types.go new file mode 100644 index 000000000..f512d7a8a --- /dev/null +++ b/pkg/uploader/cbt/types/types.go @@ -0,0 +1,59 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +// Bitmap defines the methods to store and iterate the CBT bitmap +type Bitmap interface { + // Set sets bits within the provided range + Set(uint64, uint64) + + // SetFull sets all bits to the bitmap + SetFull() + + // Snapshot returns snapshot of the bitmap + Snapshot() string + + // ChangeID returns the changeID of the bitmap + ChangeID() string + + // VolumeID return ID of the volume from which the snapshot is taken + VolumeID() string + + // Iterator returns the iterator for the CBT Bitmap + Iterator() Iterator +} + +// Iterator defines the methods to iterate the CBT bitmap and query the associated information +type Iterator interface { + // ChangeID returns the changeID of the bitmap + ChangeID() string + + // Snapshot returns snapshot of the bitmap + Snapshot() string + + // VolumeID return ID of the volume from which the snapshot is taken + VolumeID() string + + // BlockSize returns the granularity of the bitmap + BlockSize() uint + + // Count returns the toal number of count in the bitmap + Count() uint64 + + // Next returns the offset of the next set block and whether it comes to the end of the iteration + Next() (uint64, bool) +} From 6b7df3ef4c25865d0d2b9a745ce6f6c4145d6abe Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Tue, 21 Apr 2026 17:52:06 +0800 Subject: [PATCH 4/5] add CBT bitmap implementation Signed-off-by: Lyndon-Li --- pkg/uploader/cbt/set_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/uploader/cbt/set_test.go b/pkg/uploader/cbt/set_test.go index 3bfbde1d4..55701dedd 100644 --- a/pkg/uploader/cbt/set_test.go +++ b/pkg/uploader/cbt/set_test.go @@ -21,7 +21,6 @@ import ( "errors" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -129,7 +128,7 @@ func TestSetBitmapOrFull(t *testing.T) { if tt.expectedErrStr != "" { require.Error(t, err) - assert.EqualError(t, err, tt.expectedErrStr) + require.EqualError(t, err, tt.expectedErrStr) } else { require.NoError(t, err) } From 1d03217661ed17d67801d8645b78633e99743d99 Mon Sep 17 00:00:00 2001 From: Lyndon-Li Date: Thu, 21 May 2026 09:20:24 +0800 Subject: [PATCH 5/5] add CBT bitmap implementation Signed-off-by: Lyndon-Li --- pkg/uploader/cbt/types/types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/uploader/cbt/types/types.go b/pkg/uploader/cbt/types/types.go index f512d7a8a..9b13669b2 100644 --- a/pkg/uploader/cbt/types/types.go +++ b/pkg/uploader/cbt/types/types.go @@ -51,7 +51,7 @@ type Iterator interface { // BlockSize returns the granularity of the bitmap BlockSize() uint - // Count returns the toal number of count in the bitmap + // Count returns the total number of count in the bitmap Count() uint64 // Next returns the offset of the next set block and whether it comes to the end of the iteration