Files
wehub-resource-sync 5357c39144
Fuzzer / Run Fuzzer (push) Has been cancelled
Race tests / Go race tests (ubuntu-22.04) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:01:40 +08:00

309 lines
9.7 KiB
Go

// Copyright 2020 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 doltdb_test
import (
"context"
"errors"
"os"
"testing"
"github.com/dolthub/go-mysql-server/sql"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/dolthub/dolt/go/cmd/dolt/commands"
"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
"github.com/dolthub/dolt/go/libraries/doltcore/dtestutils"
"github.com/dolthub/dolt/go/libraries/doltcore/env"
"github.com/dolthub/dolt/go/libraries/doltcore/ref"
"github.com/dolthub/dolt/go/libraries/doltcore/sqle"
"github.com/dolthub/dolt/go/libraries/utils/filesys"
"github.com/dolthub/dolt/go/store/chunks"
"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/val"
)
func TestGarbageCollection(t *testing.T) {
require.True(t, true)
assert.True(t, true)
for _, gct := range gcTests {
t.Run(gct.name, func(t *testing.T) {
testGarbageCollection(t, gct)
})
}
t.Run("HasCacheDataCorruption", testGarbageCollectionHasCacheDataCorruptionBugFix)
}
type stage struct {
commands []testCommand
preStageFunc func(ctx context.Context, t *testing.T, ddb *doltdb.DoltDB, prevRes interface{}) interface{}
}
type gcTest struct {
name string
stages []stage
query string
expected []sql.Row
postGCFunc func(ctx context.Context, t *testing.T, ddb *doltdb.DoltDB, prevRes interface{})
}
var gcTests = []gcTest{
{
name: "gc test",
stages: []stage{
{
preStageFunc: func(ctx context.Context, t *testing.T, ddb *doltdb.DoltDB, i interface{}) interface{} {
return nil
},
commands: []testCommand{
{commands.CheckoutCmd{}, []string{"-b", "temp"}},
{commands.SqlCmd{}, []string{"-q", "INSERT INTO test VALUES (0),(1),(2);"}},
{commands.AddCmd{}, []string{"."}},
{commands.CommitCmd{}, []string{"-m", "commit"}},
},
},
{
preStageFunc: func(ctx context.Context, t *testing.T, ddb *doltdb.DoltDB, i interface{}) interface{} {
cm, err := ddb.ResolveCommitRef(ctx, ref.NewBranchRef("temp"))
require.NoError(t, err)
h, err := cm.HashOf()
require.NoError(t, err)
cs, err := doltdb.NewCommitSpec(h.String())
require.NoError(t, err)
_, err = ddb.Resolve(ctx, cs, nil)
require.NoError(t, err)
return h
},
commands: []testCommand{
{commands.CheckoutCmd{}, []string{env.DefaultInitBranch}},
{commands.BranchCmd{}, []string{"-D", "temp"}},
{commands.SqlCmd{}, []string{"-q", "INSERT INTO test VALUES (4),(5),(6);"}},
},
},
},
query: "select * from test;",
expected: []sql.Row{{int32(4)}, {int32(5)}, {int32(6)}},
postGCFunc: func(ctx context.Context, t *testing.T, ddb *doltdb.DoltDB, prevRes interface{}) {
h := prevRes.(hash.Hash)
cs, err := doltdb.NewCommitSpec(h.String())
require.NoError(t, err)
_, err = ddb.Resolve(ctx, cs, nil)
require.Error(t, err)
},
},
}
var gcSetupCommon = []testCommand{
{commands.SqlCmd{}, []string{"-q", "CREATE TABLE test (pk int PRIMARY KEY)"}},
{commands.AddCmd{}, []string{"."}},
{commands.CommitCmd{}, []string{"-m", "created test table"}},
}
func testGarbageCollection(t *testing.T, test gcTest) {
ctx := t.Context()
dEnv := dtestutils.CreateTestEnv()
t.Cleanup(func() {
dEnv.Close()
})
cliCtx, verr := commands.NewArgFreeCliContext(ctx, dEnv, dEnv.FS)
require.NoError(t, verr)
t.Cleanup(func() {
cliCtx.Close()
})
for _, c := range gcSetupCommon {
exitCode := c.cmd.Exec(ctx, c.cmd.Name(), c.args, dEnv, cliCtx)
require.Equal(t, 0, exitCode)
}
var res interface{}
for _, stage := range test.stages {
res = stage.preStageFunc(ctx, t, dEnv.DoltDB(ctx), res)
for _, c := range stage.commands {
exitCode := c.cmd.Exec(ctx, c.cmd.Name(), c.args, dEnv, cliCtx)
require.Equal(t, 0, exitCode)
}
}
ddb := dEnv.DoltDB(ctx)
gcConfig := chunks.GCConfig{
Mode: chunks.GCMode_Default,
ArchiveLevel: chunks.NoArchive,
IncrementalFileSize: chunks.IncrementalGCTablesDisabled,
}
err := ddb.GC(ctx, gcConfig, purgingSafepointController{ddb})
require.NoError(t, err)
test.postGCFunc(ctx, t, dEnv.DoltDB(ctx), res)
working, err := dEnv.WorkingRoot(ctx)
require.NoError(t, err)
// assert all out rows are present after gc
actual, err := sqle.ExecuteSelect(ctx, dEnv, working, test.query)
require.NoError(t, err)
assert.Equal(t, test.expected, actual)
}
// In September 2023, we found a failure to handle the `hasCache` in
// `*NomsBlockStore` appropriately while cleaning up a memtable into which
// dangling references had been written could result in writing chunks to a
// database which referenced non-existent chunks.
//
// The general pattern was to get new chunk addresses into the hasCache, but
// not written to the store, and then to have an incoming chunk add a reference
// to missing chunk. At that time, we would clear the memtable, since it had
// invalid chunks in it, but we wouldn't purge the hasCache. Later writes which
// attempted to reference the chunks which had made it into the hasCache would
// succeed.
//
// One such concrete pattern for doing this is implemented below. We do:
//
// 1) Put a new chunk to the database -- C1.
//
// 2) Run a GC.
//
// 3) Put a new chunk to the database -- C2.
//
// 4) Call NBS.Commit() with a stale last hash.Hash. This causes us to cache C2
// as present in the store, but it does not get written to disk, because the
// optimistic concurrency control on the value of the current root hash fails.
//
// 5) Put a chunk referencing C1 to the database -- R1.
//
// 5) Call NBS.Commit(). This causes ErrDanglingRef. C1 was written before the
// GC and is no longer in the store. C2 is also cleared from the pending write
// set.
//
// 6) Put a chunk referencing C2 to the database -- R2.
//
// 7) Call NBS.Commit(). This should fail, since R2 references C2 and C2 is not
// in the store. However, C2 is in the cache as a result of step #4, and so
// this does not fail. R2 gets written to disk with a dangling reference to C2.
func testGarbageCollectionHasCacheDataCorruptionBugFix(t *testing.T) {
ctx := context.Background()
d, err := os.MkdirTemp(t.TempDir(), "hascachetest-")
require.NoError(t, err)
ddb, err := doltdb.LoadDoltDB(ctx, types.Format_DOLT, "file://"+d, filesys.LocalFS)
require.NoError(t, err)
defer ddb.Close()
err = ddb.WriteEmptyRepo(ctx, "main", "Aaron Son", "aaron@dolthub.com")
require.NoError(t, err)
root, err := ddb.NomsRoot(ctx)
require.NoError(t, err)
ns := ddb.NodeStore()
c1 := newIntMap(t, ctx, ns, 1, 1)
_, err = ns.Write(ctx, c1.Node())
require.NoError(t, err)
gcConfig := chunks.GCConfig{
Mode: chunks.GCMode_Default,
ArchiveLevel: chunks.NoArchive,
IncrementalFileSize: chunks.IncrementalGCTablesDisabled,
}
err = ddb.GC(ctx, gcConfig, purgingSafepointController{ddb})
require.NoError(t, err)
c2 := newIntMap(t, ctx, ns, 2, 2)
_, err = ns.Write(ctx, c2.Node())
require.NoError(t, err)
success, err := ddb.CommitRoot(ctx, c2.HashOf(), c2.HashOf())
require.NoError(t, err)
require.False(t, success, "committing the root with a last hash which does not match the current root must fail")
r1 := newAddrMap(t, ctx, ns, "r1", c1.HashOf())
_, err = ns.Write(ctx, r1.Node())
require.NoError(t, err)
success, err = ddb.CommitRoot(ctx, root, root)
require.True(t, errors.Is(err, nbs.ErrDanglingRef), "committing a reference to just-collected c1 must fail with ErrDanglingRef")
r2 := newAddrMap(t, ctx, ns, "r2", c2.HashOf())
_, err = ns.Write(ctx, r2.Node())
require.NoError(t, err)
success, err = ddb.CommitRoot(ctx, root, root)
require.True(t, errors.Is(err, nbs.ErrDanglingRef), "committing a reference to c2, which was erased with the ErrDanglingRef above, must also fail with ErrDanglingRef")
}
func newIntMap(t *testing.T, ctx context.Context, ns tree.NodeStore, k, v int8) prolly.Map {
desc := val.NewTupleDescriptor(val.Type{
Enc: val.Int8Enc,
Nullable: false,
})
tb := val.NewTupleBuilder(desc, ns)
tb.PutInt8(0, k)
keyTuple, err := tb.Build(ctx, ns.Pool())
require.NoError(t, err)
tb.PutInt8(0, v)
valueTuple, err := tb.Build(ctx, ns.Pool())
require.NoError(t, err)
m, err := prolly.NewMapFromTuples(ctx, ns, desc, desc, keyTuple, valueTuple)
require.NoError(t, err)
return m
}
func newAddrMap(t *testing.T, ctx context.Context, ns tree.NodeStore, key string, h hash.Hash) prolly.AddressMap {
m, err := prolly.NewEmptyAddressMap(ns)
require.NoError(t, err)
editor := m.Editor()
err = editor.Add(ctx, key, h)
require.NoError(t, err)
m, err = editor.Flush(ctx)
require.NoError(t, err)
return m
}
type purgingSafepointController struct {
ddb *doltdb.DoltDB
}
var _ (types.GCSafepointController) = purgingSafepointController{}
func (c purgingSafepointController) BeginGC(ctx context.Context, keeper func(h hash.Hash) bool) error {
c.ddb.PurgeCaches()
return nil
}
func (c purgingSafepointController) EstablishPreFinalizeSafepoint(context.Context) error {
return nil
}
func (c purgingSafepointController) EstablishPostFinalizeSafepoint(context.Context) error {
return nil
}
func (c purgingSafepointController) CancelSafepoint() {
}