Files
dolt/go/store/nbs/mem_table_test.go
T

338 lines
9.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Copyright 2019 Dolthub, Inc.
//
// 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.
//
// This file incorporates work covered by the following copyright and
// permission notice:
//
// Copyright 2016 Attic Labs, Inc. All rights reserved.
// Licensed under the Apache License, version 2.0:
// http://www.apache.org/licenses/LICENSE-2.0
package nbs
import (
"bytes"
"context"
"io"
"os"
"testing"
"github.com/golang/snappy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/sync/errgroup"
"github.com/dolthub/dolt/go/store/chunks"
"github.com/dolthub/dolt/go/store/d"
"github.com/dolthub/dolt/go/store/types"
)
var testMDChunks = []chunks.Chunk{
mustChunk(types.EncodeValue(types.String("Call me Ishmael. Some years ago—never mind how long precisely—having little or no money in my purse, "), types.Format_Default)),
mustChunk(types.EncodeValue(types.String("and nothing particular to interest me on shore, I thought I would sail about a little and see the watery "), types.Format_Default)),
mustChunk(types.EncodeValue(types.String("part of the world. It is a way I have of driving off the spleen and regulating the "), types.Format_Default)),
mustChunk(types.EncodeValue(types.String("circulation. Whenever I find myself growing grim about the mouth; whenever it is a damp, drizzly "), types.Format_Default)),
mustChunk(types.EncodeValue(types.String("November in my soul; whenever I find myself involuntarily pausing before coffin warehouses, and bringing "), types.Format_Default)),
mustChunk(types.EncodeValue(types.String("funeral I meet; and especially whenever my hypos get such an upper hand of me, that it requires "), types.Format_Default)),
mustChunk(types.EncodeValue(types.String("a strong moral principle to prevent me from deliberately stepping into the street, and methodically "), types.Format_Default)),
mustChunk(types.EncodeValue(types.String("knocking peoples hats off—then, I account it high time to get to sea as soon as I can."), types.Format_Default)),
}
var testMDChunksSize uint64
func init() {
for _, chunk := range testMDChunks {
testMDChunksSize += uint64(len(chunk.Data()))
}
}
func mustChunk(chunk chunks.Chunk, err error) chunks.Chunk {
d.PanicIfError(err)
return chunk
}
func TestWriteChunks(t *testing.T) {
name, data, err := WriteChunks(testMDChunks)
if err != nil {
t.Error(err)
}
dir, err := os.MkdirTemp("", "write_chunks_test")
if err != nil {
t.Error(err)
}
err = os.WriteFile(dir+name, data, os.ModePerm)
if err != nil {
t.Error(err)
}
}
func TestMemTableAddHasGetChunk(t *testing.T) {
assert := assert.New(t)
mt := newMemTable(1024)
chunks := [][]byte{
[]byte("hello2"),
[]byte("goodbye2"),
[]byte("badbye2"),
}
for _, c := range chunks {
assert.Equal(mt.addChunk(computeAddr(c), c), chunkAdded)
}
assertChunksInReader(chunks, mt, assert)
for _, c := range chunks {
data, err := mt.get(context.Background(), computeAddr(c), &Stats{})
require.NoError(t, err)
assert.Equal(bytes.Compare(c, data), 0)
}
notPresent := []byte("nope")
assert.False(mt.has(computeAddr(notPresent)))
assert.Nil(mt.get(context.Background(), computeAddr(notPresent), &Stats{}))
}
func TestMemTableAddOverflowChunk(t *testing.T) {
memTableSize := uint64(1024)
assert := assert.New(t)
big := make([]byte, memTableSize)
little := []byte{0x01}
{
bigAddr := computeAddr(big)
mt := newMemTable(memTableSize)
assert.Equal(mt.addChunk(bigAddr, big), chunkAdded)
assert.True(mt.has(bigAddr))
assert.Equal(mt.addChunk(computeAddr(little), little), chunkNotAdded)
assert.False(mt.has(computeAddr(little)))
}
{
big := big[:memTableSize-1]
bigAddr := computeAddr(big)
mt := newMemTable(memTableSize)
assert.Equal(mt.addChunk(bigAddr, big), chunkAdded)
assert.True(mt.has(bigAddr))
assert.Equal(mt.addChunk(computeAddr(little), little), chunkAdded)
assert.True(mt.has(computeAddr(little)))
other := []byte("o")
assert.Equal(mt.addChunk(computeAddr(other), other), chunkNotAdded)
assert.False(mt.has(computeAddr(other)))
}
}
func TestMemTableWrite(t *testing.T) {
ctx := context.Background()
assert := assert.New(t)
mt := newMemTable(1024)
chunks := [][]byte{
[]byte("hello2"),
[]byte("goodbye2"),
[]byte("badbye2"),
}
for _, c := range chunks {
assert.Equal(mt.addChunk(computeAddr(c), c), chunkAdded)
}
td1, _, err := buildTable(chunks[1:2])
require.NoError(t, err)
ti1, err := parseTableIndexByCopy(ctx, td1, &UnlimitedQuotaProvider{})
require.NoError(t, err)
tr1, err := newTableReader(ti1, tableReaderAtFromBytes(td1), fileBlockSize)
require.NoError(t, err)
defer tr1.close()
assert.True(tr1.has(computeAddr(chunks[1])))
td2, _, err := buildTable(chunks[2:])
require.NoError(t, err)
ti2, err := parseTableIndexByCopy(ctx, td2, &UnlimitedQuotaProvider{})
require.NoError(t, err)
tr2, err := newTableReader(ti2, tableReaderAtFromBytes(td2), fileBlockSize)
require.NoError(t, err)
defer tr2.close()
assert.True(tr2.has(computeAddr(chunks[2])))
_, data, count, err := mt.write(chunkReaderGroup{tr1, tr2}, &Stats{})
require.NoError(t, err)
assert.Equal(uint32(1), count)
ti, err := parseTableIndexByCopy(ctx, data, &UnlimitedQuotaProvider{})
require.NoError(t, err)
outReader, err := newTableReader(ti, tableReaderAtFromBytes(data), fileBlockSize)
require.NoError(t, err)
defer outReader.close()
assert.True(outReader.has(computeAddr(chunks[0])))
assert.False(outReader.has(computeAddr(chunks[1])))
assert.False(outReader.has(computeAddr(chunks[2])))
}
type tableReaderAtAdapter struct {
br *bytes.Reader
}
func tableReaderAtFromBytes(b []byte) tableReaderAt {
return tableReaderAtAdapter{bytes.NewReader(b)}
}
func (adapter tableReaderAtAdapter) Close() error {
return nil
}
func (adapter tableReaderAtAdapter) clone() (tableReaderAt, error) {
return adapter, nil
}
func (adapter tableReaderAtAdapter) Reader(ctx context.Context) (io.ReadCloser, error) {
r := *adapter.br
return io.NopCloser(&r), nil
}
func (adapter tableReaderAtAdapter) ReadAtWithStats(ctx context.Context, p []byte, off int64, stats *Stats) (n int, err error) {
return adapter.br.ReadAt(p, off)
}
func TestMemTableSnappyWriteOutOfLine(t *testing.T) {
assert := assert.New(t)
mt := newMemTable(1024)
chunks := [][]byte{
[]byte("hello2"),
[]byte("goodbye2"),
[]byte("badbye2"),
}
for _, c := range chunks {
assert.Equal(mt.addChunk(computeAddr(c), c), chunkAdded)
}
mt.snapper = &outOfLineSnappy{[]bool{false, true, false}} // chunks[1] should trigger a panic
assert.Panics(func() { mt.write(nil, &Stats{}) })
}
type outOfLineSnappy struct {
policy []bool
}
func (o *outOfLineSnappy) Encode(dst, src []byte) []byte {
outOfLine := false
if len(o.policy) > 0 {
outOfLine = o.policy[0]
o.policy = o.policy[1:]
}
if outOfLine {
return snappy.Encode(nil, src)
}
return snappy.Encode(dst, src)
}
type chunkReaderGroup []chunkReader
func (crg chunkReaderGroup) has(h addr) (bool, error) {
for _, haver := range crg {
ok, err := haver.has(h)
if err != nil {
return false, err
}
if ok {
return true, nil
}
}
return false, nil
}
func (crg chunkReaderGroup) get(ctx context.Context, h addr, stats *Stats) ([]byte, error) {
for _, haver := range crg {
if data, err := haver.get(ctx, h, stats); err != nil {
return nil, err
} else if data != nil {
return data, nil
}
}
return nil, nil
}
func (crg chunkReaderGroup) hasMany(addrs []hasRecord) (bool, error) {
for _, haver := range crg {
remaining, err := haver.hasMany(addrs)
if err != nil {
return false, err
}
if !remaining {
return false, nil
}
}
return true, nil
}
func (crg chunkReaderGroup) getMany(ctx context.Context, eg *errgroup.Group, reqs []getRecord, found func(context.Context, *chunks.Chunk), stats *Stats) (bool, error) {
for _, haver := range crg {
remaining, err := haver.getMany(ctx, eg, reqs, found, stats)
if err != nil {
return true, err
}
if !remaining {
return false, nil
}
}
return true, nil
}
func (crg chunkReaderGroup) getManyCompressed(ctx context.Context, eg *errgroup.Group, reqs []getRecord, found func(context.Context, CompressedChunk), stats *Stats) (bool, error) {
for _, haver := range crg {
remaining, err := haver.getManyCompressed(ctx, eg, reqs, found, stats)
if err != nil {
return true, err
}
if !remaining {
return false, nil
}
}
return true, nil
}
func (crg chunkReaderGroup) count() (count uint32, err error) {
for _, haver := range crg {
count += mustUint32(haver.count())
}
return
}
func (crg chunkReaderGroup) uncompressedLen() (data uint64, err error) {
for _, haver := range crg {
data += mustUint64(haver.uncompressedLen())
}
return
}
func (crg chunkReaderGroup) close() error {
var firstErr error
for _, c := range crg {
err := c.close()
if err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}