diff --git a/driver.go b/driver.go index 92d90b3..cd15e08 100644 --- a/driver.go +++ b/driver.go @@ -68,7 +68,12 @@ func openSqlEngine(ctx context.Context, cfg config.ReadWriteConfig, fs filesys.F } go emitUsageEvent(context.Background(), mrEnv) - return engine.NewSqlEngine(ctx, mrEnv, seCfg) + se, err := engine.NewSqlEngine(ctx, mrEnv, seCfg) + if err != nil { + mrEnv.Close(ctx) + return nil, err + } + return se, nil } // Open opens and returns a connection to the datasource referenced by the string provided using the options provided. diff --git a/go.mod b/go.mod index 3230ef0..bcd429c 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.6 require ( github.com/cenkalti/backoff/v4 v4.2.1 - github.com/dolthub/dolt/go v0.40.5-0.20260316195416-ff749ef50e2e + github.com/dolthub/dolt/go v0.40.5-0.20260316200056-24e549c548e1 github.com/dolthub/eventsapi_schema v0.0.0-20260310172945-37a9265ade69 github.com/dolthub/go-mysql-server v0.20.1-0.20260313230549-0986a7fcf0fe github.com/dolthub/vitess v0.0.0-20260309181228-a99af9c518ab diff --git a/go.sum b/go.sum index b57dd7d..e38569e 100644 --- a/go.sum +++ b/go.sum @@ -331,8 +331,8 @@ github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12 h1:I github.com/dolthub/aws-sdk-go-ini-parser v0.0.0-20250305001723-2821c37f6c12/go.mod h1:rN7X8BHwkjPcfMQQ2QTAq/xM3leUSGLfb+1Js7Y6TVo= github.com/dolthub/dolt-mcp v0.3.4 h1:AyG5cw+fNWXDHXujtQnqUPZrpWtPg6FN6yYtjv1pP44= github.com/dolthub/dolt-mcp v0.3.4/go.mod h1:bCZ7KHvDYs+M0e+ySgmGiNvLhcwsN7bbf5YCyillLrk= -github.com/dolthub/dolt/go v0.40.5-0.20260316195416-ff749ef50e2e h1:kTT/Kup+JQCwmsJpTv2T2X/JLEJY8TuQespI8iTItag= -github.com/dolthub/dolt/go v0.40.5-0.20260316195416-ff749ef50e2e/go.mod h1:x0TL0LQX70EdjIcj+RdIxYqGAVRpZk3IXDrHG6NW9B8= +github.com/dolthub/dolt/go v0.40.5-0.20260316200056-24e549c548e1 h1:dCEG+v2yaZUSyqkbG1YeHxsgt+l4fpc6+X6pmlJHXSU= +github.com/dolthub/dolt/go v0.40.5-0.20260316200056-24e549c548e1/go.mod h1:x0TL0LQX70EdjIcj+RdIxYqGAVRpZk3IXDrHG6NW9B8= github.com/dolthub/eventsapi_schema v0.0.0-20260310172945-37a9265ade69 h1:JShhbqMw26nKx3pqqu/cFxOpzBkN+4elVhzuUfgDw2k= github.com/dolthub/eventsapi_schema v0.0.0-20260310172945-37a9265ade69/go.mod h1:SSLraQS/jGLYFgff3vuZ+JbVUct6vyEeMzjLBqWqoyM= github.com/dolthub/flatbuffers v1.13.0-dh.1 h1:OWJdaPep22N52O/0xsUevxJ6Qfw1M2txCjZPOdjXybE= diff --git a/lock_contention_test.go b/lock_contention_test.go new file mode 100644 index 0000000..e915e4c --- /dev/null +++ b/lock_contention_test.go @@ -0,0 +1,98 @@ +// 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 embedded + +import ( + "context" + "database/sql" + "net/url" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestMultiDBLockContention verifies that when one connection holds the write lock on a +// subdirectory database, a second connection opening the parent directory (which discovers +// and tries to open the same subdirectory) returns an error instead of panicking. +func TestMultiDBLockContention(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping on Windows: file handle cleanup is racey with t.TempDir()") + } + + metricsDisabled.Store(true) + + rootDir, err := os.MkdirTemp("", "TestMultiDBLockContention") + require.NoError(t, err) + defer os.RemoveAll(rootDir) + + ctx := context.Background() + + baseQuery := url.Values{ + "commitname": []string{"test"}, + "commitemail": []string{"test@test.com"}, + } + + // First, open the root directory and create a "first" database so that + // a .dolt directory exists at rootDir/first. + rootDSN := url.URL{Scheme: "file", Path: encodeDir(rootDir), RawQuery: baseQuery.Encode()} + rootCfg, err := ParseDSN(rootDSN.String()) + require.NoError(t, err) + rootConnector, err := NewConnector(rootCfg) + require.NoError(t, err) + setupDB := sql.OpenDB(rootConnector) + conn, err := setupDB.Conn(ctx) + require.NoError(t, err) + _, err = conn.ExecContext(ctx, "CREATE DATABASE `firstdb`") + require.NoError(t, err) + require.NoError(t, conn.Close()) + _ = rootConnector.Close() + + // Now open the subdirectory database directly — this holds the journal/write lock. + subDir := filepath.Join(rootDir, "firstdb") + subDSN := url.URL{Scheme: "file", Path: encodeDir(subDir), RawQuery: baseQuery.Encode()} + subCfg, err := ParseDSN(subDSN.String()) + require.NoError(t, err) + subConnector, err := NewConnector(subCfg) + require.NoError(t, err) + subDB := sql.OpenDB(subConnector) + require.NoError(t, subDB.PingContext(ctx)) + + // Now open the root directory again with a bounded backoff. It will discover + // the "firstdb" subdirectory and fail to acquire its lock. This must return an + // error, not panic. + rootCfg2, err := ParseDSN(rootDSN.String()) + require.NoError(t, err) + bo := backoff.NewExponentialBackOff() + bo.MaxElapsedTime = 2 * time.Second + bo.MaxInterval = 100 * time.Millisecond + rootCfg2.BackOff = bo + rootConnector2, err := NewConnector(rootCfg2) + require.NoError(t, err) + rootDB2 := sql.OpenDB(rootConnector2) + + err = rootDB2.PingContext(ctx) + assert.Error(t, err, "expected an error due to lock contention, not a panic") + + rootDB2.Close() + _ = rootConnector2.Close() + subDB.Close() + _ = subConnector.Close() +}