// Package workspace verwaltet die offenen Workspace-Dateien eines laufenden // Prozesses. // // Jede Abteilung hat ihre eigene SQLite-Datei (siehe AUSBAUSTUFEN.md 4.4). Ein // Manager öffnet sie bei Bedarf und hält sie offen, statt bei jedem Request // neu zu öffnen — das Öffnen (inklusive Migrationsprüfung) kostet spürbar mehr // als ein Map-Zugriff. package workspace import ( "fmt" "os" "path/filepath" "sync" "munin/internal/search" "munin/internal/store" ) // Workspace bündelt, was zu einer offenen Workspace-Datei gehört: die // Datenbank und ihren Suchindex. Beide sind an dieselbe Datei gebunden und // werden deshalb gemeinsam verwaltet — ein Registry, der aus Versehen an der // falschen Datenbank hängt, wäre ein stiller Fehler. type Workspace struct { ID string Store *store.Store Search *search.Registry } // Manager öffnet Workspace-Dateien bei Bedarf und hält sie offen. // // Bei 10–40 Workspaces (siehe AUSBAUSTUFEN.md 4.4) ist das unproblematisch — // Verdrängung selten gebrauchter Dateien würde erst bei einer deutlich // höheren Zahl nötig, siehe AUSBAUSTUFEN.md 1.2. type Manager struct { dir string // Ordner mit einer Datei je Workspace, z.B. "/workspaces" mu sync.Mutex open map[string]*Workspace } // NewManager legt einen Manager an. dir muss bereits existieren — das // übernimmt main.go beim Start. func NewManager(dir string) *Manager { return &Manager{dir: dir, open: make(map[string]*Workspace)} } // Get liefert den Workspace zu einer ID und öffnet ihn beim ersten Zugriff. func (m *Manager) Get(workspaceID string) (*Workspace, error) { m.mu.Lock() defer m.mu.Unlock() if w, ok := m.open[workspaceID]; ok { return w, nil } s, err := store.Open(filepath.Join(m.dir, workspaceID+".db")) if err != nil { return nil, fmt.Errorf("Workspace %s konnte nicht geöffnet werden: %w", workspaceID, err) } w := &Workspace{ID: workspaceID, Store: s, Search: search.NewRegistry(s)} m.open[workspaceID] = w return w, nil } // Delete schließt die Workspace-Datei, falls offen, und entfernt sie // unwiederbringlich von der Platte — samt WAL/SHM-Nebendateien, die SQLite im // laufenden Betrieb danebenlegt. // // Ruft nur die Datei ab — die Kontroll-Datenbank (workspaces, key_index) muss // getrennt bereinigt werden, siehe ControlStore.DeleteWorkspace. Reihenfolge // ist wichtig: Erst dort löschen, dann hier — sonst verweist die // Kontroll-Datenbank kurzzeitig auf eine Datei, die es nicht mehr gibt. func (m *Manager) Delete(workspaceID string) error { m.mu.Lock() if w, ok := m.open[workspaceID]; ok { w.Store.Close() delete(m.open, workspaceID) } m.mu.Unlock() pfad := filepath.Join(m.dir, workspaceID+".db") for _, endung := range []string{"", "-wal", "-shm"} { if err := os.Remove(pfad + endung); err != nil && !os.IsNotExist(err) { return fmt.Errorf("Workspace-Datei konnte nicht gelöscht werden: %w", err) } } return nil } // Close schließt alle offenen Workspace-Dateien. func (m *Manager) Close() error { m.mu.Lock() defer m.mu.Unlock() var erster error for id, w := range m.open { if err := w.Store.Close(); err != nil && erster == nil { erster = err } delete(m.open, id) } return erster }