241 lines
7.9 KiB
Go
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)
|
|
}
|