diff --git a/store.go b/store.go index cc5dfa2..d1baf39 100644 --- a/store.go +++ b/store.go @@ -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 } diff --git a/workspace.go b/workspace.go index 8717013..45de1d5 100644 --- a/workspace.go +++ b/workspace.go @@ -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) { diff --git a/workspace_test.go b/workspace_test.go index 78303e5..d3f04e4 100644 --- a/workspace_test.go +++ b/workspace_test.go @@ -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() +}