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

213 lines
7.1 KiB
Go

// Copyright 2026 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 main
import (
"fmt"
"io/fs"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
driver "github.com/dolthub/dolt/go/libraries/doltcore/dtestutils/sql_server_driver"
"github.com/dolthub/dolt/go/store/hash"
)
// This test asserts that running a full gc into the old gen at the
// point that it is at its conjoin limit will not generate a conjoin
// in the middle of the GC.
//
// It first generates empty commits and runs GC in a loop. After every
// GC, it checks how many table files are in the oldgen. When it sees
// them get conjoined, it assumes max table files is the observed high
// water mark.
//
// It then creates exactly that many table files in oldgen by creating
// empty commits and running gc however many times it needs to.
//
// Then it needs to create one more commit and run a
// dolt_gc(--full). Currently dolt sql-server logs whenever it beings
// a conjoin, and that determination is made synchronously on the
// write path. So, no real attempt is made to get a theoretically
// started conjoin to win the race against the newgen collection, for
// example.
//
// As described above, this test is tightly coupled with
// GenerationalNBS, NomsBlockStore and the file persisters, with
// conjoin strategy behavior.
//
// At the end of the test, there should be one file in the oldgen, the
// results of the --full. There should be exactly one "beginning
// conjoin of database" in the server logs and it should correspond to
// when we measured the high water mark. After shutting down the
// server should happily start again.
func TestFullGCNoOldgenConjoin(t *testing.T) {
t.Parallel()
var ports DynamicResources
ports.global = &GlobalPorts
ports.t = t
u, err := driver.NewDoltUser()
require.NoError(t, err)
t.Cleanup(func() {
u.Cleanup()
})
dbname := "full_gc_no_oldgen_conjoin_test"
rs, err := u.MakeRepoStore()
require.NoError(t, err)
repo, err := rs.MakeRepo(dbname)
require.NoError(t, err)
srvSettings := &driver.Server{
Args: []string{"--port", `{{get_port "server"}}`},
DynamicPort: "server",
}
var conjoinStarted, conjoinFinished atomic.Bool
upstreamLenOnConjoin := 0
server := MakeServer(t, repo, srvSettings, &ports, driver.WithOutputVisitor(func(out string) {
// Matching a line like:
// time="2026-03-28T11:57:31-07:00" level=info msg="beginning conjoin of database" database=full_gc_no_oldgen_conjoin_test generation=old pkg=store.noms upstream_len=257
// Extracting its upstream_len to see exactly when the conjoin was triggered.
// This line is logged as part of the conjoin strategy deciding to do the conjoin,
// and so reflects the state of the tableSet which triggers the conjoin.
if upstreamLenOnConjoin <= 0 && strings.Contains(out, "beginning conjoin of database") {
if i := strings.Index(out, "upstream_len="); i != -1 {
i += len("upstream_len=")
if _, err := fmt.Sscanf(out[i:], "%d", &upstreamLenOnConjoin); err != nil {
upstreamLenOnConjoin = -1
}
}
conjoinStarted.Store(true)
}
// Matching the line:
// time="2026-03-28T11:57:32-07:00" level=info msg="conjoin completed successfully" database=full_gc_no_oldgen_conjoin_test generation=old new_upstream_len=2 pkg=store.noms
// So we can block on further operations until conjoin is done.
if strings.Contains(out, "conjoin completed successfully") {
conjoinFinished.Store(true)
}
}))
server.DBName = dbname
oldgendir := filepath.Join(repo.Dir, "/.dolt/noms/oldgen")
CommitAndGCUntilConjoin(t, server, &conjoinStarted, oldgendir)
require.Greater(t, upstreamLenOnConjoin, 0)
require.Eventually(t, func() bool {
return conjoinFinished.Load()
}, 5 * time.Second, 32 * time.Millisecond)
CreateUpToNumFiles(t, server, oldgendir, upstreamLenOnConjoin)
cnt := CountTableFiles(t, oldgendir)
t.Logf("now there are %d", cnt)
RunGCFull(t, server, oldgendir)
require.NoError(t, server.GracefulStop())
output := server.Output.String()
assert.Equal(t, 1, strings.Count(output, "beginning conjoin of database"))
// The line for triggering a conjoin on policy but not proceeding because
// conjoin is dynamically disabled looks like:
// time="2026-03-28T12:01:48-07:00" level=info msg="conjoin dynamically disabled. not conjoining." database=full_gc_no_oldgen_conjoin_test generation=old pkg=store.noms
assert.Equal(t, 1, strings.Count(output, "conjoin dynamically disabled"))
cnt = CountTableFiles(t, oldgendir)
assert.Equal(t, 1, cnt)
newServer := MakeServer(t, repo, srvSettings, &ports)
newServer.DBName = dbname
db, err := newServer.DB(driver.Connection{User: "root"})
require.NoError(t, err)
defer db.Close()
require.NoError(t, db.PingContext(t.Context()))
}
func CountTableFiles(t *testing.T, dir string) int {
var count int
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
n := d.Name()
n = strings.TrimSuffix(n, ".darc")
_, ok := hash.MaybeParse(n)
if ok {
count += 1
}
return nil
})
require.NoError(t, err)
return count
}
func CommitAndGCUntilConjoin(t *testing.T, srv *driver.SqlServer, conjoinStarted *atomic.Bool, path string) {
ctx := t.Context()
db, err := srv.DB(driver.Connection{User: "root"})
require.NoError(t, err)
defer db.Close()
conn, err := db.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
for {
_, err = conn.ExecContext(ctx, "CALL DOLT_COMMIT('-A', '--allow-empty', '-m', 'creating a new commit')")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "CALL DOLT_GC()")
require.NoError(t, err)
if conjoinStarted.Load() {
return
}
}
}
func CreateUpToNumFiles(t *testing.T, srv *driver.SqlServer, path string, numFiles int) {
ctx := t.Context()
db, err := srv.DB(driver.Connection{User: "root"})
require.NoError(t, err)
defer db.Close()
conn, err := db.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
for {
cnt := CountTableFiles(t, path)
if cnt == numFiles {
return
}
_, err = conn.ExecContext(ctx, "CALL DOLT_COMMIT('-A', '--allow-empty', '-m', 'creating a new commit')")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "CALL DOLT_GC()")
require.NoError(t, err)
}
}
func RunGCFull(t *testing.T, srv *driver.SqlServer, path string) {
ctx := t.Context()
db, err := srv.DB(driver.Connection{User: "root"})
require.NoError(t, err)
defer db.Close()
conn, err := db.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
_ = CountTableFiles(t, path)
_, err = conn.ExecContext(ctx, "CALL DOLT_GC('--full')")
cnt := CountTableFiles(t, path)
require.Equal(t, 1, cnt)
}