mirror of
https://github.com/dolthub/dolt.git
synced 2026-04-29 03:06:35 -05:00
338 lines
9.2 KiB
Go
338 lines
9.2 KiB
Go
// 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 people’s 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
|
||
}
|