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

126 lines
4.5 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"
"testing"
"time"
"github.com/stretchr/testify/require"
driver "github.com/dolthub/dolt/go/libraries/doltcore/dtestutils/sql_server_driver"
)
// This test asserts the behavior of the fast-fail file-lock optimization for
// dolt CLI commands which run against a data dir while a `dolt sql-server` is
// running in that data dir.
//
// When opening a directory of databases, we don't want to wait for the
// lock-acquire timeout once we've failed to get one.
//
// This test creates many databases and runs a server in the data dir. It then
// spawns a CLI command in the data dir root and times it. The command must
// finish well under the un-optimized cost, which is ~N * 100ms.
const (
// perDatabaseLockTimeout mirrors lockFileTimeout in nbs/journal.go
perDatabaseLockTimeout = 100 * time.Millisecond
// numReadOnlyDatabases is chosen large enough that the un-optimized,
// serial lock-wait cost is unmistakably larger than the optimized cost
numReadOnlyDatabases = 32
// We will try up to this many times to see good behavior. If we have
// lots of scheduling contention, we can still be slow enough that we
// think we are waiting on the lock files but we are actually just
// running slowly.
maxTrials = 3
)
// makeEmptyDatabases creates n freshly initialized, empty databases as
// subdirectories of the data dir root |rs|. Empty databases still need
// locking, and so meet our use case.
func makeEmptyDatabases(t *testing.T, rs driver.RepoStore, n int) {
for i := 0; i < n; i++ {
_, err := rs.MakeRepo(fmt.Sprintf("db_%02d", i))
require.NoError(t, err)
}
}
// timeShowDatabases runs `dolt sql -q "show databases"` from the data dir root
// |rs| up to maxTrials times, returning the best time seen. If the time is
// ever < maxAcceptable, it returns that immediately.
func timeShowDatabases(t *testing.T, rs driver.RepoStore, maxAcceptable time.Duration) time.Duration {
var best time.Duration
for trial := 0; trial < maxTrials; trial++ {
cmd := rs.DoltCmd("sql", "-q", "show databases")
start := time.Now()
out, err := cmd.CombinedOutput()
elapsed := time.Since(start)
require.NoError(t, err, "show databases failed, output:\n%s", string(out))
require.Regexp(t, "db_00", string(out))
if trial == 0 || elapsed < best {
best = elapsed
}
if best < maxAcceptable {
// We take the very first run. Serially waiting for
// the timeouts would have definitely taken longer
// than this.
return best
}
}
return best
}
// TestReadOnlyDatabaseLoadSkipsLockTimeout asserts that loading many read-only
// databases behind a running sql-server does not serially pay the file-lock
// timeout for every database.
func TestReadOnlyDatabaseLoadSkipsLockTimeout(t *testing.T) {
// No Parallel because it's just a bit sensitive to wall-clock time.
u, err := driver.NewDoltUser()
require.NoError(t, err)
t.Cleanup(func() {
u.Cleanup()
})
rs, err := u.MakeRepoStore()
require.NoError(t, err)
makeEmptyDatabases(t, rs, numReadOnlyDatabases)
// Start a sql-server in the data dir root. It holds the file lock on every
// database, forcing the CLI below to open them all read-only. The helper
// blocks until the server is up and serving, so server startup time is not
// included in the measurement below.
var ports DynamicResources
ports.global = &GlobalPorts
ports.t = t
RunServerUntilEndOfTest(t, rs, &driver.Server{
Args: []string{"--port", `{{get_port "server"}}`},
DynamicPort: "server",
}, &ports)
unoptimizedCost := numReadOnlyDatabases * perDatabaseLockTimeout
maxAcceptable := unoptimizedCost / 2
best := timeShowDatabases(t, rs, maxAcceptable)
require.Lessf(t, best, maxAcceptable,
"loading %d read-only databases behind a running sql-server took %s; "+
"without the fast-fail lock optimization this would have taken about "+
"%s (%d serial %s file-lock waits)",
numReadOnlyDatabases, best, unoptimizedCost, numReadOnlyDatabases, perDatabaseLockTimeout)
}