455 lines
13 KiB
Go
455 lines
13 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.
|
|
|
|
package pull
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
flatbuffers "github.com/dolthub/flatbuffers/v23/go"
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/dolthub/dolt/go/gen/fb/serial"
|
|
"github.com/dolthub/dolt/go/store/chunks"
|
|
"github.com/dolthub/dolt/go/store/datas"
|
|
"github.com/dolthub/dolt/go/store/hash"
|
|
"github.com/dolthub/dolt/go/store/nbs"
|
|
"github.com/dolthub/dolt/go/store/prolly"
|
|
"github.com/dolthub/dolt/go/store/prolly/tree"
|
|
"github.com/dolthub/dolt/go/store/types"
|
|
"github.com/dolthub/dolt/go/store/util/clienttest"
|
|
"github.com/dolthub/dolt/go/store/val"
|
|
)
|
|
|
|
func TestNbsPuller(t *testing.T) {
|
|
testPuller(t, func(ctx context.Context) (tree.NodeStore, datas.Database) {
|
|
dir := filepath.Join(os.TempDir(), uuid.New().String())
|
|
err := os.MkdirAll(dir, os.ModePerm)
|
|
require.NoError(t, err)
|
|
|
|
nbf := types.Format_DOLT.VersionString()
|
|
q := nbs.NewUnlimitedMemQuotaProvider()
|
|
st, err := nbs.NewLocalStore(ctx, nbf, dir, clienttest.DefaultMemTableSize, q, false)
|
|
require.NoError(t, err)
|
|
|
|
ns := tree.NewNodeStore(st)
|
|
db := datas.NewDatabase(st)
|
|
return ns, db
|
|
})
|
|
}
|
|
|
|
func TestChunkJournalPuller(t *testing.T) {
|
|
testPuller(t, func(ctx context.Context) (tree.NodeStore, datas.Database) {
|
|
dir := filepath.Join(os.TempDir(), uuid.New().String())
|
|
err := os.MkdirAll(dir, os.ModePerm)
|
|
require.NoError(t, err)
|
|
|
|
nbf := types.Format_DOLT.VersionString()
|
|
q := nbs.NewUnlimitedMemQuotaProvider()
|
|
|
|
st, err := nbs.NewLocalJournalingStore(ctx, nbf, dir, q, false, nil)
|
|
require.NoError(t, err)
|
|
|
|
ns := tree.NewNodeStore(st)
|
|
db := datas.NewDatabase(st)
|
|
return ns, db
|
|
})
|
|
}
|
|
|
|
func TestPuller(t *testing.T) {
|
|
t.Run("GhostChunk", func(t *testing.T) {
|
|
ctx := context.Background()
|
|
gs, err := nbs.NewGhostBlockStore(t.TempDir())
|
|
waf, err := types.WalkAddrsForChunkStore(gs)
|
|
require.NoError(t, err)
|
|
|
|
ghost := hash.Parse("e6esqr35dkqnc7updhj6ap5v82sahm9r")
|
|
|
|
gs.PersistGhostHashes(ctx, hash.NewHashSet(ghost))
|
|
|
|
statsCh := make(chan Stats)
|
|
go func() {
|
|
for _ = range statsCh {
|
|
}
|
|
}()
|
|
|
|
dir := filepath.Join(os.TempDir(), uuid.New().String())
|
|
err = os.MkdirAll(dir, os.ModePerm)
|
|
require.NoError(t, err)
|
|
|
|
nbf := types.Format_DOLT.VersionString()
|
|
q := nbs.NewUnlimitedMemQuotaProvider()
|
|
|
|
st, err := nbs.NewLocalJournalingStore(ctx, nbf, dir, q, false, nil)
|
|
require.NoError(t, err)
|
|
|
|
plr, err := NewPuller(ctx, t.TempDir(), 1<<20, gs, st, waf, []hash.Hash{ghost}, statsCh)
|
|
require.NoError(t, err)
|
|
err = plr.Pull(ctx)
|
|
require.ErrorIs(t, err, nbs.ErrGhostChunkRequested)
|
|
})
|
|
}
|
|
|
|
var (
|
|
testKeyDesc = val.NewTupleDescriptor(val.Type{Enc: val.Int64Enc, Nullable: false})
|
|
testValDesc = val.NewTupleDescriptor(
|
|
val.Type{Enc: val.StringEnc, Nullable: true},
|
|
val.Type{Enc: val.StringEnc, Nullable: true},
|
|
)
|
|
)
|
|
|
|
type testTuple struct {
|
|
key int64
|
|
v0 string
|
|
v1 string
|
|
}
|
|
|
|
func buildKeyTuple(ns tree.NodeStore, k int64) val.Tuple {
|
|
tb := val.NewTupleBuilder(testKeyDesc, ns)
|
|
tb.PutInt64(0, k)
|
|
tup, _ := tb.Build(context.Background(), ns.Pool())
|
|
return tup
|
|
}
|
|
|
|
func buildValTuple(ns tree.NodeStore, v0, v1 string) val.Tuple {
|
|
tb := val.NewTupleBuilder(testValDesc, ns)
|
|
tb.PutString(0, v0)
|
|
tb.PutString(1, v1)
|
|
tup, _ := tb.Build(context.Background(), ns.Pool())
|
|
return tup
|
|
}
|
|
|
|
func createProllyMap(t *testing.T, ctx context.Context, ns tree.NodeStore, data []testTuple) prolly.Map {
|
|
tuples := make([]val.Tuple, 0, len(data)*2)
|
|
for _, p := range data {
|
|
tuples = append(tuples, buildKeyTuple(ns, p.key), buildValTuple(ns, p.v0, p.v1))
|
|
}
|
|
m, err := prolly.NewMapFromTuples(ctx, ns, testKeyDesc, testValDesc, tuples...)
|
|
require.NoError(t, err)
|
|
return m
|
|
}
|
|
|
|
func putProllyMapValues(t *testing.T, ctx context.Context, ns tree.NodeStore, m prolly.Map, data []testTuple) prolly.Map {
|
|
mut := m.Mutate()
|
|
for _, p := range data {
|
|
err := mut.Put(ctx, buildKeyTuple(ns, p.key), buildValTuple(ns, p.v0, p.v1))
|
|
require.NoError(t, err)
|
|
}
|
|
result, err := mut.Map(ctx)
|
|
require.NoError(t, err)
|
|
return result
|
|
}
|
|
|
|
func deleteProllyMapKeys(t *testing.T, ctx context.Context, ns tree.NodeStore, m prolly.Map, keys []int64) prolly.Map {
|
|
if len(keys) == 0 {
|
|
return m
|
|
}
|
|
mut := m.Mutate()
|
|
for _, k := range keys {
|
|
err := mut.Delete(ctx, buildKeyTuple(ns, k))
|
|
require.NoError(t, err)
|
|
}
|
|
result, err := mut.Map(ctx)
|
|
require.NoError(t, err)
|
|
return result
|
|
}
|
|
|
|
func buildAddressMap(t *testing.T, ctx context.Context, ns tree.NodeStore, tables map[string]prolly.Map) prolly.AddressMap {
|
|
am, err := prolly.NewEmptyAddressMap(ns)
|
|
require.NoError(t, err)
|
|
editor := am.Editor()
|
|
for name, m := range tables {
|
|
_, err := ns.Write(ctx, m.Node())
|
|
require.NoError(t, err)
|
|
err = editor.Add(ctx, name, m.HashOf())
|
|
require.NoError(t, err)
|
|
}
|
|
am, err = editor.Flush(ctx)
|
|
require.NoError(t, err)
|
|
return am
|
|
}
|
|
|
|
func updateAddressMap(t *testing.T, ctx context.Context, ns tree.NodeStore, am prolly.AddressMap, name string, m prolly.Map) prolly.AddressMap {
|
|
_, err := ns.Write(ctx, m.Node())
|
|
require.NoError(t, err)
|
|
editor := am.Editor()
|
|
err = editor.Update(ctx, name, m.HashOf())
|
|
require.NoError(t, err)
|
|
am, err = editor.Flush(ctx)
|
|
require.NoError(t, err)
|
|
return am
|
|
}
|
|
|
|
func addToAddressMap(t *testing.T, ctx context.Context, ns tree.NodeStore, am prolly.AddressMap, name string, m prolly.Map) prolly.AddressMap {
|
|
_, err := ns.Write(ctx, m.Node())
|
|
require.NoError(t, err)
|
|
editor := am.Editor()
|
|
err = editor.Add(ctx, name, m.HashOf())
|
|
require.NoError(t, err)
|
|
am, err = editor.Flush(ctx)
|
|
require.NoError(t, err)
|
|
return am
|
|
}
|
|
|
|
func deleteFromAddressMap(t *testing.T, ctx context.Context, am prolly.AddressMap, name string) prolly.AddressMap {
|
|
editor := am.Editor()
|
|
err := editor.Delete(ctx, name)
|
|
require.NoError(t, err)
|
|
am, err = editor.Flush(ctx)
|
|
require.NoError(t, err)
|
|
return am
|
|
}
|
|
|
|
func buildRootValue(am prolly.AddressMap) types.SerialMessage {
|
|
builder := flatbuffers.NewBuilder(256)
|
|
ambytes := []byte(tree.ValueFromNode(am.Node()).(types.SerialMessage))
|
|
tablesoff := builder.CreateByteVector(ambytes)
|
|
emptyAddr := make([]byte, hash.ByteLen)
|
|
fkoff := builder.CreateByteVector(emptyAddr)
|
|
serial.RootValueStart(builder)
|
|
serial.RootValueAddTables(builder, tablesoff)
|
|
serial.RootValueAddForeignKeyAddr(builder, fkoff)
|
|
bs := serial.FinishMessage(builder, serial.RootValueEnd(builder), []byte(serial.RootValueFileID))
|
|
return types.SerialMessage(bs)
|
|
}
|
|
|
|
type datasFactory func(context.Context) (tree.NodeStore, datas.Database)
|
|
|
|
func testPuller(t *testing.T, makeDB datasFactory) {
|
|
ctx := context.Background()
|
|
ns, db := makeDB(ctx)
|
|
defer db.Close()
|
|
|
|
type delta struct {
|
|
name string
|
|
sets map[string][]testTuple
|
|
deletes map[string][]int64
|
|
tblDeletes []string
|
|
}
|
|
|
|
deltas := []delta{
|
|
{
|
|
name: "empty",
|
|
sets: map[string][]testTuple{},
|
|
deletes: map[string][]int64{},
|
|
tblDeletes: []string{},
|
|
},
|
|
{
|
|
name: "employees",
|
|
sets: map[string][]testTuple{
|
|
"employees": {
|
|
{0, "Hendriks", "Software Engineer"},
|
|
{1, "Sehn", "CEO"},
|
|
{2, "Son", "Software Engineer"},
|
|
},
|
|
},
|
|
deletes: map[string][]int64{},
|
|
tblDeletes: []string{},
|
|
},
|
|
{
|
|
name: "ip to country",
|
|
sets: map[string][]testTuple{
|
|
"ip_to_country": {
|
|
{0, "5.183.230.1", "BZ"},
|
|
{1, "5.180.188.1", "AU"},
|
|
{2, "2.56.9.244", "GB"},
|
|
{3, "20.175.7.56", "US"},
|
|
},
|
|
},
|
|
deletes: map[string][]int64{},
|
|
tblDeletes: []string{},
|
|
},
|
|
{
|
|
name: "more ips",
|
|
sets: map[string][]testTuple{
|
|
"ip_to_country": {
|
|
{4, "20.175.193.85", "US"},
|
|
{5, "5.196.110.191", "FR"},
|
|
{6, "4.14.242.160", "CA"},
|
|
},
|
|
},
|
|
deletes: map[string][]int64{},
|
|
tblDeletes: []string{},
|
|
},
|
|
{
|
|
name: "more employees",
|
|
sets: map[string][]testTuple{
|
|
"employees": {
|
|
{3, "Jesuele", "Software Engineer"},
|
|
{4, "Wilkins", "Software Engineer"},
|
|
{5, "McCulloch", "Software Engineer"},
|
|
},
|
|
},
|
|
deletes: map[string][]int64{},
|
|
tblDeletes: []string{},
|
|
},
|
|
{
|
|
name: "delete ips table",
|
|
sets: map[string][]testTuple{},
|
|
deletes: map[string][]int64{},
|
|
tblDeletes: []string{"ip_to_country"},
|
|
},
|
|
{
|
|
name: "delete some employees",
|
|
sets: map[string][]testTuple{},
|
|
deletes: map[string][]int64{
|
|
"employees": {0, 1, 2},
|
|
},
|
|
tblDeletes: []string{},
|
|
},
|
|
}
|
|
|
|
ds, err := db.GetDataset(ctx, "ds")
|
|
require.NoError(t, err)
|
|
|
|
tables := make(map[string]prolly.Map)
|
|
am, err := prolly.NewEmptyAddressMap(ns)
|
|
require.NoError(t, err)
|
|
|
|
var parent []hash.Hash
|
|
states := map[string]hash.Hash{}
|
|
for _, d := range deltas {
|
|
for tbl, data := range d.sets {
|
|
existing, ok := tables[tbl]
|
|
if !ok {
|
|
m := createProllyMap(t, ctx, ns, data)
|
|
tables[tbl] = m
|
|
am = addToAddressMap(t, ctx, ns, am, tbl, m)
|
|
} else {
|
|
m := putProllyMapValues(t, ctx, ns, existing, data)
|
|
tables[tbl] = m
|
|
am = updateAddressMap(t, ctx, ns, am, tbl, m)
|
|
}
|
|
}
|
|
|
|
for tbl, keys := range d.deletes {
|
|
existing, ok := tables[tbl]
|
|
require.True(t, ok, "cannot delete from table that wasn't created")
|
|
m := deleteProllyMapKeys(t, ctx, ns, existing, keys)
|
|
tables[tbl] = m
|
|
am = updateAddressMap(t, ctx, ns, am, tbl, m)
|
|
}
|
|
|
|
for _, tbl := range d.tblDeletes {
|
|
delete(tables, tbl)
|
|
am = deleteFromAddressMap(t, ctx, am, tbl)
|
|
}
|
|
|
|
rootVal := buildRootValue(am)
|
|
epoch := datas.CommitDateAt(time.UnixMilli(0))
|
|
commitOpts := datas.CommitOptions{Parents: parent, Meta: &datas.CommitMeta{Author: datas.CommitIdent{Date: epoch}, Committer: datas.CommitIdent{Date: epoch}}}
|
|
ds, err = db.Commit(ctx, ds, rootVal, commitOpts)
|
|
require.NoError(t, err)
|
|
|
|
dsAddr, ok := ds.MaybeHeadAddr()
|
|
require.True(t, ok)
|
|
|
|
parent = []hash.Hash{dsAddr}
|
|
states[d.name] = dsAddr
|
|
}
|
|
|
|
bigTable := makeABigProllyTable(t, ctx, ns)
|
|
am = addToAddressMap(t, ctx, ns, am, "big_table", bigTable)
|
|
|
|
rootVal := buildRootValue(am)
|
|
epoch := datas.CommitDateAt(time.UnixMilli(0))
|
|
commitOpts := datas.CommitOptions{Parents: parent, Meta: &datas.CommitMeta{Author: datas.CommitIdent{Date: epoch}, Committer: datas.CommitIdent{Date: epoch}}}
|
|
ds, err = db.Commit(ctx, ds, rootVal, commitOpts)
|
|
require.NoError(t, err)
|
|
|
|
addr, ok := ds.MaybeHeadAddr()
|
|
require.True(t, ok)
|
|
states["add big table"] = addr
|
|
|
|
srcCS := datas.ChunkStoreFromDatabase(db)
|
|
|
|
for k, rootAddr := range states {
|
|
t.Run(k, func(t *testing.T) {
|
|
statsCh := make(chan Stats, 16)
|
|
wg := new(sync.WaitGroup)
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for evt := range statsCh {
|
|
jsonBytes, err := json.Marshal(evt)
|
|
if err == nil {
|
|
t.Logf("stats: %s\n", string(jsonBytes))
|
|
}
|
|
}
|
|
}()
|
|
|
|
_, sinkdb := makeDB(ctx)
|
|
defer sinkdb.Close()
|
|
|
|
tmpDir := filepath.Join(os.TempDir(), uuid.New().String())
|
|
err = os.MkdirAll(tmpDir, os.ModePerm)
|
|
require.NoError(t, err)
|
|
waf, err := types.WalkAddrsForChunkStore(srcCS)
|
|
require.NoError(t, err)
|
|
sinkCS := datas.ChunkStoreFromDatabase(sinkdb)
|
|
plr, err := NewPuller(ctx, tmpDir, 1<<20, srcCS, sinkCS, waf, []hash.Hash{rootAddr}, statsCh)
|
|
require.NoError(t, err)
|
|
|
|
err = plr.Pull(ctx)
|
|
close(statsCh)
|
|
require.NoError(t, err)
|
|
wg.Wait()
|
|
|
|
sinkDS, err := sinkdb.GetDataset(ctx, "ds")
|
|
require.NoError(t, err)
|
|
sinkDS, err = sinkdb.FastForward(ctx, sinkDS, rootAddr, "", false)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, err)
|
|
sinkRootAddr, ok := sinkDS.MaybeHeadAddr()
|
|
require.True(t, ok)
|
|
|
|
pullerHashEquality(t, ctx, rootAddr, sinkRootAddr, srcCS, sinkCS)
|
|
})
|
|
}
|
|
}
|
|
|
|
func makeABigProllyTable(t *testing.T, ctx context.Context, ns tree.NodeStore) prolly.Map {
|
|
tuples := make([]val.Tuple, 0, 256*1024*2)
|
|
for i := 0; i < 256*1024; i++ {
|
|
k := buildKeyTuple(ns, int64(i))
|
|
v := buildValTuple(ns, uuid.New().String(), uuid.New().String())
|
|
tuples = append(tuples, k, v)
|
|
}
|
|
m, err := prolly.NewMapFromTuples(ctx, ns, testKeyDesc, testValDesc, tuples...)
|
|
require.NoError(t, err)
|
|
return m
|
|
}
|
|
|
|
func pullerHashEquality(t *testing.T, ctx context.Context, expected, actual hash.Hash, srcCS, sinkCS chunks.ChunkStore) {
|
|
require.Equal(t, expected, actual)
|
|
|
|
srcChunk, err := srcCS.Get(ctx, expected)
|
|
require.NoError(t, err)
|
|
require.False(t, srcChunk.IsEmpty())
|
|
|
|
sinkChunk, err := sinkCS.Get(ctx, actual)
|
|
require.NoError(t, err)
|
|
require.False(t, sinkChunk.IsEmpty())
|
|
|
|
require.Equal(t, srcChunk.Data(), sinkChunk.Data())
|
|
}
|