From 4bd6b41d785ee9a07c84899bf97a76926b20ef66 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 09:23:05 +0000 Subject: [PATCH] fix(workspace): preserve orphan aggregates during recovery Co-Authored-By: Virgil --- workspace.go | 53 +++++++++++++++++++++++++++++++++++++++++------ workspace_test.go | 16 +++++++++++++- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/workspace.go b/workspace.go index 7f50a9c..de971ca 100644 --- a/workspace.go +++ b/workspace.go @@ -3,6 +3,7 @@ package store import ( "database/sql" "io/fs" + "maps" "slices" "sync" "time" @@ -42,6 +43,7 @@ type Workspace struct { workspaceDatabase *sql.DB databasePath string filesystem *core.Fs + orphanAggregate map[string]any closeLock sync.Mutex closed bool @@ -186,13 +188,15 @@ func discoverOrphanWorkspaces(stateDirectory string, backingStore *Store) []*Wor if err != nil { continue } - orphanWorkspaces = append(orphanWorkspaces, &Workspace{ + orphanWorkspace := &Workspace{ name: workspaceNameFromPath(stateDirectory, databasePath), backingStore: backingStore, workspaceDatabase: workspaceDatabase, databasePath: databasePath, filesystem: filesystem, - }) + } + orphanWorkspace.orphanAggregate = orphanWorkspace.captureAggregateSnapshot() + orphanWorkspaces = append(orphanWorkspaces, orphanWorkspace) } return orphanWorkspaces } @@ -238,13 +242,15 @@ func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace { if err != nil { continue } - orphanWorkspaces = append(orphanWorkspaces, &Workspace{ + orphanWorkspace := &Workspace{ name: workspaceNameFromPath(stateDirectory, databasePath), backingStore: storeInstance, workspaceDatabase: workspaceDatabase, databasePath: databasePath, filesystem: filesystem, - }) + } + orphanWorkspace.orphanAggregate = orphanWorkspace.captureAggregateSnapshot() + orphanWorkspaces = append(orphanWorkspaces, orphanWorkspace) } return orphanWorkspaces } @@ -281,13 +287,16 @@ func (workspace *Workspace) Put(kind string, data map[string]any) error { // Usage example: `summary := workspace.Aggregate()` func (workspace *Workspace) Aggregate() map[string]any { + if workspace.shouldUseOrphanAggregate() { + return workspace.aggregateFallback() + } if err := workspace.ensureReady("store.Workspace.Aggregate"); err != nil { - return map[string]any{} + return workspace.aggregateFallback() } fields, err := workspace.aggregateFields() if err != nil { - return map[string]any{} + return workspace.aggregateFallback() } return fields } @@ -345,7 +354,39 @@ func (workspace *Workspace) aggregateFields() (map[string]any, error) { if err := workspace.ensureReady("store.Workspace.aggregateFields"); err != nil { return nil, err } + return workspace.aggregateFieldsWithoutReadiness() +} +func (workspace *Workspace) captureAggregateSnapshot() map[string]any { + if workspace == nil || workspace.workspaceDatabase == nil { + return nil + } + + fields, err := workspace.aggregateFieldsWithoutReadiness() + if err != nil { + return nil + } + return fields +} + +func (workspace *Workspace) aggregateFallback() map[string]any { + if workspace == nil || workspace.orphanAggregate == nil { + return map[string]any{} + } + return maps.Clone(workspace.orphanAggregate) +} + +func (workspace *Workspace) shouldUseOrphanAggregate() bool { + if workspace == nil || workspace.orphanAggregate == nil { + return false + } + if workspace.filesystem == nil || workspace.databasePath == "" { + return false + } + return !workspace.filesystem.Exists(workspace.databasePath) +} + +func (workspace *Workspace) aggregateFieldsWithoutReadiness() (map[string]any, error) { rows, err := workspace.workspaceDatabase.Query( "SELECT entry_kind, COUNT(*) FROM " + workspaceEntriesTableName + " GROUP BY entry_kind ORDER BY entry_kind", ) diff --git a/workspace_test.go b/workspace_test.go index 905ff76..9fee387 100644 --- a/workspace_test.go +++ b/workspace_test.go @@ -231,6 +231,13 @@ func TestWorkspace_New_Good_LeavesOrphanedWorkspacesForRecovery(t *testing.T) { orphanDatabasePath := workspaceFilePath(stateDirectory, "orphan-session") orphanDatabase, err := openWorkspaceDatabase(orphanDatabasePath) require.NoError(t, err) + _, err = orphanDatabase.Exec( + "INSERT INTO "+workspaceEntriesTableName+" (entry_kind, entry_data, created_at) VALUES (?, ?, ?)", + "like", + `{"user":"@alice"}`, + time.Now().UnixMilli(), + ) + require.NoError(t, err) require.NoError(t, orphanDatabase.Close()) assert.True(t, testFilesystem().Exists(orphanDatabasePath)) @@ -256,6 +263,13 @@ func TestWorkspace_New_Good_CachesOrphansDuringConstruction(t *testing.T) { orphanDatabasePath := workspaceFilePath(stateDirectory, "orphan-session") orphanDatabase, err := openWorkspaceDatabase(orphanDatabasePath) require.NoError(t, err) + _, err = orphanDatabase.Exec( + "INSERT INTO "+workspaceEntriesTableName+" (entry_kind, entry_data, created_at) VALUES (?, ?, ?)", + "like", + `{"user":"@alice"}`, + time.Now().UnixMilli(), + ) + require.NoError(t, err) require.NoError(t, orphanDatabase.Close()) assert.True(t, testFilesystem().Exists(orphanDatabasePath)) @@ -269,7 +283,7 @@ func TestWorkspace_New_Good_CachesOrphansDuringConstruction(t *testing.T) { 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()) + assert.Equal(t, map[string]any{"like": 1}, orphans[0].Aggregate()) orphans[0].Discard() }