Skip to content
8 changes: 4 additions & 4 deletions go/cmd/dolt/commands/engine/lock_release_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ func TestCreateDatabase_ReleasesLockOnEngineClose(t *testing.T) {
})

dbLoadParams := map[string]interface{}{
dbfactory.DisableSingletonCacheParam: struct{}{},
dbfactory.FailOnJournalLockTimeoutParam: struct{}{},
dbfactory.DisableSingletonCacheParam: struct{}{},
dbfactory.BlockOnJournalLockParam: struct{}{},
}

rootEnv := env.LoadWithoutDB(ctx, env.GetCurrentUserHomeDir, fs, doltdb.LocalDirDoltDB, "test")
rootEnv.DBLoadParams = map[string]interface{}{
dbfactory.DisableSingletonCacheParam: struct{}{},
dbfactory.FailOnJournalLockTimeoutParam: struct{}{},
dbfactory.DisableSingletonCacheParam: struct{}{},
dbfactory.BlockOnJournalLockParam: struct{}{},
}
mrEnv, err := env.MultiEnvForDirectory(ctx, cfg, fs, "test", rootEnv)
require.NoError(t, err)
Expand Down
2 changes: 1 addition & 1 deletion go/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ require (
github.com/bcicen/jstream v1.0.0
github.com/boltdb/bolt v1.3.1
github.com/denisbrodbeck/machineid v1.0.1
github.com/dolthub/fslock v0.0.0-20251215194149-ef20baba2318
github.com/dolthub/fslock v0.0.4-0.20260206230739-ee1f58d31997
github.com/dolthub/ishell v0.0.0-20240701202509-2b217167d718
github.com/dolthub/sqllogictest/go v0.0.0-20201107003712-816f3ae12d81
github.com/dolthub/vitess v0.0.0-20260202234501-b14ed9b1632b
Expand Down
4 changes: 2 additions & 2 deletions go/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ github.com/dolthub/eventsapi_schema v0.0.0-20260205214132-a7a3c84c84a1 h1:QePoMp
github.com/dolthub/eventsapi_schema v0.0.0-20260205214132-a7a3c84c84a1/go.mod h1:evuptFmr/0/j0X/g+3cveHEEOM5tqyRA15FNgirtOY0=
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2 h1:u3PMzfF8RkKd3lB9pZ2bfn0qEG+1Gms9599cr0REMww=
github.com/dolthub/flatbuffers/v23 v23.3.3-dh.2/go.mod h1:mIEZOHnFx4ZMQeawhw9rhsj+0zwQj7adVsnBX7t+eKY=
github.com/dolthub/fslock v0.0.0-20251215194149-ef20baba2318 h1:n+vdH5G5Db+1qnDCpRjSQMxlTewwvTzKuuq0nJm0AqI=
github.com/dolthub/fslock v0.0.0-20251215194149-ef20baba2318/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0=
github.com/dolthub/fslock v0.0.4-0.20260206230739-ee1f58d31997 h1:IZbaTBlojHRtgZBvG759966d7kRnvnpx+wOOR3rcg+k=
github.com/dolthub/fslock v0.0.4-0.20260206230739-ee1f58d31997/go.mod h1:QWql+P17oAAMLnL4HGB5tiovtDuAjdDTPbuqx7bYfa0=
github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790 h1:zxMsH7RLiG+dlZ/y0LgJHTV26XoiSJcuWq+em6t6VVc=
github.com/dolthub/go-icu-regex v0.0.0-20250916051405-78a38d478790/go.mod h1:F3cnm+vMRK1HaU6+rNqQrOCyR03HHhR1GWG2gnPOqaE=
github.com/dolthub/go-mysql-server v0.20.1-0.20260205195710-d9c710c32e81 h1:uksnOkJM+o3nDb4zPE3mAZwisKQvLsWliN2xPOnCe1Q=
Expand Down
14 changes: 7 additions & 7 deletions go/libraries/doltcore/dbfactory/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ const (
// Intended for embedded-driver usage where callers want deterministic reopen semantics.
DisableSingletonCacheParam = "disable_singleton_cache"

// FailOnJournalLockTimeoutParam changes the journaling store open behavior to fail fast when the exclusive
// journal manifest lock cannot be acquired within Dolt's internal lock timeout, instead of falling back
// to opening the database in read-only mode.
// BlockOnJournalLockParam changes the journaling store open behavior to block until it acquires the
// exclusive journal manifest lock (or the context is canceled), instead of falling back to opening
// the database in read-only mode on lock timeout.
//
// Intended for embedded-driver usage so higher layers can implement their own retry/backoff policy.
FailOnJournalLockTimeoutParam = "fail_on_journal_lock_timeout"
BlockOnJournalLockParam = "block_on_journal_lock"
)

// DoltDataDir is the directory where noms files will be stored
Expand Down Expand Up @@ -200,12 +200,12 @@ func (fact FileFactory) CreateDbNoCache(ctx context.Context, nbf *types.NomsBinF
var newGenSt *nbs.NomsBlockStore
q := nbs.NewUnlimitedMemQuotaProvider()
if useJournal && chunkJournalFeatureFlag {
// Allow higher layers (e.g. embedded driver) to opt into fail-fast lock behavior instead of
// Allow higher layers (e.g. embedded driver) to opt into blocking lock behavior instead of
// falling back to read-only mode on lock timeout.
opts := nbs.JournalingStoreOptions{}
if params != nil {
if _, ok := params[FailOnJournalLockTimeoutParam]; ok {
opts.FailOnLockTimeout = true
if _, ok := params[BlockOnJournalLockParam]; ok {
opts.BlockOnLock = true
}
}
newGenSt, err = nbs.NewLocalJournalingStoreWithOptions(ctx, nbf.VersionString(), path, q, mmapArchiveIndexes, recCb, opts)
Expand Down
46 changes: 37 additions & 9 deletions go/libraries/doltcore/dbfactory/file_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ import (
"os"
"path/filepath"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/dolthub/dolt/go/libraries/utils/earl"
"github.com/dolthub/dolt/go/store/nbs"
"github.com/dolthub/dolt/go/store/types"
)

Expand Down Expand Up @@ -71,7 +71,7 @@ func TestFileFactory_CreateDB_SingletonCacheAndBypass(t *testing.T) {
})
}

func TestFileFactory_CreateDB_FailOnJournalLockTimeoutParam(t *testing.T) {
func TestFileFactory_CreateDB_BlockOnJournalLockParam(t *testing.T) {
ctx := context.Background()
nbf := types.Format_Default

Expand All @@ -81,18 +81,46 @@ func TestFileFactory_CreateDB_FailOnJournalLockTimeoutParam(t *testing.T) {

urlStr := earl.FileUrlFromPath(filepath.ToSlash(nomsDir), os.PathSeparator)
params := map[string]interface{}{
ChunkJournalParam: struct{}{},
DisableSingletonCacheParam: struct{}{},
FailOnJournalLockTimeoutParam: struct{}{},
ChunkJournalParam: struct{}{},
DisableSingletonCacheParam: struct{}{},
BlockOnJournalLockParam: struct{}{},
}

// First open takes the journal manifest lock and holds it until closed.
db1, _, _, err := CreateDB(ctx, nbf, urlStr, params)
require.NoError(t, err)
t.Cleanup(func() { _ = db1.Close() })

// Second open should fail fast when the fail-on-timeout flag is honored.
_, _, _, err = CreateDB(ctx, nbf, urlStr, params)
require.Error(t, err)
require.ErrorIs(t, err, nbs.ErrDatabaseLocked)
t.Run("blocks until context deadline exceeded", func(t *testing.T) {
ctx2, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
t.Cleanup(cancel)

_, _, _, err = CreateDB(ctx2, nbf, urlStr, params)
require.Error(t, err)
require.ErrorIs(t, err, context.DeadlineExceeded)
})

t.Run("blocks and succeeds when lock released", func(t *testing.T) {
ctx2, cancel := context.WithTimeout(context.Background(), 2*time.Second)
t.Cleanup(cancel)

done := make(chan error, 1)
go func() {
db2, _, _, err := CreateDB(ctx2, nbf, urlStr, params)
if err == nil {
_ = db2.Close()
}
done <- err
}()

time.Sleep(50 * time.Millisecond)
require.NoError(t, db1.Close())

select {
case err := <-done:
require.NoError(t, err)
case <-time.After(3 * time.Second):
t.Fatal("timed out waiting for second open to complete")
}
})
}
27 changes: 16 additions & 11 deletions go/store/nbs/journal.go
Original file line number Diff line number Diff line change
Expand Up @@ -547,21 +547,26 @@ func (c journalConjoiner) chooseConjoinees(upstream []tableSpec) (conjoinees, ke
return
}

// newJournalManifest makes a new file manifest.
// When failOnTimeout is true, callers want a hard error instead of falling back to read-only mode.
// (The behavior change is implemented separately; this is the plumbing flag.)
func newJournalManifest(ctx context.Context, dir string, failOnTimeout bool) (m *journalManifest, err error) {
// newJournalManifest makes a new journal manifest.
// By default it attempts to take the journal manifest lock with Dolt's internal lock timeout and
// falls back to a read-only manifest if it cannot acquire the lock. When |opts.BlockOnLock| is set,
// it will block until it acquires the lock or |ctx| is canceled.
func newJournalManifest(ctx context.Context, dir string, opts JournalingStoreOptions) (m *journalManifest, err error) {
lock := fslock.New(filepath.Join(dir, lockFileName))
// try to take the file lock. if we fail, make the manifest read-only.
// if we succeed, hold the file lock until we close the journalManifest
err = lock.LockWithTimeout(lockFileTimeout)
if errors.Is(err, fslock.ErrTimeout) {
if failOnTimeout {
return nil, ErrDatabaseLocked
if opts.BlockOnLock {
err = lock.LockWithContext(ctx)
if err != nil {
return nil, err
}
} else {
err = lock.LockWithTimeout(lockFileTimeout)
if errors.Is(err, fslock.ErrTimeout) {
lock, err = nil, nil // read only
} else if err != nil {
return nil, err
}
lock, err = nil, nil // read only
} else if err != nil {
return nil, err
}
m = &journalManifest{dir: dir, lock: lock}

Expand Down
18 changes: 11 additions & 7 deletions go/store/nbs/journal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"math/rand"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -35,7 +36,7 @@ func makeTestChunkJournal(t *testing.T) *ChunkJournal {
dir, err := os.MkdirTemp("", "")
require.NoError(t, err)
t.Cleanup(func() { file.RemoveAll(dir) })
m, err := newJournalManifest(ctx, dir, false)
m, err := newJournalManifest(ctx, dir, JournalingStoreOptions{})
require.NoError(t, err)
q := NewUnlimitedMemQuotaProvider()
p := newFSTablePersister(dir, q, false)
Expand All @@ -47,7 +48,7 @@ func makeTestChunkJournal(t *testing.T) *ChunkJournal {
}

func openTestChunkJournal(t *testing.T, dir string) *ChunkJournal {
m, err := newJournalManifest(t.Context(), dir, false)
m, err := newJournalManifest(t.Context(), dir, JournalingStoreOptions{})
require.NoError(t, err)
q := NewUnlimitedMemQuotaProvider()
p := newFSTablePersister(dir, q, false)
Expand Down Expand Up @@ -97,15 +98,18 @@ func TestChunkJournalReadOnly(t *testing.T) {
require.NotNil(t, rosource)
})

t.Run("FailOnLockTimeoutReturnsErrDatabaseLocked", func(t *testing.T) {
// A rw journal holds the journalManifest lock. With failOnTimeout enabled, a concurrent open should
// return an error instead of falling back to read-only.
t.Run("BlockOnLockRespectsContextCancel", func(t *testing.T) {
// A rw journal holds the journalManifest lock. With BlockOnLock enabled, a concurrent open should
// block until the context is canceled / deadline exceeded.
rw := makeTestChunkJournal(t)
assert.Equal(t, chunks.ExclusiveAccessMode(chunks.ExclusiveAccessMode_Exclusive), rw.AccessMode())

_, err := newJournalManifest(t.Context(), rw.backing.dir, true)
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
t.Cleanup(cancel)

_, err := newJournalManifest(ctx, rw.backing.dir, JournalingStoreOptions{BlockOnLock: true})
require.Error(t, err)
require.ErrorIs(t, err, ErrDatabaseLocked)
require.ErrorIs(t, err, context.DeadlineExceeded)
})
}

Expand Down
9 changes: 5 additions & 4 deletions go/store/nbs/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -672,9 +672,10 @@ func NewLocalJournalingStore(ctx context.Context, nbfVers, dir string, q MemoryQ

// JournalingStoreOptions controls behavior of local journaling store creation.
type JournalingStoreOptions struct {
// FailOnLockTimeout returns an error if the exclusive journal manifest lock cannot be acquired
// within Dolt's internal lock timeout, instead of falling back to opening in read-only mode.
FailOnLockTimeout bool
// BlockOnLock causes the journaling store open to block until it acquires the exclusive
// journal manifest lock, returning an error only if the context is canceled or its
// deadline is exceeded.
BlockOnLock bool
}

func NewLocalJournalingStoreWithOptions(ctx context.Context, nbfVers, dir string, q MemoryQuotaProvider, mmapArchiveIndexes bool, warningsCb func(error), opts JournalingStoreOptions) (*NomsBlockStore, error) {
Expand All @@ -683,7 +684,7 @@ func NewLocalJournalingStoreWithOptions(ctx context.Context, nbfVers, dir string
return nil, err
}

m, err := newJournalManifest(ctx, dir, opts.FailOnLockTimeout)
m, err := newJournalManifest(ctx, dir, opts)
if err != nil {
return nil, err
}
Expand Down
Loading