feat(store): scan orphan workspaces on startup

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-03 07:22:12 +00:00
parent 79581e9824
commit a2067baa5a
2 changed files with 37 additions and 14 deletions

View file

@ -128,6 +128,8 @@ func WithPurgeInterval(interval time.Duration) StoreOption {
}
// Usage example: `storeInstance, err := store.NewConfigured(store.StoreConfig{DatabasePath: ":memory:", Journal: store.JournalConfiguration{EndpointURL: "http://127.0.0.1:8086", Organisation: "core", BucketName: "events"}, PurgeInterval: 20 * time.Millisecond})`
// NewConfigured also scans `.core/state` for leftover `.duckdb` workspace files
// so orphan recovery can happen before the first explicit recovery call.
func NewConfigured(config StoreConfig) (*Store, error) {
storeInstance, err := openStore("store.NewConfigured", config.DatabasePath)
if err != nil {
@ -145,11 +147,14 @@ func NewConfigured(config StoreConfig) (*Store, error) {
storeInstance.purgeInterval = config.PurgeInterval
}
_ = discoverOrphanWorkspacePaths(defaultWorkspaceStateDirectory)
storeInstance.startBackgroundPurge()
return storeInstance, nil
}
// Usage example: `storeInstance, err := store.New("/tmp/go-store.db", store.WithJournal("http://127.0.0.1:8086", "core", "events"))`
// New scans `.core/state` for leftover `.duckdb` workspace files before the
// store starts its background purge loop.
func New(databasePath string, options ...StoreOption) (*Store, error) {
storeInstance, err := openStore("store.New", databasePath)
if err != nil {
@ -160,6 +165,7 @@ func New(databasePath string, options ...StoreOption) (*Store, error) {
option(storeInstance)
}
}
_ = discoverOrphanWorkspacePaths(defaultWorkspaceStateDirectory)
storeInstance.startBackgroundPurge()
return storeInstance, nil
}

View file

@ -109,19 +109,13 @@ func (storeInstance *Store) NewWorkspace(name string) (*Workspace, error) {
}, nil
}
// RecoverOrphans(".core/state") returns orphaned workspaces such as
// `scroll-session.duckdb` so callers can inspect Aggregate() and then Discard().
// Usage example: `orphans := storeInstance.RecoverOrphans(".core/state")`
func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace {
if storeInstance == nil {
return nil
}
// discoverOrphanWorkspacePaths(".core/state") returns leftover workspace files
// such as `scroll-session.duckdb` without opening them.
func discoverOrphanWorkspacePaths(stateDirectory string) []string {
filesystem := (&core.Fs{}).NewUnrestricted()
if stateDirectory == "" {
stateDirectory = defaultWorkspaceStateDirectory
}
filesystem := (&core.Fs{}).NewUnrestricted()
if !filesystem.Exists(stateDirectory) {
return nil
}
@ -147,19 +141,42 @@ func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace {
}
})
var orphanWorkspaces []*Workspace
orphanPaths := make([]string, 0, len(directoryEntries))
for _, dirEntry := range directoryEntries {
if dirEntry.IsDir() || !core.HasSuffix(dirEntry.Name(), ".duckdb") {
continue
}
name := core.TrimSuffix(dirEntry.Name(), ".duckdb")
databasePath := workspaceFilePath(stateDirectory, name)
orphanPaths = append(orphanPaths, workspaceFilePath(stateDirectory, core.TrimSuffix(dirEntry.Name(), ".duckdb")))
}
return orphanPaths
}
func workspaceNameFromPath(stateDirectory, databasePath string) string {
relativePath := core.TrimPrefix(databasePath, joinPath(stateDirectory, ""))
return core.TrimSuffix(relativePath, ".duckdb")
}
// RecoverOrphans(".core/state") returns orphaned workspaces such as
// `scroll-session.duckdb` so callers can inspect Aggregate() and then Discard().
// Usage example: `orphans := storeInstance.RecoverOrphans(".core/state")`
func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace {
if storeInstance == nil {
return nil
}
if stateDirectory == "" {
stateDirectory = defaultWorkspaceStateDirectory
}
filesystem := (&core.Fs{}).NewUnrestricted()
var orphanWorkspaces []*Workspace
for _, databasePath := range discoverOrphanWorkspacePaths(stateDirectory) {
workspaceDatabase, err := openWorkspaceDatabase(databasePath)
if err != nil {
continue
}
orphanWorkspaces = append(orphanWorkspaces, &Workspace{
name: name,
name: workspaceNameFromPath(stateDirectory, databasePath),
backingStore: storeInstance,
database: workspaceDatabase,
databasePath: databasePath,