fix(store): cache orphan workspaces during startup

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 07:57:24 +00:00
parent 1b5f59ebc5
commit 1c92e47b24
3 changed files with 64 additions and 1 deletions

View file

@ -78,6 +78,9 @@ type Store struct {
watchersLock sync.RWMutex // protects watcher registration and dispatch
callbacksLock sync.RWMutex // protects callback registration and dispatch
nextCallbackRegistrationID uint64 // monotonic ID for callback registrations
orphanWorkspacesLock sync.Mutex
orphanWorkspaces []*Workspace
}
func (storeInstance *Store) ensureReady(operation string) error {
@ -172,7 +175,7 @@ func openConfiguredStore(operation string, config StoreConfig) (*Store, error) {
// New() performs a non-destructive orphan scan so callers can discover
// leftover workspaces via RecoverOrphans().
discoverOrphanWorkspacePaths(defaultWorkspaceStateDirectory)
storeInstance.orphanWorkspaces = discoverOrphanWorkspaces(defaultWorkspaceStateDirectory, storeInstance)
storeInstance.startBackgroundPurge()
return storeInstance, nil
}
@ -254,6 +257,13 @@ func (storeInstance *Store) Close() error {
storeInstance.callbacks = nil
storeInstance.callbacksLock.Unlock()
storeInstance.orphanWorkspacesLock.Lock()
for _, orphanWorkspace := range storeInstance.orphanWorkspaces {
orphanWorkspace.Discard()
}
storeInstance.orphanWorkspaces = nil
storeInstance.orphanWorkspacesLock.Unlock()
if storeInstance.database == nil {
return nil
}

View file

@ -168,6 +168,25 @@ func discoverOrphanWorkspacePaths(stateDirectory string) []string {
return orphanPaths
}
func discoverOrphanWorkspaces(stateDirectory string, backingStore *Store) []*Workspace {
filesystem := (&core.Fs{}).NewUnrestricted()
orphanWorkspaces := make([]*Workspace, 0)
for _, databasePath := range discoverOrphanWorkspacePaths(stateDirectory) {
workspaceDatabase, err := openWorkspaceDatabase(databasePath)
if err != nil {
continue
}
orphanWorkspaces = append(orphanWorkspaces, &Workspace{
name: workspaceNameFromPath(stateDirectory, databasePath),
backingStore: backingStore,
database: workspaceDatabase,
databasePath: databasePath,
filesystem: filesystem,
})
}
return orphanWorkspaces
}
func workspaceNameFromPath(stateDirectory, databasePath string) string {
relativePath := core.TrimPrefix(databasePath, joinPath(stateDirectory, ""))
return core.TrimSuffix(relativePath, ".duckdb")
@ -185,6 +204,16 @@ func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace {
stateDirectory = defaultWorkspaceStateDirectory
}
if stateDirectory == defaultWorkspaceStateDirectory {
storeInstance.orphanWorkspacesLock.Lock()
cachedWorkspaces := storeInstance.orphanWorkspaces
storeInstance.orphanWorkspaces = nil
storeInstance.orphanWorkspacesLock.Unlock()
if len(cachedWorkspaces) > 0 {
return cachedWorkspaces
}
}
filesystem := (&core.Fs{}).NewUnrestricted()
var orphanWorkspaces []*Workspace
for _, databasePath := range discoverOrphanWorkspacePaths(stateDirectory) {

View file

@ -222,3 +222,27 @@ func TestWorkspace_New_Good_LeavesOrphanedWorkspacesForRecovery(t *testing.T) {
assert.False(t, testFilesystem().Exists(orphanDatabasePath+"-wal"))
assert.False(t, testFilesystem().Exists(orphanDatabasePath+"-shm"))
}
func TestWorkspace_New_Good_CachesOrphansDuringConstruction(t *testing.T) {
stateDirectory := useWorkspaceStateDirectory(t)
requireCoreOK(t, testFilesystem().EnsureDir(stateDirectory))
orphanDatabasePath := workspaceFilePath(stateDirectory, "orphan-session")
orphanDatabase, err := openWorkspaceDatabase(orphanDatabasePath)
require.NoError(t, err)
require.NoError(t, orphanDatabase.Close())
assert.True(t, testFilesystem().Exists(orphanDatabasePath))
storeInstance, err := New(":memory:")
require.NoError(t, err)
defer storeInstance.Close()
requireCoreOK(t, testFilesystem().DeleteAll(stateDirectory))
assert.False(t, testFilesystem().Exists(orphanDatabasePath))
orphans := storeInstance.RecoverOrphans(stateDirectory)
require.Len(t, orphans, 1)
assert.Equal(t, "orphan-session", orphans[0].Name())
assert.Equal(t, map[string]any{}, orphans[0].Aggregate())
orphans[0].Discard()
}