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

155 lines
5.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 (
"testing"
"github.com/stretchr/testify/require"
driver "github.com/dolthub/dolt/go/libraries/doltcore/dtestutils/sql_server_driver"
)
// TestCloneAfterPushStaleSingleton surfaces a bug in the interaction
// between NomsBlockStore's TableFileSource (Sources()) implementation
// and the dbfactory singleton cache for file:// URLs.
//
// When a long-running dolt process clones a file:// URL, the source
// NomsBlockStore is opened and stored in the dbfactory singleton cache,
// keyed by path. A subsequent clone of the same file:// URL reuses that
// cached NomsBlockStore. Meanwhile, an external process can push new
// writes to the file remote, adding new table files and rewriting its
// manifest on disk.
//
// On the second clone, NomsBlockStore.Sources() re-reads the on-disk
// manifest (which now references the newly-pushed table files), but
// builds its chunkSource map from the cached, in-memory table set, which
// is stale and does not contain those new table files. The lookup fails
// with ErrSpecWithoutChunkSource ("manifest referenced table file for
// which there is no chunkSource."), causing the second clone to fail.
func TestCloneAfterPushStaleSingleton(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()
})
ctx := t.Context()
// The "running dolt process" which performs the clones. Its
// singleton cache will hold the source NomsBlockStore across both
// clones.
clonerRS, err := u.MakeRepoStore()
require.NoError(t, err)
clonerRepo, err := clonerRS.MakeRepo("cloner")
require.NoError(t, err)
clonerSrvSettings := &driver.Server{
Args: []string{"-P", `{{get_port "cloner_port"}}`},
DynamicPort: "cloner_port",
}
clonerServer := MakeServer(t, clonerRepo, clonerSrvSettings, &ports)
clonerServer.DBName = "cloner"
// A separate process which owns the source database and pushes new
// writes to the file remote in between the two clones.
remoteRS, err := u.MakeRepoStore()
require.NoError(t, err)
remoteRepo, err := remoteRS.MakeRepo("source")
require.NoError(t, err)
remoteSrvSettings := &driver.Server{
Args: []string{"-P", `{{get_port "remote_port"}}`},
DynamicPort: "remote_port",
}
remoteServer := MakeServer(t, remoteRepo, remoteSrvSettings, &ports)
remoteServer.DBName = "source"
// The file:// remote which the source pushes to and the cloner
// clones from.
remoteDir := t.TempDir()
remoteURL := "file://" + remoteDir
clonerDB, err := clonerServer.DB(driver.Connection{User: "root"})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, clonerDB.Close())
})
remoteDB, err := remoteServer.DB(driver.Connection{User: "root"})
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, remoteDB.Close())
})
// Set up the file remote on the source server and push an initial
// commit to it.
func() {
conn, err := remoteDB.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
_, err = conn.ExecContext(ctx, "CALL DOLT_REMOTE('add', 'rendezvous', '"+remoteURL+"')")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "CREATE TABLE t (pk int primary key, v int)")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "INSERT INTO t VALUES (1, 1)")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "CALL DOLT_COMMIT('-A', '-m', 'first commit')")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "CALL DOLT_PUSH('rendezvous', 'main:main')")
require.NoError(t, err)
}()
// First clone. This opens the source NomsBlockStore and caches it in
// the cloner process's dbfactory singleton cache.
func() {
conn, err := clonerDB.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
_, err = conn.ExecContext(ctx, "CALL DOLT_CLONE('"+remoteURL+"', 'clone_one')")
require.NoError(t, err)
}()
// Push more writes to the file remote from the source process. This
// adds a new table file and rewrites the manifest on disk. The
// cloner process's cached NomsBlockStore is unaware of this change.
func() {
conn, err := remoteDB.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
_, err = conn.ExecContext(ctx, "INSERT INTO t VALUES (2, 2)")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "CALL DOLT_COMMIT('-A', '-m', 'second commit')")
require.NoError(t, err)
_, err = conn.ExecContext(ctx, "CALL DOLT_PUSH('rendezvous', 'main:main')")
require.NoError(t, err)
}()
// Second clone of the same file:// URL. It reuses the cached, stale
// source NomsBlockStore. Before the fix, Sources() reads the fresh
// on-disk manifest but cannot find the newly-pushed table file in its
// stale in-memory table set, and this clone fails with
// "manifest referenced table file for which there is no chunkSource."
func() {
conn, err := clonerDB.Conn(ctx)
require.NoError(t, err)
defer conn.Close()
_, err = conn.ExecContext(ctx, "CALL DOLT_CLONE('"+remoteURL+"', 'clone_two')")
require.NoError(t, err)
}()
}