Files
2026-07-13 12:32:25 +08:00

241 lines
7.9 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/doltgresql/integration-tests/go-sql-server-driver/driver"
)
// 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 the sql-server logs whenever it begins
// 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. Doltgres uses the same Dolt storage
// engine, so the conjoin log lines (pkg=store.noms) and the on-disk
// oldgen table-file layout are identical.
//
// 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.
//
// NOTE: a successful online dolt_gc() invalidates every open connection
// to the doltgres server, so the helpers below open a fresh connection
// for each commit/gc iteration (db.SetMaxIdleConns(0) ensures each
// connection is a new one rather than a pooled, now-invalid connection).
func TestFullGCNoOldgenConjoin(t *testing.T) {
t.Parallel()
ports := newPorts(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{},
DynamicPort: "server",
}
var conjoinStarted, conjoinFinished atomic.Bool
upstreamLenOnConjoin := 0
server := MakeServer(t, rs, rs.Dir, srvSettings, ports, driver.WithOutputVisitor(func(out string) {
// Matching a line like:
// time="..." 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.
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="..." 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="..." 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, rs, rs.Dir, srvSettings, ports)
newServer.DBName = dbname
db, err := newServer.DB(driver.Connection{})
require.NoError(t, err)
defer db.Close()
require.NoError(t, db.PingContext(t.Context()))
}
// isTableFileName reports whether n is the name of a Dolt/Noms table file: a
// 32-character string in Noms' base32 hash alphabet. This mirrors
// hash.MaybeParse from Dolt's store/hash package without importing it.
func isTableFileName(n string) bool {
const alphabet = "0123456789abcdefghijklmnopqrstuv"
if len(n) != 32 {
return false
}
for _, c := range n {
if !strings.ContainsRune(alphabet, c) {
return false
}
}
return true
}
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()
// Archive table files carry a .darc suffix.
n = strings.TrimSuffix(n, ".darc")
if isTableFileName(n) {
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{})
require.NoError(t, err)
defer db.Close()
// Ensure each db.Conn() hands out a new connection rather than a pooled
// one that a prior online GC has invalidated.
db.SetMaxIdleConns(0)
for {
func() {
conn, err := db.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
_, err = conn.ExecContext(ctx, "SELECT DOLT_COMMIT('-A', '--allow-empty', '-m', 'creating a new commit')")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "SELECT 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{})
require.NoError(t, err)
defer db.Close()
db.SetMaxIdleConns(0)
for {
cnt := CountTableFiles(t, path)
if cnt == numFiles {
return
}
func() {
conn, err := db.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
_, err = conn.ExecContext(ctx, "SELECT DOLT_COMMIT('-A', '--allow-empty', '-m', 'creating a new commit')")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "SELECT 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{})
require.NoError(t, err)
defer db.Close()
db.SetMaxIdleConns(0)
conn, err := db.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
_ = CountTableFiles(t, path)
_, err = conn.ExecContext(ctx, "SELECT DOLT_GC('--full')")
require.NoError(t, err)
cnt := CountTableFiles(t, path)
require.Equal(t, 1, cnt)
}