mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 10:54:59 +00:00
move to using roaring iterators for UnmarshalBinary
The new roaring iterator used for the remap and importroaring things could also be used for unmarshalling roaring streams, and it's a slightly simpler design that doesn't need two passes through the data. This patch cleans that up a bit, makes it work better with ops logs, and uses that instead. It appears to noticably but not immensely reduce the time imports take, but it also gets us back down to one thing parsing roaring formats. There are a couple of subtle changes to errors we were testing for in various tests, and one of the fuzz tests goes away because it was actually itself an erroneous error message -- it was reporting the header of a roaring file as an invalid op because the op log reader was running on the header for roaring files with zero containers. Oops.
This commit is contained in:
parent
29a1db4550
commit
b04037900c
6 changed files with 101 additions and 131 deletions
|
|
@ -93,8 +93,9 @@ func TestCheckCommand_Run(t *testing.T) {
|
|||
t.Fatalf("copy: %v", err)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(err.Error(), "checking bitmap: unmarshalling: reading roaring header:") {
|
||||
t.Fatalf("expect error: invalid roaring file, actual: '%s'", err)
|
||||
expectedPrefix := "checking bitmap: unmarshalling: unknown roaring magic number 12849"
|
||||
if !strings.HasPrefix(err.Error(), expectedPrefix) {
|
||||
t.Fatalf("expect error: '%s...', actual: '%s'", expectedPrefix, err)
|
||||
}
|
||||
// Todo: need correct roaring file for happy path
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,9 @@ func TestInspectCommand_Run(t *testing.T) {
|
|||
file.Close()
|
||||
cm.Path = file.Name()
|
||||
err = cm.Run(context.Background())
|
||||
if err != nil && err.Error() != "unmarshalling: reading roaring header: did not find expected serialCookie in header" {
|
||||
t.Fatalf("can't run command: %v", err)
|
||||
expectedError := "unmarshalling: unknown roaring magic number 12849"
|
||||
if err != nil && err.Error() != expectedError {
|
||||
t.Fatalf("expected error '%s', got '%v'", expectedError, err)
|
||||
}
|
||||
|
||||
w.Close()
|
||||
|
|
|
|||
|
|
@ -399,6 +399,8 @@ func (f *fragment) openStorage(unmarshalData bool) error {
|
|||
}
|
||||
}()
|
||||
}
|
||||
// set the preference for mapping based on whether the data's mmapped
|
||||
f.storage.PreferMapping(newStorageData != nil)
|
||||
// so we have a problem here: if this fails, it's unclear whether
|
||||
// *either* or *both* of old and new storage data might be in use.
|
||||
// So we call the thing that should unconditionally unmap both of them...
|
||||
|
|
|
|||
|
|
@ -25,53 +25,50 @@ func TestUnmarshalBinary(t *testing.T) {
|
|||
}{
|
||||
{ // Checks for the zero containers situation
|
||||
cr: []byte(":0\x00\x00\x01\x00\x00\x000000"), //":000000"
|
||||
expected: "reading roaring header: malformed bitmap, key-cardinality slice overruns buffer at 12",
|
||||
},
|
||||
{ // Checks for int overflow
|
||||
cr: []byte("<0\x000\x00\x00\x00\x00000000000000" +
|
||||
"0"), //"<000000000000000"
|
||||
expected: "unmarshaling as pilosa roaring: unknown op type: 48",
|
||||
expected: "reading official header: malformed bitmap, key-cardinality slice overruns buffer at 12",
|
||||
},
|
||||
{ // The next 5 check for malformed bitmaps
|
||||
cr: []byte("<0\x0000000000000000000" +
|
||||
"\x00\x00\xec\x00\x03\x00\x00\x00\xec000"), //"<000000000000000000ÏÏ000"
|
||||
expected: "unmarshaling as pilosa roaring: malformed bitmap, key-cardinality not provided for 67372036 containers",
|
||||
expected: "insufficient data for header + offsets: want 12935430920 bytes, got 32",
|
||||
},
|
||||
{
|
||||
cr: []byte("<0\x00\x02\x00\x00\x00\\f\x01\xb5\x8d\x009\v\x01\x00\x00\x00\x00" +
|
||||
"\x00\x00e\x04\x00\x00\x00\x04\xfd\x00\x01\x00"), //"<0\fµç9e˝"
|
||||
expected: "unmarshaling as pilosa roaring: malformed bitmap, key-cardinality not provided for 128625322 containers",
|
||||
expected: "insufficient data for header + offsets: want 24696061960 bytes, got 32",
|
||||
},
|
||||
{
|
||||
cr: []byte("<0\x00\x02\x00\x00\x00&x.field safe"), //"<0&x.field safe"
|
||||
expected: "unmarshaling as pilosa roaring: malformed bitmap, key-cardinality not provided for 53127850 containers",
|
||||
expected: "insufficient data for header + offsets: want 10200547336 bytes, got 20",
|
||||
},
|
||||
{
|
||||
cr: []byte("<0\x00\x00\x14\x00\x00\x00\x80\xffp\x05_ 4\x114089" +
|
||||
"\x00\x00\xff\x000\x00\x02\x00\x00\x00\x00\xff\u007f\x00\x00\x01\x10\x00\x00j" +
|
||||
"\x02\x00\x00$\x04_\x00\xff\u007f\xff062616163\x00" + //"<0ġp_ 44089ˇ0ˇj$_ˇˇ0626161630ø¸ad$j√"
|
||||
"0\x00\x02\x00\x01\xbf\x00\x04\x00\xfcad$\x00\x00j\x10\x00\x00\xc3"),
|
||||
expected: "unmarshaling as pilosa roaring: malformed bitmap, key-cardinality not provided for 1 containers",
|
||||
expected: "insufficient data for header + offsets: want 328 bytes, got 80",
|
||||
},
|
||||
{ // 0 containers because the container is partially formed, but not fully (ie. 3/12 = 0)
|
||||
cr: []byte("<0\x00\x02\x03\x00\x00\x00쳫\v\x00d9\v\x00\x009\v"), //<0쳫d99
|
||||
expected: "unmarshaling as pilosa roaring: malformed bitmap, key-cardinality not provided for 0 containers",
|
||||
expected: "insufficient data for header + offsets: want 56 bytes, got 20",
|
||||
},
|
||||
{ // Checks for incomplete offset in readWithRuns
|
||||
cr: []byte(";0\x00\x00\v00000"), //";0000000"
|
||||
expected: "reading offsets from official roaring format: offset incomplete: len=10",
|
||||
expected: "container 0/1, expect run length at 9/10 bytes",
|
||||
},
|
||||
{ // Checks for incomplete offset in readOffsets
|
||||
cr: []byte(":0\x00\x00\x03\x00\x00\x00000000000000" +
|
||||
"\x00"), //:0000000000000
|
||||
expected: "reading offsets from official roaring format: offset incomplete: len=1",
|
||||
expected: "insufficient data for offsets (need 12 bytes, found 1)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, crash := range confirmedCrashers {
|
||||
err := b.UnmarshalBinary(crash.cr)
|
||||
if err.Error() != crash.expected {
|
||||
t.Errorf("Expected: %s, Got: %s", crash.expected, err)
|
||||
if err == nil {
|
||||
t.Errorf("expected: %s, got: no error", crash.expected)
|
||||
} else if err.Error() != crash.expected {
|
||||
t.Errorf("expected: %s, got: %s", crash.expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,6 +147,8 @@ type Bitmap struct {
|
|||
|
||||
// User-defined flags.
|
||||
Flags byte
|
||||
// should we try to keep things mapped?
|
||||
preferMapping bool
|
||||
|
||||
// Number of bit change operations written to the writer. Some operations
|
||||
// contain multiple values, so "ops" represents the number of distinct
|
||||
|
|
@ -1125,7 +1127,11 @@ func (b *Bitmap) writeToUnoptimized(w io.Writer) (n int64, err error) {
|
|||
// bitmap and yield information about containers, including type, size, and
|
||||
// the location of their data structures.
|
||||
type roaringIterator interface {
|
||||
// Next yields the information about the next container
|
||||
Next() (key uint64, cType byte, n int, length int, pointer *uint16, err error)
|
||||
// Remaining yields the bytes left over past the end of the roaring data,
|
||||
// which is typically an ops log in our case.
|
||||
Remaining() []byte
|
||||
}
|
||||
|
||||
// baseRoaringIterator holds values used by both Pilosa and official Roaring
|
||||
|
|
@ -1142,6 +1148,7 @@ type baseRoaringIterator struct {
|
|||
currentLen int
|
||||
currentPointer *uint16
|
||||
currentDataOffset uint32
|
||||
lastDataOffset int64
|
||||
lastErr error
|
||||
}
|
||||
|
||||
|
|
@ -1189,10 +1196,14 @@ func newOfficialRoaringIterator(data []byte) (*officialRoaringIterator, error) {
|
|||
r.headers = data[headerOffset:offsetOffset]
|
||||
// note: offsets are only actually used with the no-run headers.
|
||||
if r.haveRuns {
|
||||
// start out pointed at where the offsets would have been.
|
||||
r.currentDataOffset = uint32(offsetOffset)
|
||||
} else {
|
||||
if len(r.data) < offsetOffset+int(r.keys*4) {
|
||||
return nil, fmt.Errorf("insufficient data for offsets (need %d bytes, found %d)",
|
||||
r.keys*4, len(r.data)-offsetOffset)
|
||||
}
|
||||
r.offsets = data[offsetOffset : offsetOffset+int(r.keys*4)]
|
||||
r.currentDataOffset = uint32(offsetOffset)
|
||||
}
|
||||
// set key to -1; user should call Next first.
|
||||
r.currentIdx = -1
|
||||
|
|
@ -1212,6 +1223,12 @@ func newPilosaRoaringIterator(data []byte) (*pilosaRoaringIterator, error) {
|
|||
r.keys = int64(binary.LittleEndian.Uint32(data[3+1 : 8]))
|
||||
// it could happen
|
||||
if r.keys == 0 {
|
||||
// special case: what if we have zero containers, but a valid ops log after them?
|
||||
// set currentDataOffset so that Done will set lastDataOffset and Remaining() will
|
||||
// work.
|
||||
if len(data) > headerBaseSize {
|
||||
r.currentDataOffset = headerBaseSize
|
||||
}
|
||||
// not an error, exactly. it's valid and well-formed, we just have nothing to do
|
||||
r.Done(io.EOF)
|
||||
return r, nil
|
||||
|
|
@ -1227,6 +1244,10 @@ func newPilosaRoaringIterator(data []byte) (*pilosaRoaringIterator, error) {
|
|||
offsetEnd := offsetStart + (r.keys * 4)
|
||||
r.headers = data[headerStart:headerEnd]
|
||||
r.offsets = data[offsetStart:offsetEnd]
|
||||
// if there's no containers, we want to act as though data started at the end
|
||||
// of the list of offsets, which was also empty, so we don't think the entire thing
|
||||
// is actually a malformed op
|
||||
r.currentDataOffset = uint32(offsetEnd)
|
||||
// set key to -1; user should call Next first.
|
||||
r.currentIdx = -1
|
||||
r.currentKey = ^uint64(0)
|
||||
|
|
@ -1257,9 +1278,17 @@ func (r *baseRoaringIterator) Done(err error) {
|
|||
r.currentN = 0
|
||||
r.currentLen = 0
|
||||
r.currentPointer = nil
|
||||
r.lastDataOffset = int64(r.currentDataOffset)
|
||||
r.currentDataOffset = 0
|
||||
}
|
||||
|
||||
func (r *baseRoaringIterator) Remaining() []byte {
|
||||
if r.lastDataOffset == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.data[r.lastDataOffset:]
|
||||
}
|
||||
|
||||
func (r *pilosaRoaringIterator) Next() (key uint64, cType byte, n int, length int, pointer *uint16, err error) {
|
||||
if r.currentIdx >= r.keys {
|
||||
// we're already done
|
||||
|
|
@ -1306,6 +1335,7 @@ func (r *pilosaRoaringIterator) Next() (key uint64, cType byte, n int, length in
|
|||
r.currentIdx, r.keys, r.currentKey, r.currentDataOffset, size, len(r.data)))
|
||||
return r.Current()
|
||||
}
|
||||
r.currentDataOffset += uint32(size)
|
||||
r.lastErr = nil
|
||||
return r.Current()
|
||||
}
|
||||
|
|
@ -1333,6 +1363,11 @@ func (r *officialRoaringIterator) Next() (key uint64, cType byte, n int, length
|
|||
// a run container keeps its data after an initial 2 byte length header
|
||||
var runCount uint16
|
||||
if r.currentType == containerRun {
|
||||
if int(r.currentDataOffset)+2 > len(r.data) {
|
||||
r.Done(fmt.Errorf("container %d/%d, expect run length at %d/%d bytes",
|
||||
r.currentIdx, r.keys, r.currentDataOffset, len(r.data)))
|
||||
return r.Current()
|
||||
}
|
||||
runCount = binary.LittleEndian.Uint16(r.data[r.currentDataOffset : r.currentDataOffset+runCountHeaderSize])
|
||||
r.currentDataOffset += 2
|
||||
}
|
||||
|
|
@ -1554,78 +1589,62 @@ func (b *Bitmap) ImportRoaringBits(data []byte, clear bool, log bool, rowSize ui
|
|||
err = b.writeOp(&op)
|
||||
}
|
||||
return changed, rowSet, err
|
||||
}
|
||||
|
||||
func (b *Bitmap) PreferMapping(preferred bool) {
|
||||
b.preferMapping = preferred
|
||||
}
|
||||
|
||||
// unmarshalPilosaRoaring treats data as being encoded in Pilosa's 64 bit
|
||||
// roaring format and decodes it into b.
|
||||
func (b *Bitmap) unmarshalPilosaRoaring(data []byte) error {
|
||||
if len(data) < headerBaseSize {
|
||||
return errors.New("data too small")
|
||||
func (b *Bitmap) UnmarshalBinary(data []byte) (err error) {
|
||||
if data == nil {
|
||||
return errors.New("no roaring bitmap provided")
|
||||
}
|
||||
var itr roaringIterator
|
||||
var itrKey uint64
|
||||
var itrCType byte
|
||||
var itrN int
|
||||
var itrLen int
|
||||
var itrPointer *uint16
|
||||
var itrErr error
|
||||
|
||||
itr, err = newRoaringIterator(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if itr == nil {
|
||||
return errors.New("failed to create roaring iterator, but don't know why")
|
||||
}
|
||||
|
||||
// Verify the first two bytes are a valid MagicNumber, and second two bytes match current storageVersion.
|
||||
fileMagic := uint32(binary.LittleEndian.Uint16(data[0:2]))
|
||||
fileVersion := uint32(data[2])
|
||||
b.Flags = data[3]
|
||||
if fileMagic != MagicNumber {
|
||||
return fmt.Errorf("invalid roaring file, magic number %v is incorrect", fileMagic)
|
||||
}
|
||||
b.Containers.Reset()
|
||||
|
||||
if fileVersion != storageVersion {
|
||||
return fmt.Errorf("wrong roaring version, file is v%d, server requires v%d", fileVersion, storageVersion)
|
||||
}
|
||||
|
||||
// Read key count in bytes sizeof(cookie)+sizeof(flag):(sizeof(cookie)+sizeof(uint32)).
|
||||
keyN := binary.LittleEndian.Uint32(data[3+1 : 8])
|
||||
if uint32(len(data)) < headerBaseSize+keyN*12 {
|
||||
return fmt.Errorf("malformed bitmap, key-cardinality not provided for %d containers", int(keyN)/12)
|
||||
}
|
||||
|
||||
headerSize := headerBaseSize
|
||||
b.Containers.ResetN(int(keyN))
|
||||
// Descriptive header section: Read container keys and cardinalities.
|
||||
for i, buf := 0, data[headerSize:]; i < int(keyN); i, buf = i+1, buf[12:] {
|
||||
b.Containers.PutContainerValues(
|
||||
binary.LittleEndian.Uint64(buf[0:8]),
|
||||
byte(binary.LittleEndian.Uint16(buf[8:10])),
|
||||
int(binary.LittleEndian.Uint16(buf[10:12]))+1,
|
||||
true)
|
||||
}
|
||||
opsOffset := headerSize + int(keyN)*12
|
||||
|
||||
// Read container offsets and attach data.
|
||||
citer, _ := b.Containers.Iterator(0)
|
||||
for i, buf := 0, data[opsOffset:]; i < int(keyN); i, buf = i+1, buf[4:] {
|
||||
offset := binary.LittleEndian.Uint32(buf[0:4])
|
||||
// Verify the offset is within the bounds of the input data.
|
||||
if int(offset) >= len(data) {
|
||||
return fmt.Errorf("offset out of bounds: off=%d, len=%d", offset, len(data))
|
||||
itrKey, itrCType, itrN, itrLen, itrPointer, itrErr = itr.Next()
|
||||
for itrErr == nil {
|
||||
newC := &Container{
|
||||
typeID: itrCType,
|
||||
n: int32(itrN),
|
||||
len: int32(itrLen),
|
||||
cap: int32(itrLen),
|
||||
pointer: itrPointer,
|
||||
flags: flagMapped,
|
||||
}
|
||||
|
||||
// Map byte slice directly to the container data.
|
||||
citer.Next()
|
||||
_, c := citer.Value()
|
||||
// this shouldn't happen, since we don't normally store nils.
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
switch c.typ() {
|
||||
case containerRun:
|
||||
runCount := binary.LittleEndian.Uint16(data[offset : offset+runCountHeaderSize])
|
||||
c.setRuns((*[0xFFFFFFF]interval16)(unsafe.Pointer(&data[offset+runCountHeaderSize]))[:runCount:runCount])
|
||||
opsOffset = int(offset) + runCountHeaderSize + len(c.runs())*interval16Size
|
||||
case containerArray:
|
||||
c.setArray((*[0xFFFFFFF]uint16)(unsafe.Pointer(&data[offset]))[:c.N():c.N()])
|
||||
opsOffset = int(offset) + len(c.array())*2 // sizeof(uint32)
|
||||
case containerBitmap:
|
||||
c.setBitmap((*[0xFFFFFFF]uint64)(unsafe.Pointer(&data[offset]))[:bitmapN:bitmapN])
|
||||
opsOffset = int(offset) + len(c.bitmap())*8 // sizeof(uint64)
|
||||
if !b.preferMapping {
|
||||
newC.unmapOrClone()
|
||||
}
|
||||
b.Containers.Put(itrKey, newC)
|
||||
itrKey, itrCType, itrN, itrLen, itrPointer, itrErr = itr.Next()
|
||||
}
|
||||
// note: if we get a non-EOF err, it's possible that we made SOME
|
||||
// changes but didn't log them. I don't have a good solution to this.
|
||||
if itrErr != io.EOF {
|
||||
return itrErr
|
||||
}
|
||||
|
||||
// Read ops log until the end of the file.
|
||||
buf := data[opsOffset:]
|
||||
b.ops = 0
|
||||
b.opN = 0
|
||||
buf := itr.Remaining()
|
||||
for {
|
||||
// Exit when there are no more ops to parse.
|
||||
if len(buf) == 0 {
|
||||
|
|
@ -1648,7 +1667,6 @@ func (b *Bitmap) unmarshalPilosaRoaring(data []byte) error {
|
|||
// Move the buffer forward.
|
||||
buf = buf[opr.size():]
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -5138,56 +5156,7 @@ func readOfficialHeader(buf []byte) (size uint32, containerTyper func(index uint
|
|||
return size, containerTyper, header, pos, haveRuns, err
|
||||
}
|
||||
|
||||
// UnmarshalBinary decodes b from a binary-encoded byte slice. data can be in
|
||||
// either official roaring format or Pilosa's roaring format.
|
||||
func (b *Bitmap) UnmarshalBinary(data []byte) error {
|
||||
if data == nil {
|
||||
// Nothing to unmarshal
|
||||
return nil
|
||||
}
|
||||
statsHit("Bitmap/UnmarshalBinary")
|
||||
b.opN = 0 // reset opN since we're reading new data.
|
||||
fileMagic := uint32(binary.LittleEndian.Uint16(data[0:2]))
|
||||
if fileMagic == MagicNumber { // if pilosa roaring
|
||||
return errors.Wrap(b.unmarshalPilosaRoaring(data), "unmarshaling as pilosa roaring")
|
||||
}
|
||||
|
||||
keyN, containerTyper, header, pos, haveRuns, err := readOfficialHeader(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading roaring header")
|
||||
}
|
||||
// Only the Pilosa roaring format has flags. The official Roaring format
|
||||
// hasn't got space in its header for flags.
|
||||
b.Flags = 0
|
||||
|
||||
b.Containers.ResetN(int(keyN))
|
||||
// Descriptive header section: Read container keys and cardinalities.
|
||||
for i, buf := uint(0), data[header:]; i < uint(keyN); i, buf = i+1, buf[4:] {
|
||||
card := int(binary.LittleEndian.Uint16(buf[2:4])) + 1
|
||||
b.Containers.PutContainerValues(
|
||||
uint64(binary.LittleEndian.Uint16(buf[0:2])),
|
||||
containerTyper(i, card), /// container type voodo with isRunBitmap
|
||||
card,
|
||||
true)
|
||||
}
|
||||
|
||||
// Read container offsets and attach data.
|
||||
if haveRuns {
|
||||
err := readWithRuns(b, data, pos, keyN)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading offsets from official roaring format")
|
||||
}
|
||||
} else {
|
||||
err := readOffsets(b, data, pos, keyN)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "reading offsets from official roaring format")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readOffsets(b *Bitmap, data []byte, pos int, keyN uint32) error {
|
||||
|
||||
citer, _ := b.Containers.Iterator(0)
|
||||
for i, buf := 0, data[pos:]; i < int(keyN); i, buf = i+1, buf[4:] {
|
||||
// Verify the offset is fully formed
|
||||
|
|
|
|||
|
|
@ -3409,11 +3409,11 @@ func TestUnmarshalRoaringWithErrors(t *testing.T) {
|
|||
}{
|
||||
{ // Runs a bitmap without runs and no containers through the official roaring
|
||||
hexString: "3A30000000000000",
|
||||
expectedError: "reading roaring header: malformed bitmap, key-cardinality slice overruns buffer at 8",
|
||||
expectedError: "reading official header: malformed bitmap, key-cardinality slice overruns buffer at 8",
|
||||
},
|
||||
{ // Runs a bitmap with runs and no containers through the official roaring
|
||||
hexString: "3B30000000000000",
|
||||
expectedError: "reading roaring header: malformed bitmap, key-cardinality slice overruns buffer at 9",
|
||||
expectedError: "reading official header: malformed bitmap, key-cardinality slice overruns buffer at 9",
|
||||
},
|
||||
{ // Runs a bitmap in the Pilosa format through the Pilosa roaring
|
||||
hexString: "3C30000000000000",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue