new bitmap allocator with offset

Implement a new bitmap allocator using an allocation strategy that accepts
and offset and subdivides the range based on this offset, prioritizing the
upper range for dynamic allocation.
This commit is contained in:
Antonio Ojea 2022-01-11 09:23:13 +01:00
parent 95e30f66c3
commit 96d71f01eb
3 changed files with 558 additions and 99 deletions

View File

@ -18,6 +18,7 @@ package allocator
import (
"errors"
"fmt"
"math/big"
"math/rand"
"sync"
@ -60,15 +61,25 @@ type bitAllocator interface {
// NewAllocationMap creates an allocation bitmap using the random scan strategy.
func NewAllocationMap(max int, rangeSpec string) *AllocationBitmap {
return NewAllocationMapWithOffset(max, rangeSpec, 0)
}
// NewAllocationMapWithOffset creates an allocation bitmap using a random scan strategy that
// allows to pass an offset that divides the allocation bitmap in two blocks.
// The first block of values will not be used for random value assigned by the AllocateNext()
// method until the second block of values has been exhausted.
func NewAllocationMapWithOffset(max int, rangeSpec string, offset int) *AllocationBitmap {
a := AllocationBitmap{
strategy: randomScanStrategy{
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
strategy: randomScanStrategyWithOffset{
rand: rand.New(rand.NewSource(time.Now().UnixNano())),
offset: offset,
},
allocated: big.NewInt(0),
count: 0,
max: max,
rangeSpec: rangeSpec,
}
return &a
}
@ -78,6 +89,10 @@ func (r *AllocationBitmap) Allocate(offset int) (bool, error) {
r.lock.Lock()
defer r.lock.Unlock()
// max is the maximum size of the usable items in the range
if offset < 0 || offset >= r.max {
return false, fmt.Errorf("offset %d out of range [0,%d]", offset, r.max)
}
if r.allocated.Bit(offset) == 1 {
return false, nil
}
@ -205,3 +220,40 @@ func (rss randomScanStrategy) AllocateBit(allocated *big.Int, max, count int) (i
}
var _ bitAllocator = randomScanStrategy{}
// randomScanStrategyWithOffset choose a random address from the provided big.Int and then scans
// forward looking for the next available address. The big.Int range is subdivided so it will try
// to allocate first from the reserved upper range of addresses (it will wrap the upper subrange if necessary).
// If there is no free address it will try to allocate one from the lower range too.
type randomScanStrategyWithOffset struct {
rand *rand.Rand
offset int
}
func (rss randomScanStrategyWithOffset) AllocateBit(allocated *big.Int, max, count int) (int, bool) {
if count >= max {
return 0, false
}
// size of the upper subrange, prioritized for random allocation
subrangeMax := max - rss.offset
// try to get a value from the upper range [rss.reserved, max]
start := rss.rand.Intn(subrangeMax)
for i := 0; i < subrangeMax; i++ {
at := rss.offset + ((start + i) % subrangeMax)
if allocated.Bit(at) == 0 {
return at, true
}
}
start = rss.rand.Intn(rss.offset)
// subrange full, try to get the value from the first block before giving up.
for i := 0; i < rss.offset; i++ {
at := (start + i) % rss.offset
if allocated.Bit(at) == 0 {
return i, true
}
}
return 0, false
}
var _ bitAllocator = randomScanStrategyWithOffset{}

View File

@ -23,116 +23,521 @@ import (
)
func TestAllocate(t *testing.T) {
max := 10
m := NewAllocationMap(max, "test")
testCases := []struct {
name string
allocator func(max int, rangeSpec string, reserved int) *AllocationBitmap
max int
reserved int
}{
{
name: "NewAllocationMap",
allocator: NewAllocationMapWithOffset,
max: 32,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max < 16",
allocator: NewAllocationMapWithOffset,
max: 8,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max > 16",
allocator: NewAllocationMapWithOffset,
max: 128,
reserved: 16,
},
{
name: "NewAllocationMapWithOffset max > 256",
allocator: NewAllocationMapWithOffset,
max: 1024,
reserved: 64,
},
{
name: "NewAllocationMapWithOffset max value",
allocator: NewAllocationMapWithOffset,
max: 65535,
reserved: 256,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m := tc.allocator(tc.max, "test", tc.reserved)
if _, ok, _ := m.AllocateNext(); !ok {
t.Fatalf("unexpected error")
}
if m.count != 1 {
t.Errorf("expect to get %d, but got %d", 1, m.count)
}
if f := m.Free(); f != max-1 {
t.Errorf("expect to get %d, but got %d", max-1, f)
if _, ok, _ := m.AllocateNext(); !ok {
t.Fatalf("unexpected error")
}
if m.count != 1 {
t.Errorf("expect to get %d, but got %d", 1, m.count)
}
if f := m.Free(); f != tc.max-1 {
t.Errorf("expect to get %d, but got %d", tc.max-1, f)
}
})
}
}
func TestAllocateMax(t *testing.T) {
max := 10
m := NewAllocationMap(max, "test")
for i := 0; i < max; i++ {
testCases := []struct {
name string
allocator func(max int, rangeSpec string, reserved int) *AllocationBitmap
max int
reserved int
}{
{
name: "NewAllocationMap",
allocator: NewAllocationMapWithOffset,
max: 32,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max < 16",
allocator: NewAllocationMapWithOffset,
max: 8,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max > 16",
allocator: NewAllocationMapWithOffset,
max: 128,
reserved: 16,
},
{
name: "NewAllocationMapWithOffset max > 256",
allocator: NewAllocationMapWithOffset,
max: 1024,
reserved: 64,
},
{
name: "NewAllocationMapWithOffset max value",
allocator: NewAllocationMapWithOffset,
max: 65535,
reserved: 256,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m := tc.allocator(tc.max, "test", tc.reserved)
for i := 0; i < tc.max; i++ {
if ok, err := m.Allocate(i); !ok || err != nil {
t.Fatalf("unexpected error")
}
}
if _, ok, _ := m.AllocateNext(); ok {
t.Errorf("unexpected success")
}
if ok, err := m.Allocate(tc.max); ok || err == nil {
t.Fatalf("unexpected allocation")
}
if f := m.Free(); f != 0 {
t.Errorf("expect to get %d, but got %d", 0, f)
}
})
}
}
func TestAllocateNextMax(t *testing.T) {
testCases := []struct {
name string
allocator func(max int, rangeSpec string, reserved int) *AllocationBitmap
max int
reserved int
}{
{
name: "NewAllocationMap",
allocator: NewAllocationMapWithOffset,
max: 32,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max < 16",
allocator: NewAllocationMapWithOffset,
max: 8,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max > 16",
allocator: NewAllocationMapWithOffset,
max: 128,
reserved: 16,
},
{
name: "NewAllocationMapWithOffset max > 256",
allocator: NewAllocationMapWithOffset,
max: 1024,
reserved: 64,
},
{
name: "NewAllocationMapWithOffset max value",
allocator: NewAllocationMapWithOffset,
max: 65535,
reserved: 256,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m := tc.allocator(tc.max, "test", tc.reserved)
for i := 0; i < tc.max; i++ {
if _, ok, _ := m.AllocateNext(); !ok {
t.Fatalf("unexpected error")
}
}
if _, ok, _ := m.AllocateNext(); ok {
t.Errorf("unexpected success")
}
if f := m.Free(); f != 0 {
t.Errorf("expect to get %d, but got %d", 0, f)
}
})
}
}
func TestAllocateError(t *testing.T) {
testCases := []struct {
name string
allocator func(max int, rangeSpec string, reserved int) *AllocationBitmap
max int
reserved int
}{
{
name: "NewAllocationMap",
allocator: NewAllocationMapWithOffset,
max: 32,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max < 16",
allocator: NewAllocationMapWithOffset,
max: 8,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max > 16",
allocator: NewAllocationMapWithOffset,
max: 128,
reserved: 16,
},
{
name: "NewAllocationMapWithOffset max > 256",
allocator: NewAllocationMapWithOffset,
max: 1024,
reserved: 64,
},
{
name: "NewAllocationMapWithOffset max value",
allocator: NewAllocationMapWithOffset,
max: 65535,
reserved: 256,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m := tc.allocator(tc.max, "test", tc.reserved)
if ok, _ := m.Allocate(3); !ok {
t.Errorf("error allocate offset %v", 3)
}
if ok, _ := m.Allocate(3); ok {
t.Errorf("unexpected success")
}
})
}
}
func TestRelease(t *testing.T) {
testCases := []struct {
name string
allocator func(max int, rangeSpec string, reserved int) *AllocationBitmap
max int
reserved int
}{
{
name: "NewAllocationMap",
allocator: NewAllocationMapWithOffset,
max: 32,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max < 16",
allocator: NewAllocationMapWithOffset,
max: 8,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max > 16",
allocator: NewAllocationMapWithOffset,
max: 128,
reserved: 16,
},
{
name: "NewAllocationMapWithOffset max > 256",
allocator: NewAllocationMapWithOffset,
max: 1024,
reserved: 64,
},
{
name: "NewAllocationMapWithOffset max value",
allocator: NewAllocationMapWithOffset,
max: 65535,
reserved: 256,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m := tc.allocator(tc.max, "test", tc.reserved)
offset := 3
if ok, _ := m.Allocate(offset); !ok {
t.Errorf("error allocate offset %v", offset)
}
if !m.Has(offset) {
t.Errorf("expect offset %v allocated", offset)
}
if err := m.Release(offset); err != nil {
t.Errorf("unexpected error: %v", err)
}
if m.Has(offset) {
t.Errorf("expect offset %v not allocated", offset)
}
})
}
}
func TestForEach(t *testing.T) {
testCases := []struct {
name string
allocator func(max int, rangeSpec string, reserved int) *AllocationBitmap
max int
reserved int
}{
{
name: "NewAllocationMap",
allocator: NewAllocationMapWithOffset,
max: 32,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max < 16",
allocator: NewAllocationMapWithOffset,
max: 8,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max > 16",
allocator: NewAllocationMapWithOffset,
max: 128,
reserved: 16,
},
{
name: "NewAllocationMapWithOffset max > 256",
allocator: NewAllocationMapWithOffset,
max: 1024,
reserved: 64,
},
{
name: "NewAllocationMapWithOffset max value",
allocator: NewAllocationMapWithOffset,
max: 65535,
reserved: 256,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
subTests := []sets.Int{
sets.NewInt(),
sets.NewInt(0),
sets.NewInt(0, 2, 5),
sets.NewInt(0, 1, 2, 3, 4, 5, 6, 7),
}
for i, ts := range subTests {
m := tc.allocator(tc.max, "test", tc.reserved)
for offset := range ts {
if ok, _ := m.Allocate(offset); !ok {
t.Errorf("[%d] error allocate offset %v", i, offset)
}
if !m.Has(offset) {
t.Errorf("[%d] expect offset %v allocated", i, offset)
}
}
calls := sets.NewInt()
m.ForEach(func(i int) {
calls.Insert(i)
})
if len(calls) != len(ts) {
t.Errorf("[%d] expected %d calls, got %d", i, len(ts), len(calls))
}
if !calls.Equal(ts) {
t.Errorf("[%d] expected calls to equal testcase: %v vs %v", i, calls.List(), ts.List())
}
}
})
}
}
func TestSnapshotAndRestore(t *testing.T) {
testCases := []struct {
name string
allocator func(max int, rangeSpec string, reserved int) *AllocationBitmap
max int
reserved int
}{
{
name: "NewAllocationMap",
allocator: NewAllocationMapWithOffset,
max: 32,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max < 16",
allocator: NewAllocationMapWithOffset,
max: 8,
reserved: 0,
},
{
name: "NewAllocationMapWithOffset max > 16",
allocator: NewAllocationMapWithOffset,
max: 128,
reserved: 16,
},
{
name: "NewAllocationMapWithOffset max > 256",
allocator: NewAllocationMapWithOffset,
max: 1024,
reserved: 64,
},
{
name: "NewAllocationMapWithOffset max value",
allocator: NewAllocationMapWithOffset,
max: 65535,
reserved: 256,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
m := tc.allocator(tc.max, "test", tc.reserved)
offset := 3
if ok, _ := m.Allocate(offset); !ok {
t.Errorf("error allocate offset %v", offset)
}
spec, bytes := m.Snapshot()
m2 := tc.allocator(10, "test", tc.reserved)
err := m2.Restore(spec, bytes)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if m2.count != 1 {
t.Errorf("expect count to %d, but got %d", 0, m.count)
}
if !m2.Has(offset) {
t.Errorf("expect offset %v allocated", offset)
}
})
}
}
// TestAllocateMaxReserved should allocate first values greater or equal than the reserved values
func TestAllocateMax_BitmapReserved(t *testing.T) {
max := 128
dynamicOffset := 16
// just to double check off by one errors
allocated := 0
// modify if necessary
m := NewAllocationMapWithOffset(max, "test", dynamicOffset)
for i := 0; i < max-dynamicOffset; i++ {
if _, ok, _ := m.AllocateNext(); !ok {
t.Fatalf("unexpected error")
}
allocated++
}
if f := m.Free(); f != dynamicOffset {
t.Errorf("expect to get %d, but got %d", dynamicOffset-1, f)
}
for i := 0; i < dynamicOffset; i++ {
if m.Has(i) {
t.Errorf("unexpected allocated value %d", i)
}
}
// it should allocate one value of the reserved block
if _, ok, _ := m.AllocateNext(); !ok {
t.Fatalf("unexpected error")
}
allocated++
if allocated != m.count {
t.Errorf("expect to get %d, but got %d", allocated, m.count)
}
if m.count != max-dynamicOffset+1 {
t.Errorf("expect to get %d, but got %d", max-dynamicOffset+1, m.count)
}
if f := m.Free(); f != max-allocated {
t.Errorf("expect to get %d, but got %d", max-allocated, f)
}
}
func TestPreAllocateReservedFull_BitmapReserved(t *testing.T) {
max := 128
dynamicOffset := 16
// just to double check off by one errors
allocated := 0
m := NewAllocationMapWithOffset(max, "test", dynamicOffset)
// Allocate all possible values except the reserved
for i := dynamicOffset; i < max; i++ {
if ok, _ := m.Allocate(i); !ok {
t.Errorf("error allocate i %v", i)
} else {
allocated++
}
}
// Allocate all the values of the reserved block except one
for i := 0; i < dynamicOffset-1; i++ {
if ok, _ := m.Allocate(i); !ok {
t.Errorf("error allocate i %v", i)
} else {
allocated++
}
}
// there should be only one free value
if f := m.Free(); f != 1 {
t.Errorf("expect to get %d, but got %d", 1, f)
}
// check if the last free value is in the lower band
count := 0
for i := 0; i < dynamicOffset; i++ {
if !m.Has(i) {
count++
}
}
if count != 1 {
t.Errorf("expected one remaining free value, got %d", count)
}
if _, ok, _ := m.AllocateNext(); !ok {
t.Errorf("unexpected allocation error")
} else {
allocated++
}
if f := m.Free(); f != 0 {
t.Errorf("expect to get %d, but got %d", max-1, f)
}
if _, ok, _ := m.AllocateNext(); ok {
t.Errorf("unexpected success")
}
if m.count != allocated {
t.Errorf("expect to get %d, but got %d", max, m.count)
}
if f := m.Free(); f != 0 {
t.Errorf("expect to get %d, but got %d", 0, f)
}
}
func TestAllocateError(t *testing.T) {
m := NewAllocationMap(10, "test")
if ok, _ := m.Allocate(3); !ok {
t.Errorf("error allocate offset %v", 3)
}
if ok, _ := m.Allocate(3); ok {
t.Errorf("unexpected success")
}
}
func TestRelease(t *testing.T) {
offset := 3
m := NewAllocationMap(10, "test")
if ok, _ := m.Allocate(offset); !ok {
t.Errorf("error allocate offset %v", offset)
}
if !m.Has(offset) {
t.Errorf("expect offset %v allocated", offset)
}
if err := m.Release(offset); err != nil {
t.Errorf("unexpected error: %v", err)
}
if m.Has(offset) {
t.Errorf("expect offset %v not allocated", offset)
}
}
func TestForEach(t *testing.T) {
testCases := []sets.Int{
sets.NewInt(),
sets.NewInt(0),
sets.NewInt(0, 2, 5, 9),
sets.NewInt(0, 1, 2, 3, 4, 5, 6, 7, 8, 9),
}
for i, tc := range testCases {
m := NewAllocationMap(10, "test")
for offset := range tc {
if ok, _ := m.Allocate(offset); !ok {
t.Errorf("[%d] error allocate offset %v", i, offset)
}
if !m.Has(offset) {
t.Errorf("[%d] expect offset %v allocated", i, offset)
}
}
calls := sets.NewInt()
m.ForEach(func(i int) {
calls.Insert(i)
})
if len(calls) != len(tc) {
t.Errorf("[%d] expected %d calls, got %d", i, len(tc), len(calls))
}
if !calls.Equal(tc) {
t.Errorf("[%d] expected calls to equal testcase: %v vs %v", i, calls.List(), tc.List())
}
}
}
func TestSnapshotAndRestore(t *testing.T) {
offset := 3
m := NewAllocationMap(10, "test")
if ok, _ := m.Allocate(offset); !ok {
t.Errorf("error allocate offset %v", offset)
}
spec, bytes := m.Snapshot()
m2 := NewAllocationMap(10, "test")
err := m2.Restore(spec, bytes)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if m2.count != 1 {
t.Errorf("expect count to %d, but got %d", 0, m.count)
}
if !m2.Has(offset) {
t.Errorf("expect offset %v allocated", offset)
t.Errorf("expect to get %d, but got %d", max-1, f)
}
}

View File

@ -40,3 +40,5 @@ type Snapshottable interface {
}
type AllocatorFactory func(max int, rangeSpec string) (Interface, error)
type AllocatorWithOffsetFactory func(max int, rangeSpec string, offset int) (Interface, error)